From 61b9fe61b83b9b0fb912e8c4a569ad6672bf147f Mon Sep 17 00:00:00 2001 From: yh928 Date: Fri, 17 Jul 2026 14:47:06 +0900 Subject: [PATCH] harness(agent_loop): let a model response keep the floor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A response carrying tool calls continues the turn; one without them is taken as the turn's final answer. That leaves no way for a model to say something *before* it acts — a text-mode protocol, where tool calls are encoded as prose, has no tool call to ride and so ends its turn on the first sentence. Working around it means faking a tool call purely to keep the loop going. Add `ModelResponse::continue_turn: Option`. When set, the loop appends the carried string as the next user turn and asks for another reply instead of finalizing. `None` is the default, so existing callers are unaffected. It carries a string rather than a bool because most chat APIs reject two assistant turns in a row — something must be appended — and letting the adapter choose keeps the protocol's vocabulary out of the harness. Checked after truncated-empty recovery (a truncated response is broken, not a deliberate continue) and before structured extraction (which would treat it as terminal). No new cap: each continue costs one model call, so `max_model_calls` already bounds a model that never stops. --- examples/agent_loop_tools.rs | 2 + src/graph/subagent_node/test.rs | 1 + src/harness/agent_loop/run_loop.rs | 15 ++++ src/harness/agent_loop/test.rs | 93 +++++++++++++++++++++++ src/harness/middleware/test.rs | 2 + src/harness/model/mod.rs | 2 + src/harness/model/types.rs | 28 +++++++ src/harness/providers/mock.rs | 2 + src/harness/providers/openai/convert.rs | 1 + src/harness/providers/openai/responses.rs | 1 + src/harness/providers/openai/sse.rs | 1 + src/harness/steering/test.rs | 2 + src/harness/subagent/test.rs | 2 + tests/e2e_agent_graph.rs | 2 + tests/e2e_budget.rs | 4 + tests/e2e_fuzz_graph_agents.rs | 2 + tests/e2e_graph_subagent_node.rs | 1 + tests/e2e_graph_todos.rs | 2 + tests/e2e_harness_provider_contracts.rs | 1 + tests/e2e_middleware.rs | 2 + tests/e2e_observability.rs | 2 + tests/e2e_reasoning_and_selection.rs | 1 + tests/e2e_subagents.rs | 2 + tests/e2e_tool_policy.rs | 2 + tests/e2e_unknown_tool_policy.rs | 2 + tests/e2e_workspace_and_registry.rs | 2 + tests/feature_harness_agent_loop.rs | 2 + tests/feature_harness_structured.rs | 3 + tests/harness_agent_loop.rs | 2 + 29 files changed, 184 insertions(+) diff --git a/examples/agent_loop_tools.rs b/examples/agent_loop_tools.rs index a9c1e87..5ebb57d 100644 --- a/examples/agent_loop_tools.rs +++ b/examples/agent_loop_tools.rs @@ -80,6 +80,7 @@ fn tool_call_response(id: &str, name: &str, arguments: serde_json::Value) -> Mod finish_reason: Some("tool_calls".to_string()), raw: None, resolved_model: None, + continue_turn: None, } } @@ -96,6 +97,7 @@ fn text_response(text: &str) -> ModelResponse { finish_reason: Some("stop".to_string()), raw: None, resolved_model: None, + continue_turn: None, } } diff --git a/src/graph/subagent_node/test.rs b/src/graph/subagent_node/test.rs index 2bfbbc1..42e3802 100644 --- a/src/graph/subagent_node/test.rs +++ b/src/graph/subagent_node/test.rs @@ -38,6 +38,7 @@ fn tool_call_response(id: &str, name: &str) -> ModelResponse { finish_reason: Some("tool_calls".to_string()), raw: None, resolved_model: None, + continue_turn: None, } } diff --git a/src/harness/agent_loop/run_loop.rs b/src/harness/agent_loop/run_loop.rs index 85ceb4a..1dfe343 100644 --- a/src/harness/agent_loop/run_loop.rs +++ b/src/harness/agent_loop/run_loop.rs @@ -333,6 +333,21 @@ impl AgentHarness { continue; } + // The model says it is not finished (`ModelResponse::continue_turn`). + // Hand the floor back and ask for another reply instead of taking + // this response as the turn's answer. Checked after truncated-empty + // recovery — a truncated response is broken, not a deliberate + // continue — and before structured extraction, which would treat it + // as terminal. + // + // The assistant row is already on `messages` (appended above), so + // only the nudge is needed. `max_model_calls` bounds the resulting + // loop exactly as it bounds a tool-calling one. + if !structured_tool_hit && let Some(nudge) = response.continue_turn.clone() { + messages.push(Message::user(nudge)); + continue; + } + // Final response: optionally extract structured output using the // resolved plan (provider-native schema or tool-call arguments). if let Some((strategy, name, schema)) = &structured_plan { diff --git a/src/harness/agent_loop/test.rs b/src/harness/agent_loop/test.rs index eab6fdd..4346a38 100644 --- a/src/harness/agent_loop/test.rs +++ b/src/harness/agent_loop/test.rs @@ -146,6 +146,7 @@ fn tool_call_response(id: &str, name: &str, arguments: serde_json::Value) -> Mod finish_reason: Some("tool_calls".to_string()), raw: None, resolved_model: None, + continue_turn: None, } } @@ -169,6 +170,7 @@ fn invalid_tool_call_response(id: &str, name: &str, raw: &str) -> ModelResponse finish_reason: Some("tool_calls".to_string()), raw: None, resolved_model: None, + continue_turn: None, } } @@ -185,6 +187,7 @@ fn text_response(text: &str, input: u64, output: u64) -> ModelResponse { finish_reason: Some("stop".to_string()), raw: None, resolved_model: None, + continue_turn: None, } } @@ -205,6 +208,7 @@ fn truncated_empty_response(reasoning_tokens: u64) -> ModelResponse { finish_reason: Some("length".to_string()), raw: None, resolved_model: None, + continue_turn: None, } } @@ -2346,6 +2350,7 @@ fn multi_tool_call_response(calls: Vec<(&str, &str)>) -> ModelResponse { finish_reason: Some("tool_calls".to_string()), raw: None, resolved_model: None, + continue_turn: None, } } @@ -2842,3 +2847,91 @@ async fn tool_completed_event_carries_outcome() { assert!(duration_ms.is_some(), "wall-clock duration present"); assert_eq!(output_bytes, Some(4), "\"nope\".len() == 4"); } + +// ── `ModelResponse::continue_turn` ─────────────────────────────────────────── + +/// A tool-less response that keeps the floor by carrying `nudge`. +fn continuing(text: &str, nudge: &str) -> ModelResponse { + let mut response = ModelResponse::assistant(text.to_string()); + response.continue_turn = Some(nudge.to_string()); + response +} + +/// A tool-less response normally ends the turn. `continue_turn` overrides that: +/// the loop appends the carried nudge as the next user turn and asks for another +/// reply — which is what lets a model speak before it acts. +#[tokio::test] +async fn continue_turn_keeps_the_floor_and_appends_the_nudge() { + let mut harness: AgentHarness<()> = AgentHarness::new(); + harness.register_model( + "mock", + Arc::new(MockModel::with_responses(vec![ + continuing("let me look that up", "(next)"), + ModelResponse::assistant("found it".to_string()), + ])), + ); + + let run = harness + .invoke_default(&(), vec![Message::user("hi")]) + .await + .expect("run succeeds"); + + assert_eq!( + run.model_calls, 2, + "the continuing reply must not end the turn" + ); + assert_eq!(run.text(), Some("found it".to_string())); + // user, assistant(continuing), user(nudge), assistant(final) + assert_eq!(run.messages.len(), 4); + assert_eq!(run.messages[2].text(), "(next)"); +} + +/// The default is `None`, which must leave the historical behaviour byte-for-byte +/// intact: the first tool-less reply ends the turn. +#[tokio::test] +async fn no_continue_turn_ends_on_the_first_tool_less_reply() { + let mut harness: AgentHarness<()> = AgentHarness::new(); + harness.register_model( + "mock", + Arc::new(MockModel::with_responses(vec![ + ModelResponse::assistant("done".to_string()), + ModelResponse::assistant("never reached".to_string()), + ])), + ); + + let run = harness + .invoke_default(&(), vec![Message::user("hi")]) + .await + .expect("run succeeds"); + + assert_eq!(run.model_calls, 1); + assert_eq!(run.text(), Some("done".to_string())); +} + +/// Continuing needs no cap of its own: each continue costs one model call, so a +/// model that never stops is already bounded by `max_model_calls`. +#[tokio::test] +async fn an_endless_continue_is_bounded_by_max_model_calls() { + let mut harness: AgentHarness<()> = AgentHarness::new(); + harness.register_model( + "mock", + Arc::new(MockModel::with_responses(vec![ + continuing("still going", "(next)"), + continuing("still going", "(next)"), + continuing("still going", "(next)"), + continuing("still going", "(next)"), + continuing("still going", "(next)"), + ])), + ); + + let config = RunConfig::new("continue-cap").with_max_model_calls(3); + let err = harness + .invoke_in_context(&(), RunContext::new(config, ()), vec![Message::user("hi")]) + .await + .expect_err("an endless continue must hit the model-call cap"); + + assert!( + err.to_string().contains("max model calls"), + "expected the model-call cap to stop it, got: {err}" + ); +} diff --git a/src/harness/middleware/test.rs b/src/harness/middleware/test.rs index 0363d3d..0d4655b 100644 --- a/src/harness/middleware/test.rs +++ b/src/harness/middleware/test.rs @@ -38,6 +38,7 @@ fn response_with_usage(usage: Usage) -> ModelResponse { finish_reason: None, raw: None, resolved_model: None, + continue_turn: None, } } @@ -997,6 +998,7 @@ fn response_text(text: &str) -> ModelResponse { finish_reason: None, raw: None, resolved_model: None, + continue_turn: None, } } diff --git a/src/harness/model/mod.rs b/src/harness/model/mod.rs index 6e5f58a..e4a4bbe 100644 --- a/src/harness/model/mod.rs +++ b/src/harness/model/mod.rs @@ -425,6 +425,7 @@ impl ModelResponse { finish_reason: None, raw: None, resolved_model: None, + continue_turn: None, } } @@ -851,6 +852,7 @@ impl StreamAccumulator { finish_reason: None, raw: None, resolved_model: None, + continue_turn: None, }) } } diff --git a/src/harness/model/types.rs b/src/harness/model/types.rs index 2994da4..2dff6f4 100644 --- a/src/harness/model/types.rs +++ b/src/harness/model/types.rs @@ -422,6 +422,34 @@ pub struct ModelResponse { /// Model selected by the harness/registry for this response. #[serde(default, skip_serializing_if = "Option::is_none")] pub resolved_model: Option, + /// When set, this response does **not** end the turn. + /// + /// A response carrying tool calls always continues the turn — the loop runs + /// them and asks for another reply. A response *without* tool calls is + /// otherwise the turn's final answer. This field lets a model adapter say + /// "not done" anyway: the loop appends the carried string as the next user + /// turn and requests another reply. + /// + /// `None` (the default) is the historical behaviour, so every existing + /// caller is unaffected. + /// + /// **Why it exists.** Text-mode protocols encode tool calls as prose, and a + /// model that wants to say something *before* acting has no tool call to + /// ride. Without this the loop ends its turn on the first sentence, so such + /// a protocol cannot narrate — it must either stay silent until the work is + /// done, or fake a tool call purely to keep the floor. + /// + /// The carried string is the nudge to hand back (e.g. `"(next)"`). It is a + /// string rather than a `bool` because most chat APIs reject two assistant + /// turns in a row, so *something* must be appended; letting the adapter + /// choose keeps the protocol's vocabulary out of the harness. + /// + /// A continuing turn stays bounded by + /// [`RunLimits::max_model_calls`](crate::harness::runtime::RunLimits) — it + /// costs one model call per continue, like any other loop iteration — so + /// this needs no cap of its own. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub continue_turn: Option, } /// An incremental streamed chunk of a model response. diff --git a/src/harness/providers/mock.rs b/src/harness/providers/mock.rs index f781662..1c4ff7f 100644 --- a/src/harness/providers/mock.rs +++ b/src/harness/providers/mock.rs @@ -227,6 +227,7 @@ impl ChatModel for MockModel { finish_reason: Some("tool_calls".to_string()), raw: None, resolved_model: None, + continue_turn: None, } } @@ -327,6 +328,7 @@ impl MockModel { finish_reason: Some("stop".to_string()), raw: None, resolved_model: None, + continue_turn: None, } } } diff --git a/src/harness/providers/openai/convert.rs b/src/harness/providers/openai/convert.rs index a46c68a..f3b2657 100644 --- a/src/harness/providers/openai/convert.rs +++ b/src/harness/providers/openai/convert.rs @@ -303,6 +303,7 @@ pub(super) fn parse_chat_response( finish_reason: choice.finish_reason, raw: Some(value), resolved_model: None, + continue_turn: None, }) } diff --git a/src/harness/providers/openai/responses.rs b/src/harness/providers/openai/responses.rs index 6187f22..e220a57 100644 --- a/src/harness/providers/openai/responses.rs +++ b/src/harness/providers/openai/responses.rs @@ -199,6 +199,7 @@ pub(super) fn parse_responses_response(value: Value) -> ModelResponse { finish_reason: Some("stop".to_string()), raw: Some(value), resolved_model: None, + continue_turn: None, } } diff --git a/src/harness/providers/openai/sse.rs b/src/harness/providers/openai/sse.rs index 71cf50e..76fc62d 100644 --- a/src/harness/providers/openai/sse.rs +++ b/src/harness/providers/openai/sse.rs @@ -274,6 +274,7 @@ impl OpenAiStreamAcc { finish_reason: self.finish_reason, raw: None, resolved_model: None, + continue_turn: None, } } } diff --git a/src/harness/steering/test.rs b/src/harness/steering/test.rs index db6f7bb..58b517a 100644 --- a/src/harness/steering/test.rs +++ b/src/harness/steering/test.rs @@ -42,6 +42,7 @@ fn text_response(text: &str) -> ModelResponse { finish_reason: Some("stop".to_string()), raw: None, resolved_model: None, + continue_turn: None, } } @@ -82,6 +83,7 @@ impl ChatModel<()> for RecordingModel { finish_reason: Some("tool_calls".to_string()), raw: None, resolved_model: None, + continue_turn: None, }) } else { Ok(text_response("done")) diff --git a/src/harness/subagent/test.rs b/src/harness/subagent/test.rs index 2d39e72..4ff5e65 100644 --- a/src/harness/subagent/test.rs +++ b/src/harness/subagent/test.rs @@ -40,6 +40,7 @@ fn tool_call_response(id: &str, name: &str, arguments: serde_json::Value) -> Mod finish_reason: Some("tool_calls".to_string()), raw: None, resolved_model: None, + continue_turn: None, } } @@ -56,6 +57,7 @@ fn text_response(text: &str) -> ModelResponse { finish_reason: Some("stop".to_string()), raw: None, resolved_model: None, + continue_turn: None, } } diff --git a/tests/e2e_agent_graph.rs b/tests/e2e_agent_graph.rs index 64ac58a..94fa816 100644 --- a/tests/e2e_agent_graph.rs +++ b/tests/e2e_agent_graph.rs @@ -50,6 +50,7 @@ fn tool_call_response(id: &str, name: &str, arguments: serde_json::Value) -> Mod finish_reason: Some("tool_calls".to_string()), raw: None, resolved_model: None, + continue_turn: None, } } @@ -66,6 +67,7 @@ fn text_response(text: &str, input: u64, output: u64) -> ModelResponse { finish_reason: Some("stop".to_string()), raw: None, resolved_model: None, + continue_turn: None, } } diff --git a/tests/e2e_budget.rs b/tests/e2e_budget.rs index f85b4d9..1960f57 100644 --- a/tests/e2e_budget.rs +++ b/tests/e2e_budget.rs @@ -58,6 +58,7 @@ fn tool_call_response(id: &str, name: &str, input: u64, output: u64) -> ModelRes finish_reason: Some("tool_calls".into()), raw: None, resolved_model: None, + continue_turn: None, } } @@ -74,6 +75,7 @@ fn text_response(text: &str, input: u64, output: u64) -> ModelResponse { finish_reason: Some("stop".into()), raw: None, resolved_model: None, + continue_turn: None, } } @@ -290,6 +292,7 @@ async fn cost_pricing_records_and_enforces_money_budget() { requested: None, source: ModelResolutionSource::RegistryDefault, }), + continue_turn: None, }; stack @@ -515,6 +518,7 @@ async fn cached_input_budget_blocks_next_call() { finish_reason: Some("stop".into()), raw: None, resolved_model: None, + continue_turn: None, }; stack .run_after_model(&mut ctx, &(), &mut resp) diff --git a/tests/e2e_fuzz_graph_agents.rs b/tests/e2e_fuzz_graph_agents.rs index 6b3ba23..64f142e 100644 --- a/tests/e2e_fuzz_graph_agents.rs +++ b/tests/e2e_fuzz_graph_agents.rs @@ -261,6 +261,7 @@ fn tool_call_response(calls: Vec) -> ModelResponse { finish_reason: Some("tool_calls".to_string()), raw: None, resolved_model: None, + continue_turn: None, } } @@ -276,6 +277,7 @@ fn text_response(text: impl Into) -> ModelResponse { finish_reason: Some("stop".to_string()), raw: None, resolved_model: None, + continue_turn: None, } } diff --git a/tests/e2e_graph_subagent_node.rs b/tests/e2e_graph_subagent_node.rs index 6e8f7b9..5d11c3f 100644 --- a/tests/e2e_graph_subagent_node.rs +++ b/tests/e2e_graph_subagent_node.rs @@ -31,6 +31,7 @@ fn tool_call_response(id: &str, name: &str) -> ModelResponse { finish_reason: Some("tool_calls".to_string()), raw: None, resolved_model: None, + continue_turn: None, } } diff --git a/tests/e2e_graph_todos.rs b/tests/e2e_graph_todos.rs index 59971da..320ccda 100644 --- a/tests/e2e_graph_todos.rs +++ b/tests/e2e_graph_todos.rs @@ -29,6 +29,7 @@ fn tool_call_response(id: &str, name: &str, arguments: serde_json::Value) -> Mod finish_reason: Some("tool_calls".to_string()), raw: None, resolved_model: None, + continue_turn: None, } } @@ -44,6 +45,7 @@ fn text_response(text: &str) -> ModelResponse { finish_reason: Some("stop".to_string()), raw: None, resolved_model: None, + continue_turn: None, } } diff --git a/tests/e2e_harness_provider_contracts.rs b/tests/e2e_harness_provider_contracts.rs index 1fd5cae..48b4b51 100644 --- a/tests/e2e_harness_provider_contracts.rs +++ b/tests/e2e_harness_provider_contracts.rs @@ -454,6 +454,7 @@ fn structured_output_supports_provider_schema_and_tool_fallbacks() { finish_reason: Some("tool_calls".into()), raw: None, resolved_model: None, + continue_turn: None, }; let tool_output = StructuredExtractor::new(StructuredStrategy::ToolCall, "score", schema) .extract(&tool_response) diff --git a/tests/e2e_middleware.rs b/tests/e2e_middleware.rs index b1981e1..cad6eda 100644 --- a/tests/e2e_middleware.rs +++ b/tests/e2e_middleware.rs @@ -142,6 +142,7 @@ fn tool_call_response(id: &str, name: &str, arguments: serde_json::Value) -> Mod finish_reason: Some("tool_calls".to_string()), raw: None, resolved_model: None, + continue_turn: None, } } @@ -158,6 +159,7 @@ fn text_response(text: &str) -> ModelResponse { finish_reason: Some("stop".to_string()), raw: None, resolved_model: None, + continue_turn: None, } } diff --git a/tests/e2e_observability.rs b/tests/e2e_observability.rs index 1e7a693..57e2770 100644 --- a/tests/e2e_observability.rs +++ b/tests/e2e_observability.rs @@ -54,6 +54,7 @@ fn tool_call_response(id: &str, name: &str, arguments: serde_json::Value) -> Mod finish_reason: Some("tool_calls".to_string()), raw: None, resolved_model: None, + continue_turn: None, } } @@ -69,6 +70,7 @@ fn text_response(text: &str) -> ModelResponse { finish_reason: Some("stop".to_string()), raw: None, resolved_model: None, + continue_turn: None, } } diff --git a/tests/e2e_reasoning_and_selection.rs b/tests/e2e_reasoning_and_selection.rs index 61c51b2..99d8f8c 100644 --- a/tests/e2e_reasoning_and_selection.rs +++ b/tests/e2e_reasoning_and_selection.rs @@ -45,6 +45,7 @@ fn text_response(text: &str) -> ModelResponse { finish_reason: Some("stop".into()), raw: None, resolved_model: None, + continue_turn: None, } } diff --git a/tests/e2e_subagents.rs b/tests/e2e_subagents.rs index 645699b..74fbe8f 100644 --- a/tests/e2e_subagents.rs +++ b/tests/e2e_subagents.rs @@ -46,6 +46,7 @@ fn tool_call_response(id: &str, name: &str, arguments: serde_json::Value) -> Mod finish_reason: Some("tool_calls".to_string()), raw: None, resolved_model: None, + continue_turn: None, } } @@ -62,6 +63,7 @@ fn text_response(text: &str) -> ModelResponse { finish_reason: Some("stop".to_string()), raw: None, resolved_model: None, + continue_turn: None, } } diff --git a/tests/e2e_tool_policy.rs b/tests/e2e_tool_policy.rs index d41f41f..aaf542d 100644 --- a/tests/e2e_tool_policy.rs +++ b/tests/e2e_tool_policy.rs @@ -48,6 +48,7 @@ fn tool_call_response(id: &str, name: &str, arguments: serde_json::Value) -> Mod finish_reason: Some("tool_calls".into()), raw: None, resolved_model: None, + continue_turn: None, } } @@ -63,6 +64,7 @@ fn text_response(text: &str) -> ModelResponse { finish_reason: Some("stop".into()), raw: None, resolved_model: None, + continue_turn: None, } } diff --git a/tests/e2e_unknown_tool_policy.rs b/tests/e2e_unknown_tool_policy.rs index 73a58d5..d26fd05 100644 --- a/tests/e2e_unknown_tool_policy.rs +++ b/tests/e2e_unknown_tool_policy.rs @@ -38,6 +38,7 @@ fn tool_call_response(id: &str, name: &str, arguments: serde_json::Value) -> Mod finish_reason: Some("tool_calls".into()), raw: None, resolved_model: None, + continue_turn: None, } } @@ -53,6 +54,7 @@ fn text_response(text: &str) -> ModelResponse { finish_reason: Some("stop".into()), raw: None, resolved_model: None, + continue_turn: None, } } diff --git a/tests/e2e_workspace_and_registry.rs b/tests/e2e_workspace_and_registry.rs index 683882f..4f19b6f 100644 --- a/tests/e2e_workspace_and_registry.rs +++ b/tests/e2e_workspace_and_registry.rs @@ -231,6 +231,7 @@ async fn harness_run_threads_workspace_and_enforces_out_of_root() { finish_reason: Some("tool_calls".into()), raw: None, resolved_model: None, + continue_turn: None, } } fn text_response(text: &str) -> ModelResponse { @@ -245,6 +246,7 @@ async fn harness_run_threads_workspace_and_enforces_out_of_root() { finish_reason: Some("stop".into()), raw: None, resolved_model: None, + continue_turn: None, } } diff --git a/tests/feature_harness_agent_loop.rs b/tests/feature_harness_agent_loop.rs index 6d56c47..7f49788 100644 --- a/tests/feature_harness_agent_loop.rs +++ b/tests/feature_harness_agent_loop.rs @@ -45,6 +45,7 @@ fn multi_tool_call_response(calls: Vec) -> ModelResponse { finish_reason: Some("tool_calls".into()), raw: None, resolved_model: None, + continue_turn: None, } } @@ -64,6 +65,7 @@ fn text_response(text: &str) -> ModelResponse { finish_reason: Some("stop".into()), raw: None, resolved_model: None, + continue_turn: None, } } diff --git a/tests/feature_harness_structured.rs b/tests/feature_harness_structured.rs index bebb6d7..3715105 100644 --- a/tests/feature_harness_structured.rs +++ b/tests/feature_harness_structured.rs @@ -61,6 +61,7 @@ fn tool_call_response(id: &str, name: &str, arguments: serde_json::Value) -> Mod finish_reason: Some("tool_calls".into()), raw: None, resolved_model: None, + continue_turn: None, } } @@ -161,6 +162,7 @@ async fn tool_call_strategy_reads_named_tool_arguments() { finish_reason: Some("tool_calls".into()), raw: None, resolved_model: None, + continue_turn: None, }; let output = extractor @@ -334,6 +336,7 @@ async fn provider_schema_reads_text_content_blocks() { finish_reason: Some("stop".into()), raw: None, resolved_model: None, + continue_turn: None, }; let parsed: Answer = extractor diff --git a/tests/harness_agent_loop.rs b/tests/harness_agent_loop.rs index 4abf86b..d2349c9 100644 --- a/tests/harness_agent_loop.rs +++ b/tests/harness_agent_loop.rs @@ -66,6 +66,7 @@ fn tool_call_response(id: &str, name: &str, arguments: serde_json::Value) -> Mod finish_reason: Some("tool_calls".to_string()), raw: None, resolved_model: None, + continue_turn: None, } } @@ -81,6 +82,7 @@ fn text_response(text: &str, input: u64, output: u64) -> ModelResponse { finish_reason: Some("stop".to_string()), raw: None, resolved_model: None, + continue_turn: None, } }