Skip to content

Commit bd8ed7a

Browse files
authored
fix: persist effective tool config for response continuations (vllm-project#87)
## Summary Fixes vllm-project#86. Persist response checkpoint metadata from the enriched request that was actually sent upstream, rather than the raw client request. This preserves inherited `tools`, `tool_choice`, and instructions after `previous_response_id` rehydration so later continuations do not lose model-visible state. This matches the response-store intent in `docs/adr/ADR-02_response_store.md`, which requires stored responses to preserve enough state to rehydrate the next turn, including effective tool configuration and tool-choice information. ## Test Plan - `cargo +stable test -p agentic-core` - `cargo +stable fmt --all --check` Added an integration regression test covering a three-checkpoint scenario: first turn defines a function tool, second turn inherits it via `previous_response_id`, and the persisted second response retains the inherited tool metadata for subsequent continuations. Signed-off-by: harivilasp <harivilasp@gmail.com>
1 parent bf5fe8b commit bd8ed7a

3 files changed

Lines changed: 86 additions & 6 deletions

File tree

crates/agentic-core/src/executor/modes/conversation.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,9 @@ impl ConversationHandler {
9292
let metadata = ResponseMetadata {
9393
model: ctx.enriched_request.model,
9494
previous_response_id: ctx.original_request.previous_response_id,
95-
effective_tools: ctx.original_request.tools,
96-
effective_tool_choice: ctx.original_request.tool_choice,
97-
effective_instructions: ctx.original_request.instructions,
95+
effective_tools: ctx.enriched_request.tools,
96+
effective_tool_choice: ctx.enriched_request.tool_choice,
97+
effective_instructions: ctx.enriched_request.instructions,
9898
};
9999

100100
let mut new_items = Vec::with_capacity(ctx.new_input_items.len() + output_items.len());

crates/agentic-core/src/executor/modes/response.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,9 @@ impl ResponseHandler {
7272
let metadata = ResponseMetadata {
7373
model: ctx.enriched_request.model,
7474
previous_response_id: ctx.original_request.previous_response_id,
75-
effective_tools: ctx.original_request.tools,
76-
effective_tool_choice: ctx.original_request.tool_choice,
77-
effective_instructions: ctx.original_request.instructions,
75+
effective_tools: ctx.enriched_request.tools,
76+
effective_tool_choice: ctx.enriched_request.tool_choice,
77+
effective_instructions: ctx.enriched_request.instructions,
7878
};
7979

8080
let mut new_items = Vec::with_capacity(ctx.new_input_items.len() + output_items.len());

crates/agentic-core/tests/stateful_responses_integration.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
mod support;
77

88
use agentic_core::executor::execute;
9+
use agentic_core::executor::request::RequestContext;
10+
use agentic_core::types::io::ToolChoice;
11+
use agentic_core::types::request_response::RequestPayload;
12+
use agentic_core::types::tools::{FunctionToolParam, NonEmptyToolName, ResponsesTool};
913
use std::sync::Arc;
1014
use support::{
1115
TestFixture, collect_stream, expected_text, load_cassette, make_request, output_text, request_input_texts,
@@ -247,6 +251,82 @@ async fn test_store_false_with_previous_response_id_hydrates_but_does_not_persis
247251
assert!(result.is_err(), "store=false response should not be persisted");
248252
}
249253

254+
#[tokio::test]
255+
async fn test_previous_response_id_persists_inherited_tools_and_choice() {
256+
let fixture =
257+
TestFixture::new_with_responses(vec![text_response("seed answer"), text_response("follow up answer")]).await;
258+
259+
let tool = ResponsesTool::Function(FunctionToolParam {
260+
name: NonEmptyToolName::try_from("lookup_weather").expect("valid tool name"),
261+
description: Some("Look up weather".to_string()),
262+
parameters: Some(serde_json::json!({
263+
"type": "object",
264+
"properties": {
265+
"city": {"type": "string"}
266+
}
267+
})),
268+
strict: Some(true),
269+
});
270+
271+
let mut first_request = make_request("seed", true, false, None, None);
272+
first_request.tools = Some(vec![tool]);
273+
first_request.tool_choice = ToolChoice::Required;
274+
275+
let p1 = unwrap_blocking(
276+
execute(first_request, Arc::clone(&fixture.exec_ctx))
277+
.await
278+
.expect("seed turn"),
279+
);
280+
281+
let mut second_request = make_request("follow up", true, false, Some(p1.id.clone()), None);
282+
second_request.tools = None;
283+
second_request.tool_choice = ToolChoice::Auto;
284+
285+
let p2 = unwrap_blocking(
286+
execute(second_request.clone(), Arc::clone(&fixture.exec_ctx))
287+
.await
288+
.expect("follow-up turn"),
289+
);
290+
291+
assert_eq!(output_text(&p2), "follow up answer");
292+
293+
let lookup_ctx = RequestContext {
294+
original_request: RequestPayload {
295+
previous_response_id: Some(p2.id.clone()),
296+
..second_request
297+
},
298+
enriched_request: RequestPayload {
299+
previous_response_id: Some(p2.id.clone()),
300+
..make_request("lookup", true, false, None, None)
301+
},
302+
new_input_items: vec![],
303+
response_id: "resp_lookup".into(),
304+
conversation_id: None,
305+
};
306+
307+
let stored = fixture
308+
.exec_ctx
309+
.resp_handler
310+
.get(&lookup_ctx)
311+
.await
312+
.expect("fetch persisted response");
313+
314+
assert_eq!(stored.metadata.model, "test-model");
315+
assert!(matches!(stored.metadata.effective_tool_choice, ToolChoice::Required));
316+
317+
let tools = stored.metadata.effective_tools.expect("expected persisted tools");
318+
assert_eq!(tools.len(), 1);
319+
match &tools[0] {
320+
ResponsesTool::Function(p) => {
321+
assert_eq!(p.name.as_str(), "lookup_weather");
322+
assert_eq!(p.description.as_deref(), Some("Look up weather"));
323+
assert_eq!(p.strict, Some(true));
324+
assert_eq!(p.parameters.as_ref().and_then(|v| v["type"].as_str()), Some("object"));
325+
}
326+
_ => panic!("expected function tool"),
327+
}
328+
}
329+
250330
#[tokio::test]
251331
async fn test_conversation_id_and_previous_response_id_are_rejected_together() {
252332
let fixture = TestFixture::new_with_responses(vec![]).await;

0 commit comments

Comments
 (0)