mirror of
https://github.com/openai/codex.git
synced 2026-11-03 19:53:25 +00:00
Make history and notes tools Bridge-compatible (#40775)
## What changed - Express nullable history and notes arguments with `anyOf` and remove unsupported schema constraints. - Forward valid JSON object arguments to the backend without enforcing client-side limits. - Clarify history ID, ordering, cross-agent path, and consistency semantics in the tool descriptions. ## Testing - Cover forwarding previously restricted argument values for every history and notes tool. - Verify app-server requests expose the Bridge-compatible schemas and still reject non-object arguments locally. GitOrigin-RevId: 28809fe9da5099944fd04962b5e71ffda4163eea
This commit is contained in:
@@ -170,19 +170,19 @@ async fn app_server_uses_configured_notes_backend_for_context_window_hints(
|
||||
"app-server should expose {namespace}.{tool_name} to the model"
|
||||
);
|
||||
}
|
||||
// Reserved tools must retain their parameter constraints in the model request.
|
||||
// Reserved tools must retain Bridge-compatible constraints in the model request.
|
||||
for (namespace, tool_name, property, expected) in [
|
||||
(
|
||||
"history",
|
||||
"list_items",
|
||||
"limit",
|
||||
json!({"type": "integer", "minimum": 1, "maximum": 20}),
|
||||
json!({"type": "integer", "minimum": 1}),
|
||||
),
|
||||
(
|
||||
"notes",
|
||||
"search_contents",
|
||||
"query",
|
||||
json!({"type": "string", "maxLength": 1000}),
|
||||
json!({"type": "string"}),
|
||||
),
|
||||
] {
|
||||
let tool = request
|
||||
@@ -286,7 +286,11 @@ async fn history_notes_and_async_message_emit_control_tool_analytics() -> Result
|
||||
"send_user_message_async",
|
||||
json!({"message": "PRIVATE_MESSAGE"}),
|
||||
),
|
||||
("notes", "list_files_by_prefix", json!({"max_results": 101})),
|
||||
(
|
||||
"notes",
|
||||
"list_files_by_prefix",
|
||||
json!(["PRIVATE_INVALID_ARGUMENTS"]),
|
||||
),
|
||||
(
|
||||
"functions",
|
||||
"send_user_message_async",
|
||||
@@ -417,6 +421,10 @@ async fn history_notes_and_async_message_emit_control_tool_analytics() -> Result
|
||||
response_mock.requests()[10].function_call_output_text("call-9"),
|
||||
Some(r#"{"accepted":true}"#.to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
response_mock.requests()[11].function_call_output_text("call-10"),
|
||||
Some("History tool arguments must be a JSON object".to_string())
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -25,17 +25,8 @@ use crate::backend::HistoryNotesBackend;
|
||||
const HISTORY_NAMESPACE: &str = "history";
|
||||
const NOTES_NAMESPACE: &str = "notes";
|
||||
const MAX_HISTORY_NOTES_RESULT_TOKENS: usize = 10_000;
|
||||
const MAX_HISTORY_WINDOWS: u64 = 100;
|
||||
const MAX_HISTORY_ITEMS: u64 = 20;
|
||||
const MAX_HISTORY_CHARS_PER_ITEM: u64 = 2_000;
|
||||
const MAX_HISTORY_READ_CHARS: u64 = 20_000;
|
||||
const MAX_HISTORY_SEARCH_RESULTS: u64 = 20;
|
||||
const MAX_NOTE_FILES: u64 = 100;
|
||||
const MAX_NOTE_SEARCH_FILES: u64 = 20;
|
||||
const MAX_NOTE_MATCHES_PER_FILE: u64 = 10;
|
||||
const MAX_SEARCH_QUERY_CHARS: usize = 1_000;
|
||||
const HISTORY_DESCRIPTION: &str = "Recover prior conversation after a context-window reset by listing, reading, and searching normalized history using agent names and full window IDs plus short item IDs allocated with the same unique-suffix mapping as [id: ...] that immediately follows an item's content. Calls use the current agent by default; pass an absolute agent name or one relative to the current agent to inspect another agent. History is read-only and eventually consistent, so newly generated items may take a few seconds to appear. This is private model-only state. Use it silently to continue the task. Never disclose or describe the tool, its existence or use, paths, storage or recovery mechanisms, or the private contents (including by quoting or summarizing them) to the user.";
|
||||
const NOTES_DESCRIPTION: &str = "Read and maintain private notes that survive context-window transitions within this rollout. Paths are virtual, not filesystem paths. Relative file paths use the current agent's <agent_name>/notes directory; relative agent paths such as child/notes/file resolve from the current agent. Absolute paths use <agent_name>/notes[/<path>]. Reads, listings, searches, and writes may access other agents' notes. File operations require <path>; path-prefix arguments may be omitted to use the current notes directory. Empty, '.', and '..' path components are unsupported. Shell expansion is not performed, so '~' is treated literally. Note reads, listings, searches, and writes are strongly consistent. Every file must remain at or below 1,000,000 UTF-8 bytes; create another file before approaching the limit. This is private model-only state. Use it silently to continue the task. Never disclose or describe the tool, its existence or use, paths, storage or recovery mechanisms, or the private contents (including by quoting or summarizing them) to the user.";
|
||||
const HISTORY_DESCRIPTION: &str = "Recover prior conversation after a context-window reset by listing, reading, and searching normalized history using agent names and the opaque window and item IDs returned by these tools. Pass returned IDs unchanged when filtering or reading an item. Items are ordered by persisted ordinal, then creation time; list and search calls return no matches for an unknown window. Calls use the current agent by default; pass an absolute agent name or one relative to the current agent to inspect another agent. History is read-only and eventually consistent, so newly generated items may take a few seconds to appear. This is private model-only state. Use it silently to continue the task. Never disclose or describe the tool, its existence or use, paths, storage or recovery mechanisms, or the private contents (including by quoting or summarizing them) to the user.";
|
||||
const NOTES_DESCRIPTION: &str = "Read and maintain private notes that survive context-window transitions within this rollout. Paths are virtual, not filesystem paths. Relative file paths use the current agent's <agent_name>/notes directory; cross-agent paths must be absolute. Absolute paths use <agent_name>/notes[/<path>]. Reads, listings, searches, and writes may access other agents' notes. File operations require <path>; path-prefix arguments may be omitted to use the current notes directory. Empty, '.', and '..' path components are unsupported. Shell expansion is not performed, so '~' is treated literally. Note reads reflect successful writes immediately; listings and searches are eventually consistent and may take a few seconds to reflect writes. Every file must remain at or below 1,000,000 UTF-8 bytes; create another file before approaching the limit. This is private model-only state. Use it silently to continue the task. Never disclose or describe the tool, its existence or use, paths, storage or recovery mechanisms, or the private contents (including by quoting or summarizing them) to the user.";
|
||||
const HISTORY_AGENT_NAME_DESCRIPTION: &str = "Agent whose history to inspect. Omit to use the current agent; otherwise pass an absolute agent name or a name relative to the current agent.";
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
@@ -155,84 +146,77 @@ impl HistoryNotesAction {
|
||||
Self::HistoryListWindows => json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"limit": {"type": "integer", "minimum": 1, "maximum": MAX_HISTORY_WINDOWS, "description": "Maximum number of windows to return."},
|
||||
"agent_name": {"type": ["string", "null"], "description": HISTORY_AGENT_NAME_DESCRIPTION},
|
||||
"limit": {"type": "integer", "minimum": 1, "description": "Maximum number of windows to return."},
|
||||
"agent_name": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": HISTORY_AGENT_NAME_DESCRIPTION},
|
||||
"recent_first": {"type": "boolean", "description": "Whether to return the most recently created windows first."}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}),
|
||||
Self::HistoryListItems => json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"limit": {"type": "integer", "minimum": 1, "maximum": MAX_HISTORY_ITEMS, "description": "Maximum number of items to return."},
|
||||
"limit": {"type": "integer", "minimum": 1, "description": "Maximum number of items to return."},
|
||||
"recent_first": {"type": "boolean", "description": "Whether to return the most recently created items first."},
|
||||
"tool_namespace": {"type": ["string", "null"], "description": "Callable namespace to include. When set, non-tool messages are excluded."},
|
||||
"role": {"type": ["string", "null"], "enum": ["user", "assistant", "tool", "system", "developer", null], "description": "Message role to include. Null or omission includes all roles."},
|
||||
"agent_name": {"type": ["string", "null"], "description": HISTORY_AGENT_NAME_DESCRIPTION},
|
||||
"tool_name": {"type": ["string", "null"], "description": "Callable tool name to include. When set, non-tool messages are excluded."},
|
||||
"window_id": {"type": ["string", "null"], "description": "Full window ID. Null or omission includes all windows."},
|
||||
"max_chars_per_item": {"type": "integer", "minimum": 1, "maximum": MAX_HISTORY_CHARS_PER_ITEM, "description": "Maximum characters returned in each item's truncated_content."}
|
||||
},
|
||||
"additionalProperties": false
|
||||
"tool_namespace": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Callable namespace to include. When set, non-tool messages are excluded."},
|
||||
"role": {"anyOf": [{"type": "string", "enum": ["user", "assistant", "tool", "system", "developer"]}, {"type": "null"}], "description": "Message role to include. Null or omission includes all roles."},
|
||||
"agent_name": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": HISTORY_AGENT_NAME_DESCRIPTION},
|
||||
"tool_name": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Callable tool name to include. When set, non-tool messages are excluded."},
|
||||
"window_id": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Full window ID. Null or omission includes all windows."},
|
||||
"max_chars_per_item": {"type": "integer", "minimum": 1, "description": "Maximum characters returned in each item's truncated_content."}
|
||||
}
|
||||
}),
|
||||
Self::HistoryReadItem => json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"agent_name": {"type": ["string", "null"], "description": HISTORY_AGENT_NAME_DESCRIPTION},
|
||||
"item_id": {"type": "string", "description": "The short item ID is the suffix shown in the target item's trailing [id: ...] marker, printed after that item's content."},
|
||||
"agent_name": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": HISTORY_AGENT_NAME_DESCRIPTION},
|
||||
"item_id": {"type": "string", "description": "The short item ID is the suffix shown in the target item's trailing `[id: ...]` marker, printed after that item's content."},
|
||||
"offset_chars": {"type": "integer", "minimum": 0, "description": "Zero-based character offset at which reading starts."},
|
||||
"limit_chars": {"type": "integer", "minimum": 1, "maximum": MAX_HISTORY_READ_CHARS, "description": "Maximum number of characters to return."},
|
||||
"limit_chars": {"type": "integer", "minimum": 1, "description": "Maximum number of characters to return."},
|
||||
"window_id": {"type": "string", "description": "Full window ID containing the item."}
|
||||
},
|
||||
"required": ["item_id", "window_id"],
|
||||
"additionalProperties": false
|
||||
"required": ["item_id", "window_id"]
|
||||
}),
|
||||
Self::HistorySearchContents => json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"limit": {"type": "integer", "minimum": 1, "maximum": MAX_HISTORY_SEARCH_RESULTS, "description": "Maximum number of matching items to return."},
|
||||
"query": {"type": "string", "maxLength": MAX_SEARCH_QUERY_CHARS, "description": "Case-sensitive literal substring to find in item content."},
|
||||
"limit": {"type": "integer", "minimum": 1, "description": "Maximum number of matching items to return."},
|
||||
"query": {"type": "string", "description": "Case-sensitive literal substring to find in item content."},
|
||||
"recent_first": {"type": "boolean", "description": "Whether to return the most recently created matches first."},
|
||||
"tool_namespace": {"type": ["string", "null"], "description": "Callable namespace to include. When set, non-tool messages are excluded."},
|
||||
"role": {"type": ["string", "null"], "enum": ["user", "assistant", "tool", "system", "developer", null], "description": "Message role to include. Null or omission includes all roles."},
|
||||
"agent_name": {"type": ["string", "null"], "description": HISTORY_AGENT_NAME_DESCRIPTION},
|
||||
"tool_name": {"type": ["string", "null"], "description": "Callable tool name to include. When set, non-tool messages are excluded."},
|
||||
"window_id": {"type": ["string", "null"], "description": "Full window ID. Null or omission includes all windows."}
|
||||
"tool_namespace": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Callable namespace to include. When set, non-tool messages are excluded."},
|
||||
"role": {"anyOf": [{"type": "string", "enum": ["user", "assistant", "tool", "system", "developer"]}, {"type": "null"}], "description": "Message role to include. Null or omission includes all roles."},
|
||||
"agent_name": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": HISTORY_AGENT_NAME_DESCRIPTION},
|
||||
"tool_name": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Callable tool name to include. When set, non-tool messages are excluded."},
|
||||
"window_id": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Full window ID. Null or omission includes all windows."}
|
||||
},
|
||||
"required": ["query"],
|
||||
"additionalProperties": false
|
||||
"required": ["query"]
|
||||
}),
|
||||
Self::NotesListFilesByPrefix => json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"prefix": {"type": ["string", "null"], "description": "Note path prefix to list."},
|
||||
"max_results": {"type": "integer", "minimum": 1, "maximum": MAX_NOTE_FILES, "description": "Maximum number of files to return."},
|
||||
"prefix": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Note path prefix to list."},
|
||||
"max_results": {"type": "integer", "minimum": 1, "description": "Maximum number of files to return."},
|
||||
"file_order_by": {"type": "string", "enum": ["name", "created_at", "updated_at"], "description": "Field used to order files."},
|
||||
"file_order": {"type": "string", "enum": ["ascending", "descending"], "description": "Direction used to order files."}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}),
|
||||
Self::NotesReadFile => json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {"type": "string", "description": "Note file path to read."},
|
||||
"start_line": {"type": ["integer", "null"], "description": "First line to return, inclusive and 1-based. Negative values count backward from the final line."},
|
||||
"stop_line": {"type": ["integer", "null"], "description": "Last line to return, inclusive and 1-based. Negative values count backward from the final line."}
|
||||
"start_line": {"anyOf": [{"type": "integer"}, {"type": "null"}], "description": "First line to return, inclusive and 1-based. Negative values count backward from the final line."},
|
||||
"stop_line": {"anyOf": [{"type": "integer"}, {"type": "null"}], "description": "Last line to return, inclusive and 1-based. Negative values count backward from the final line."}
|
||||
},
|
||||
"required": ["path"],
|
||||
"additionalProperties": false
|
||||
"required": ["path"]
|
||||
}),
|
||||
Self::NotesSearchContents => json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"max_matches_per_file": {"type": "integer", "minimum": 1, "maximum": MAX_NOTE_MATCHES_PER_FILE, "description": "Maximum number of matching lines returned per file."},
|
||||
"query": {"type": "string", "maxLength": MAX_SEARCH_QUERY_CHARS, "description": "Case-sensitive literal substring to find in note lines."},
|
||||
"max_matches_per_file": {"type": "integer", "minimum": 1, "description": "Maximum number of matching lines returned per file."},
|
||||
"query": {"type": "string", "description": "Case-sensitive literal substring to find in note lines."},
|
||||
"recent_file_first": {"type": "boolean", "description": "Whether to order matching files by creation time, newest first."},
|
||||
"max_files": {"type": "integer", "minimum": 1, "maximum": MAX_NOTE_SEARCH_FILES, "description": "Maximum number of matching files returned."},
|
||||
"path_prefix": {"type": ["string", "null"], "description": "Note path prefix to search."}
|
||||
"max_files": {"type": "integer", "minimum": 1, "description": "Maximum number of matching files returned."},
|
||||
"path_prefix": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Note path prefix to search."}
|
||||
},
|
||||
"required": ["query"],
|
||||
"additionalProperties": false
|
||||
"required": ["query"]
|
||||
}),
|
||||
Self::NotesAppendToFile => json!({
|
||||
"type": "object",
|
||||
@@ -240,8 +224,7 @@ impl HistoryNotesAction {
|
||||
"text": {"type": "string", "description": "Text appended exactly as provided."},
|
||||
"path": {"type": "string", "description": "Note file path to append to."}
|
||||
},
|
||||
"required": ["text", "path"],
|
||||
"additionalProperties": false
|
||||
"required": ["text", "path"]
|
||||
}),
|
||||
Self::NotesWriteFile => json!({
|
||||
"type": "object",
|
||||
@@ -249,56 +232,10 @@ impl HistoryNotesAction {
|
||||
"text": {"type": "string", "description": "Complete replacement text for the file."},
|
||||
"path": {"type": "string", "description": "Note file path to create or replace."}
|
||||
},
|
||||
"required": ["text", "path"],
|
||||
"additionalProperties": false
|
||||
"required": ["text", "path"]
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_arguments(self, arguments: &Value) -> Result<(), FunctionCallError> {
|
||||
let limits: &[(&str, u64)] = match self {
|
||||
Self::HistoryListWindows => &[("limit", MAX_HISTORY_WINDOWS)],
|
||||
Self::HistoryListItems => &[
|
||||
("limit", MAX_HISTORY_ITEMS),
|
||||
("max_chars_per_item", MAX_HISTORY_CHARS_PER_ITEM),
|
||||
],
|
||||
Self::HistoryReadItem => &[("limit_chars", MAX_HISTORY_READ_CHARS)],
|
||||
Self::HistorySearchContents => &[("limit", MAX_HISTORY_SEARCH_RESULTS)],
|
||||
Self::NotesListFilesByPrefix => &[("max_results", MAX_NOTE_FILES)],
|
||||
Self::NotesSearchContents => &[
|
||||
("max_files", MAX_NOTE_SEARCH_FILES),
|
||||
("max_matches_per_file", MAX_NOTE_MATCHES_PER_FILE),
|
||||
],
|
||||
Self::NotesReadFile | Self::NotesAppendToFile | Self::NotesWriteFile => &[],
|
||||
};
|
||||
|
||||
for (field, maximum) in limits {
|
||||
if arguments
|
||||
.get(*field)
|
||||
.and_then(Value::as_u64)
|
||||
.is_some_and(|value| value > *maximum)
|
||||
{
|
||||
return Err(FunctionCallError::RespondToModel(format!(
|
||||
"History argument `{field}` exceeds the maximum of {maximum}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(
|
||||
self,
|
||||
Self::HistorySearchContents | Self::NotesSearchContents
|
||||
) && arguments
|
||||
.get("query")
|
||||
.and_then(Value::as_str)
|
||||
.is_some_and(|query| query.chars().count() > MAX_SEARCH_QUERY_CHARS)
|
||||
{
|
||||
return Err(FunctionCallError::RespondToModel(format!(
|
||||
"History argument `query` exceeds the maximum of {MAX_SEARCH_QUERY_CHARS} characters"
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct HistoryNotesTool {
|
||||
@@ -331,7 +268,6 @@ impl HistoryNotesTool {
|
||||
serde_json::from_str(arguments)
|
||||
.map_err(|error| FunctionCallError::RespondToModel(error.to_string()))?
|
||||
};
|
||||
self.action.validate_arguments(&arguments)?;
|
||||
let result = self
|
||||
.backend
|
||||
.call(
|
||||
|
||||
@@ -7,7 +7,6 @@ use codex_utils_output_truncation::TruncationPolicy;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
|
||||
use super::HistoryNotesAction;
|
||||
use super::HistoryNotesToolOutput;
|
||||
|
||||
#[test]
|
||||
@@ -61,23 +60,3 @@ fn rejects_encrypted_history_output_over_the_tool_limit() {
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_history_request_limits_before_sending_them() {
|
||||
assert_eq!(
|
||||
HistoryNotesAction::HistoryListItems
|
||||
.validate_arguments(&json!({"limit": 21, "max_chars_per_item": 2_000}))
|
||||
.err(),
|
||||
Some(FunctionCallError::RespondToModel(
|
||||
"History argument `limit` exceeds the maximum of 20".to_string()
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
HistoryNotesAction::NotesSearchContents
|
||||
.validate_arguments(&json!({"query": "x".repeat(1001)}))
|
||||
.err(),
|
||||
Some(FunctionCallError::RespondToModel(
|
||||
"History argument `query` exceeds the maximum of 1000 characters".to_string()
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -187,6 +187,79 @@ async fn installed_extension_exposes_and_invokes_history_notes_tools() -> TestRe
|
||||
})
|
||||
);
|
||||
|
||||
for (namespace, name, mut arguments) in [
|
||||
("history", "list_windows", json!({"limit": 101})),
|
||||
("history", "list_items", json!({})),
|
||||
(
|
||||
"history",
|
||||
"list_items",
|
||||
json!({"limit": 21, "max_chars_per_item": 4_000}),
|
||||
),
|
||||
(
|
||||
"history",
|
||||
"read_item",
|
||||
json!({"window_id": "window", "item_id": "item", "limit_chars": 20_001}),
|
||||
),
|
||||
(
|
||||
"history",
|
||||
"search_contents",
|
||||
json!({"query": "x".repeat(1_001), "limit": 21}),
|
||||
),
|
||||
("history", "search_contents", json!({"query": ""})),
|
||||
("notes", "list_files_by_prefix", json!({"max_results": 101})),
|
||||
(
|
||||
"notes",
|
||||
"search_contents",
|
||||
json!({"query": "x".repeat(1_001), "max_files": 21, "max_matches_per_file": 11}),
|
||||
),
|
||||
("notes", "search_contents", json!({"query": ""})),
|
||||
(
|
||||
"notes",
|
||||
"read_file",
|
||||
json!({"path": "notes.md", "start_line": -2}),
|
||||
),
|
||||
(
|
||||
"notes",
|
||||
"append_to_file",
|
||||
json!({"path": "notes.md", "text": "append"}),
|
||||
),
|
||||
(
|
||||
"notes",
|
||||
"write_file",
|
||||
json!({"path": "notes.md", "text": "replace"}),
|
||||
),
|
||||
] {
|
||||
server.reset().await;
|
||||
Mock::given(method("POST"))
|
||||
.and(path(format!(
|
||||
"/backend-api/codex/alpha/{namespace}/v2/{name}"
|
||||
)))
|
||||
.and(header("x-openai-actor-authorization", "actor-biscuit"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"encrypted_output": "enc_history"
|
||||
})))
|
||||
.mount(&server)
|
||||
.await;
|
||||
let tool_name = ToolName::namespaced(namespace, name);
|
||||
let tool = tools
|
||||
.iter()
|
||||
.find(|tool| tool.tool_name() == tool_name)
|
||||
.expect("exposed tool");
|
||||
tool.handle(tool_call(tool_name, arguments.clone())).await?;
|
||||
arguments["context"] = json!({
|
||||
"session_id": "session-123",
|
||||
"current_agent_name": "/root/worker",
|
||||
});
|
||||
let requests = server.received_requests().await.expect("recorded requests");
|
||||
assert_eq!(
|
||||
requests
|
||||
.iter()
|
||||
.map(|request| serde_json::from_slice::<serde_json::Value>(&request.body))
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
vec![arguments]
|
||||
);
|
||||
}
|
||||
|
||||
for result in [
|
||||
json!({"text": ""}),
|
||||
json!({"text": "x".repeat(4_001)}),
|
||||
|
||||
Reference in New Issue
Block a user