diff --git a/e2e/config/pr-comment-scenarios.json b/e2e/config/pr-comment-scenarios.json index 30ada879e..407d03fc2 100644 --- a/e2e/config/pr-comment-scenarios.json +++ b/e2e/config/pr-comment-scenarios.json @@ -224,6 +224,12 @@ { "variantKey": "flue-v1-0-0-beta-3", "label": "v1.0.0-beta.3" } ] }, + { + "scenarioDirName": "eve-instrumentation", + "label": "Eve Instrumentation", + "metadataScenario": "eve-instrumentation", + "variants": [{ "variantKey": "eve-v0-20-0", "label": "v0.20.0" }] + }, { "scenarioDirName": "github-copilot-instrumentation", "label": "GitHub Copilot SDK Instrumentation", diff --git a/e2e/scenarios/eve-instrumentation/__cassettes__/eve-v0-20-0.cassette.json b/e2e/scenarios/eve-instrumentation/__cassettes__/eve-v0-20-0.cassette.json new file mode 100644 index 000000000..3e0f496b7 --- /dev/null +++ b/e2e/scenarios/eve-instrumentation/__cassettes__/eve-v0-20-0.cassette.json @@ -0,0 +1,1274 @@ +{ + "entries": [ + { + "callIndex": 0, + "id": "63740c316833eadb", + "matchKey": "POST api.openai.com/v1/responses", + "recordedAt": "2026-07-06T12:42:40.161Z", + "request": { + "body": { + "kind": "json", + "value": { + "include": ["web_search_call.action.sources"], + "input": [ + { + "content": "Instructions (instructions)\nYou are a deterministic fixture agent for Braintrust Eve instrumentation tests.\n\nFor every user task:\n\n1. Call the search tool exactly once with the full user message as the query.\n2. After the search result is available, call the read tool exactly once with the\n URL returned by search.\n3. After the read result is available, answer with a single sentence that starts\n with \"Final answer from read:\" and includes the search title, URL, and read\n excerpt.\n\nTool execution\nA single tool or subagent call runs as one serial action. If you call multiple independent tools or subagents in one response, eve treats that batch as parallel work. Only batch work that is independent and does not rely on another call in the same response.", + "role": "developer" + }, + { + "content": [ + { + "text": "Run the Braintrust Eve instrumentation e2e scenario", + "type": "input_text" + } + ], + "role": "user" + } + ], + "model": "gpt-5.4-mini", + "stream": true, + "tool_choice": "auto", + "tools": [ + { + "description": "Ask the user a question and wait for their response before continuing. Use this when you need clarification or a choice from the user.", + "name": "ask_question", + "parameters": { + "additionalProperties": false, + "properties": { + "allowFreeform": { + "description": "Whether the user may answer with freeform text instead of selecting one of the provided options.", + "type": "boolean" + }, + "options": { + "description": "Selectable answer options to present to the user.", + "items": { + "additionalProperties": false, + "properties": { + "description": { + "description": "Optional additional context for this option.", + "type": "string" + }, + "id": { + "description": "Stable identifier for the option.", + "type": "string" + }, + "label": { + "description": "User-facing label for the option.", + "type": "string" + }, + "style": { + "description": "Visual treatment hint for the option.", + "enum": ["primary", "danger", "default"], + "type": "string" + } + }, + "required": ["id", "label"], + "type": "object" + }, + "type": "array" + }, + "prompt": { + "description": "The prompt to present to the user.", + "type": "string" + } + }, + "required": ["prompt"], + "type": "object" + }, + "type": "function" + }, + { + "description": "Execute a shell command in the shared workspace environment.", + "name": "bash", + "parameters": { + "additionalProperties": false, + "properties": { + "command": { + "description": "The shell command to execute.", + "type": "string" + } + }, + "required": ["command"], + "type": "object" + }, + "type": "function" + }, + { + "description": "Fast file pattern matching tool that works with any codebase size.\n\nUsage:\n- Supports glob patterns like \"**/*.js\" or \"src/**/*.ts\".\n- Returns matching file paths.\n- Use this tool when you need to find files by name patterns.\n- If you are unsure of the correct file path, use the glob tool to look up filenames by glob pattern.\n- Use the grep tool instead if you need to search file contents.\n- Call this tool in parallel when you know there are multiple patterns to search for.", + "name": "glob", + "parameters": { + "additionalProperties": false, + "properties": { + "limit": { + "description": "Maximum number of results to return. Defaults to 100.", + "maximum": 1000, + "minimum": 1, + "type": "integer" + }, + "path": { + "description": "The directory to search in. Defaults to /workspace. Must be an absolute path. Omit to use the default.", + "type": "string" + }, + "pattern": { + "description": "The glob pattern to match files against (e.g. \"**/*.ts\", \"src/**/*.js\").", + "type": "string" + } + }, + "required": ["pattern"], + "type": "object" + }, + "type": "function" + }, + { + "description": "Fast content search tool that works with any codebase size.\n\nUsage:\n- Searches file contents using regular expressions.\n- Supports full regex syntax (e.g. \"log.*Error\", \"function\\s+\\w+\").\n- Filter files by pattern with the glob parameter (e.g. \"*.js\", \"*.{ts,tsx}\").\n- Returns matching lines with file paths and line numbers.\n- Use this tool when you need to find files containing specific patterns.\n- Use the glob tool instead if you only need to find files by name.\n- Call this tool in parallel when you have multiple independent searches.\n- Any line longer than 2000 characters is truncated.", + "name": "grep", + "parameters": { + "additionalProperties": false, + "properties": { + "context": { + "description": "Number of surrounding context lines to include before and after each match. Defaults to 0.", + "minimum": 0, + "type": "integer" + }, + "glob": { + "description": "Filter files by glob pattern (e.g. \"*.ts\", \"*.{ts,tsx}\").", + "type": "string" + }, + "ignoreCase": { + "description": "Perform case-insensitive search. Defaults to false.", + "type": "boolean" + }, + "limit": { + "description": "Maximum number of matches to return per file. Defaults to 100.", + "maximum": 1000, + "minimum": 1, + "type": "integer" + }, + "literal": { + "description": "Treat the pattern as a literal string instead of a regular expression. Defaults to false.", + "type": "boolean" + }, + "path": { + "description": "The directory or file to search in. Defaults to /workspace. Must be an absolute path. Omit to use the default.", + "type": "string" + }, + "pattern": { + "description": "The regex pattern to search for in file contents (e.g. \"log.*Error\", \"function\\s+\\w+\").", + "type": "string" + } + }, + "required": ["pattern"], + "type": "object" + }, + "type": "function" + }, + { + "description": "Read a file from the local filesystem. If the path does not exist, an error is returned.\n\nUsage:\n- The filePath parameter should be an absolute path.\n- By default, this tool returns up to 2000 lines from the start of the file.\n- The offset parameter is the line number to start from (1-indexed).\n- To read later sections, call this tool again with a larger offset.\n- Use the grep tool to find specific content in large files or files with long lines.\n- If you are unsure of the correct file path, use the glob tool to look up filenames by glob pattern.\n- Contents are returned with each line prefixed by its line number as `: `. For example, if a file has contents \"foo\\n\", you will receive \"1: foo\\n\".\n- Any line longer than 2000 characters is truncated.\n- Call this tool in parallel when you know there are multiple files you want to read.\n- Avoid tiny repeated slices (30 line chunks). If you need more context, read a larger window.", + "name": "read_file", + "parameters": { + "additionalProperties": false, + "properties": { + "filePath": { + "description": "The absolute path to the file to read.", + "type": "string" + }, + "limit": { + "description": "Maximum number of lines to return. Defaults to 2000.", + "minimum": 1, + "type": "integer" + }, + "offset": { + "description": "1-based line number to start from. Defaults to 1.", + "minimum": 1, + "type": "integer" + } + }, + "required": ["filePath"], + "type": "object" + }, + "type": "function" + }, + { + "description": "Writes a file to the local filesystem.\n\nUsage:\n- This tool will overwrite the existing file if there is one at the provided path.\n- If this is an existing file, you MUST use the read_file tool first to read the file's contents. This tool will fail if you did not read the file first.\n- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.\n- NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.\n- Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked.", + "name": "write_file", + "parameters": { + "additionalProperties": false, + "properties": { + "content": { + "description": "Complete replacement file contents.", + "type": "string" + }, + "filePath": { + "description": "The absolute path to the file to write (must be absolute, not relative).", + "type": "string" + } + }, + "required": ["filePath", "content"], + "type": "object" + }, + "type": "function" + }, + { + "description": "Use this tool to create and manage a structured task list for the current session.\nThis helps you track progress, organize complex tasks, and demonstrate thoroughness.\n\nWhen to use:\n- Complex multistep tasks requiring 3 or more distinct steps\n- When the user provides multiple tasks or a numbered list\n- After receiving new instructions, to capture requirements\n- After completing a task, to mark it complete and add follow-ups\n\nWhen NOT to use:\n- Single, straightforward tasks that need no tracking\n- Purely conversational or informational requests\n\nUsage:\n- Call with `todos` to replace the entire list (full replacement write)\n- Call without `todos` to read the current list\n- Both return the full current list with status counts\n- Mark tasks in_progress when you start, completed when done\n- Only have ONE task in_progress at a time", + "name": "todo", + "parameters": { + "additionalProperties": false, + "properties": { + "todos": { + "description": "The updated todo list. Omit to read the current list without modifying it.", + "items": { + "additionalProperties": false, + "properties": { + "content": { + "description": "Brief description of the task.", + "type": "string" + }, + "priority": { + "description": "Priority level of the task.", + "enum": ["high", "medium", "low"], + "type": "string" + }, + "status": { + "description": "Current status of the task.", + "enum": [ + "pending", + "in_progress", + "completed", + "cancelled" + ], + "type": "string" + } + }, + "required": ["content", "status", "priority"], + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + }, + "type": "function" + }, + { + "description": "Fetch a webpage and return its content in the requested format. Use this to retrieve and analyze content from URLs.\n\nUsage notes:\n- The URL must be a fully-formed valid URL starting with http:// or https://\n- HTML responses are automatically converted to markdown or plain text based on the requested format\n- Format options: \"markdown\" (default), \"text\", or \"html\"\n- Default timeout is 30 seconds (max 120 seconds)\n- Maximum response size is 5 MB; content is further capped at the shared tool-output budget (50 KB / 2000 lines)\n- This tool is read-only and does not modify any files", + "name": "web_fetch", + "parameters": { + "additionalProperties": false, + "properties": { + "format": { + "description": "The format to return the content in (text, markdown, or html). HTML responses are automatically converted to the requested format. Defaults to \"markdown\".", + "enum": ["markdown", "text", "html"], + "type": "string" + }, + "timeout": { + "description": "Optional timeout in seconds. Defaults to 30, max 120.", + "type": "number" + }, + "url": { + "description": "The fully-formed URL to fetch content from. Must start with http:// or https://.", + "type": "string" + } + }, + "required": ["url"], + "type": "object" + }, + "type": "function" + }, + { + "type": "web_search" + }, + { + "description": "Load the full instructions for one available skill by name or id. Use this tool when the request clearly matches a listed skill description or when the user explicitly asks for that skill. This is not for MCP connections; use connection_search to access an installed connection. Loading adds the skill instructions to the current turn. Choose the \"skill\" value from the Available skills block.", + "name": "load_skill", + "parameters": { + "additionalProperties": false, + "properties": { + "skill": { + "description": "Available skill name or id.", + "type": "string" + } + }, + "required": ["skill"], + "type": "object" + }, + "type": "function" + }, + { + "description": "Reads a deterministic Eve documentation page fixture.", + "name": "read", + "parameters": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "url": { + "type": "string" + } + }, + "required": ["url"], + "type": "object" + }, + "type": "function" + }, + { + "description": "Searches the Eve instrumentation documentation fixture.", + "name": "search", + "parameters": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "query": { + "type": "string" + } + }, + "required": ["query"], + "type": "object" + }, + "type": "function" + }, + { + "description": "Delegate a focused subtask to a fresh copy of yourself. Use it to isolate complex work or split a large task into independent pieces. Issue multiple `agent` calls in one response to run a small fixed set in parallel. Each child has fresh history and state but shares your tools and sandbox, so include essential context in `message` and give parallel writers non-overlapping scopes.", + "name": "agent", + "parameters": { + "additionalProperties": false, + "properties": { + "message": { + "description": "The message to send to the subagent. Provide all context the subagent needs to complete the task; the subagent does not see the parent's history.", + "type": "string" + }, + "outputSchema": { + "description": "When provided, the subagent runs in task mode and must produce structured output matching this JSON Schema. The structured output becomes the tool result.", + "type": "object" + } + }, + "required": ["message"], + "type": "object" + }, + "type": "function" + } + ] + } + }, + "headers": {}, + "method": "POST", + "url": "https://api.openai.com/v1/responses" + }, + "response": { + "body": { + "chunks": [ + "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_0fea08d8f9bfc463006a4ba2bed894819f941441a0e61d48b8\",\"object\":\"response\",\"created_at\":1783341758,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-mini-2026-03-17\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":true,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Ask the user a question and wait for their response before continuing. Use this when you need clarification or a choice from the user.\",\"name\":\"ask_question\",\"parameters\":{\"type\":\"object\",\"properties\":{\"allowFreeform\":{\"type\":\"boolean\",\"description\":\"Whether the user may answer with freeform text instead of selecting one of the provided options.\"},\"options\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Optional additional context for this option.\"},\"id\":{\"type\":\"string\",\"description\":\"Stable identifier for the option.\"},\"label\":{\"type\":\"string\",\"description\":\"User-facing label for the option.\"},\"style\":{\"type\":\"string\",\"enum\":[\"primary\",\"danger\",\"default\"],\"description\":\"Visual treatment hint for the option.\"}},\"required\":[\"description\",\"id\",\"label\",\"style\"],\"additionalProperties\":false},\"description\":\"Selectable answer options to present to the user.\"},\"prompt\":{\"type\":\"string\",\"description\":\"The prompt to present to the user.\"}},\"required\":[\"allowFreeform\",\"options\",\"prompt\"],\"additionalProperties\":false},\"strict\":true},{\"type\":\"function\",\"description\":\"Execute a shell command in the shared workspace environment.\",\"name\":\"bash\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The shell command to execute.\",\"type\":\"string\"}},\"required\":[\"command\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Fast file pattern matching tool that works with any codebase size.\\n\\nUsage:\\n- Supports glob patterns like \\\"**/*.js\\\" or \\\"src/**/*.ts\\\".\\n- Returns matching file paths.\\n- Use this tool when you need to find files by name patterns.\\n- If you are unsure of the correct file path, use the glob tool to look up filenames by glob pattern.\\n- Use the grep tool instead if you need to search file contents.\\n- Call this tool in parallel when you know there are multiple patterns to search for.\",\"name\":\"glob\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"limit\":{\"description\":\"Maximum number of results to return. Defaults to 100.\",\"maximum\":1000,\"minimum\":1,\"type\":\"integer\"},\"path\":{\"description\":\"The directory to search in. Defaults to /workspace. Must be an absolute path. Omit to use the default.\",\"type\":\"string\"},\"pattern\":{\"description\":\"The glob pattern to match files against (e.g. \\\"**/*.ts\\\", \\\"src/**/*.js\\\").\",\"type\":\"string\"}},\"required\":[\"limit\",\"path\",\"pattern\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Fast content search tool that works with any codebase size.\\n\\nUsage:\\n- Searches file contents using regular expressions.\\n- Supports full regex syntax (e.g. \\\"log.*Error\\\", \\\"function\\\\s+\\\\w+\\\").\\n- Filter files by pattern with the glob parameter (e.g. \\\"*.js\\\", \\\"*.{ts,tsx}\\\").\\n- Returns matching lines with file paths and line numbers.\\n- Use this tool when you need to find files containing specific patterns.\\n- Use the glob tool instead if you only need to find files by name.\\n- Call this tool in parallel when you have multiple independent searches.\\n- Any line longer than 2000 characters is truncated.\",\"name\":\"grep\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"context\":{\"description\":\"Number of surrounding context lines to include before and after each match. Defaults to 0.\",\"minimum\":0,\"type\":\"integer\"},\"glob\":{\"description\":\"Filter files by glob pattern (e.g. \\\"*.ts\\\", \\\"*.{ts,tsx}\\\").\",\"type\":\"string\"},\"ignoreCase\":{\"description\":\"Perform case-insensitive search. Defaults to false.\",\"type\":\"boolean\"},\"limit\":{\"description\":\"Maximum number of matches to return per file. Defaults to 100.\",\"maximum\":1000,\"minimum\":1,\"type\":\"integer\"},\"literal\":{\"description\":\"Treat the pattern as a literal string instead of a regular expression. Defaults to false.\",\"type\":\"boolean\"},\"path\":{\"description\":\"The directory or file to search in. Defaults to /workspace. Must be an absolute path. Omit to use the default.\",\"type\":\"string\"},\"pattern\":{\"description\":\"The regex pattern to search for in file contents (e.g. \\\"log.*Error\\\", \\\"function\\\\s+\\\\w+\\\").\",\"type\":\"string\"}},\"required\":[\"context\",\"glob\",\"ignoreCase\",\"limit\",\"literal\",\"path\",\"pattern\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Read a file from the local filesystem. If the path does not exist, an error is returned.\\n\\nUsage:\\n- The filePath parameter should be an absolute path.\\n- By default, this tool returns up to 2000 lines from the start of the file.\\n- The offset parameter is the line number to start from (1-indexed).\\n- To read later sections, call this tool again with a larger offset.\\n- Use the grep tool to find specific content in large files or files with long lines.\\n- If you are unsure of the correct file path, use the glob tool to look up filenames by glob pattern.\\n- Contents are returned with each line prefixed by its line number as `: `. For example, if a file has contents \\\"foo\\\\n\\\", you will receive \\\"1: foo\\\\n\\\".\\n- Any line longer than 2000 characters is truncated.\\n- Call this tool in parallel when you know there are multiple files you want to read.\\n- Avoid tiny repeated slices (30 line chunks). If you need more context, read a larger window.\",\"name\":\"read_file\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"filePath\":{\"description\":\"The absolute path to the file to read.\",\"type\":\"string\"},\"limit\":{\"description\":\"Maximum number of lines to return. Defaults to 2000.\",\"minimum\":1,\"type\":\"integer\"},\"offset\":{\"description\":\"1-based line number to start from. Defaults to 1.\",\"minimum\":1,\"type\":\"integer\"}},\"required\":[\"filePath\",\"limit\",\"offset\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Writes a file to the local filesystem.\\n\\nUsage:\\n- This tool will overwrite the existing file if there is one at the provided path.\\n- If this is an existing file, you MUST use the read_file tool first to read the file's contents. This tool will fail if you did not read the file first.\\n- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.\\n- NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.\\n- Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked.\",\"name\":\"write_file\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"content\":{\"description\":\"Complete replacement file contents.\",\"type\":\"string\"},\"filePath\":{\"description\":\"The absolute path to the file to write (must be absolute, not relative).\",\"type\":\"string\"}},\"required\":[\"content\",\"filePath\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Use this tool to create and manage a structured task list for the current session.\\nThis helps you track progress, organize complex tasks, and demonstrate thoroughness.\\n\\nWhen to use:\\n- Complex multistep tasks requiring 3 or more distinct steps\\n- When the user provides multiple tasks or a numbered list\\n- After receiving new instructions, to capture requirements\\n- After completing a task, to mark it complete and add follow-ups\\n\\nWhen NOT to use:\\n- Single, straightforward tasks that need no tracking\\n- Purely conversational or informational requests\\n\\nUsage:\\n- Call with `todos` to replace the entire list (full replacement write)\\n- Call without `todos` to read the current list\\n- Both return the full current list with status counts\\n- Mark tasks in_progress when you start, completed when done\\n- Only have ONE task in_progress at a time\",\"name\":\"todo\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"todos\":{\"description\":\"The updated todo list. Omit to read the current list without modifying it.\",\"items\":{\"additionalProperties\":false,\"properties\":{\"content\":{\"description\":\"Brief description of the task.\",\"type\":\"string\"},\"priority\":{\"description\":\"Priority level of the task.\",\"enum\":[\"high\",\"medium\",\"low\"],\"type\":\"string\"},\"status\":{\"description\":\"Current status of the task.\",\"enum\":[\"pending\",\"in_progress\",\"completed\",\"cancelled\"],\"type\":\"string\"}},\"required\":[\"content\",\"priority\",\"status\"],\"type\":\"object\"},\"type\":\"array\"}},\"type\":\"object\",\"required\":[\"todos\"]},\"strict\":true},{\"type\":\"function\",\"description\":\"Fetch a webpage and return its content in the requested format. Use this to retrieve and analyze content from URLs.\\n\\nUsage notes:\\n- The URL must be a fully-formed valid URL starting with http:// or https://\\n- HTML responses are automatically converted to markdown or plain text based on the requested format\\n- Format options: \\\"markdown\\\" (default), \\\"text\\\", or \\\"html\\\"\\n- Default timeout is 30 seconds (max 120 seconds)\\n- Maximum response size is 5 MB; content is further capped at the shared tool-output budget (50 KB / 2000 lines)\\n- This tool is read-only and does not modify any files\",\"name\":\"web_fetch\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"format\":{\"description\":\"The format to return the content in (text, markdown, or html). HTML responses are automatically converted to the requested format. Defaults to \\\"markdown\\\".\",\"enum\":[\"markdown\",\"text\",\"html\"],\"type\":\"string\"},\"timeout\":{\"description\":\"Optional timeout in seconds. Defaults to 30, max 120.\",\"type\":\"number\"},\"url\":{\"description\":\"The fully-formed URL to fetch content from. Must start with http:// or https://.\",\"type\":\"string\"}},\"required\":[\"format\",\"timeout\",\"url\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Load the full instructions for one available skill by name or id. Use this tool when the request clearly matches a listed skill description or when the user explicitly asks for that skill. This is not for MCP connections; use connection_search to access an installed connection. Loading adds the skill instructions to the current turn. Choose the \\\"skill\\\" value from the Available skills block.\",\"name\":\"load_skill\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"skill\":{\"description\":\"Available skill name or id.\",\"type\":\"string\"}},\"required\":[\"skill\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Reads a deterministic Eve documentation page fixture.\",\"name\":\"read\",\"parameters\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"url\":{\"type\":\"string\"}},\"required\":[\"url\"],\"additionalProperties\":false},\"strict\":true},{\"type\":\"function\",\"description\":\"Searches the Eve instrumentation documentation fixture.\",\"name\":\"search\",\"parameters\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\"}},\"required\":[\"query\"],\"additionalProperties\":false},\"strict\":true},{\"type\":\"function\",\"description\":\"Delegate a focused subtask to a fresh copy of yourself. Use it to isolate complex work or split a large task into independent pieces. Issue multiple `agent` calls in one response to run a small fixed set in parallel. Each child has fresh history and state but shares your tools and sandbox, so include essential context in `message` and give parallel writers non-overlapping scopes.\",\"name\":\"agent\",\"parameters\":{\"type\":\"object\",\"properties\":{\"message\":{\"type\":\"string\",\"description\":\"The message to send to the subagent. Provide all context the subagent needs to complete the task; the subagent does not see the parent's history.\"},\"outputSchema\":{\"type\":\"object\",\"description\":\"When provided, the subagent runs in task mode and must produce structured output matching this JSON Schema. The structured output becomes the tool result.\"}},\"required\":[\"message\"],\"additionalProperties\":false},\"strict\":false},{\"type\":\"web_search\",\"return_token_budget\":\"default\",\"search_content_types\":[\"text\"],\"search_context_size\":\"medium\",\"user_location\":{\"type\":\"approximate\",\"city\":null,\"country\":\"US\",\"region\":null,\"timezone\":null}}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}", + "event: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_0fea08d8f9bfc463006a4ba2bed894819f941441a0e61d48b8\",\"object\":\"response\",\"created_at\":1783341758,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-mini-2026-03-17\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":true,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Ask the user a question and wait for their response before continuing. Use this when you need clarification or a choice from the user.\",\"name\":\"ask_question\",\"parameters\":{\"type\":\"object\",\"properties\":{\"allowFreeform\":{\"type\":\"boolean\",\"description\":\"Whether the user may answer with freeform text instead of selecting one of the provided options.\"},\"options\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Optional additional context for this option.\"},\"id\":{\"type\":\"string\",\"description\":\"Stable identifier for the option.\"},\"label\":{\"type\":\"string\",\"description\":\"User-facing label for the option.\"},\"style\":{\"type\":\"string\",\"enum\":[\"primary\",\"danger\",\"default\"],\"description\":\"Visual treatment hint for the option.\"}},\"required\":[\"description\",\"id\",\"label\",\"style\"],\"additionalProperties\":false},\"description\":\"Selectable answer options to present to the user.\"},\"prompt\":{\"type\":\"string\",\"description\":\"The prompt to present to the user.\"}},\"required\":[\"allowFreeform\",\"options\",\"prompt\"],\"additionalProperties\":false},\"strict\":true},{\"type\":\"function\",\"description\":\"Execute a shell command in the shared workspace environment.\",\"name\":\"bash\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The shell command to execute.\",\"type\":\"string\"}},\"required\":[\"command\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Fast file pattern matching tool that works with any codebase size.\\n\\nUsage:\\n- Supports glob patterns like \\\"**/*.js\\\" or \\\"src/**/*.ts\\\".\\n- Returns matching file paths.\\n- Use this tool when you need to find files by name patterns.\\n- If you are unsure of the correct file path, use the glob tool to look up filenames by glob pattern.\\n- Use the grep tool instead if you need to search file contents.\\n- Call this tool in parallel when you know there are multiple patterns to search for.\",\"name\":\"glob\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"limit\":{\"description\":\"Maximum number of results to return. Defaults to 100.\",\"maximum\":1000,\"minimum\":1,\"type\":\"integer\"},\"path\":{\"description\":\"The directory to search in. Defaults to /workspace. Must be an absolute path. Omit to use the default.\",\"type\":\"string\"},\"pattern\":{\"description\":\"The glob pattern to match files against (e.g. \\\"**/*.ts\\\", \\\"src/**/*.js\\\").\",\"type\":\"string\"}},\"required\":[\"limit\",\"path\",\"pattern\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Fast content search tool that works with any codebase size.\\n\\nUsage:\\n- Searches file contents using regular expressions.\\n- Supports full regex syntax (e.g. \\\"log.*Error\\\", \\\"function\\\\s+\\\\w+\\\").\\n- Filter files by pattern with the glob parameter (e.g. \\\"*.js\\\", \\\"*.{ts,tsx}\\\").\\n- Returns matching lines with file paths and line numbers.\\n- Use this tool when you need to find files containing specific patterns.\\n- Use the glob tool instead if you only need to find files by name.\\n- Call this tool in parallel when you have multiple independent searches.\\n- Any line longer than 2000 characters is truncated.\",\"name\":\"grep\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"context\":{\"description\":\"Number of surrounding context lines to include before and after each match. Defaults to 0.\",\"minimum\":0,\"type\":\"integer\"},\"glob\":{\"description\":\"Filter files by glob pattern (e.g. \\\"*.ts\\\", \\\"*.{ts,tsx}\\\").\",\"type\":\"string\"},\"ignoreCase\":{\"description\":\"Perform case-insensitive search. Defaults to false.\",\"type\":\"boolean\"},\"limit\":{\"description\":\"Maximum number of matches to return per file. Defaults to 100.\",\"maximum\":1000,\"minimum\":1,\"type\":\"integer\"},\"literal\":{\"description\":\"Treat the pattern as a literal string instead of a regular expression. Defaults to false.\",\"type\":\"boolean\"},\"path\":{\"description\":\"The directory or file to search in. Defaults to /workspace. Must be an absolute path. Omit to use the default.\",\"type\":\"string\"},\"pattern\":{\"description\":\"The regex pattern to search for in file contents (e.g. \\\"log.*Error\\\", \\\"function\\\\s+\\\\w+\\\").\",\"type\":\"string\"}},\"required\":[\"context\",\"glob\",\"ignoreCase\",\"limit\",\"literal\",\"path\",\"pattern\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Read a file from the local filesystem. If the path does not exist, an error is returned.\\n\\nUsage:\\n- The filePath parameter should be an absolute path.\\n- By default, this tool returns up to 2000 lines from the start of the file.\\n- The offset parameter is the line number to start from (1-indexed).\\n- To read later sections, call this tool again with a larger offset.\\n- Use the grep tool to find specific content in large files or files with long lines.\\n- If you are unsure of the correct file path, use the glob tool to look up filenames by glob pattern.\\n- Contents are returned with each line prefixed by its line number as `: `. For example, if a file has contents \\\"foo\\\\n\\\", you will receive \\\"1: foo\\\\n\\\".\\n- Any line longer than 2000 characters is truncated.\\n- Call this tool in parallel when you know there are multiple files you want to read.\\n- Avoid tiny repeated slices (30 line chunks). If you need more context, read a larger window.\",\"name\":\"read_file\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"filePath\":{\"description\":\"The absolute path to the file to read.\",\"type\":\"string\"},\"limit\":{\"description\":\"Maximum number of lines to return. Defaults to 2000.\",\"minimum\":1,\"type\":\"integer\"},\"offset\":{\"description\":\"1-based line number to start from. Defaults to 1.\",\"minimum\":1,\"type\":\"integer\"}},\"required\":[\"filePath\",\"limit\",\"offset\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Writes a file to the local filesystem.\\n\\nUsage:\\n- This tool will overwrite the existing file if there is one at the provided path.\\n- If this is an existing file, you MUST use the read_file tool first to read the file's contents. This tool will fail if you did not read the file first.\\n- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.\\n- NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.\\n- Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked.\",\"name\":\"write_file\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"content\":{\"description\":\"Complete replacement file contents.\",\"type\":\"string\"},\"filePath\":{\"description\":\"The absolute path to the file to write (must be absolute, not relative).\",\"type\":\"string\"}},\"required\":[\"content\",\"filePath\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Use this tool to create and manage a structured task list for the current session.\\nThis helps you track progress, organize complex tasks, and demonstrate thoroughness.\\n\\nWhen to use:\\n- Complex multistep tasks requiring 3 or more distinct steps\\n- When the user provides multiple tasks or a numbered list\\n- After receiving new instructions, to capture requirements\\n- After completing a task, to mark it complete and add follow-ups\\n\\nWhen NOT to use:\\n- Single, straightforward tasks that need no tracking\\n- Purely conversational or informational requests\\n\\nUsage:\\n- Call with `todos` to replace the entire list (full replacement write)\\n- Call without `todos` to read the current list\\n- Both return the full current list with status counts\\n- Mark tasks in_progress when you start, completed when done\\n- Only have ONE task in_progress at a time\",\"name\":\"todo\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"todos\":{\"description\":\"The updated todo list. Omit to read the current list without modifying it.\",\"items\":{\"additionalProperties\":false,\"properties\":{\"content\":{\"description\":\"Brief description of the task.\",\"type\":\"string\"},\"priority\":{\"description\":\"Priority level of the task.\",\"enum\":[\"high\",\"medium\",\"low\"],\"type\":\"string\"},\"status\":{\"description\":\"Current status of the task.\",\"enum\":[\"pending\",\"in_progress\",\"completed\",\"cancelled\"],\"type\":\"string\"}},\"required\":[\"content\",\"priority\",\"status\"],\"type\":\"object\"},\"type\":\"array\"}},\"type\":\"object\",\"required\":[\"todos\"]},\"strict\":true},{\"type\":\"function\",\"description\":\"Fetch a webpage and return its content in the requested format. Use this to retrieve and analyze content from URLs.\\n\\nUsage notes:\\n- The URL must be a fully-formed valid URL starting with http:// or https://\\n- HTML responses are automatically converted to markdown or plain text based on the requested format\\n- Format options: \\\"markdown\\\" (default), \\\"text\\\", or \\\"html\\\"\\n- Default timeout is 30 seconds (max 120 seconds)\\n- Maximum response size is 5 MB; content is further capped at the shared tool-output budget (50 KB / 2000 lines)\\n- This tool is read-only and does not modify any files\",\"name\":\"web_fetch\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"format\":{\"description\":\"The format to return the content in (text, markdown, or html). HTML responses are automatically converted to the requested format. Defaults to \\\"markdown\\\".\",\"enum\":[\"markdown\",\"text\",\"html\"],\"type\":\"string\"},\"timeout\":{\"description\":\"Optional timeout in seconds. Defaults to 30, max 120.\",\"type\":\"number\"},\"url\":{\"description\":\"The fully-formed URL to fetch content from. Must start with http:// or https://.\",\"type\":\"string\"}},\"required\":[\"format\",\"timeout\",\"url\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Load the full instructions for one available skill by name or id. Use this tool when the request clearly matches a listed skill description or when the user explicitly asks for that skill. This is not for MCP connections; use connection_search to access an installed connection. Loading adds the skill instructions to the current turn. Choose the \\\"skill\\\" value from the Available skills block.\",\"name\":\"load_skill\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"skill\":{\"description\":\"Available skill name or id.\",\"type\":\"string\"}},\"required\":[\"skill\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Reads a deterministic Eve documentation page fixture.\",\"name\":\"read\",\"parameters\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"url\":{\"type\":\"string\"}},\"required\":[\"url\"],\"additionalProperties\":false},\"strict\":true},{\"type\":\"function\",\"description\":\"Searches the Eve instrumentation documentation fixture.\",\"name\":\"search\",\"parameters\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\"}},\"required\":[\"query\"],\"additionalProperties\":false},\"strict\":true},{\"type\":\"function\",\"description\":\"Delegate a focused subtask to a fresh copy of yourself. Use it to isolate complex work or split a large task into independent pieces. Issue multiple `agent` calls in one response to run a small fixed set in parallel. Each child has fresh history and state but shares your tools and sandbox, so include essential context in `message` and give parallel writers non-overlapping scopes.\",\"name\":\"agent\",\"parameters\":{\"type\":\"object\",\"properties\":{\"message\":{\"type\":\"string\",\"description\":\"The message to send to the subagent. Provide all context the subagent needs to complete the task; the subagent does not see the parent's history.\"},\"outputSchema\":{\"type\":\"object\",\"description\":\"When provided, the subagent runs in task mode and must produce structured output matching this JSON Schema. The structured output becomes the tool result.\"}},\"required\":[\"message\"],\"additionalProperties\":false},\"strict\":false},{\"type\":\"web_search\",\"return_token_budget\":\"default\",\"search_content_types\":[\"text\"],\"search_context_size\":\"medium\",\"user_location\":{\"type\":\"approximate\",\"city\":null,\"country\":\"US\",\"region\":null,\"timezone\":null}}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}", + "event: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"fc_0fea08d8f9bfc463006a4ba2bfcbe0819fb89c3fb8976a78ea\",\"type\":\"function_call\",\"status\":\"in_progress\",\"arguments\":\"\",\"call_id\":\"call_OJGkAZ5ouXaH7NjuUH2mSbk6\",\"name\":\"search\"},\"output_index\":0,\"sequence_number\":2}", + "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"{\\\"\",\"item_id\":\"fc_0fea08d8f9bfc463006a4ba2bfcbe0819fb89c3fb8976a78ea\",\"obfuscation\":\"XnL4BuCUjYo2Ga\",\"output_index\":0,\"sequence_number\":3}", + "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"query\",\"item_id\":\"fc_0fea08d8f9bfc463006a4ba2bfcbe0819fb89c3fb8976a78ea\",\"obfuscation\":\"l6rkWWYxmHg\",\"output_index\":0,\"sequence_number\":4}", + "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\":\\\"\",\"item_id\":\"fc_0fea08d8f9bfc463006a4ba2bfcbe0819fb89c3fb8976a78ea\",\"obfuscation\":\"GymIr2w2XZVpA\",\"output_index\":0,\"sequence_number\":5}", + "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"Run\",\"item_id\":\"fc_0fea08d8f9bfc463006a4ba2bfcbe0819fb89c3fb8976a78ea\",\"obfuscation\":\"OwgJorNkY8F7z\",\"output_index\":0,\"sequence_number\":6}", + "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" the\",\"item_id\":\"fc_0fea08d8f9bfc463006a4ba2bfcbe0819fb89c3fb8976a78ea\",\"obfuscation\":\"zI4Q0bRZ70XL\",\"output_index\":0,\"sequence_number\":7}", + "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" Brain\",\"item_id\":\"fc_0fea08d8f9bfc463006a4ba2bfcbe0819fb89c3fb8976a78ea\",\"obfuscation\":\"1mvqmuEoZG\",\"output_index\":0,\"sequence_number\":8}", + "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"trust\",\"item_id\":\"fc_0fea08d8f9bfc463006a4ba2bfcbe0819fb89c3fb8976a78ea\",\"obfuscation\":\"7LdGHqhYJm5\",\"output_index\":0,\"sequence_number\":9}", + "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" Eve\",\"item_id\":\"fc_0fea08d8f9bfc463006a4ba2bfcbe0819fb89c3fb8976a78ea\",\"obfuscation\":\"SUjuR9onJ7HP\",\"output_index\":0,\"sequence_number\":10}", + "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" instrumentation\",\"item_id\":\"fc_0fea08d8f9bfc463006a4ba2bfcbe0819fb89c3fb8976a78ea\",\"obfuscation\":\"\",\"output_index\":0,\"sequence_number\":11}", + "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" e\",\"item_id\":\"fc_0fea08d8f9bfc463006a4ba2bfcbe0819fb89c3fb8976a78ea\",\"obfuscation\":\"9vSg1jlhKVxvd4\",\"output_index\":0,\"sequence_number\":12}", + "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"2\",\"item_id\":\"fc_0fea08d8f9bfc463006a4ba2bfcbe0819fb89c3fb8976a78ea\",\"obfuscation\":\"lkED6zGdV426rzv\",\"output_index\":0,\"sequence_number\":13}", + "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"e\",\"item_id\":\"fc_0fea08d8f9bfc463006a4ba2bfcbe0819fb89c3fb8976a78ea\",\"obfuscation\":\"7ZbUxm5q3VLOlzU\",\"output_index\":0,\"sequence_number\":14}", + "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\" scenario\",\"item_id\":\"fc_0fea08d8f9bfc463006a4ba2bfcbe0819fb89c3fb8976a78ea\",\"obfuscation\":\"fjhDwnd\",\"output_index\":0,\"sequence_number\":15}", + "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\"}\",\"item_id\":\"fc_0fea08d8f9bfc463006a4ba2bfcbe0819fb89c3fb8976a78ea\",\"obfuscation\":\"rtm0Kzc5YO8U7T\",\"output_index\":0,\"sequence_number\":16}", + "event: response.function_call_arguments.done\ndata: {\"type\":\"response.function_call_arguments.done\",\"arguments\":\"{\\\"query\\\":\\\"Run the Braintrust Eve instrumentation e2e scenario\\\"}\",\"item_id\":\"fc_0fea08d8f9bfc463006a4ba2bfcbe0819fb89c3fb8976a78ea\",\"output_index\":0,\"sequence_number\":17}", + "event: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"fc_0fea08d8f9bfc463006a4ba2bfcbe0819fb89c3fb8976a78ea\",\"type\":\"function_call\",\"status\":\"completed\",\"arguments\":\"{\\\"query\\\":\\\"Run the Braintrust Eve instrumentation e2e scenario\\\"}\",\"call_id\":\"call_OJGkAZ5ouXaH7NjuUH2mSbk6\",\"name\":\"search\"},\"output_index\":0,\"sequence_number\":18}", + "event: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_0fea08d8f9bfc463006a4ba2bed894819f941441a0e61d48b8\",\"object\":\"response\",\"created_at\":1783341758,\"status\":\"completed\",\"background\":false,\"completed_at\":1783341760,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-mini-2026-03-17\",\"moderation\":null,\"output\":[{\"id\":\"fc_0fea08d8f9bfc463006a4ba2bfcbe0819fb89c3fb8976a78ea\",\"type\":\"function_call\",\"status\":\"completed\",\"arguments\":\"{\\\"query\\\":\\\"Run the Braintrust Eve instrumentation e2e scenario\\\"}\",\"call_id\":\"call_OJGkAZ5ouXaH7NjuUH2mSbk6\",\"name\":\"search\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":true,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Ask the user a question and wait for their response before continuing. Use this when you need clarification or a choice from the user.\",\"name\":\"ask_question\",\"parameters\":{\"type\":\"object\",\"properties\":{\"allowFreeform\":{\"type\":\"boolean\",\"description\":\"Whether the user may answer with freeform text instead of selecting one of the provided options.\"},\"options\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Optional additional context for this option.\"},\"id\":{\"type\":\"string\",\"description\":\"Stable identifier for the option.\"},\"label\":{\"type\":\"string\",\"description\":\"User-facing label for the option.\"},\"style\":{\"type\":\"string\",\"enum\":[\"primary\",\"danger\",\"default\"],\"description\":\"Visual treatment hint for the option.\"}},\"required\":[\"description\",\"id\",\"label\",\"style\"],\"additionalProperties\":false},\"description\":\"Selectable answer options to present to the user.\"},\"prompt\":{\"type\":\"string\",\"description\":\"The prompt to present to the user.\"}},\"required\":[\"allowFreeform\",\"options\",\"prompt\"],\"additionalProperties\":false},\"strict\":true},{\"type\":\"function\",\"description\":\"Execute a shell command in the shared workspace environment.\",\"name\":\"bash\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The shell command to execute.\",\"type\":\"string\"}},\"required\":[\"command\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Fast file pattern matching tool that works with any codebase size.\\n\\nUsage:\\n- Supports glob patterns like \\\"**/*.js\\\" or \\\"src/**/*.ts\\\".\\n- Returns matching file paths.\\n- Use this tool when you need to find files by name patterns.\\n- If you are unsure of the correct file path, use the glob tool to look up filenames by glob pattern.\\n- Use the grep tool instead if you need to search file contents.\\n- Call this tool in parallel when you know there are multiple patterns to search for.\",\"name\":\"glob\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"limit\":{\"description\":\"Maximum number of results to return. Defaults to 100.\",\"maximum\":1000,\"minimum\":1,\"type\":\"integer\"},\"path\":{\"description\":\"The directory to search in. Defaults to /workspace. Must be an absolute path. Omit to use the default.\",\"type\":\"string\"},\"pattern\":{\"description\":\"The glob pattern to match files against (e.g. \\\"**/*.ts\\\", \\\"src/**/*.js\\\").\",\"type\":\"string\"}},\"required\":[\"limit\",\"path\",\"pattern\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Fast content search tool that works with any codebase size.\\n\\nUsage:\\n- Searches file contents using regular expressions.\\n- Supports full regex syntax (e.g. \\\"log.*Error\\\", \\\"function\\\\s+\\\\w+\\\").\\n- Filter files by pattern with the glob parameter (e.g. \\\"*.js\\\", \\\"*.{ts,tsx}\\\").\\n- Returns matching lines with file paths and line numbers.\\n- Use this tool when you need to find files containing specific patterns.\\n- Use the glob tool instead if you only need to find files by name.\\n- Call this tool in parallel when you have multiple independent searches.\\n- Any line longer than 2000 characters is truncated.\",\"name\":\"grep\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"context\":{\"description\":\"Number of surrounding context lines to include before and after each match. Defaults to 0.\",\"minimum\":0,\"type\":\"integer\"},\"glob\":{\"description\":\"Filter files by glob pattern (e.g. \\\"*.ts\\\", \\\"*.{ts,tsx}\\\").\",\"type\":\"string\"},\"ignoreCase\":{\"description\":\"Perform case-insensitive search. Defaults to false.\",\"type\":\"boolean\"},\"limit\":{\"description\":\"Maximum number of matches to return per file. Defaults to 100.\",\"maximum\":1000,\"minimum\":1,\"type\":\"integer\"},\"literal\":{\"description\":\"Treat the pattern as a literal string instead of a regular expression. Defaults to false.\",\"type\":\"boolean\"},\"path\":{\"description\":\"The directory or file to search in. Defaults to /workspace. Must be an absolute path. Omit to use the default.\",\"type\":\"string\"},\"pattern\":{\"description\":\"The regex pattern to search for in file contents (e.g. \\\"log.*Error\\\", \\\"function\\\\s+\\\\w+\\\").\",\"type\":\"string\"}},\"required\":[\"context\",\"glob\",\"ignoreCase\",\"limit\",\"literal\",\"path\",\"pattern\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Read a file from the local filesystem. If the path does not exist, an error is returned.\\n\\nUsage:\\n- The filePath parameter should be an absolute path.\\n- By default, this tool returns up to 2000 lines from the start of the file.\\n- The offset parameter is the line number to start from (1-indexed).\\n- To read later sections, call this tool again with a larger offset.\\n- Use the grep tool to find specific content in large files or files with long lines.\\n- If you are unsure of the correct file path, use the glob tool to look up filenames by glob pattern.\\n- Contents are returned with each line prefixed by its line number as `: `. For example, if a file has contents \\\"foo\\\\n\\\", you will receive \\\"1: foo\\\\n\\\".\\n- Any line longer than 2000 characters is truncated.\\n- Call this tool in parallel when you know there are multiple files you want to read.\\n- Avoid tiny repeated slices (30 line chunks). If you need more context, read a larger window.\",\"name\":\"read_file\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"filePath\":{\"description\":\"The absolute path to the file to read.\",\"type\":\"string\"},\"limit\":{\"description\":\"Maximum number of lines to return. Defaults to 2000.\",\"minimum\":1,\"type\":\"integer\"},\"offset\":{\"description\":\"1-based line number to start from. Defaults to 1.\",\"minimum\":1,\"type\":\"integer\"}},\"required\":[\"filePath\",\"limit\",\"offset\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Writes a file to the local filesystem.\\n\\nUsage:\\n- This tool will overwrite the existing file if there is one at the provided path.\\n- If this is an existing file, you MUST use the read_file tool first to read the file's contents. This tool will fail if you did not read the file first.\\n- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.\\n- NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.\\n- Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked.\",\"name\":\"write_file\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"content\":{\"description\":\"Complete replacement file contents.\",\"type\":\"string\"},\"filePath\":{\"description\":\"The absolute path to the file to write (must be absolute, not relative).\",\"type\":\"string\"}},\"required\":[\"content\",\"filePath\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Use this tool to create and manage a structured task list for the current session.\\nThis helps you track progress, organize complex tasks, and demonstrate thoroughness.\\n\\nWhen to use:\\n- Complex multistep tasks requiring 3 or more distinct steps\\n- When the user provides multiple tasks or a numbered list\\n- After receiving new instructions, to capture requirements\\n- After completing a task, to mark it complete and add follow-ups\\n\\nWhen NOT to use:\\n- Single, straightforward tasks that need no tracking\\n- Purely conversational or informational requests\\n\\nUsage:\\n- Call with `todos` to replace the entire list (full replacement write)\\n- Call without `todos` to read the current list\\n- Both return the full current list with status counts\\n- Mark tasks in_progress when you start, completed when done\\n- Only have ONE task in_progress at a time\",\"name\":\"todo\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"todos\":{\"description\":\"The updated todo list. Omit to read the current list without modifying it.\",\"items\":{\"additionalProperties\":false,\"properties\":{\"content\":{\"description\":\"Brief description of the task.\",\"type\":\"string\"},\"priority\":{\"description\":\"Priority level of the task.\",\"enum\":[\"high\",\"medium\",\"low\"],\"type\":\"string\"},\"status\":{\"description\":\"Current status of the task.\",\"enum\":[\"pending\",\"in_progress\",\"completed\",\"cancelled\"],\"type\":\"string\"}},\"required\":[\"content\",\"priority\",\"status\"],\"type\":\"object\"},\"type\":\"array\"}},\"type\":\"object\",\"required\":[\"todos\"]},\"strict\":true},{\"type\":\"function\",\"description\":\"Fetch a webpage and return its content in the requested format. Use this to retrieve and analyze content from URLs.\\n\\nUsage notes:\\n- The URL must be a fully-formed valid URL starting with http:// or https://\\n- HTML responses are automatically converted to markdown or plain text based on the requested format\\n- Format options: \\\"markdown\\\" (default), \\\"text\\\", or \\\"html\\\"\\n- Default timeout is 30 seconds (max 120 seconds)\\n- Maximum response size is 5 MB; content is further capped at the shared tool-output budget (50 KB / 2000 lines)\\n- This tool is read-only and does not modify any files\",\"name\":\"web_fetch\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"format\":{\"description\":\"The format to return the content in (text, markdown, or html). HTML responses are automatically converted to the requested format. Defaults to \\\"markdown\\\".\",\"enum\":[\"markdown\",\"text\",\"html\"],\"type\":\"string\"},\"timeout\":{\"description\":\"Optional timeout in seconds. Defaults to 30, max 120.\",\"type\":\"number\"},\"url\":{\"description\":\"The fully-formed URL to fetch content from. Must start with http:// or https://.\",\"type\":\"string\"}},\"required\":[\"format\",\"timeout\",\"url\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Load the full instructions for one available skill by name or id. Use this tool when the request clearly matches a listed skill description or when the user explicitly asks for that skill. This is not for MCP connections; use connection_search to access an installed connection. Loading adds the skill instructions to the current turn. Choose the \\\"skill\\\" value from the Available skills block.\",\"name\":\"load_skill\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"skill\":{\"description\":\"Available skill name or id.\",\"type\":\"string\"}},\"required\":[\"skill\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Reads a deterministic Eve documentation page fixture.\",\"name\":\"read\",\"parameters\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"url\":{\"type\":\"string\"}},\"required\":[\"url\"],\"additionalProperties\":false},\"strict\":true},{\"type\":\"function\",\"description\":\"Searches the Eve instrumentation documentation fixture.\",\"name\":\"search\",\"parameters\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\"}},\"required\":[\"query\"],\"additionalProperties\":false},\"strict\":true},{\"type\":\"function\",\"description\":\"Delegate a focused subtask to a fresh copy of yourself. Use it to isolate complex work or split a large task into independent pieces. Issue multiple `agent` calls in one response to run a small fixed set in parallel. Each child has fresh history and state but shares your tools and sandbox, so include essential context in `message` and give parallel writers non-overlapping scopes.\",\"name\":\"agent\",\"parameters\":{\"type\":\"object\",\"properties\":{\"message\":{\"type\":\"string\",\"description\":\"The message to send to the subagent. Provide all context the subagent needs to complete the task; the subagent does not see the parent's history.\"},\"outputSchema\":{\"type\":\"object\",\"description\":\"When provided, the subagent runs in task mode and must produce structured output matching this JSON Schema. The structured output becomes the tool result.\"}},\"required\":[\"message\"],\"additionalProperties\":false},\"strict\":false},{\"type\":\"web_search\",\"return_token_budget\":\"default\",\"search_content_types\":[\"text\"],\"search_context_size\":\"medium\",\"user_location\":{\"type\":\"approximate\",\"city\":null,\"country\":\"US\",\"region\":null,\"timezone\":null}}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":6564,\"input_tokens_details\":{\"cached_tokens\":0},\"output_tokens\":26,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":6590},\"user\":null,\"metadata\":{}},\"sequence_number\":19}" + ], + "kind": "sse" + }, + "headers": { + "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", + "alt-svc": "h3=\":443\"; ma=86400", + "cf-cache-status": "DYNAMIC", + "cf-ray": "a16eb0c61b905b89-VIE", + "connection": "keep-alive", + "content-type": "text/event-stream; charset=utf-8", + "date": "Mon, 06 Jul 2026 12:42:39 GMT", + "openai-processing-ms": "301", + "openai-version": "2020-10-01", + "server": "cloudflare", + "set-cookie": "[REDACTED]", + "strict-transport-security": "max-age=31536000; includeSubDomains; preload", + "transfer-encoding": "chunked", + "x-content-type-options": "nosniff", + "x-ratelimit-limit-requests": "30000", + "x-ratelimit-limit-tokens": "180000000", + "x-ratelimit-remaining-requests": "29999", + "x-ratelimit-remaining-tokens": "179992794", + "x-ratelimit-reset-requests": "2ms", + "x-ratelimit-reset-tokens": "2ms", + "x-request-id": "c268729b-cdae-494b-a9f6-d154a9096847" + }, + "status": 200, + "statusText": "OK" + } + }, + { + "callIndex": 1, + "id": "6eaa461531ba23e9", + "matchKey": "POST api.openai.com/v1/responses", + "recordedAt": "2026-07-06T12:42:41.865Z", + "request": { + "body": { + "kind": "json", + "value": { + "include": ["web_search_call.action.sources"], + "input": [ + { + "content": "Instructions (instructions)\nYou are a deterministic fixture agent for Braintrust Eve instrumentation tests.\n\nFor every user task:\n\n1. Call the search tool exactly once with the full user message as the query.\n2. After the search result is available, call the read tool exactly once with the\n URL returned by search.\n3. After the read result is available, answer with a single sentence that starts\n with \"Final answer from read:\" and includes the search title, URL, and read\n excerpt.\n\nTool execution\nA single tool or subagent call runs as one serial action. If you call multiple independent tools or subagents in one response, eve treats that batch as parallel work. Only batch work that is independent and does not rely on another call in the same response.", + "role": "developer" + }, + { + "content": [ + { + "text": "Run the Braintrust Eve instrumentation e2e scenario", + "type": "input_text" + } + ], + "role": "user" + }, + { + "id": "fc_0fea08d8f9bfc463006a4ba2bfcbe0819fb89c3fb8976a78ea", + "type": "item_reference" + }, + { + "call_id": "call_OJGkAZ5ouXaH7NjuUH2mSbk6", + "output": "{\"query\":\"Run the Braintrust Eve instrumentation e2e scenario\",\"title\":\"Eve instrumentation\",\"url\":\"https://eve.dev/docs/guides/instrumentation\"}", + "type": "function_call_output" + } + ], + "model": "gpt-5.4-mini", + "stream": true, + "tool_choice": "auto", + "tools": [ + { + "description": "Ask the user a question and wait for their response before continuing. Use this when you need clarification or a choice from the user.", + "name": "ask_question", + "parameters": { + "additionalProperties": false, + "properties": { + "allowFreeform": { + "description": "Whether the user may answer with freeform text instead of selecting one of the provided options.", + "type": "boolean" + }, + "options": { + "description": "Selectable answer options to present to the user.", + "items": { + "additionalProperties": false, + "properties": { + "description": { + "description": "Optional additional context for this option.", + "type": "string" + }, + "id": { + "description": "Stable identifier for the option.", + "type": "string" + }, + "label": { + "description": "User-facing label for the option.", + "type": "string" + }, + "style": { + "description": "Visual treatment hint for the option.", + "enum": ["primary", "danger", "default"], + "type": "string" + } + }, + "required": ["id", "label"], + "type": "object" + }, + "type": "array" + }, + "prompt": { + "description": "The prompt to present to the user.", + "type": "string" + } + }, + "required": ["prompt"], + "type": "object" + }, + "type": "function" + }, + { + "description": "Execute a shell command in the shared workspace environment.", + "name": "bash", + "parameters": { + "additionalProperties": false, + "properties": { + "command": { + "description": "The shell command to execute.", + "type": "string" + } + }, + "required": ["command"], + "type": "object" + }, + "type": "function" + }, + { + "description": "Fast file pattern matching tool that works with any codebase size.\n\nUsage:\n- Supports glob patterns like \"**/*.js\" or \"src/**/*.ts\".\n- Returns matching file paths.\n- Use this tool when you need to find files by name patterns.\n- If you are unsure of the correct file path, use the glob tool to look up filenames by glob pattern.\n- Use the grep tool instead if you need to search file contents.\n- Call this tool in parallel when you know there are multiple patterns to search for.", + "name": "glob", + "parameters": { + "additionalProperties": false, + "properties": { + "limit": { + "description": "Maximum number of results to return. Defaults to 100.", + "maximum": 1000, + "minimum": 1, + "type": "integer" + }, + "path": { + "description": "The directory to search in. Defaults to /workspace. Must be an absolute path. Omit to use the default.", + "type": "string" + }, + "pattern": { + "description": "The glob pattern to match files against (e.g. \"**/*.ts\", \"src/**/*.js\").", + "type": "string" + } + }, + "required": ["pattern"], + "type": "object" + }, + "type": "function" + }, + { + "description": "Fast content search tool that works with any codebase size.\n\nUsage:\n- Searches file contents using regular expressions.\n- Supports full regex syntax (e.g. \"log.*Error\", \"function\\s+\\w+\").\n- Filter files by pattern with the glob parameter (e.g. \"*.js\", \"*.{ts,tsx}\").\n- Returns matching lines with file paths and line numbers.\n- Use this tool when you need to find files containing specific patterns.\n- Use the glob tool instead if you only need to find files by name.\n- Call this tool in parallel when you have multiple independent searches.\n- Any line longer than 2000 characters is truncated.", + "name": "grep", + "parameters": { + "additionalProperties": false, + "properties": { + "context": { + "description": "Number of surrounding context lines to include before and after each match. Defaults to 0.", + "minimum": 0, + "type": "integer" + }, + "glob": { + "description": "Filter files by glob pattern (e.g. \"*.ts\", \"*.{ts,tsx}\").", + "type": "string" + }, + "ignoreCase": { + "description": "Perform case-insensitive search. Defaults to false.", + "type": "boolean" + }, + "limit": { + "description": "Maximum number of matches to return per file. Defaults to 100.", + "maximum": 1000, + "minimum": 1, + "type": "integer" + }, + "literal": { + "description": "Treat the pattern as a literal string instead of a regular expression. Defaults to false.", + "type": "boolean" + }, + "path": { + "description": "The directory or file to search in. Defaults to /workspace. Must be an absolute path. Omit to use the default.", + "type": "string" + }, + "pattern": { + "description": "The regex pattern to search for in file contents (e.g. \"log.*Error\", \"function\\s+\\w+\").", + "type": "string" + } + }, + "required": ["pattern"], + "type": "object" + }, + "type": "function" + }, + { + "description": "Read a file from the local filesystem. If the path does not exist, an error is returned.\n\nUsage:\n- The filePath parameter should be an absolute path.\n- By default, this tool returns up to 2000 lines from the start of the file.\n- The offset parameter is the line number to start from (1-indexed).\n- To read later sections, call this tool again with a larger offset.\n- Use the grep tool to find specific content in large files or files with long lines.\n- If you are unsure of the correct file path, use the glob tool to look up filenames by glob pattern.\n- Contents are returned with each line prefixed by its line number as `: `. For example, if a file has contents \"foo\\n\", you will receive \"1: foo\\n\".\n- Any line longer than 2000 characters is truncated.\n- Call this tool in parallel when you know there are multiple files you want to read.\n- Avoid tiny repeated slices (30 line chunks). If you need more context, read a larger window.", + "name": "read_file", + "parameters": { + "additionalProperties": false, + "properties": { + "filePath": { + "description": "The absolute path to the file to read.", + "type": "string" + }, + "limit": { + "description": "Maximum number of lines to return. Defaults to 2000.", + "minimum": 1, + "type": "integer" + }, + "offset": { + "description": "1-based line number to start from. Defaults to 1.", + "minimum": 1, + "type": "integer" + } + }, + "required": ["filePath"], + "type": "object" + }, + "type": "function" + }, + { + "description": "Writes a file to the local filesystem.\n\nUsage:\n- This tool will overwrite the existing file if there is one at the provided path.\n- If this is an existing file, you MUST use the read_file tool first to read the file's contents. This tool will fail if you did not read the file first.\n- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.\n- NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.\n- Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked.", + "name": "write_file", + "parameters": { + "additionalProperties": false, + "properties": { + "content": { + "description": "Complete replacement file contents.", + "type": "string" + }, + "filePath": { + "description": "The absolute path to the file to write (must be absolute, not relative).", + "type": "string" + } + }, + "required": ["filePath", "content"], + "type": "object" + }, + "type": "function" + }, + { + "description": "Use this tool to create and manage a structured task list for the current session.\nThis helps you track progress, organize complex tasks, and demonstrate thoroughness.\n\nWhen to use:\n- Complex multistep tasks requiring 3 or more distinct steps\n- When the user provides multiple tasks or a numbered list\n- After receiving new instructions, to capture requirements\n- After completing a task, to mark it complete and add follow-ups\n\nWhen NOT to use:\n- Single, straightforward tasks that need no tracking\n- Purely conversational or informational requests\n\nUsage:\n- Call with `todos` to replace the entire list (full replacement write)\n- Call without `todos` to read the current list\n- Both return the full current list with status counts\n- Mark tasks in_progress when you start, completed when done\n- Only have ONE task in_progress at a time", + "name": "todo", + "parameters": { + "additionalProperties": false, + "properties": { + "todos": { + "description": "The updated todo list. Omit to read the current list without modifying it.", + "items": { + "additionalProperties": false, + "properties": { + "content": { + "description": "Brief description of the task.", + "type": "string" + }, + "priority": { + "description": "Priority level of the task.", + "enum": ["high", "medium", "low"], + "type": "string" + }, + "status": { + "description": "Current status of the task.", + "enum": [ + "pending", + "in_progress", + "completed", + "cancelled" + ], + "type": "string" + } + }, + "required": ["content", "status", "priority"], + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + }, + "type": "function" + }, + { + "description": "Fetch a webpage and return its content in the requested format. Use this to retrieve and analyze content from URLs.\n\nUsage notes:\n- The URL must be a fully-formed valid URL starting with http:// or https://\n- HTML responses are automatically converted to markdown or plain text based on the requested format\n- Format options: \"markdown\" (default), \"text\", or \"html\"\n- Default timeout is 30 seconds (max 120 seconds)\n- Maximum response size is 5 MB; content is further capped at the shared tool-output budget (50 KB / 2000 lines)\n- This tool is read-only and does not modify any files", + "name": "web_fetch", + "parameters": { + "additionalProperties": false, + "properties": { + "format": { + "description": "The format to return the content in (text, markdown, or html). HTML responses are automatically converted to the requested format. Defaults to \"markdown\".", + "enum": ["markdown", "text", "html"], + "type": "string" + }, + "timeout": { + "description": "Optional timeout in seconds. Defaults to 30, max 120.", + "type": "number" + }, + "url": { + "description": "The fully-formed URL to fetch content from. Must start with http:// or https://.", + "type": "string" + } + }, + "required": ["url"], + "type": "object" + }, + "type": "function" + }, + { + "type": "web_search" + }, + { + "description": "Load the full instructions for one available skill by name or id. Use this tool when the request clearly matches a listed skill description or when the user explicitly asks for that skill. This is not for MCP connections; use connection_search to access an installed connection. Loading adds the skill instructions to the current turn. Choose the \"skill\" value from the Available skills block.", + "name": "load_skill", + "parameters": { + "additionalProperties": false, + "properties": { + "skill": { + "description": "Available skill name or id.", + "type": "string" + } + }, + "required": ["skill"], + "type": "object" + }, + "type": "function" + }, + { + "description": "Reads a deterministic Eve documentation page fixture.", + "name": "read", + "parameters": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "url": { + "type": "string" + } + }, + "required": ["url"], + "type": "object" + }, + "type": "function" + }, + { + "description": "Searches the Eve instrumentation documentation fixture.", + "name": "search", + "parameters": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "query": { + "type": "string" + } + }, + "required": ["query"], + "type": "object" + }, + "type": "function" + }, + { + "description": "Delegate a focused subtask to a fresh copy of yourself. Use it to isolate complex work or split a large task into independent pieces. Issue multiple `agent` calls in one response to run a small fixed set in parallel. Each child has fresh history and state but shares your tools and sandbox, so include essential context in `message` and give parallel writers non-overlapping scopes.", + "name": "agent", + "parameters": { + "additionalProperties": false, + "properties": { + "message": { + "description": "The message to send to the subagent. Provide all context the subagent needs to complete the task; the subagent does not see the parent's history.", + "type": "string" + }, + "outputSchema": { + "description": "When provided, the subagent runs in task mode and must produce structured output matching this JSON Schema. The structured output becomes the tool result.", + "type": "object" + } + }, + "required": ["message"], + "type": "object" + }, + "type": "function" + } + ] + } + }, + "headers": {}, + "method": "POST", + "url": "https://api.openai.com/v1/responses" + }, + "response": { + "body": { + "chunks": [ + "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_0fea08d8f9bfc463006a4ba2c082ac819fa0b53db4088f4b4b\",\"object\":\"response\",\"created_at\":1783341760,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-mini-2026-03-17\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":true,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Ask the user a question and wait for their response before continuing. Use this when you need clarification or a choice from the user.\",\"name\":\"ask_question\",\"parameters\":{\"type\":\"object\",\"properties\":{\"allowFreeform\":{\"type\":\"boolean\",\"description\":\"Whether the user may answer with freeform text instead of selecting one of the provided options.\"},\"options\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Optional additional context for this option.\"},\"id\":{\"type\":\"string\",\"description\":\"Stable identifier for the option.\"},\"label\":{\"type\":\"string\",\"description\":\"User-facing label for the option.\"},\"style\":{\"type\":\"string\",\"enum\":[\"primary\",\"danger\",\"default\"],\"description\":\"Visual treatment hint for the option.\"}},\"required\":[\"description\",\"id\",\"label\",\"style\"],\"additionalProperties\":false},\"description\":\"Selectable answer options to present to the user.\"},\"prompt\":{\"type\":\"string\",\"description\":\"The prompt to present to the user.\"}},\"required\":[\"allowFreeform\",\"options\",\"prompt\"],\"additionalProperties\":false},\"strict\":true},{\"type\":\"function\",\"description\":\"Execute a shell command in the shared workspace environment.\",\"name\":\"bash\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The shell command to execute.\",\"type\":\"string\"}},\"required\":[\"command\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Fast file pattern matching tool that works with any codebase size.\\n\\nUsage:\\n- Supports glob patterns like \\\"**/*.js\\\" or \\\"src/**/*.ts\\\".\\n- Returns matching file paths.\\n- Use this tool when you need to find files by name patterns.\\n- If you are unsure of the correct file path, use the glob tool to look up filenames by glob pattern.\\n- Use the grep tool instead if you need to search file contents.\\n- Call this tool in parallel when you know there are multiple patterns to search for.\",\"name\":\"glob\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"limit\":{\"description\":\"Maximum number of results to return. Defaults to 100.\",\"maximum\":1000,\"minimum\":1,\"type\":\"integer\"},\"path\":{\"description\":\"The directory to search in. Defaults to /workspace. Must be an absolute path. Omit to use the default.\",\"type\":\"string\"},\"pattern\":{\"description\":\"The glob pattern to match files against (e.g. \\\"**/*.ts\\\", \\\"src/**/*.js\\\").\",\"type\":\"string\"}},\"required\":[\"limit\",\"path\",\"pattern\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Fast content search tool that works with any codebase size.\\n\\nUsage:\\n- Searches file contents using regular expressions.\\n- Supports full regex syntax (e.g. \\\"log.*Error\\\", \\\"function\\\\s+\\\\w+\\\").\\n- Filter files by pattern with the glob parameter (e.g. \\\"*.js\\\", \\\"*.{ts,tsx}\\\").\\n- Returns matching lines with file paths and line numbers.\\n- Use this tool when you need to find files containing specific patterns.\\n- Use the glob tool instead if you only need to find files by name.\\n- Call this tool in parallel when you have multiple independent searches.\\n- Any line longer than 2000 characters is truncated.\",\"name\":\"grep\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"context\":{\"description\":\"Number of surrounding context lines to include before and after each match. Defaults to 0.\",\"minimum\":0,\"type\":\"integer\"},\"glob\":{\"description\":\"Filter files by glob pattern (e.g. \\\"*.ts\\\", \\\"*.{ts,tsx}\\\").\",\"type\":\"string\"},\"ignoreCase\":{\"description\":\"Perform case-insensitive search. Defaults to false.\",\"type\":\"boolean\"},\"limit\":{\"description\":\"Maximum number of matches to return per file. Defaults to 100.\",\"maximum\":1000,\"minimum\":1,\"type\":\"integer\"},\"literal\":{\"description\":\"Treat the pattern as a literal string instead of a regular expression. Defaults to false.\",\"type\":\"boolean\"},\"path\":{\"description\":\"The directory or file to search in. Defaults to /workspace. Must be an absolute path. Omit to use the default.\",\"type\":\"string\"},\"pattern\":{\"description\":\"The regex pattern to search for in file contents (e.g. \\\"log.*Error\\\", \\\"function\\\\s+\\\\w+\\\").\",\"type\":\"string\"}},\"required\":[\"context\",\"glob\",\"ignoreCase\",\"limit\",\"literal\",\"path\",\"pattern\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Read a file from the local filesystem. If the path does not exist, an error is returned.\\n\\nUsage:\\n- The filePath parameter should be an absolute path.\\n- By default, this tool returns up to 2000 lines from the start of the file.\\n- The offset parameter is the line number to start from (1-indexed).\\n- To read later sections, call this tool again with a larger offset.\\n- Use the grep tool to find specific content in large files or files with long lines.\\n- If you are unsure of the correct file path, use the glob tool to look up filenames by glob pattern.\\n- Contents are returned with each line prefixed by its line number as `: `. For example, if a file has contents \\\"foo\\\\n\\\", you will receive \\\"1: foo\\\\n\\\".\\n- Any line longer than 2000 characters is truncated.\\n- Call this tool in parallel when you know there are multiple files you want to read.\\n- Avoid tiny repeated slices (30 line chunks). If you need more context, read a larger window.\",\"name\":\"read_file\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"filePath\":{\"description\":\"The absolute path to the file to read.\",\"type\":\"string\"},\"limit\":{\"description\":\"Maximum number of lines to return. Defaults to 2000.\",\"minimum\":1,\"type\":\"integer\"},\"offset\":{\"description\":\"1-based line number to start from. Defaults to 1.\",\"minimum\":1,\"type\":\"integer\"}},\"required\":[\"filePath\",\"limit\",\"offset\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Writes a file to the local filesystem.\\n\\nUsage:\\n- This tool will overwrite the existing file if there is one at the provided path.\\n- If this is an existing file, you MUST use the read_file tool first to read the file's contents. This tool will fail if you did not read the file first.\\n- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.\\n- NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.\\n- Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked.\",\"name\":\"write_file\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"content\":{\"description\":\"Complete replacement file contents.\",\"type\":\"string\"},\"filePath\":{\"description\":\"The absolute path to the file to write (must be absolute, not relative).\",\"type\":\"string\"}},\"required\":[\"content\",\"filePath\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Use this tool to create and manage a structured task list for the current session.\\nThis helps you track progress, organize complex tasks, and demonstrate thoroughness.\\n\\nWhen to use:\\n- Complex multistep tasks requiring 3 or more distinct steps\\n- When the user provides multiple tasks or a numbered list\\n- After receiving new instructions, to capture requirements\\n- After completing a task, to mark it complete and add follow-ups\\n\\nWhen NOT to use:\\n- Single, straightforward tasks that need no tracking\\n- Purely conversational or informational requests\\n\\nUsage:\\n- Call with `todos` to replace the entire list (full replacement write)\\n- Call without `todos` to read the current list\\n- Both return the full current list with status counts\\n- Mark tasks in_progress when you start, completed when done\\n- Only have ONE task in_progress at a time\",\"name\":\"todo\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"todos\":{\"description\":\"The updated todo list. Omit to read the current list without modifying it.\",\"items\":{\"additionalProperties\":false,\"properties\":{\"content\":{\"description\":\"Brief description of the task.\",\"type\":\"string\"},\"priority\":{\"description\":\"Priority level of the task.\",\"enum\":[\"high\",\"medium\",\"low\"],\"type\":\"string\"},\"status\":{\"description\":\"Current status of the task.\",\"enum\":[\"pending\",\"in_progress\",\"completed\",\"cancelled\"],\"type\":\"string\"}},\"required\":[\"content\",\"priority\",\"status\"],\"type\":\"object\"},\"type\":\"array\"}},\"type\":\"object\",\"required\":[\"todos\"]},\"strict\":true},{\"type\":\"function\",\"description\":\"Fetch a webpage and return its content in the requested format. Use this to retrieve and analyze content from URLs.\\n\\nUsage notes:\\n- The URL must be a fully-formed valid URL starting with http:// or https://\\n- HTML responses are automatically converted to markdown or plain text based on the requested format\\n- Format options: \\\"markdown\\\" (default), \\\"text\\\", or \\\"html\\\"\\n- Default timeout is 30 seconds (max 120 seconds)\\n- Maximum response size is 5 MB; content is further capped at the shared tool-output budget (50 KB / 2000 lines)\\n- This tool is read-only and does not modify any files\",\"name\":\"web_fetch\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"format\":{\"description\":\"The format to return the content in (text, markdown, or html). HTML responses are automatically converted to the requested format. Defaults to \\\"markdown\\\".\",\"enum\":[\"markdown\",\"text\",\"html\"],\"type\":\"string\"},\"timeout\":{\"description\":\"Optional timeout in seconds. Defaults to 30, max 120.\",\"type\":\"number\"},\"url\":{\"description\":\"The fully-formed URL to fetch content from. Must start with http:// or https://.\",\"type\":\"string\"}},\"required\":[\"format\",\"timeout\",\"url\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Load the full instructions for one available skill by name or id. Use this tool when the request clearly matches a listed skill description or when the user explicitly asks for that skill. This is not for MCP connections; use connection_search to access an installed connection. Loading adds the skill instructions to the current turn. Choose the \\\"skill\\\" value from the Available skills block.\",\"name\":\"load_skill\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"skill\":{\"description\":\"Available skill name or id.\",\"type\":\"string\"}},\"required\":[\"skill\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Reads a deterministic Eve documentation page fixture.\",\"name\":\"read\",\"parameters\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"url\":{\"type\":\"string\"}},\"required\":[\"url\"],\"additionalProperties\":false},\"strict\":true},{\"type\":\"function\",\"description\":\"Searches the Eve instrumentation documentation fixture.\",\"name\":\"search\",\"parameters\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\"}},\"required\":[\"query\"],\"additionalProperties\":false},\"strict\":true},{\"type\":\"function\",\"description\":\"Delegate a focused subtask to a fresh copy of yourself. Use it to isolate complex work or split a large task into independent pieces. Issue multiple `agent` calls in one response to run a small fixed set in parallel. Each child has fresh history and state but shares your tools and sandbox, so include essential context in `message` and give parallel writers non-overlapping scopes.\",\"name\":\"agent\",\"parameters\":{\"type\":\"object\",\"properties\":{\"message\":{\"type\":\"string\",\"description\":\"The message to send to the subagent. Provide all context the subagent needs to complete the task; the subagent does not see the parent's history.\"},\"outputSchema\":{\"type\":\"object\",\"description\":\"When provided, the subagent runs in task mode and must produce structured output matching this JSON Schema. The structured output becomes the tool result.\"}},\"required\":[\"message\"],\"additionalProperties\":false},\"strict\":false},{\"type\":\"web_search\",\"return_token_budget\":\"default\",\"search_content_types\":[\"text\"],\"search_context_size\":\"medium\",\"user_location\":{\"type\":\"approximate\",\"city\":null,\"country\":\"US\",\"region\":null,\"timezone\":null}}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}", + "event: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_0fea08d8f9bfc463006a4ba2c082ac819fa0b53db4088f4b4b\",\"object\":\"response\",\"created_at\":1783341760,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-mini-2026-03-17\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":true,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Ask the user a question and wait for their response before continuing. Use this when you need clarification or a choice from the user.\",\"name\":\"ask_question\",\"parameters\":{\"type\":\"object\",\"properties\":{\"allowFreeform\":{\"type\":\"boolean\",\"description\":\"Whether the user may answer with freeform text instead of selecting one of the provided options.\"},\"options\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Optional additional context for this option.\"},\"id\":{\"type\":\"string\",\"description\":\"Stable identifier for the option.\"},\"label\":{\"type\":\"string\",\"description\":\"User-facing label for the option.\"},\"style\":{\"type\":\"string\",\"enum\":[\"primary\",\"danger\",\"default\"],\"description\":\"Visual treatment hint for the option.\"}},\"required\":[\"description\",\"id\",\"label\",\"style\"],\"additionalProperties\":false},\"description\":\"Selectable answer options to present to the user.\"},\"prompt\":{\"type\":\"string\",\"description\":\"The prompt to present to the user.\"}},\"required\":[\"allowFreeform\",\"options\",\"prompt\"],\"additionalProperties\":false},\"strict\":true},{\"type\":\"function\",\"description\":\"Execute a shell command in the shared workspace environment.\",\"name\":\"bash\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The shell command to execute.\",\"type\":\"string\"}},\"required\":[\"command\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Fast file pattern matching tool that works with any codebase size.\\n\\nUsage:\\n- Supports glob patterns like \\\"**/*.js\\\" or \\\"src/**/*.ts\\\".\\n- Returns matching file paths.\\n- Use this tool when you need to find files by name patterns.\\n- If you are unsure of the correct file path, use the glob tool to look up filenames by glob pattern.\\n- Use the grep tool instead if you need to search file contents.\\n- Call this tool in parallel when you know there are multiple patterns to search for.\",\"name\":\"glob\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"limit\":{\"description\":\"Maximum number of results to return. Defaults to 100.\",\"maximum\":1000,\"minimum\":1,\"type\":\"integer\"},\"path\":{\"description\":\"The directory to search in. Defaults to /workspace. Must be an absolute path. Omit to use the default.\",\"type\":\"string\"},\"pattern\":{\"description\":\"The glob pattern to match files against (e.g. \\\"**/*.ts\\\", \\\"src/**/*.js\\\").\",\"type\":\"string\"}},\"required\":[\"limit\",\"path\",\"pattern\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Fast content search tool that works with any codebase size.\\n\\nUsage:\\n- Searches file contents using regular expressions.\\n- Supports full regex syntax (e.g. \\\"log.*Error\\\", \\\"function\\\\s+\\\\w+\\\").\\n- Filter files by pattern with the glob parameter (e.g. \\\"*.js\\\", \\\"*.{ts,tsx}\\\").\\n- Returns matching lines with file paths and line numbers.\\n- Use this tool when you need to find files containing specific patterns.\\n- Use the glob tool instead if you only need to find files by name.\\n- Call this tool in parallel when you have multiple independent searches.\\n- Any line longer than 2000 characters is truncated.\",\"name\":\"grep\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"context\":{\"description\":\"Number of surrounding context lines to include before and after each match. Defaults to 0.\",\"minimum\":0,\"type\":\"integer\"},\"glob\":{\"description\":\"Filter files by glob pattern (e.g. \\\"*.ts\\\", \\\"*.{ts,tsx}\\\").\",\"type\":\"string\"},\"ignoreCase\":{\"description\":\"Perform case-insensitive search. Defaults to false.\",\"type\":\"boolean\"},\"limit\":{\"description\":\"Maximum number of matches to return per file. Defaults to 100.\",\"maximum\":1000,\"minimum\":1,\"type\":\"integer\"},\"literal\":{\"description\":\"Treat the pattern as a literal string instead of a regular expression. Defaults to false.\",\"type\":\"boolean\"},\"path\":{\"description\":\"The directory or file to search in. Defaults to /workspace. Must be an absolute path. Omit to use the default.\",\"type\":\"string\"},\"pattern\":{\"description\":\"The regex pattern to search for in file contents (e.g. \\\"log.*Error\\\", \\\"function\\\\s+\\\\w+\\\").\",\"type\":\"string\"}},\"required\":[\"context\",\"glob\",\"ignoreCase\",\"limit\",\"literal\",\"path\",\"pattern\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Read a file from the local filesystem. If the path does not exist, an error is returned.\\n\\nUsage:\\n- The filePath parameter should be an absolute path.\\n- By default, this tool returns up to 2000 lines from the start of the file.\\n- The offset parameter is the line number to start from (1-indexed).\\n- To read later sections, call this tool again with a larger offset.\\n- Use the grep tool to find specific content in large files or files with long lines.\\n- If you are unsure of the correct file path, use the glob tool to look up filenames by glob pattern.\\n- Contents are returned with each line prefixed by its line number as `: `. For example, if a file has contents \\\"foo\\\\n\\\", you will receive \\\"1: foo\\\\n\\\".\\n- Any line longer than 2000 characters is truncated.\\n- Call this tool in parallel when you know there are multiple files you want to read.\\n- Avoid tiny repeated slices (30 line chunks). If you need more context, read a larger window.\",\"name\":\"read_file\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"filePath\":{\"description\":\"The absolute path to the file to read.\",\"type\":\"string\"},\"limit\":{\"description\":\"Maximum number of lines to return. Defaults to 2000.\",\"minimum\":1,\"type\":\"integer\"},\"offset\":{\"description\":\"1-based line number to start from. Defaults to 1.\",\"minimum\":1,\"type\":\"integer\"}},\"required\":[\"filePath\",\"limit\",\"offset\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Writes a file to the local filesystem.\\n\\nUsage:\\n- This tool will overwrite the existing file if there is one at the provided path.\\n- If this is an existing file, you MUST use the read_file tool first to read the file's contents. This tool will fail if you did not read the file first.\\n- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.\\n- NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.\\n- Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked.\",\"name\":\"write_file\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"content\":{\"description\":\"Complete replacement file contents.\",\"type\":\"string\"},\"filePath\":{\"description\":\"The absolute path to the file to write (must be absolute, not relative).\",\"type\":\"string\"}},\"required\":[\"content\",\"filePath\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Use this tool to create and manage a structured task list for the current session.\\nThis helps you track progress, organize complex tasks, and demonstrate thoroughness.\\n\\nWhen to use:\\n- Complex multistep tasks requiring 3 or more distinct steps\\n- When the user provides multiple tasks or a numbered list\\n- After receiving new instructions, to capture requirements\\n- After completing a task, to mark it complete and add follow-ups\\n\\nWhen NOT to use:\\n- Single, straightforward tasks that need no tracking\\n- Purely conversational or informational requests\\n\\nUsage:\\n- Call with `todos` to replace the entire list (full replacement write)\\n- Call without `todos` to read the current list\\n- Both return the full current list with status counts\\n- Mark tasks in_progress when you start, completed when done\\n- Only have ONE task in_progress at a time\",\"name\":\"todo\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"todos\":{\"description\":\"The updated todo list. Omit to read the current list without modifying it.\",\"items\":{\"additionalProperties\":false,\"properties\":{\"content\":{\"description\":\"Brief description of the task.\",\"type\":\"string\"},\"priority\":{\"description\":\"Priority level of the task.\",\"enum\":[\"high\",\"medium\",\"low\"],\"type\":\"string\"},\"status\":{\"description\":\"Current status of the task.\",\"enum\":[\"pending\",\"in_progress\",\"completed\",\"cancelled\"],\"type\":\"string\"}},\"required\":[\"content\",\"priority\",\"status\"],\"type\":\"object\"},\"type\":\"array\"}},\"type\":\"object\",\"required\":[\"todos\"]},\"strict\":true},{\"type\":\"function\",\"description\":\"Fetch a webpage and return its content in the requested format. Use this to retrieve and analyze content from URLs.\\n\\nUsage notes:\\n- The URL must be a fully-formed valid URL starting with http:// or https://\\n- HTML responses are automatically converted to markdown or plain text based on the requested format\\n- Format options: \\\"markdown\\\" (default), \\\"text\\\", or \\\"html\\\"\\n- Default timeout is 30 seconds (max 120 seconds)\\n- Maximum response size is 5 MB; content is further capped at the shared tool-output budget (50 KB / 2000 lines)\\n- This tool is read-only and does not modify any files\",\"name\":\"web_fetch\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"format\":{\"description\":\"The format to return the content in (text, markdown, or html). HTML responses are automatically converted to the requested format. Defaults to \\\"markdown\\\".\",\"enum\":[\"markdown\",\"text\",\"html\"],\"type\":\"string\"},\"timeout\":{\"description\":\"Optional timeout in seconds. Defaults to 30, max 120.\",\"type\":\"number\"},\"url\":{\"description\":\"The fully-formed URL to fetch content from. Must start with http:// or https://.\",\"type\":\"string\"}},\"required\":[\"format\",\"timeout\",\"url\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Load the full instructions for one available skill by name or id. Use this tool when the request clearly matches a listed skill description or when the user explicitly asks for that skill. This is not for MCP connections; use connection_search to access an installed connection. Loading adds the skill instructions to the current turn. Choose the \\\"skill\\\" value from the Available skills block.\",\"name\":\"load_skill\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"skill\":{\"description\":\"Available skill name or id.\",\"type\":\"string\"}},\"required\":[\"skill\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Reads a deterministic Eve documentation page fixture.\",\"name\":\"read\",\"parameters\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"url\":{\"type\":\"string\"}},\"required\":[\"url\"],\"additionalProperties\":false},\"strict\":true},{\"type\":\"function\",\"description\":\"Searches the Eve instrumentation documentation fixture.\",\"name\":\"search\",\"parameters\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\"}},\"required\":[\"query\"],\"additionalProperties\":false},\"strict\":true},{\"type\":\"function\",\"description\":\"Delegate a focused subtask to a fresh copy of yourself. Use it to isolate complex work or split a large task into independent pieces. Issue multiple `agent` calls in one response to run a small fixed set in parallel. Each child has fresh history and state but shares your tools and sandbox, so include essential context in `message` and give parallel writers non-overlapping scopes.\",\"name\":\"agent\",\"parameters\":{\"type\":\"object\",\"properties\":{\"message\":{\"type\":\"string\",\"description\":\"The message to send to the subagent. Provide all context the subagent needs to complete the task; the subagent does not see the parent's history.\"},\"outputSchema\":{\"type\":\"object\",\"description\":\"When provided, the subagent runs in task mode and must produce structured output matching this JSON Schema. The structured output becomes the tool result.\"}},\"required\":[\"message\"],\"additionalProperties\":false},\"strict\":false},{\"type\":\"web_search\",\"return_token_budget\":\"default\",\"search_content_types\":[\"text\"],\"search_context_size\":\"medium\",\"user_location\":{\"type\":\"approximate\",\"city\":null,\"country\":\"US\",\"region\":null,\"timezone\":null}}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}", + "event: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"fc_0fea08d8f9bfc463006a4ba2c1380c819f949f689dbcdeda46\",\"type\":\"function_call\",\"status\":\"in_progress\",\"arguments\":\"\",\"call_id\":\"call_kZ7AzZ9mCqataCzrSSLxRkFh\",\"name\":\"read\"},\"output_index\":0,\"sequence_number\":2}", + "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"{\\\"\",\"item_id\":\"fc_0fea08d8f9bfc463006a4ba2c1380c819f949f689dbcdeda46\",\"obfuscation\":\"hCZj3eURcxA5u7\",\"output_index\":0,\"sequence_number\":3}", + "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"url\",\"item_id\":\"fc_0fea08d8f9bfc463006a4ba2c1380c819f949f689dbcdeda46\",\"obfuscation\":\"YTahm2y9wc2a2\",\"output_index\":0,\"sequence_number\":4}", + "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\":\\\"\",\"item_id\":\"fc_0fea08d8f9bfc463006a4ba2c1380c819f949f689dbcdeda46\",\"obfuscation\":\"O9KdvSTHznEA4\",\"output_index\":0,\"sequence_number\":5}", + "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"https\",\"item_id\":\"fc_0fea08d8f9bfc463006a4ba2c1380c819f949f689dbcdeda46\",\"obfuscation\":\"cZXggUplvDp\",\"output_index\":0,\"sequence_number\":6}", + "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"://\",\"item_id\":\"fc_0fea08d8f9bfc463006a4ba2c1380c819f949f689dbcdeda46\",\"obfuscation\":\"W1IPVLgHRdfnK\",\"output_index\":0,\"sequence_number\":7}", + "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"eve\",\"item_id\":\"fc_0fea08d8f9bfc463006a4ba2c1380c819f949f689dbcdeda46\",\"obfuscation\":\"IlAAdBPloI7ZI\",\"output_index\":0,\"sequence_number\":8}", + "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\".dev\",\"item_id\":\"fc_0fea08d8f9bfc463006a4ba2c1380c819f949f689dbcdeda46\",\"obfuscation\":\"vuTEb7koKwkU\",\"output_index\":0,\"sequence_number\":9}", + "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/docs\",\"item_id\":\"fc_0fea08d8f9bfc463006a4ba2c1380c819f949f689dbcdeda46\",\"obfuscation\":\"3tkcCXVFaVS\",\"output_index\":0,\"sequence_number\":10}", + "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/g\",\"item_id\":\"fc_0fea08d8f9bfc463006a4ba2c1380c819f949f689dbcdeda46\",\"obfuscation\":\"Fnb0kAAbyEFxmo\",\"output_index\":0,\"sequence_number\":11}", + "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"uid\",\"item_id\":\"fc_0fea08d8f9bfc463006a4ba2c1380c819f949f689dbcdeda46\",\"obfuscation\":\"F53YeZTZPgt2h\",\"output_index\":0,\"sequence_number\":12}", + "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"es\",\"item_id\":\"fc_0fea08d8f9bfc463006a4ba2c1380c819f949f689dbcdeda46\",\"obfuscation\":\"wc5Z1NagkfGlho\",\"output_index\":0,\"sequence_number\":13}", + "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"/in\",\"item_id\":\"fc_0fea08d8f9bfc463006a4ba2c1380c819f949f689dbcdeda46\",\"obfuscation\":\"YVqWvIHZvcnXv\",\"output_index\":0,\"sequence_number\":14}", + "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"strument\",\"item_id\":\"fc_0fea08d8f9bfc463006a4ba2c1380c819f949f689dbcdeda46\",\"obfuscation\":\"9e6Bnngc\",\"output_index\":0,\"sequence_number\":15}", + "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"ation\",\"item_id\":\"fc_0fea08d8f9bfc463006a4ba2c1380c819f949f689dbcdeda46\",\"obfuscation\":\"5nPU7ASnvsR\",\"output_index\":0,\"sequence_number\":16}", + "event: response.function_call_arguments.delta\ndata: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"\\\"}\",\"item_id\":\"fc_0fea08d8f9bfc463006a4ba2c1380c819f949f689dbcdeda46\",\"obfuscation\":\"tYrx4WFsbNsUju\",\"output_index\":0,\"sequence_number\":17}", + "event: response.function_call_arguments.done\ndata: {\"type\":\"response.function_call_arguments.done\",\"arguments\":\"{\\\"url\\\":\\\"https://eve.dev/docs/guides/instrumentation\\\"}\",\"item_id\":\"fc_0fea08d8f9bfc463006a4ba2c1380c819f949f689dbcdeda46\",\"output_index\":0,\"sequence_number\":18}", + "event: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"fc_0fea08d8f9bfc463006a4ba2c1380c819f949f689dbcdeda46\",\"type\":\"function_call\",\"status\":\"completed\",\"arguments\":\"{\\\"url\\\":\\\"https://eve.dev/docs/guides/instrumentation\\\"}\",\"call_id\":\"call_kZ7AzZ9mCqataCzrSSLxRkFh\",\"name\":\"read\"},\"output_index\":0,\"sequence_number\":19}", + "event: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_0fea08d8f9bfc463006a4ba2c082ac819fa0b53db4088f4b4b\",\"object\":\"response\",\"created_at\":1783341760,\"status\":\"completed\",\"background\":false,\"completed_at\":1783341761,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-mini-2026-03-17\",\"moderation\":null,\"output\":[{\"id\":\"fc_0fea08d8f9bfc463006a4ba2c1380c819f949f689dbcdeda46\",\"type\":\"function_call\",\"status\":\"completed\",\"arguments\":\"{\\\"url\\\":\\\"https://eve.dev/docs/guides/instrumentation\\\"}\",\"call_id\":\"call_kZ7AzZ9mCqataCzrSSLxRkFh\",\"name\":\"read\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":true,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Ask the user a question and wait for their response before continuing. Use this when you need clarification or a choice from the user.\",\"name\":\"ask_question\",\"parameters\":{\"type\":\"object\",\"properties\":{\"allowFreeform\":{\"type\":\"boolean\",\"description\":\"Whether the user may answer with freeform text instead of selecting one of the provided options.\"},\"options\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Optional additional context for this option.\"},\"id\":{\"type\":\"string\",\"description\":\"Stable identifier for the option.\"},\"label\":{\"type\":\"string\",\"description\":\"User-facing label for the option.\"},\"style\":{\"type\":\"string\",\"enum\":[\"primary\",\"danger\",\"default\"],\"description\":\"Visual treatment hint for the option.\"}},\"required\":[\"description\",\"id\",\"label\",\"style\"],\"additionalProperties\":false},\"description\":\"Selectable answer options to present to the user.\"},\"prompt\":{\"type\":\"string\",\"description\":\"The prompt to present to the user.\"}},\"required\":[\"allowFreeform\",\"options\",\"prompt\"],\"additionalProperties\":false},\"strict\":true},{\"type\":\"function\",\"description\":\"Execute a shell command in the shared workspace environment.\",\"name\":\"bash\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The shell command to execute.\",\"type\":\"string\"}},\"required\":[\"command\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Fast file pattern matching tool that works with any codebase size.\\n\\nUsage:\\n- Supports glob patterns like \\\"**/*.js\\\" or \\\"src/**/*.ts\\\".\\n- Returns matching file paths.\\n- Use this tool when you need to find files by name patterns.\\n- If you are unsure of the correct file path, use the glob tool to look up filenames by glob pattern.\\n- Use the grep tool instead if you need to search file contents.\\n- Call this tool in parallel when you know there are multiple patterns to search for.\",\"name\":\"glob\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"limit\":{\"description\":\"Maximum number of results to return. Defaults to 100.\",\"maximum\":1000,\"minimum\":1,\"type\":\"integer\"},\"path\":{\"description\":\"The directory to search in. Defaults to /workspace. Must be an absolute path. Omit to use the default.\",\"type\":\"string\"},\"pattern\":{\"description\":\"The glob pattern to match files against (e.g. \\\"**/*.ts\\\", \\\"src/**/*.js\\\").\",\"type\":\"string\"}},\"required\":[\"limit\",\"path\",\"pattern\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Fast content search tool that works with any codebase size.\\n\\nUsage:\\n- Searches file contents using regular expressions.\\n- Supports full regex syntax (e.g. \\\"log.*Error\\\", \\\"function\\\\s+\\\\w+\\\").\\n- Filter files by pattern with the glob parameter (e.g. \\\"*.js\\\", \\\"*.{ts,tsx}\\\").\\n- Returns matching lines with file paths and line numbers.\\n- Use this tool when you need to find files containing specific patterns.\\n- Use the glob tool instead if you only need to find files by name.\\n- Call this tool in parallel when you have multiple independent searches.\\n- Any line longer than 2000 characters is truncated.\",\"name\":\"grep\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"context\":{\"description\":\"Number of surrounding context lines to include before and after each match. Defaults to 0.\",\"minimum\":0,\"type\":\"integer\"},\"glob\":{\"description\":\"Filter files by glob pattern (e.g. \\\"*.ts\\\", \\\"*.{ts,tsx}\\\").\",\"type\":\"string\"},\"ignoreCase\":{\"description\":\"Perform case-insensitive search. Defaults to false.\",\"type\":\"boolean\"},\"limit\":{\"description\":\"Maximum number of matches to return per file. Defaults to 100.\",\"maximum\":1000,\"minimum\":1,\"type\":\"integer\"},\"literal\":{\"description\":\"Treat the pattern as a literal string instead of a regular expression. Defaults to false.\",\"type\":\"boolean\"},\"path\":{\"description\":\"The directory or file to search in. Defaults to /workspace. Must be an absolute path. Omit to use the default.\",\"type\":\"string\"},\"pattern\":{\"description\":\"The regex pattern to search for in file contents (e.g. \\\"log.*Error\\\", \\\"function\\\\s+\\\\w+\\\").\",\"type\":\"string\"}},\"required\":[\"context\",\"glob\",\"ignoreCase\",\"limit\",\"literal\",\"path\",\"pattern\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Read a file from the local filesystem. If the path does not exist, an error is returned.\\n\\nUsage:\\n- The filePath parameter should be an absolute path.\\n- By default, this tool returns up to 2000 lines from the start of the file.\\n- The offset parameter is the line number to start from (1-indexed).\\n- To read later sections, call this tool again with a larger offset.\\n- Use the grep tool to find specific content in large files or files with long lines.\\n- If you are unsure of the correct file path, use the glob tool to look up filenames by glob pattern.\\n- Contents are returned with each line prefixed by its line number as `: `. For example, if a file has contents \\\"foo\\\\n\\\", you will receive \\\"1: foo\\\\n\\\".\\n- Any line longer than 2000 characters is truncated.\\n- Call this tool in parallel when you know there are multiple files you want to read.\\n- Avoid tiny repeated slices (30 line chunks). If you need more context, read a larger window.\",\"name\":\"read_file\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"filePath\":{\"description\":\"The absolute path to the file to read.\",\"type\":\"string\"},\"limit\":{\"description\":\"Maximum number of lines to return. Defaults to 2000.\",\"minimum\":1,\"type\":\"integer\"},\"offset\":{\"description\":\"1-based line number to start from. Defaults to 1.\",\"minimum\":1,\"type\":\"integer\"}},\"required\":[\"filePath\",\"limit\",\"offset\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Writes a file to the local filesystem.\\n\\nUsage:\\n- This tool will overwrite the existing file if there is one at the provided path.\\n- If this is an existing file, you MUST use the read_file tool first to read the file's contents. This tool will fail if you did not read the file first.\\n- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.\\n- NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.\\n- Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked.\",\"name\":\"write_file\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"content\":{\"description\":\"Complete replacement file contents.\",\"type\":\"string\"},\"filePath\":{\"description\":\"The absolute path to the file to write (must be absolute, not relative).\",\"type\":\"string\"}},\"required\":[\"content\",\"filePath\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Use this tool to create and manage a structured task list for the current session.\\nThis helps you track progress, organize complex tasks, and demonstrate thoroughness.\\n\\nWhen to use:\\n- Complex multistep tasks requiring 3 or more distinct steps\\n- When the user provides multiple tasks or a numbered list\\n- After receiving new instructions, to capture requirements\\n- After completing a task, to mark it complete and add follow-ups\\n\\nWhen NOT to use:\\n- Single, straightforward tasks that need no tracking\\n- Purely conversational or informational requests\\n\\nUsage:\\n- Call with `todos` to replace the entire list (full replacement write)\\n- Call without `todos` to read the current list\\n- Both return the full current list with status counts\\n- Mark tasks in_progress when you start, completed when done\\n- Only have ONE task in_progress at a time\",\"name\":\"todo\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"todos\":{\"description\":\"The updated todo list. Omit to read the current list without modifying it.\",\"items\":{\"additionalProperties\":false,\"properties\":{\"content\":{\"description\":\"Brief description of the task.\",\"type\":\"string\"},\"priority\":{\"description\":\"Priority level of the task.\",\"enum\":[\"high\",\"medium\",\"low\"],\"type\":\"string\"},\"status\":{\"description\":\"Current status of the task.\",\"enum\":[\"pending\",\"in_progress\",\"completed\",\"cancelled\"],\"type\":\"string\"}},\"required\":[\"content\",\"priority\",\"status\"],\"type\":\"object\"},\"type\":\"array\"}},\"type\":\"object\",\"required\":[\"todos\"]},\"strict\":true},{\"type\":\"function\",\"description\":\"Fetch a webpage and return its content in the requested format. Use this to retrieve and analyze content from URLs.\\n\\nUsage notes:\\n- The URL must be a fully-formed valid URL starting with http:// or https://\\n- HTML responses are automatically converted to markdown or plain text based on the requested format\\n- Format options: \\\"markdown\\\" (default), \\\"text\\\", or \\\"html\\\"\\n- Default timeout is 30 seconds (max 120 seconds)\\n- Maximum response size is 5 MB; content is further capped at the shared tool-output budget (50 KB / 2000 lines)\\n- This tool is read-only and does not modify any files\",\"name\":\"web_fetch\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"format\":{\"description\":\"The format to return the content in (text, markdown, or html). HTML responses are automatically converted to the requested format. Defaults to \\\"markdown\\\".\",\"enum\":[\"markdown\",\"text\",\"html\"],\"type\":\"string\"},\"timeout\":{\"description\":\"Optional timeout in seconds. Defaults to 30, max 120.\",\"type\":\"number\"},\"url\":{\"description\":\"The fully-formed URL to fetch content from. Must start with http:// or https://.\",\"type\":\"string\"}},\"required\":[\"format\",\"timeout\",\"url\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Load the full instructions for one available skill by name or id. Use this tool when the request clearly matches a listed skill description or when the user explicitly asks for that skill. This is not for MCP connections; use connection_search to access an installed connection. Loading adds the skill instructions to the current turn. Choose the \\\"skill\\\" value from the Available skills block.\",\"name\":\"load_skill\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"skill\":{\"description\":\"Available skill name or id.\",\"type\":\"string\"}},\"required\":[\"skill\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Reads a deterministic Eve documentation page fixture.\",\"name\":\"read\",\"parameters\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"url\":{\"type\":\"string\"}},\"required\":[\"url\"],\"additionalProperties\":false},\"strict\":true},{\"type\":\"function\",\"description\":\"Searches the Eve instrumentation documentation fixture.\",\"name\":\"search\",\"parameters\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\"}},\"required\":[\"query\"],\"additionalProperties\":false},\"strict\":true},{\"type\":\"function\",\"description\":\"Delegate a focused subtask to a fresh copy of yourself. Use it to isolate complex work or split a large task into independent pieces. Issue multiple `agent` calls in one response to run a small fixed set in parallel. Each child has fresh history and state but shares your tools and sandbox, so include essential context in `message` and give parallel writers non-overlapping scopes.\",\"name\":\"agent\",\"parameters\":{\"type\":\"object\",\"properties\":{\"message\":{\"type\":\"string\",\"description\":\"The message to send to the subagent. Provide all context the subagent needs to complete the task; the subagent does not see the parent's history.\"},\"outputSchema\":{\"type\":\"object\",\"description\":\"When provided, the subagent runs in task mode and must produce structured output matching this JSON Schema. The structured output becomes the tool result.\"}},\"required\":[\"message\"],\"additionalProperties\":false},\"strict\":false},{\"type\":\"web_search\",\"return_token_budget\":\"default\",\"search_content_types\":[\"text\"],\"search_context_size\":\"medium\",\"user_location\":{\"type\":\"approximate\",\"city\":null,\"country\":\"US\",\"region\":null,\"timezone\":null}}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":6634,\"input_tokens_details\":{\"cached_tokens\":0},\"output_tokens\":27,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":6661},\"user\":null,\"metadata\":{}},\"sequence_number\":20}" + ], + "kind": "sse" + }, + "headers": { + "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", + "alt-svc": "h3=\":443\"; ma=86400", + "cf-cache-status": "DYNAMIC", + "cf-ray": "a16eb0d248325b89-VIE", + "connection": "keep-alive", + "content-type": "text/event-stream; charset=utf-8", + "date": "Mon, 06 Jul 2026 12:42:41 GMT", + "openai-processing-ms": "505", + "openai-version": "2020-10-01", + "server": "cloudflare", + "set-cookie": "[REDACTED]", + "strict-transport-security": "max-age=31536000; includeSubDomains; preload", + "transfer-encoding": "chunked", + "x-content-type-options": "nosniff", + "x-ratelimit-limit-requests": "30000", + "x-ratelimit-limit-tokens": "180000000", + "x-ratelimit-remaining-requests": "29999", + "x-ratelimit-remaining-tokens": "179992725", + "x-ratelimit-reset-requests": "2ms", + "x-ratelimit-reset-tokens": "2ms", + "x-request-id": "2a2e63ed-abb5-4d63-9732-9e1203d43cd5" + }, + "status": 200, + "statusText": "OK" + } + }, + { + "callIndex": 2, + "id": "4b05a20680820f51", + "matchKey": "POST api.openai.com/v1/responses", + "recordedAt": "2026-07-06T12:42:43.115Z", + "request": { + "body": { + "kind": "json", + "value": { + "include": ["web_search_call.action.sources"], + "input": [ + { + "content": "Instructions (instructions)\nYou are a deterministic fixture agent for Braintrust Eve instrumentation tests.\n\nFor every user task:\n\n1. Call the search tool exactly once with the full user message as the query.\n2. After the search result is available, call the read tool exactly once with the\n URL returned by search.\n3. After the read result is available, answer with a single sentence that starts\n with \"Final answer from read:\" and includes the search title, URL, and read\n excerpt.\n\nTool execution\nA single tool or subagent call runs as one serial action. If you call multiple independent tools or subagents in one response, eve treats that batch as parallel work. Only batch work that is independent and does not rely on another call in the same response.", + "role": "developer" + }, + { + "content": [ + { + "text": "Run the Braintrust Eve instrumentation e2e scenario", + "type": "input_text" + } + ], + "role": "user" + }, + { + "id": "fc_0fea08d8f9bfc463006a4ba2bfcbe0819fb89c3fb8976a78ea", + "type": "item_reference" + }, + { + "call_id": "call_OJGkAZ5ouXaH7NjuUH2mSbk6", + "output": "{\"query\":\"Run the Braintrust Eve instrumentation e2e scenario\",\"title\":\"Eve instrumentation\",\"url\":\"https://eve.dev/docs/guides/instrumentation\"}", + "type": "function_call_output" + }, + { + "id": "fc_0fea08d8f9bfc463006a4ba2c1380c819f949f689dbcdeda46", + "type": "item_reference" + }, + { + "call_id": "call_kZ7AzZ9mCqataCzrSSLxRkFh", + "output": "{\"excerpt\":\"Eve hooks expose runtime stream events that Braintrust maps into a flat turn trace.\",\"section\":\"Runtime context\",\"title\":\"Eve instrumentation\",\"url\":\"https://eve.dev/docs/guides/instrumentation\"}", + "type": "function_call_output" + } + ], + "model": "gpt-5.4-mini", + "stream": true, + "tool_choice": "auto", + "tools": [ + { + "description": "Ask the user a question and wait for their response before continuing. Use this when you need clarification or a choice from the user.", + "name": "ask_question", + "parameters": { + "additionalProperties": false, + "properties": { + "allowFreeform": { + "description": "Whether the user may answer with freeform text instead of selecting one of the provided options.", + "type": "boolean" + }, + "options": { + "description": "Selectable answer options to present to the user.", + "items": { + "additionalProperties": false, + "properties": { + "description": { + "description": "Optional additional context for this option.", + "type": "string" + }, + "id": { + "description": "Stable identifier for the option.", + "type": "string" + }, + "label": { + "description": "User-facing label for the option.", + "type": "string" + }, + "style": { + "description": "Visual treatment hint for the option.", + "enum": ["primary", "danger", "default"], + "type": "string" + } + }, + "required": ["id", "label"], + "type": "object" + }, + "type": "array" + }, + "prompt": { + "description": "The prompt to present to the user.", + "type": "string" + } + }, + "required": ["prompt"], + "type": "object" + }, + "type": "function" + }, + { + "description": "Execute a shell command in the shared workspace environment.", + "name": "bash", + "parameters": { + "additionalProperties": false, + "properties": { + "command": { + "description": "The shell command to execute.", + "type": "string" + } + }, + "required": ["command"], + "type": "object" + }, + "type": "function" + }, + { + "description": "Fast file pattern matching tool that works with any codebase size.\n\nUsage:\n- Supports glob patterns like \"**/*.js\" or \"src/**/*.ts\".\n- Returns matching file paths.\n- Use this tool when you need to find files by name patterns.\n- If you are unsure of the correct file path, use the glob tool to look up filenames by glob pattern.\n- Use the grep tool instead if you need to search file contents.\n- Call this tool in parallel when you know there are multiple patterns to search for.", + "name": "glob", + "parameters": { + "additionalProperties": false, + "properties": { + "limit": { + "description": "Maximum number of results to return. Defaults to 100.", + "maximum": 1000, + "minimum": 1, + "type": "integer" + }, + "path": { + "description": "The directory to search in. Defaults to /workspace. Must be an absolute path. Omit to use the default.", + "type": "string" + }, + "pattern": { + "description": "The glob pattern to match files against (e.g. \"**/*.ts\", \"src/**/*.js\").", + "type": "string" + } + }, + "required": ["pattern"], + "type": "object" + }, + "type": "function" + }, + { + "description": "Fast content search tool that works with any codebase size.\n\nUsage:\n- Searches file contents using regular expressions.\n- Supports full regex syntax (e.g. \"log.*Error\", \"function\\s+\\w+\").\n- Filter files by pattern with the glob parameter (e.g. \"*.js\", \"*.{ts,tsx}\").\n- Returns matching lines with file paths and line numbers.\n- Use this tool when you need to find files containing specific patterns.\n- Use the glob tool instead if you only need to find files by name.\n- Call this tool in parallel when you have multiple independent searches.\n- Any line longer than 2000 characters is truncated.", + "name": "grep", + "parameters": { + "additionalProperties": false, + "properties": { + "context": { + "description": "Number of surrounding context lines to include before and after each match. Defaults to 0.", + "minimum": 0, + "type": "integer" + }, + "glob": { + "description": "Filter files by glob pattern (e.g. \"*.ts\", \"*.{ts,tsx}\").", + "type": "string" + }, + "ignoreCase": { + "description": "Perform case-insensitive search. Defaults to false.", + "type": "boolean" + }, + "limit": { + "description": "Maximum number of matches to return per file. Defaults to 100.", + "maximum": 1000, + "minimum": 1, + "type": "integer" + }, + "literal": { + "description": "Treat the pattern as a literal string instead of a regular expression. Defaults to false.", + "type": "boolean" + }, + "path": { + "description": "The directory or file to search in. Defaults to /workspace. Must be an absolute path. Omit to use the default.", + "type": "string" + }, + "pattern": { + "description": "The regex pattern to search for in file contents (e.g. \"log.*Error\", \"function\\s+\\w+\").", + "type": "string" + } + }, + "required": ["pattern"], + "type": "object" + }, + "type": "function" + }, + { + "description": "Read a file from the local filesystem. If the path does not exist, an error is returned.\n\nUsage:\n- The filePath parameter should be an absolute path.\n- By default, this tool returns up to 2000 lines from the start of the file.\n- The offset parameter is the line number to start from (1-indexed).\n- To read later sections, call this tool again with a larger offset.\n- Use the grep tool to find specific content in large files or files with long lines.\n- If you are unsure of the correct file path, use the glob tool to look up filenames by glob pattern.\n- Contents are returned with each line prefixed by its line number as `: `. For example, if a file has contents \"foo\\n\", you will receive \"1: foo\\n\".\n- Any line longer than 2000 characters is truncated.\n- Call this tool in parallel when you know there are multiple files you want to read.\n- Avoid tiny repeated slices (30 line chunks). If you need more context, read a larger window.", + "name": "read_file", + "parameters": { + "additionalProperties": false, + "properties": { + "filePath": { + "description": "The absolute path to the file to read.", + "type": "string" + }, + "limit": { + "description": "Maximum number of lines to return. Defaults to 2000.", + "minimum": 1, + "type": "integer" + }, + "offset": { + "description": "1-based line number to start from. Defaults to 1.", + "minimum": 1, + "type": "integer" + } + }, + "required": ["filePath"], + "type": "object" + }, + "type": "function" + }, + { + "description": "Writes a file to the local filesystem.\n\nUsage:\n- This tool will overwrite the existing file if there is one at the provided path.\n- If this is an existing file, you MUST use the read_file tool first to read the file's contents. This tool will fail if you did not read the file first.\n- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.\n- NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.\n- Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked.", + "name": "write_file", + "parameters": { + "additionalProperties": false, + "properties": { + "content": { + "description": "Complete replacement file contents.", + "type": "string" + }, + "filePath": { + "description": "The absolute path to the file to write (must be absolute, not relative).", + "type": "string" + } + }, + "required": ["filePath", "content"], + "type": "object" + }, + "type": "function" + }, + { + "description": "Use this tool to create and manage a structured task list for the current session.\nThis helps you track progress, organize complex tasks, and demonstrate thoroughness.\n\nWhen to use:\n- Complex multistep tasks requiring 3 or more distinct steps\n- When the user provides multiple tasks or a numbered list\n- After receiving new instructions, to capture requirements\n- After completing a task, to mark it complete and add follow-ups\n\nWhen NOT to use:\n- Single, straightforward tasks that need no tracking\n- Purely conversational or informational requests\n\nUsage:\n- Call with `todos` to replace the entire list (full replacement write)\n- Call without `todos` to read the current list\n- Both return the full current list with status counts\n- Mark tasks in_progress when you start, completed when done\n- Only have ONE task in_progress at a time", + "name": "todo", + "parameters": { + "additionalProperties": false, + "properties": { + "todos": { + "description": "The updated todo list. Omit to read the current list without modifying it.", + "items": { + "additionalProperties": false, + "properties": { + "content": { + "description": "Brief description of the task.", + "type": "string" + }, + "priority": { + "description": "Priority level of the task.", + "enum": ["high", "medium", "low"], + "type": "string" + }, + "status": { + "description": "Current status of the task.", + "enum": [ + "pending", + "in_progress", + "completed", + "cancelled" + ], + "type": "string" + } + }, + "required": ["content", "status", "priority"], + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + }, + "type": "function" + }, + { + "description": "Fetch a webpage and return its content in the requested format. Use this to retrieve and analyze content from URLs.\n\nUsage notes:\n- The URL must be a fully-formed valid URL starting with http:// or https://\n- HTML responses are automatically converted to markdown or plain text based on the requested format\n- Format options: \"markdown\" (default), \"text\", or \"html\"\n- Default timeout is 30 seconds (max 120 seconds)\n- Maximum response size is 5 MB; content is further capped at the shared tool-output budget (50 KB / 2000 lines)\n- This tool is read-only and does not modify any files", + "name": "web_fetch", + "parameters": { + "additionalProperties": false, + "properties": { + "format": { + "description": "The format to return the content in (text, markdown, or html). HTML responses are automatically converted to the requested format. Defaults to \"markdown\".", + "enum": ["markdown", "text", "html"], + "type": "string" + }, + "timeout": { + "description": "Optional timeout in seconds. Defaults to 30, max 120.", + "type": "number" + }, + "url": { + "description": "The fully-formed URL to fetch content from. Must start with http:// or https://.", + "type": "string" + } + }, + "required": ["url"], + "type": "object" + }, + "type": "function" + }, + { + "type": "web_search" + }, + { + "description": "Load the full instructions for one available skill by name or id. Use this tool when the request clearly matches a listed skill description or when the user explicitly asks for that skill. This is not for MCP connections; use connection_search to access an installed connection. Loading adds the skill instructions to the current turn. Choose the \"skill\" value from the Available skills block.", + "name": "load_skill", + "parameters": { + "additionalProperties": false, + "properties": { + "skill": { + "description": "Available skill name or id.", + "type": "string" + } + }, + "required": ["skill"], + "type": "object" + }, + "type": "function" + }, + { + "description": "Reads a deterministic Eve documentation page fixture.", + "name": "read", + "parameters": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "url": { + "type": "string" + } + }, + "required": ["url"], + "type": "object" + }, + "type": "function" + }, + { + "description": "Searches the Eve instrumentation documentation fixture.", + "name": "search", + "parameters": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "query": { + "type": "string" + } + }, + "required": ["query"], + "type": "object" + }, + "type": "function" + }, + { + "description": "Delegate a focused subtask to a fresh copy of yourself. Use it to isolate complex work or split a large task into independent pieces. Issue multiple `agent` calls in one response to run a small fixed set in parallel. Each child has fresh history and state but shares your tools and sandbox, so include essential context in `message` and give parallel writers non-overlapping scopes.", + "name": "agent", + "parameters": { + "additionalProperties": false, + "properties": { + "message": { + "description": "The message to send to the subagent. Provide all context the subagent needs to complete the task; the subagent does not see the parent's history.", + "type": "string" + }, + "outputSchema": { + "description": "When provided, the subagent runs in task mode and must produce structured output matching this JSON Schema. The structured output becomes the tool result.", + "type": "object" + } + }, + "required": ["message"], + "type": "object" + }, + "type": "function" + } + ] + } + }, + "headers": {}, + "method": "POST", + "url": "https://api.openai.com/v1/responses" + }, + "response": { + "body": { + "chunks": [ + "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_0fea08d8f9bfc463006a4ba2c24358819fb60d53217b74c525\",\"object\":\"response\",\"created_at\":1783341762,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-mini-2026-03-17\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":true,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Ask the user a question and wait for their response before continuing. Use this when you need clarification or a choice from the user.\",\"name\":\"ask_question\",\"parameters\":{\"type\":\"object\",\"properties\":{\"allowFreeform\":{\"type\":\"boolean\",\"description\":\"Whether the user may answer with freeform text instead of selecting one of the provided options.\"},\"options\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Optional additional context for this option.\"},\"id\":{\"type\":\"string\",\"description\":\"Stable identifier for the option.\"},\"label\":{\"type\":\"string\",\"description\":\"User-facing label for the option.\"},\"style\":{\"type\":\"string\",\"enum\":[\"primary\",\"danger\",\"default\"],\"description\":\"Visual treatment hint for the option.\"}},\"required\":[\"description\",\"id\",\"label\",\"style\"],\"additionalProperties\":false},\"description\":\"Selectable answer options to present to the user.\"},\"prompt\":{\"type\":\"string\",\"description\":\"The prompt to present to the user.\"}},\"required\":[\"allowFreeform\",\"options\",\"prompt\"],\"additionalProperties\":false},\"strict\":true},{\"type\":\"function\",\"description\":\"Execute a shell command in the shared workspace environment.\",\"name\":\"bash\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The shell command to execute.\",\"type\":\"string\"}},\"required\":[\"command\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Fast file pattern matching tool that works with any codebase size.\\n\\nUsage:\\n- Supports glob patterns like \\\"**/*.js\\\" or \\\"src/**/*.ts\\\".\\n- Returns matching file paths.\\n- Use this tool when you need to find files by name patterns.\\n- If you are unsure of the correct file path, use the glob tool to look up filenames by glob pattern.\\n- Use the grep tool instead if you need to search file contents.\\n- Call this tool in parallel when you know there are multiple patterns to search for.\",\"name\":\"glob\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"limit\":{\"description\":\"Maximum number of results to return. Defaults to 100.\",\"maximum\":1000,\"minimum\":1,\"type\":\"integer\"},\"path\":{\"description\":\"The directory to search in. Defaults to /workspace. Must be an absolute path. Omit to use the default.\",\"type\":\"string\"},\"pattern\":{\"description\":\"The glob pattern to match files against (e.g. \\\"**/*.ts\\\", \\\"src/**/*.js\\\").\",\"type\":\"string\"}},\"required\":[\"limit\",\"path\",\"pattern\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Fast content search tool that works with any codebase size.\\n\\nUsage:\\n- Searches file contents using regular expressions.\\n- Supports full regex syntax (e.g. \\\"log.*Error\\\", \\\"function\\\\s+\\\\w+\\\").\\n- Filter files by pattern with the glob parameter (e.g. \\\"*.js\\\", \\\"*.{ts,tsx}\\\").\\n- Returns matching lines with file paths and line numbers.\\n- Use this tool when you need to find files containing specific patterns.\\n- Use the glob tool instead if you only need to find files by name.\\n- Call this tool in parallel when you have multiple independent searches.\\n- Any line longer than 2000 characters is truncated.\",\"name\":\"grep\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"context\":{\"description\":\"Number of surrounding context lines to include before and after each match. Defaults to 0.\",\"minimum\":0,\"type\":\"integer\"},\"glob\":{\"description\":\"Filter files by glob pattern (e.g. \\\"*.ts\\\", \\\"*.{ts,tsx}\\\").\",\"type\":\"string\"},\"ignoreCase\":{\"description\":\"Perform case-insensitive search. Defaults to false.\",\"type\":\"boolean\"},\"limit\":{\"description\":\"Maximum number of matches to return per file. Defaults to 100.\",\"maximum\":1000,\"minimum\":1,\"type\":\"integer\"},\"literal\":{\"description\":\"Treat the pattern as a literal string instead of a regular expression. Defaults to false.\",\"type\":\"boolean\"},\"path\":{\"description\":\"The directory or file to search in. Defaults to /workspace. Must be an absolute path. Omit to use the default.\",\"type\":\"string\"},\"pattern\":{\"description\":\"The regex pattern to search for in file contents (e.g. \\\"log.*Error\\\", \\\"function\\\\s+\\\\w+\\\").\",\"type\":\"string\"}},\"required\":[\"context\",\"glob\",\"ignoreCase\",\"limit\",\"literal\",\"path\",\"pattern\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Read a file from the local filesystem. If the path does not exist, an error is returned.\\n\\nUsage:\\n- The filePath parameter should be an absolute path.\\n- By default, this tool returns up to 2000 lines from the start of the file.\\n- The offset parameter is the line number to start from (1-indexed).\\n- To read later sections, call this tool again with a larger offset.\\n- Use the grep tool to find specific content in large files or files with long lines.\\n- If you are unsure of the correct file path, use the glob tool to look up filenames by glob pattern.\\n- Contents are returned with each line prefixed by its line number as `: `. For example, if a file has contents \\\"foo\\\\n\\\", you will receive \\\"1: foo\\\\n\\\".\\n- Any line longer than 2000 characters is truncated.\\n- Call this tool in parallel when you know there are multiple files you want to read.\\n- Avoid tiny repeated slices (30 line chunks). If you need more context, read a larger window.\",\"name\":\"read_file\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"filePath\":{\"description\":\"The absolute path to the file to read.\",\"type\":\"string\"},\"limit\":{\"description\":\"Maximum number of lines to return. Defaults to 2000.\",\"minimum\":1,\"type\":\"integer\"},\"offset\":{\"description\":\"1-based line number to start from. Defaults to 1.\",\"minimum\":1,\"type\":\"integer\"}},\"required\":[\"filePath\",\"limit\",\"offset\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Writes a file to the local filesystem.\\n\\nUsage:\\n- This tool will overwrite the existing file if there is one at the provided path.\\n- If this is an existing file, you MUST use the read_file tool first to read the file's contents. This tool will fail if you did not read the file first.\\n- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.\\n- NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.\\n- Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked.\",\"name\":\"write_file\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"content\":{\"description\":\"Complete replacement file contents.\",\"type\":\"string\"},\"filePath\":{\"description\":\"The absolute path to the file to write (must be absolute, not relative).\",\"type\":\"string\"}},\"required\":[\"content\",\"filePath\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Use this tool to create and manage a structured task list for the current session.\\nThis helps you track progress, organize complex tasks, and demonstrate thoroughness.\\n\\nWhen to use:\\n- Complex multistep tasks requiring 3 or more distinct steps\\n- When the user provides multiple tasks or a numbered list\\n- After receiving new instructions, to capture requirements\\n- After completing a task, to mark it complete and add follow-ups\\n\\nWhen NOT to use:\\n- Single, straightforward tasks that need no tracking\\n- Purely conversational or informational requests\\n\\nUsage:\\n- Call with `todos` to replace the entire list (full replacement write)\\n- Call without `todos` to read the current list\\n- Both return the full current list with status counts\\n- Mark tasks in_progress when you start, completed when done\\n- Only have ONE task in_progress at a time\",\"name\":\"todo\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"todos\":{\"description\":\"The updated todo list. Omit to read the current list without modifying it.\",\"items\":{\"additionalProperties\":false,\"properties\":{\"content\":{\"description\":\"Brief description of the task.\",\"type\":\"string\"},\"priority\":{\"description\":\"Priority level of the task.\",\"enum\":[\"high\",\"medium\",\"low\"],\"type\":\"string\"},\"status\":{\"description\":\"Current status of the task.\",\"enum\":[\"pending\",\"in_progress\",\"completed\",\"cancelled\"],\"type\":\"string\"}},\"required\":[\"content\",\"priority\",\"status\"],\"type\":\"object\"},\"type\":\"array\"}},\"type\":\"object\",\"required\":[\"todos\"]},\"strict\":true},{\"type\":\"function\",\"description\":\"Fetch a webpage and return its content in the requested format. Use this to retrieve and analyze content from URLs.\\n\\nUsage notes:\\n- The URL must be a fully-formed valid URL starting with http:// or https://\\n- HTML responses are automatically converted to markdown or plain text based on the requested format\\n- Format options: \\\"markdown\\\" (default), \\\"text\\\", or \\\"html\\\"\\n- Default timeout is 30 seconds (max 120 seconds)\\n- Maximum response size is 5 MB; content is further capped at the shared tool-output budget (50 KB / 2000 lines)\\n- This tool is read-only and does not modify any files\",\"name\":\"web_fetch\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"format\":{\"description\":\"The format to return the content in (text, markdown, or html). HTML responses are automatically converted to the requested format. Defaults to \\\"markdown\\\".\",\"enum\":[\"markdown\",\"text\",\"html\"],\"type\":\"string\"},\"timeout\":{\"description\":\"Optional timeout in seconds. Defaults to 30, max 120.\",\"type\":\"number\"},\"url\":{\"description\":\"The fully-formed URL to fetch content from. Must start with http:// or https://.\",\"type\":\"string\"}},\"required\":[\"format\",\"timeout\",\"url\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Load the full instructions for one available skill by name or id. Use this tool when the request clearly matches a listed skill description or when the user explicitly asks for that skill. This is not for MCP connections; use connection_search to access an installed connection. Loading adds the skill instructions to the current turn. Choose the \\\"skill\\\" value from the Available skills block.\",\"name\":\"load_skill\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"skill\":{\"description\":\"Available skill name or id.\",\"type\":\"string\"}},\"required\":[\"skill\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Reads a deterministic Eve documentation page fixture.\",\"name\":\"read\",\"parameters\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"url\":{\"type\":\"string\"}},\"required\":[\"url\"],\"additionalProperties\":false},\"strict\":true},{\"type\":\"function\",\"description\":\"Searches the Eve instrumentation documentation fixture.\",\"name\":\"search\",\"parameters\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\"}},\"required\":[\"query\"],\"additionalProperties\":false},\"strict\":true},{\"type\":\"function\",\"description\":\"Delegate a focused subtask to a fresh copy of yourself. Use it to isolate complex work or split a large task into independent pieces. Issue multiple `agent` calls in one response to run a small fixed set in parallel. Each child has fresh history and state but shares your tools and sandbox, so include essential context in `message` and give parallel writers non-overlapping scopes.\",\"name\":\"agent\",\"parameters\":{\"type\":\"object\",\"properties\":{\"message\":{\"type\":\"string\",\"description\":\"The message to send to the subagent. Provide all context the subagent needs to complete the task; the subagent does not see the parent's history.\"},\"outputSchema\":{\"type\":\"object\",\"description\":\"When provided, the subagent runs in task mode and must produce structured output matching this JSON Schema. The structured output becomes the tool result.\"}},\"required\":[\"message\"],\"additionalProperties\":false},\"strict\":false},{\"type\":\"web_search\",\"return_token_budget\":\"default\",\"search_content_types\":[\"text\"],\"search_context_size\":\"medium\",\"user_location\":{\"type\":\"approximate\",\"city\":null,\"country\":\"US\",\"region\":null,\"timezone\":null}}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}", + "event: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_0fea08d8f9bfc463006a4ba2c24358819fb60d53217b74c525\",\"object\":\"response\",\"created_at\":1783341762,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-mini-2026-03-17\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":true,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Ask the user a question and wait for their response before continuing. Use this when you need clarification or a choice from the user.\",\"name\":\"ask_question\",\"parameters\":{\"type\":\"object\",\"properties\":{\"allowFreeform\":{\"type\":\"boolean\",\"description\":\"Whether the user may answer with freeform text instead of selecting one of the provided options.\"},\"options\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Optional additional context for this option.\"},\"id\":{\"type\":\"string\",\"description\":\"Stable identifier for the option.\"},\"label\":{\"type\":\"string\",\"description\":\"User-facing label for the option.\"},\"style\":{\"type\":\"string\",\"enum\":[\"primary\",\"danger\",\"default\"],\"description\":\"Visual treatment hint for the option.\"}},\"required\":[\"description\",\"id\",\"label\",\"style\"],\"additionalProperties\":false},\"description\":\"Selectable answer options to present to the user.\"},\"prompt\":{\"type\":\"string\",\"description\":\"The prompt to present to the user.\"}},\"required\":[\"allowFreeform\",\"options\",\"prompt\"],\"additionalProperties\":false},\"strict\":true},{\"type\":\"function\",\"description\":\"Execute a shell command in the shared workspace environment.\",\"name\":\"bash\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The shell command to execute.\",\"type\":\"string\"}},\"required\":[\"command\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Fast file pattern matching tool that works with any codebase size.\\n\\nUsage:\\n- Supports glob patterns like \\\"**/*.js\\\" or \\\"src/**/*.ts\\\".\\n- Returns matching file paths.\\n- Use this tool when you need to find files by name patterns.\\n- If you are unsure of the correct file path, use the glob tool to look up filenames by glob pattern.\\n- Use the grep tool instead if you need to search file contents.\\n- Call this tool in parallel when you know there are multiple patterns to search for.\",\"name\":\"glob\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"limit\":{\"description\":\"Maximum number of results to return. Defaults to 100.\",\"maximum\":1000,\"minimum\":1,\"type\":\"integer\"},\"path\":{\"description\":\"The directory to search in. Defaults to /workspace. Must be an absolute path. Omit to use the default.\",\"type\":\"string\"},\"pattern\":{\"description\":\"The glob pattern to match files against (e.g. \\\"**/*.ts\\\", \\\"src/**/*.js\\\").\",\"type\":\"string\"}},\"required\":[\"limit\",\"path\",\"pattern\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Fast content search tool that works with any codebase size.\\n\\nUsage:\\n- Searches file contents using regular expressions.\\n- Supports full regex syntax (e.g. \\\"log.*Error\\\", \\\"function\\\\s+\\\\w+\\\").\\n- Filter files by pattern with the glob parameter (e.g. \\\"*.js\\\", \\\"*.{ts,tsx}\\\").\\n- Returns matching lines with file paths and line numbers.\\n- Use this tool when you need to find files containing specific patterns.\\n- Use the glob tool instead if you only need to find files by name.\\n- Call this tool in parallel when you have multiple independent searches.\\n- Any line longer than 2000 characters is truncated.\",\"name\":\"grep\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"context\":{\"description\":\"Number of surrounding context lines to include before and after each match. Defaults to 0.\",\"minimum\":0,\"type\":\"integer\"},\"glob\":{\"description\":\"Filter files by glob pattern (e.g. \\\"*.ts\\\", \\\"*.{ts,tsx}\\\").\",\"type\":\"string\"},\"ignoreCase\":{\"description\":\"Perform case-insensitive search. Defaults to false.\",\"type\":\"boolean\"},\"limit\":{\"description\":\"Maximum number of matches to return per file. Defaults to 100.\",\"maximum\":1000,\"minimum\":1,\"type\":\"integer\"},\"literal\":{\"description\":\"Treat the pattern as a literal string instead of a regular expression. Defaults to false.\",\"type\":\"boolean\"},\"path\":{\"description\":\"The directory or file to search in. Defaults to /workspace. Must be an absolute path. Omit to use the default.\",\"type\":\"string\"},\"pattern\":{\"description\":\"The regex pattern to search for in file contents (e.g. \\\"log.*Error\\\", \\\"function\\\\s+\\\\w+\\\").\",\"type\":\"string\"}},\"required\":[\"context\",\"glob\",\"ignoreCase\",\"limit\",\"literal\",\"path\",\"pattern\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Read a file from the local filesystem. If the path does not exist, an error is returned.\\n\\nUsage:\\n- The filePath parameter should be an absolute path.\\n- By default, this tool returns up to 2000 lines from the start of the file.\\n- The offset parameter is the line number to start from (1-indexed).\\n- To read later sections, call this tool again with a larger offset.\\n- Use the grep tool to find specific content in large files or files with long lines.\\n- If you are unsure of the correct file path, use the glob tool to look up filenames by glob pattern.\\n- Contents are returned with each line prefixed by its line number as `: `. For example, if a file has contents \\\"foo\\\\n\\\", you will receive \\\"1: foo\\\\n\\\".\\n- Any line longer than 2000 characters is truncated.\\n- Call this tool in parallel when you know there are multiple files you want to read.\\n- Avoid tiny repeated slices (30 line chunks). If you need more context, read a larger window.\",\"name\":\"read_file\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"filePath\":{\"description\":\"The absolute path to the file to read.\",\"type\":\"string\"},\"limit\":{\"description\":\"Maximum number of lines to return. Defaults to 2000.\",\"minimum\":1,\"type\":\"integer\"},\"offset\":{\"description\":\"1-based line number to start from. Defaults to 1.\",\"minimum\":1,\"type\":\"integer\"}},\"required\":[\"filePath\",\"limit\",\"offset\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Writes a file to the local filesystem.\\n\\nUsage:\\n- This tool will overwrite the existing file if there is one at the provided path.\\n- If this is an existing file, you MUST use the read_file tool first to read the file's contents. This tool will fail if you did not read the file first.\\n- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.\\n- NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.\\n- Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked.\",\"name\":\"write_file\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"content\":{\"description\":\"Complete replacement file contents.\",\"type\":\"string\"},\"filePath\":{\"description\":\"The absolute path to the file to write (must be absolute, not relative).\",\"type\":\"string\"}},\"required\":[\"content\",\"filePath\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Use this tool to create and manage a structured task list for the current session.\\nThis helps you track progress, organize complex tasks, and demonstrate thoroughness.\\n\\nWhen to use:\\n- Complex multistep tasks requiring 3 or more distinct steps\\n- When the user provides multiple tasks or a numbered list\\n- After receiving new instructions, to capture requirements\\n- After completing a task, to mark it complete and add follow-ups\\n\\nWhen NOT to use:\\n- Single, straightforward tasks that need no tracking\\n- Purely conversational or informational requests\\n\\nUsage:\\n- Call with `todos` to replace the entire list (full replacement write)\\n- Call without `todos` to read the current list\\n- Both return the full current list with status counts\\n- Mark tasks in_progress when you start, completed when done\\n- Only have ONE task in_progress at a time\",\"name\":\"todo\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"todos\":{\"description\":\"The updated todo list. Omit to read the current list without modifying it.\",\"items\":{\"additionalProperties\":false,\"properties\":{\"content\":{\"description\":\"Brief description of the task.\",\"type\":\"string\"},\"priority\":{\"description\":\"Priority level of the task.\",\"enum\":[\"high\",\"medium\",\"low\"],\"type\":\"string\"},\"status\":{\"description\":\"Current status of the task.\",\"enum\":[\"pending\",\"in_progress\",\"completed\",\"cancelled\"],\"type\":\"string\"}},\"required\":[\"content\",\"priority\",\"status\"],\"type\":\"object\"},\"type\":\"array\"}},\"type\":\"object\",\"required\":[\"todos\"]},\"strict\":true},{\"type\":\"function\",\"description\":\"Fetch a webpage and return its content in the requested format. Use this to retrieve and analyze content from URLs.\\n\\nUsage notes:\\n- The URL must be a fully-formed valid URL starting with http:// or https://\\n- HTML responses are automatically converted to markdown or plain text based on the requested format\\n- Format options: \\\"markdown\\\" (default), \\\"text\\\", or \\\"html\\\"\\n- Default timeout is 30 seconds (max 120 seconds)\\n- Maximum response size is 5 MB; content is further capped at the shared tool-output budget (50 KB / 2000 lines)\\n- This tool is read-only and does not modify any files\",\"name\":\"web_fetch\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"format\":{\"description\":\"The format to return the content in (text, markdown, or html). HTML responses are automatically converted to the requested format. Defaults to \\\"markdown\\\".\",\"enum\":[\"markdown\",\"text\",\"html\"],\"type\":\"string\"},\"timeout\":{\"description\":\"Optional timeout in seconds. Defaults to 30, max 120.\",\"type\":\"number\"},\"url\":{\"description\":\"The fully-formed URL to fetch content from. Must start with http:// or https://.\",\"type\":\"string\"}},\"required\":[\"format\",\"timeout\",\"url\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Load the full instructions for one available skill by name or id. Use this tool when the request clearly matches a listed skill description or when the user explicitly asks for that skill. This is not for MCP connections; use connection_search to access an installed connection. Loading adds the skill instructions to the current turn. Choose the \\\"skill\\\" value from the Available skills block.\",\"name\":\"load_skill\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"skill\":{\"description\":\"Available skill name or id.\",\"type\":\"string\"}},\"required\":[\"skill\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Reads a deterministic Eve documentation page fixture.\",\"name\":\"read\",\"parameters\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"url\":{\"type\":\"string\"}},\"required\":[\"url\"],\"additionalProperties\":false},\"strict\":true},{\"type\":\"function\",\"description\":\"Searches the Eve instrumentation documentation fixture.\",\"name\":\"search\",\"parameters\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\"}},\"required\":[\"query\"],\"additionalProperties\":false},\"strict\":true},{\"type\":\"function\",\"description\":\"Delegate a focused subtask to a fresh copy of yourself. Use it to isolate complex work or split a large task into independent pieces. Issue multiple `agent` calls in one response to run a small fixed set in parallel. Each child has fresh history and state but shares your tools and sandbox, so include essential context in `message` and give parallel writers non-overlapping scopes.\",\"name\":\"agent\",\"parameters\":{\"type\":\"object\",\"properties\":{\"message\":{\"type\":\"string\",\"description\":\"The message to send to the subagent. Provide all context the subagent needs to complete the task; the subagent does not see the parent's history.\"},\"outputSchema\":{\"type\":\"object\",\"description\":\"When provided, the subagent runs in task mode and must produce structured output matching this JSON Schema. The structured output becomes the tool result.\"}},\"required\":[\"message\"],\"additionalProperties\":false},\"strict\":false},{\"type\":\"web_search\",\"return_token_budget\":\"default\",\"search_content_types\":[\"text\"],\"search_context_size\":\"medium\",\"user_location\":{\"type\":\"approximate\",\"city\":null,\"country\":\"US\",\"region\":null,\"timezone\":null}}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}", + "event: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"msg_0fea08d8f9bfc463006a4ba2c2a888819fb82702a3a6fe95de\",\"type\":\"message\",\"status\":\"in_progress\",\"content\":[],\"phase\":\"final_answer\",\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":2}", + "event: response.content_part.added\ndata: {\"type\":\"response.content_part.added\",\"content_index\":0,\"item_id\":\"msg_0fea08d8f9bfc463006a4ba2c2a888819fb82702a3a6fe95de\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"\"},\"sequence_number\":3}", + "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"Final\",\"item_id\":\"msg_0fea08d8f9bfc463006a4ba2c2a888819fb82702a3a6fe95de\",\"logprobs\":[],\"obfuscation\":\"ANAxDak0zuB\",\"output_index\":0,\"sequence_number\":4}", + "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" answer\",\"item_id\":\"msg_0fea08d8f9bfc463006a4ba2c2a888819fb82702a3a6fe95de\",\"logprobs\":[],\"obfuscation\":\"A7UjBuGVk\",\"output_index\":0,\"sequence_number\":5}", + "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" from\",\"item_id\":\"msg_0fea08d8f9bfc463006a4ba2c2a888819fb82702a3a6fe95de\",\"logprobs\":[],\"obfuscation\":\"yeX5bNWtb13\",\"output_index\":0,\"sequence_number\":6}", + "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" read\",\"item_id\":\"msg_0fea08d8f9bfc463006a4ba2c2a888819fb82702a3a6fe95de\",\"logprobs\":[],\"obfuscation\":\"LZJe1rtmYvV\",\"output_index\":0,\"sequence_number\":7}", + "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\":\",\"item_id\":\"msg_0fea08d8f9bfc463006a4ba2c2a888819fb82702a3a6fe95de\",\"logprobs\":[],\"obfuscation\":\"ubVMHAlNIlruqHa\",\"output_index\":0,\"sequence_number\":8}", + "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" Eve\",\"item_id\":\"msg_0fea08d8f9bfc463006a4ba2c2a888819fb82702a3a6fe95de\",\"logprobs\":[],\"obfuscation\":\"jNlbYXRxMdDm\",\"output_index\":0,\"sequence_number\":9}", + "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" instrumentation\",\"item_id\":\"msg_0fea08d8f9bfc463006a4ba2c2a888819fb82702a3a6fe95de\",\"logprobs\":[],\"obfuscation\":\"\",\"output_index\":0,\"sequence_number\":10}", + "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" —\",\"item_id\":\"msg_0fea08d8f9bfc463006a4ba2c2a888819fb82702a3a6fe95de\",\"logprobs\":[],\"obfuscation\":\"r53TzX4FOBazqH\",\"output_index\":0,\"sequence_number\":11}", + "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" https\",\"item_id\":\"msg_0fea08d8f9bfc463006a4ba2c2a888819fb82702a3a6fe95de\",\"logprobs\":[],\"obfuscation\":\"DO7NjjcMxI\",\"output_index\":0,\"sequence_number\":12}", + "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"://\",\"item_id\":\"msg_0fea08d8f9bfc463006a4ba2c2a888819fb82702a3a6fe95de\",\"logprobs\":[],\"obfuscation\":\"ICOb1qKdbbUrP\",\"output_index\":0,\"sequence_number\":13}", + "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"eve\",\"item_id\":\"msg_0fea08d8f9bfc463006a4ba2c2a888819fb82702a3a6fe95de\",\"logprobs\":[],\"obfuscation\":\"SaQSy8Bew6PYA\",\"output_index\":0,\"sequence_number\":14}", + "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\".dev\",\"item_id\":\"msg_0fea08d8f9bfc463006a4ba2c2a888819fb82702a3a6fe95de\",\"logprobs\":[],\"obfuscation\":\"zMGbfUgBaedB\",\"output_index\":0,\"sequence_number\":15}", + "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"/docs\",\"item_id\":\"msg_0fea08d8f9bfc463006a4ba2c2a888819fb82702a3a6fe95de\",\"logprobs\":[],\"obfuscation\":\"PRFSTu78jMq\",\"output_index\":0,\"sequence_number\":16}", + "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"/g\",\"item_id\":\"msg_0fea08d8f9bfc463006a4ba2c2a888819fb82702a3a6fe95de\",\"logprobs\":[],\"obfuscation\":\"TZIiRl9V1OzAAJ\",\"output_index\":0,\"sequence_number\":17}", + "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"uid\",\"item_id\":\"msg_0fea08d8f9bfc463006a4ba2c2a888819fb82702a3a6fe95de\",\"logprobs\":[],\"obfuscation\":\"ztrmEJnKeliLB\",\"output_index\":0,\"sequence_number\":18}", + "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"es\",\"item_id\":\"msg_0fea08d8f9bfc463006a4ba2c2a888819fb82702a3a6fe95de\",\"logprobs\":[],\"obfuscation\":\"vJXlAF5E6no9TP\",\"output_index\":0,\"sequence_number\":19}", + "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"/in\",\"item_id\":\"msg_0fea08d8f9bfc463006a4ba2c2a888819fb82702a3a6fe95de\",\"logprobs\":[],\"obfuscation\":\"2jUQoFtlTAB9n\",\"output_index\":0,\"sequence_number\":20}", + "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"strument\",\"item_id\":\"msg_0fea08d8f9bfc463006a4ba2c2a888819fb82702a3a6fe95de\",\"logprobs\":[],\"obfuscation\":\"Q0nLWTe1\",\"output_index\":0,\"sequence_number\":21}", + "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"ation\",\"item_id\":\"msg_0fea08d8f9bfc463006a4ba2c2a888819fb82702a3a6fe95de\",\"logprobs\":[],\"obfuscation\":\"xUWKmOn2oyt\",\"output_index\":0,\"sequence_number\":22}", + "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" —\",\"item_id\":\"msg_0fea08d8f9bfc463006a4ba2c2a888819fb82702a3a6fe95de\",\"logprobs\":[],\"obfuscation\":\"zNi8pD4u1Ja7L1\",\"output_index\":0,\"sequence_number\":23}", + "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" Eve\",\"item_id\":\"msg_0fea08d8f9bfc463006a4ba2c2a888819fb82702a3a6fe95de\",\"logprobs\":[],\"obfuscation\":\"1Zebv58iK37e\",\"output_index\":0,\"sequence_number\":24}", + "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" hooks\",\"item_id\":\"msg_0fea08d8f9bfc463006a4ba2c2a888819fb82702a3a6fe95de\",\"logprobs\":[],\"obfuscation\":\"S97nFDYp7A\",\"output_index\":0,\"sequence_number\":25}", + "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" expose\",\"item_id\":\"msg_0fea08d8f9bfc463006a4ba2c2a888819fb82702a3a6fe95de\",\"logprobs\":[],\"obfuscation\":\"ZmTvzTqky\",\"output_index\":0,\"sequence_number\":26}", + "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" runtime\",\"item_id\":\"msg_0fea08d8f9bfc463006a4ba2c2a888819fb82702a3a6fe95de\",\"logprobs\":[],\"obfuscation\":\"fB1yh7Uw\",\"output_index\":0,\"sequence_number\":27}", + "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" stream\",\"item_id\":\"msg_0fea08d8f9bfc463006a4ba2c2a888819fb82702a3a6fe95de\",\"logprobs\":[],\"obfuscation\":\"zU1vbebdy\",\"output_index\":0,\"sequence_number\":28}", + "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" events\",\"item_id\":\"msg_0fea08d8f9bfc463006a4ba2c2a888819fb82702a3a6fe95de\",\"logprobs\":[],\"obfuscation\":\"gYKd55Lzb\",\"output_index\":0,\"sequence_number\":29}", + "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" that\",\"item_id\":\"msg_0fea08d8f9bfc463006a4ba2c2a888819fb82702a3a6fe95de\",\"logprobs\":[],\"obfuscation\":\"3EZn0CVOJaT\",\"output_index\":0,\"sequence_number\":30}", + "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" Brain\",\"item_id\":\"msg_0fea08d8f9bfc463006a4ba2c2a888819fb82702a3a6fe95de\",\"logprobs\":[],\"obfuscation\":\"atGReXeapP\",\"output_index\":0,\"sequence_number\":31}", + "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"trust\",\"item_id\":\"msg_0fea08d8f9bfc463006a4ba2c2a888819fb82702a3a6fe95de\",\"logprobs\":[],\"obfuscation\":\"zWKPFGInVjG\",\"output_index\":0,\"sequence_number\":32}", + "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" maps\",\"item_id\":\"msg_0fea08d8f9bfc463006a4ba2c2a888819fb82702a3a6fe95de\",\"logprobs\":[],\"obfuscation\":\"EeGFqBdfAME\",\"output_index\":0,\"sequence_number\":33}", + "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" into\",\"item_id\":\"msg_0fea08d8f9bfc463006a4ba2c2a888819fb82702a3a6fe95de\",\"logprobs\":[],\"obfuscation\":\"iabXIGBKBpJ\",\"output_index\":0,\"sequence_number\":34}", + "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" a\",\"item_id\":\"msg_0fea08d8f9bfc463006a4ba2c2a888819fb82702a3a6fe95de\",\"logprobs\":[],\"obfuscation\":\"5CXblMcSRN8S5K\",\"output_index\":0,\"sequence_number\":35}", + "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" flat\",\"item_id\":\"msg_0fea08d8f9bfc463006a4ba2c2a888819fb82702a3a6fe95de\",\"logprobs\":[],\"obfuscation\":\"P6u4aXRbe8f\",\"output_index\":0,\"sequence_number\":36}", + "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" turn\",\"item_id\":\"msg_0fea08d8f9bfc463006a4ba2c2a888819fb82702a3a6fe95de\",\"logprobs\":[],\"obfuscation\":\"9P4dWv48mQN\",\"output_index\":0,\"sequence_number\":37}", + "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" trace\",\"item_id\":\"msg_0fea08d8f9bfc463006a4ba2c2a888819fb82702a3a6fe95de\",\"logprobs\":[],\"obfuscation\":\"cZVEaJUhkd\",\"output_index\":0,\"sequence_number\":38}", + "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\".\",\"item_id\":\"msg_0fea08d8f9bfc463006a4ba2c2a888819fb82702a3a6fe95de\",\"logprobs\":[],\"obfuscation\":\"qL2yxmkMIJgYbnb\",\"output_index\":0,\"sequence_number\":39}", + "event: response.output_text.done\ndata: {\"type\":\"response.output_text.done\",\"content_index\":0,\"item_id\":\"msg_0fea08d8f9bfc463006a4ba2c2a888819fb82702a3a6fe95de\",\"logprobs\":[],\"output_index\":0,\"sequence_number\":40,\"text\":\"Final answer from read: Eve instrumentation — https://eve.dev/docs/guides/instrumentation — Eve hooks expose runtime stream events that Braintrust maps into a flat turn trace.\"}", + "event: response.content_part.done\ndata: {\"type\":\"response.content_part.done\",\"content_index\":0,\"item_id\":\"msg_0fea08d8f9bfc463006a4ba2c2a888819fb82702a3a6fe95de\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Final answer from read: Eve instrumentation — https://eve.dev/docs/guides/instrumentation — Eve hooks expose runtime stream events that Braintrust maps into a flat turn trace.\"},\"sequence_number\":41}", + "event: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"msg_0fea08d8f9bfc463006a4ba2c2a888819fb82702a3a6fe95de\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Final answer from read: Eve instrumentation — https://eve.dev/docs/guides/instrumentation — Eve hooks expose runtime stream events that Braintrust maps into a flat turn trace.\"}],\"phase\":\"final_answer\",\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":42}", + "event: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_0fea08d8f9bfc463006a4ba2c24358819fb60d53217b74c525\",\"object\":\"response\",\"created_at\":1783341762,\"status\":\"completed\",\"background\":false,\"completed_at\":1783341762,\"error\":null,\"frequency_penalty\":0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-5.4-mini-2026-03-17\",\"moderation\":null,\"output\":[{\"id\":\"msg_0fea08d8f9bfc463006a4ba2c2a888819fb82702a3a6fe95de\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Final answer from read: Eve instrumentation — https://eve.dev/docs/guides/instrumentation — Eve hooks expose runtime stream events that Braintrust maps into a flat turn trace.\"}],\"phase\":\"final_answer\",\"role\":\"assistant\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"none\",\"mode\":\"standard\",\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":true,\"temperature\":1,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":0,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"output_tokens\":0,\"output_tokens_details\":{\"image_tokens\":0,\"text_tokens\":0},\"total_tokens\":0},\"web_search\":{\"num_requests\":0}},\"tools\":[{\"type\":\"function\",\"description\":\"Ask the user a question and wait for their response before continuing. Use this when you need clarification or a choice from the user.\",\"name\":\"ask_question\",\"parameters\":{\"type\":\"object\",\"properties\":{\"allowFreeform\":{\"type\":\"boolean\",\"description\":\"Whether the user may answer with freeform text instead of selecting one of the provided options.\"},\"options\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"description\":{\"type\":\"string\",\"description\":\"Optional additional context for this option.\"},\"id\":{\"type\":\"string\",\"description\":\"Stable identifier for the option.\"},\"label\":{\"type\":\"string\",\"description\":\"User-facing label for the option.\"},\"style\":{\"type\":\"string\",\"enum\":[\"primary\",\"danger\",\"default\"],\"description\":\"Visual treatment hint for the option.\"}},\"required\":[\"description\",\"id\",\"label\",\"style\"],\"additionalProperties\":false},\"description\":\"Selectable answer options to present to the user.\"},\"prompt\":{\"type\":\"string\",\"description\":\"The prompt to present to the user.\"}},\"required\":[\"allowFreeform\",\"options\",\"prompt\"],\"additionalProperties\":false},\"strict\":true},{\"type\":\"function\",\"description\":\"Execute a shell command in the shared workspace environment.\",\"name\":\"bash\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The shell command to execute.\",\"type\":\"string\"}},\"required\":[\"command\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Fast file pattern matching tool that works with any codebase size.\\n\\nUsage:\\n- Supports glob patterns like \\\"**/*.js\\\" or \\\"src/**/*.ts\\\".\\n- Returns matching file paths.\\n- Use this tool when you need to find files by name patterns.\\n- If you are unsure of the correct file path, use the glob tool to look up filenames by glob pattern.\\n- Use the grep tool instead if you need to search file contents.\\n- Call this tool in parallel when you know there are multiple patterns to search for.\",\"name\":\"glob\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"limit\":{\"description\":\"Maximum number of results to return. Defaults to 100.\",\"maximum\":1000,\"minimum\":1,\"type\":\"integer\"},\"path\":{\"description\":\"The directory to search in. Defaults to /workspace. Must be an absolute path. Omit to use the default.\",\"type\":\"string\"},\"pattern\":{\"description\":\"The glob pattern to match files against (e.g. \\\"**/*.ts\\\", \\\"src/**/*.js\\\").\",\"type\":\"string\"}},\"required\":[\"limit\",\"path\",\"pattern\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Fast content search tool that works with any codebase size.\\n\\nUsage:\\n- Searches file contents using regular expressions.\\n- Supports full regex syntax (e.g. \\\"log.*Error\\\", \\\"function\\\\s+\\\\w+\\\").\\n- Filter files by pattern with the glob parameter (e.g. \\\"*.js\\\", \\\"*.{ts,tsx}\\\").\\n- Returns matching lines with file paths and line numbers.\\n- Use this tool when you need to find files containing specific patterns.\\n- Use the glob tool instead if you only need to find files by name.\\n- Call this tool in parallel when you have multiple independent searches.\\n- Any line longer than 2000 characters is truncated.\",\"name\":\"grep\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"context\":{\"description\":\"Number of surrounding context lines to include before and after each match. Defaults to 0.\",\"minimum\":0,\"type\":\"integer\"},\"glob\":{\"description\":\"Filter files by glob pattern (e.g. \\\"*.ts\\\", \\\"*.{ts,tsx}\\\").\",\"type\":\"string\"},\"ignoreCase\":{\"description\":\"Perform case-insensitive search. Defaults to false.\",\"type\":\"boolean\"},\"limit\":{\"description\":\"Maximum number of matches to return per file. Defaults to 100.\",\"maximum\":1000,\"minimum\":1,\"type\":\"integer\"},\"literal\":{\"description\":\"Treat the pattern as a literal string instead of a regular expression. Defaults to false.\",\"type\":\"boolean\"},\"path\":{\"description\":\"The directory or file to search in. Defaults to /workspace. Must be an absolute path. Omit to use the default.\",\"type\":\"string\"},\"pattern\":{\"description\":\"The regex pattern to search for in file contents (e.g. \\\"log.*Error\\\", \\\"function\\\\s+\\\\w+\\\").\",\"type\":\"string\"}},\"required\":[\"context\",\"glob\",\"ignoreCase\",\"limit\",\"literal\",\"path\",\"pattern\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Read a file from the local filesystem. If the path does not exist, an error is returned.\\n\\nUsage:\\n- The filePath parameter should be an absolute path.\\n- By default, this tool returns up to 2000 lines from the start of the file.\\n- The offset parameter is the line number to start from (1-indexed).\\n- To read later sections, call this tool again with a larger offset.\\n- Use the grep tool to find specific content in large files or files with long lines.\\n- If you are unsure of the correct file path, use the glob tool to look up filenames by glob pattern.\\n- Contents are returned with each line prefixed by its line number as `: `. For example, if a file has contents \\\"foo\\\\n\\\", you will receive \\\"1: foo\\\\n\\\".\\n- Any line longer than 2000 characters is truncated.\\n- Call this tool in parallel when you know there are multiple files you want to read.\\n- Avoid tiny repeated slices (30 line chunks). If you need more context, read a larger window.\",\"name\":\"read_file\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"filePath\":{\"description\":\"The absolute path to the file to read.\",\"type\":\"string\"},\"limit\":{\"description\":\"Maximum number of lines to return. Defaults to 2000.\",\"minimum\":1,\"type\":\"integer\"},\"offset\":{\"description\":\"1-based line number to start from. Defaults to 1.\",\"minimum\":1,\"type\":\"integer\"}},\"required\":[\"filePath\",\"limit\",\"offset\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Writes a file to the local filesystem.\\n\\nUsage:\\n- This tool will overwrite the existing file if there is one at the provided path.\\n- If this is an existing file, you MUST use the read_file tool first to read the file's contents. This tool will fail if you did not read the file first.\\n- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.\\n- NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.\\n- Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked.\",\"name\":\"write_file\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"content\":{\"description\":\"Complete replacement file contents.\",\"type\":\"string\"},\"filePath\":{\"description\":\"The absolute path to the file to write (must be absolute, not relative).\",\"type\":\"string\"}},\"required\":[\"content\",\"filePath\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Use this tool to create and manage a structured task list for the current session.\\nThis helps you track progress, organize complex tasks, and demonstrate thoroughness.\\n\\nWhen to use:\\n- Complex multistep tasks requiring 3 or more distinct steps\\n- When the user provides multiple tasks or a numbered list\\n- After receiving new instructions, to capture requirements\\n- After completing a task, to mark it complete and add follow-ups\\n\\nWhen NOT to use:\\n- Single, straightforward tasks that need no tracking\\n- Purely conversational or informational requests\\n\\nUsage:\\n- Call with `todos` to replace the entire list (full replacement write)\\n- Call without `todos` to read the current list\\n- Both return the full current list with status counts\\n- Mark tasks in_progress when you start, completed when done\\n- Only have ONE task in_progress at a time\",\"name\":\"todo\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"todos\":{\"description\":\"The updated todo list. Omit to read the current list without modifying it.\",\"items\":{\"additionalProperties\":false,\"properties\":{\"content\":{\"description\":\"Brief description of the task.\",\"type\":\"string\"},\"priority\":{\"description\":\"Priority level of the task.\",\"enum\":[\"high\",\"medium\",\"low\"],\"type\":\"string\"},\"status\":{\"description\":\"Current status of the task.\",\"enum\":[\"pending\",\"in_progress\",\"completed\",\"cancelled\"],\"type\":\"string\"}},\"required\":[\"content\",\"priority\",\"status\"],\"type\":\"object\"},\"type\":\"array\"}},\"type\":\"object\",\"required\":[\"todos\"]},\"strict\":true},{\"type\":\"function\",\"description\":\"Fetch a webpage and return its content in the requested format. Use this to retrieve and analyze content from URLs.\\n\\nUsage notes:\\n- The URL must be a fully-formed valid URL starting with http:// or https://\\n- HTML responses are automatically converted to markdown or plain text based on the requested format\\n- Format options: \\\"markdown\\\" (default), \\\"text\\\", or \\\"html\\\"\\n- Default timeout is 30 seconds (max 120 seconds)\\n- Maximum response size is 5 MB; content is further capped at the shared tool-output budget (50 KB / 2000 lines)\\n- This tool is read-only and does not modify any files\",\"name\":\"web_fetch\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"format\":{\"description\":\"The format to return the content in (text, markdown, or html). HTML responses are automatically converted to the requested format. Defaults to \\\"markdown\\\".\",\"enum\":[\"markdown\",\"text\",\"html\"],\"type\":\"string\"},\"timeout\":{\"description\":\"Optional timeout in seconds. Defaults to 30, max 120.\",\"type\":\"number\"},\"url\":{\"description\":\"The fully-formed URL to fetch content from. Must start with http:// or https://.\",\"type\":\"string\"}},\"required\":[\"format\",\"timeout\",\"url\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Load the full instructions for one available skill by name or id. Use this tool when the request clearly matches a listed skill description or when the user explicitly asks for that skill. This is not for MCP connections; use connection_search to access an installed connection. Loading adds the skill instructions to the current turn. Choose the \\\"skill\\\" value from the Available skills block.\",\"name\":\"load_skill\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"skill\":{\"description\":\"Available skill name or id.\",\"type\":\"string\"}},\"required\":[\"skill\"],\"type\":\"object\"},\"strict\":true},{\"type\":\"function\",\"description\":\"Reads a deterministic Eve documentation page fixture.\",\"name\":\"read\",\"parameters\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"url\":{\"type\":\"string\"}},\"required\":[\"url\"],\"additionalProperties\":false},\"strict\":true},{\"type\":\"function\",\"description\":\"Searches the Eve instrumentation documentation fixture.\",\"name\":\"search\",\"parameters\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\"}},\"required\":[\"query\"],\"additionalProperties\":false},\"strict\":true},{\"type\":\"function\",\"description\":\"Delegate a focused subtask to a fresh copy of yourself. Use it to isolate complex work or split a large task into independent pieces. Issue multiple `agent` calls in one response to run a small fixed set in parallel. Each child has fresh history and state but shares your tools and sandbox, so include essential context in `message` and give parallel writers non-overlapping scopes.\",\"name\":\"agent\",\"parameters\":{\"type\":\"object\",\"properties\":{\"message\":{\"type\":\"string\",\"description\":\"The message to send to the subagent. Provide all context the subagent needs to complete the task; the subagent does not see the parent's history.\"},\"outputSchema\":{\"type\":\"object\",\"description\":\"When provided, the subagent runs in task mode and must produce structured output matching this JSON Schema. The structured output becomes the tool result.\"}},\"required\":[\"message\"],\"additionalProperties\":false},\"strict\":false},{\"type\":\"web_search\",\"return_token_budget\":\"default\",\"search_content_types\":[\"text\"],\"search_context_size\":\"medium\",\"user_location\":{\"type\":\"approximate\",\"city\":null,\"country\":\"US\",\"region\":null,\"timezone\":null}}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":6716,\"input_tokens_details\":{\"cached_tokens\":0},\"output_tokens\":40,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":6756},\"user\":null,\"metadata\":{}},\"sequence_number\":43}" + ], + "kind": "sse" + }, + "headers": { + "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", + "alt-svc": "h3=\":443\"; ma=86400", + "cf-cache-status": "DYNAMIC", + "cf-ray": "a16eb0dd6acb5b89-VIE", + "connection": "keep-alive", + "content-type": "text/event-stream; charset=utf-8", + "date": "Mon, 06 Jul 2026 12:42:42 GMT", + "openai-processing-ms": "277", + "openai-version": "2020-10-01", + "server": "cloudflare", + "set-cookie": "[REDACTED]", + "strict-transport-security": "max-age=31536000; includeSubDomains; preload", + "transfer-encoding": "chunked", + "x-content-type-options": "nosniff", + "x-ratelimit-limit-requests": "30000", + "x-ratelimit-limit-tokens": "180000000", + "x-ratelimit-remaining-requests": "29999", + "x-ratelimit-remaining-tokens": "179992644", + "x-ratelimit-reset-requests": "2ms", + "x-ratelimit-reset-tokens": "2ms", + "x-request-id": "526d60e7-9511-4966-9fc6-ec90737f89a8" + }, + "status": 200, + "statusText": "OK" + } + } + ], + "meta": { + "createdAt": "2026-07-06T10:11:31.744Z" + } +} diff --git a/e2e/scenarios/eve-instrumentation/__snapshots__/eve-instrumentation.span-tree.json b/e2e/scenarios/eve-instrumentation/__snapshots__/eve-instrumentation.span-tree.json new file mode 100644 index 000000000..a6efff06c --- /dev/null +++ b/e2e/scenarios/eve-instrumentation/__snapshots__/eve-instrumentation.span-tree.json @@ -0,0 +1,293 @@ +{ + "span_tree": [ + { + "name": "eve.turn", + "type": "task", + "children": [ + { + "name": "eve.step", + "type": "llm", + "children": [], + "input": [ + { + "content": "Run the Braintrust Eve instrumentation e2e scenario", + "role": "user" + } + ], + "output": { + "message": { + "content": null, + "role": "assistant", + "tool_calls": [ + { + "function": { + "arguments": "{\"query\":\"Run the Braintrust Eve instrumentation e2e scenario\"}", + "name": "search" + }, + "id": "", + "type": "function" + } + ] + } + }, + "metadata": { + "eve.agent.name": "eve-instrumentation-scenario", + "eve.channel.kind": "http", + "eve.finish_reason": "tool-calls", + "eve.model.id": "openai/gpt-5.4-mini", + "eve.session.id": "", + "eve.step.index": 0, + "eve.turn.id": "", + "eve.turn.sequence": 0, + "model": "gpt-5.4-mini", + "provider": "openai", + "scenario": "eve-instrumentation", + "testRunId": "" + }, + "metrics": { + "completion_tokens": 26, + "prompt_cached_tokens": 0, + "prompt_tokens": 6564, + "tokens": 6590 + } + }, + { + "name": "search", + "type": "tool", + "children": [], + "input": { + "query": "Run the Braintrust Eve instrumentation e2e scenario" + }, + "output": { + "query": "Run the Braintrust Eve instrumentation e2e scenario", + "title": "Eve instrumentation", + "url": "https://eve.dev/docs/guides/instrumentation" + }, + "metadata": { + "eve.action.status": "completed", + "eve.agent.name": "eve-instrumentation-scenario", + "eve.channel.kind": "http", + "eve.session.id": "", + "eve.step.index": 0, + "eve.tool.call_id": "", + "eve.tool.name": "search", + "eve.turn.id": "", + "eve.turn.sequence": 0, + "scenario": "eve-instrumentation", + "testRunId": "" + } + }, + { + "name": "eve.step", + "type": "llm", + "children": [], + "input": [ + { + "content": "Run the Braintrust Eve instrumentation e2e scenario", + "role": "user" + }, + { + "content": null, + "role": "assistant", + "tool_calls": [ + { + "function": { + "arguments": "{\"query\":\"Run the Braintrust Eve instrumentation e2e scenario\"}", + "name": "search" + }, + "id": "", + "type": "function" + } + ] + }, + { + "content": { + "query": "Run the Braintrust Eve instrumentation e2e scenario", + "title": "Eve instrumentation", + "url": "https://eve.dev/docs/guides/instrumentation" + }, + "name": "search", + "role": "tool", + "tool_call_id": "call_OJGkAZ5ouXaH7NjuUH2mSbk6" + } + ], + "output": { + "message": { + "content": null, + "role": "assistant", + "tool_calls": [ + { + "function": { + "arguments": "{\"url\":\"https://eve.dev/docs/guides/instrumentation\"}", + "name": "read" + }, + "id": "", + "type": "function" + } + ] + } + }, + "metadata": { + "eve.agent.name": "eve-instrumentation-scenario", + "eve.channel.kind": "http", + "eve.finish_reason": "tool-calls", + "eve.model.id": "openai/gpt-5.4-mini", + "eve.session.id": "", + "eve.step.index": 1, + "eve.turn.id": "", + "eve.turn.sequence": 0, + "model": "gpt-5.4-mini", + "provider": "openai", + "scenario": "eve-instrumentation", + "testRunId": "" + }, + "metrics": { + "completion_tokens": 27, + "prompt_cached_tokens": 0, + "prompt_tokens": 6634, + "tokens": 6661 + } + }, + { + "name": "read", + "type": "tool", + "children": [], + "input": { + "url": "https://eve.dev/docs/guides/instrumentation" + }, + "output": { + "excerpt": "Eve hooks expose runtime stream events that Braintrust maps into a flat turn trace.", + "section": "Runtime context", + "title": "Eve instrumentation", + "url": "https://eve.dev/docs/guides/instrumentation" + }, + "metadata": { + "eve.action.status": "completed", + "eve.agent.name": "eve-instrumentation-scenario", + "eve.channel.kind": "http", + "eve.session.id": "", + "eve.step.index": 1, + "eve.tool.call_id": "", + "eve.tool.name": "read", + "eve.turn.id": "", + "eve.turn.sequence": 0, + "scenario": "eve-instrumentation", + "testRunId": "" + } + }, + { + "name": "eve.step", + "type": "llm", + "children": [], + "input": [ + { + "content": "Run the Braintrust Eve instrumentation e2e scenario", + "role": "user" + }, + { + "content": null, + "role": "assistant", + "tool_calls": [ + { + "function": { + "arguments": "{\"query\":\"Run the Braintrust Eve instrumentation e2e scenario\"}", + "name": "search" + }, + "id": "", + "type": "function" + } + ] + }, + { + "content": { + "query": "Run the Braintrust Eve instrumentation e2e scenario", + "title": "Eve instrumentation", + "url": "https://eve.dev/docs/guides/instrumentation" + }, + "name": "search", + "role": "tool", + "tool_call_id": "call_OJGkAZ5ouXaH7NjuUH2mSbk6" + }, + { + "content": null, + "role": "assistant", + "tool_calls": [ + { + "function": { + "arguments": "{\"url\":\"https://eve.dev/docs/guides/instrumentation\"}", + "name": "read" + }, + "id": "", + "type": "function" + } + ] + }, + { + "content": { + "excerpt": "Eve hooks expose runtime stream events that Braintrust maps into a flat turn trace.", + "section": "Runtime context", + "title": "Eve instrumentation", + "url": "https://eve.dev/docs/guides/instrumentation" + }, + "name": "read", + "role": "tool", + "tool_call_id": "call_kZ7AzZ9mCqataCzrSSLxRkFh" + } + ], + "output": { + "finish_reason": "stop", + "message": { + "content": "Final answer from read: Eve instrumentation — https://eve.dev/docs/guides/instrumentation — Eve hooks expose runtime stream events that Braintrust maps into a flat turn trace.", + "role": "assistant" + } + }, + "metadata": { + "eve.agent.name": "eve-instrumentation-scenario", + "eve.channel.kind": "http", + "eve.finish_reason": "stop", + "eve.model.id": "openai/gpt-5.4-mini", + "eve.session.id": "", + "eve.step.index": 2, + "eve.turn.id": "", + "eve.turn.sequence": 0, + "model": "gpt-5.4-mini", + "provider": "openai", + "scenario": "eve-instrumentation", + "testRunId": "" + }, + "metrics": { + "completion_tokens": 40, + "prompt_cached_tokens": 0, + "prompt_tokens": 6716, + "tokens": 6756 + } + } + ], + "input": [ + { + "content": "Run the Braintrust Eve instrumentation e2e scenario", + "role": "user" + } + ], + "output": "Final answer from read: Eve instrumentation — https://eve.dev/docs/guides/instrumentation — Eve hooks expose runtime stream events that Braintrust maps into a flat turn trace.", + "metadata": { + "eve.agent.name": "eve-instrumentation-scenario", + "eve.channel.kind": "http", + "eve.model.id": "openai/gpt-5.4-mini", + "eve.session.id": "", + "eve.turn.id": "", + "eve.turn.sequence": 0, + "model": "gpt-5.4-mini", + "provider": "openai", + "scenario": "eve-instrumentation", + "testRunId": "" + }, + "metrics": { + "completion_tokens": 93, + "prompt_cached_tokens": 0, + "prompt_tokens": 19914, + "tokens": 20007 + } + } + ] +} diff --git a/e2e/scenarios/eve-instrumentation/__snapshots__/eve-instrumentation.span-tree.txt b/e2e/scenarios/eve-instrumentation/__snapshots__/eve-instrumentation.span-tree.txt new file mode 100644 index 000000000..7c22627b6 --- /dev/null +++ b/e2e/scenarios/eve-instrumentation/__snapshots__/eve-instrumentation.span-tree.txt @@ -0,0 +1,265 @@ +span_tree: +└── eve.turn [task] + input: [ + { + "content": "Run the Braintrust Eve instrumentation e2e scenario", + "role": "user" + } + ] + output: "Final answer from read: Eve instrumentation — https://eve.dev/docs/guides/instrumentation — Eve hooks expose runtime stream events that Braintrust maps into a flat turn trace." + metadata: { + "eve.agent.name": "eve-instrumentation-scenario", + "eve.channel.kind": "http", + "eve.model.id": "openai/gpt-5.4-mini", + "eve.session.id": "", + "eve.turn.id": "", + "eve.turn.sequence": 0, + "model": "gpt-5.4-mini", + "provider": "openai", + "scenario": "eve-instrumentation", + "testRunId": "" + } + metrics: { + "completion_tokens": 93, + "prompt_cached_tokens": 0, + "prompt_tokens": 19914, + "tokens": 20007 + } + ├── eve.step [llm] + │ input: [ + │ { + │ "content": "Run the Braintrust Eve instrumentation e2e scenario", + │ "role": "user" + │ } + │ ] + │ output: { + │ "message": { + │ "content": null, + │ "role": "assistant", + │ "tool_calls": [ + │ { + │ "function": { + │ "arguments": "{\"query\":\"Run the Braintrust Eve instrumentation e2e scenario\"}", + │ "name": "search" + │ }, + │ "id": "", + │ "type": "function" + │ } + │ ] + │ } + │ } + │ metadata: { + │ "eve.agent.name": "eve-instrumentation-scenario", + │ "eve.channel.kind": "http", + │ "eve.finish_reason": "tool-calls", + │ "eve.model.id": "openai/gpt-5.4-mini", + │ "eve.session.id": "", + │ "eve.step.index": 0, + │ "eve.turn.id": "", + │ "eve.turn.sequence": 0, + │ "model": "gpt-5.4-mini", + │ "provider": "openai", + │ "scenario": "eve-instrumentation", + │ "testRunId": "" + │ } + │ metrics: { + │ "completion_tokens": 26, + │ "prompt_cached_tokens": 0, + │ "prompt_tokens": 6564, + │ "tokens": 6590 + │ } + ├── search [tool] + │ input: { + │ "query": "Run the Braintrust Eve instrumentation e2e scenario" + │ } + │ output: { + │ "query": "Run the Braintrust Eve instrumentation e2e scenario", + │ "title": "Eve instrumentation", + │ "url": "https://eve.dev/docs/guides/instrumentation" + │ } + │ metadata: { + │ "eve.action.status": "completed", + │ "eve.agent.name": "eve-instrumentation-scenario", + │ "eve.channel.kind": "http", + │ "eve.session.id": "", + │ "eve.step.index": 0, + │ "eve.tool.call_id": "", + │ "eve.tool.name": "search", + │ "eve.turn.id": "", + │ "eve.turn.sequence": 0, + │ "scenario": "eve-instrumentation", + │ "testRunId": "" + │ } + ├── eve.step [llm] + │ input: [ + │ { + │ "content": "Run the Braintrust Eve instrumentation e2e scenario", + │ "role": "user" + │ }, + │ { + │ "content": null, + │ "role": "assistant", + │ "tool_calls": [ + │ { + │ "function": { + │ "arguments": "{\"query\":\"Run the Braintrust Eve instrumentation e2e scenario\"}", + │ "name": "search" + │ }, + │ "id": "", + │ "type": "function" + │ } + │ ] + │ }, + │ { + │ "content": { + │ "query": "Run the Braintrust Eve instrumentation e2e scenario", + │ "title": "Eve instrumentation", + │ "url": "https://eve.dev/docs/guides/instrumentation" + │ }, + │ "name": "search", + │ "role": "tool", + │ "tool_call_id": "call_OJGkAZ5ouXaH7NjuUH2mSbk6" + │ } + │ ] + │ output: { + │ "message": { + │ "content": null, + │ "role": "assistant", + │ "tool_calls": [ + │ { + │ "function": { + │ "arguments": "{\"url\":\"https://eve.dev/docs/guides/instrumentation\"}", + │ "name": "read" + │ }, + │ "id": "", + │ "type": "function" + │ } + │ ] + │ } + │ } + │ metadata: { + │ "eve.agent.name": "eve-instrumentation-scenario", + │ "eve.channel.kind": "http", + │ "eve.finish_reason": "tool-calls", + │ "eve.model.id": "openai/gpt-5.4-mini", + │ "eve.session.id": "", + │ "eve.step.index": 1, + │ "eve.turn.id": "", + │ "eve.turn.sequence": 0, + │ "model": "gpt-5.4-mini", + │ "provider": "openai", + │ "scenario": "eve-instrumentation", + │ "testRunId": "" + │ } + │ metrics: { + │ "completion_tokens": 27, + │ "prompt_cached_tokens": 0, + │ "prompt_tokens": 6634, + │ "tokens": 6661 + │ } + ├── read [tool] + │ input: { + │ "url": "https://eve.dev/docs/guides/instrumentation" + │ } + │ output: { + │ "excerpt": "Eve hooks expose runtime stream events that Braintrust maps into a flat turn trace.", + │ "section": "Runtime context", + │ "title": "Eve instrumentation", + │ "url": "https://eve.dev/docs/guides/instrumentation" + │ } + │ metadata: { + │ "eve.action.status": "completed", + │ "eve.agent.name": "eve-instrumentation-scenario", + │ "eve.channel.kind": "http", + │ "eve.session.id": "", + │ "eve.step.index": 1, + │ "eve.tool.call_id": "", + │ "eve.tool.name": "read", + │ "eve.turn.id": "", + │ "eve.turn.sequence": 0, + │ "scenario": "eve-instrumentation", + │ "testRunId": "" + │ } + └── eve.step [llm] + input: [ + { + "content": "Run the Braintrust Eve instrumentation e2e scenario", + "role": "user" + }, + { + "content": null, + "role": "assistant", + "tool_calls": [ + { + "function": { + "arguments": "{\"query\":\"Run the Braintrust Eve instrumentation e2e scenario\"}", + "name": "search" + }, + "id": "", + "type": "function" + } + ] + }, + { + "content": { + "query": "Run the Braintrust Eve instrumentation e2e scenario", + "title": "Eve instrumentation", + "url": "https://eve.dev/docs/guides/instrumentation" + }, + "name": "search", + "role": "tool", + "tool_call_id": "call_OJGkAZ5ouXaH7NjuUH2mSbk6" + }, + { + "content": null, + "role": "assistant", + "tool_calls": [ + { + "function": { + "arguments": "{\"url\":\"https://eve.dev/docs/guides/instrumentation\"}", + "name": "read" + }, + "id": "", + "type": "function" + } + ] + }, + { + "content": { + "excerpt": "Eve hooks expose runtime stream events that Braintrust maps into a flat turn trace.", + "section": "Runtime context", + "title": "Eve instrumentation", + "url": "https://eve.dev/docs/guides/instrumentation" + }, + "name": "read", + "role": "tool", + "tool_call_id": "call_kZ7AzZ9mCqataCzrSSLxRkFh" + } + ] + output: { + "finish_reason": "stop", + "message": { + "content": "Final answer from read: Eve instrumentation — https://eve.dev/docs/guides/instrumentation — Eve hooks expose runtime stream events that Braintrust maps into a flat turn trace.", + "role": "assistant" + } + } + metadata: { + "eve.agent.name": "eve-instrumentation-scenario", + "eve.channel.kind": "http", + "eve.finish_reason": "stop", + "eve.model.id": "openai/gpt-5.4-mini", + "eve.session.id": "", + "eve.step.index": 2, + "eve.turn.id": "", + "eve.turn.sequence": 0, + "model": "gpt-5.4-mini", + "provider": "openai", + "scenario": "eve-instrumentation", + "testRunId": "" + } + metrics: { + "completion_tokens": 40, + "prompt_cached_tokens": 0, + "prompt_tokens": 6716, + "tokens": 6756 + } diff --git a/e2e/scenarios/eve-instrumentation/agent/agent.ts b/e2e/scenarios/eve-instrumentation/agent/agent.ts new file mode 100644 index 000000000..467390630 --- /dev/null +++ b/e2e/scenarios/eve-instrumentation/agent/agent.ts @@ -0,0 +1,13 @@ +import { defineAgent } from "eve"; +import { createOpenAI } from "@ai-sdk/openai"; + +const openai = createOpenAI({ + ...(process.env.OPENAI_BASE_URL + ? { baseURL: process.env.OPENAI_BASE_URL } + : {}), +}); + +export default defineAgent({ + model: openai("gpt-5.4-mini"), + modelContextWindowTokens: 8_192, +}); diff --git a/e2e/scenarios/eve-instrumentation/agent/hooks/braintrust.ts b/e2e/scenarios/eve-instrumentation/agent/hooks/braintrust.ts new file mode 100644 index 000000000..2597dc318 --- /dev/null +++ b/e2e/scenarios/eve-instrumentation/agent/hooks/braintrust.ts @@ -0,0 +1,13 @@ +import { braintrustEveHook } from "braintrust"; +import { defineHook } from "eve/hooks"; + +export default defineHook( + braintrustEveHook({ + metadata: { + scenario: "eve-instrumentation", + ...(process.env.BRAINTRUST_E2E_RUN_ID + ? { testRunId: process.env.BRAINTRUST_E2E_RUN_ID } + : {}), + }, + }), +); diff --git a/e2e/scenarios/eve-instrumentation/agent/instructions.md b/e2e/scenarios/eve-instrumentation/agent/instructions.md new file mode 100644 index 000000000..a4715d143 --- /dev/null +++ b/e2e/scenarios/eve-instrumentation/agent/instructions.md @@ -0,0 +1,10 @@ +You are a deterministic fixture agent for Braintrust Eve instrumentation tests. + +For every user task: + +1. Call the search tool exactly once with the full user message as the query. +2. After the search result is available, call the read tool exactly once with the + URL returned by search. +3. After the read result is available, answer with a single sentence that starts + with "Final answer from read:" and includes the search title, URL, and read + excerpt. diff --git a/e2e/scenarios/eve-instrumentation/agent/instrumentation.ts b/e2e/scenarios/eve-instrumentation/agent/instrumentation.ts new file mode 100644 index 000000000..297f3555f --- /dev/null +++ b/e2e/scenarios/eve-instrumentation/agent/instrumentation.ts @@ -0,0 +1,10 @@ +import { initLogger } from "braintrust"; +import { defineInstrumentation } from "eve/instrumentation"; + +export default defineInstrumentation({ + setup: ({ agentName }) => { + initLogger({ + projectName: process.env.BRAINTRUST_E2E_PROJECT_NAME || agentName, + }); + }, +}); diff --git a/e2e/scenarios/eve-instrumentation/agent/tools/read.ts b/e2e/scenarios/eve-instrumentation/agent/tools/read.ts new file mode 100644 index 000000000..66e0e261b --- /dev/null +++ b/e2e/scenarios/eve-instrumentation/agent/tools/read.ts @@ -0,0 +1,18 @@ +import { defineTool } from "eve/tools"; +import { z } from "zod"; + +export default defineTool({ + description: "Reads a deterministic Eve documentation page fixture.", + inputSchema: z.object({ + url: z.string(), + }), + async execute({ url }) { + return { + excerpt: + "Eve hooks expose runtime stream events that Braintrust maps into a flat turn trace.", + section: "Runtime context", + title: "Eve instrumentation", + url, + }; + }, +}); diff --git a/e2e/scenarios/eve-instrumentation/agent/tools/search.ts b/e2e/scenarios/eve-instrumentation/agent/tools/search.ts new file mode 100644 index 000000000..dc95b8ea1 --- /dev/null +++ b/e2e/scenarios/eve-instrumentation/agent/tools/search.ts @@ -0,0 +1,16 @@ +import { defineTool } from "eve/tools"; +import { z } from "zod"; + +export default defineTool({ + description: "Searches the Eve instrumentation documentation fixture.", + inputSchema: z.object({ + query: z.string(), + }), + async execute({ query }) { + return { + query, + title: "Eve instrumentation", + url: "https://eve.dev/docs/guides/instrumentation", + }; + }, +}); diff --git a/e2e/scenarios/eve-instrumentation/cassette-filter.mjs b/e2e/scenarios/eve-instrumentation/cassette-filter.mjs new file mode 100644 index 000000000..f73d1a5ef --- /dev/null +++ b/e2e/scenarios/eve-instrumentation/cassette-filter.mjs @@ -0,0 +1,21 @@ +// @ts-check +export { filter } from "../ai-sdk-instrumentation/cassette-filter.mjs"; + +/** @type {import("@braintrust/seinfeld").RedactionSpec} */ +export const redact = [ + "paranoid", + { + redactResponse(response) { + return { + ...response, + headers: Object.fromEntries( + Object.entries(response.headers).filter( + ([key]) => + key.toLowerCase() !== "openai-organization" && + key.toLowerCase() !== "openai-project", + ), + ), + }; + }, + }, +]; diff --git a/e2e/scenarios/eve-instrumentation/package.json b/e2e/scenarios/eve-instrumentation/package.json new file mode 100644 index 000000000..a24533ef9 --- /dev/null +++ b/e2e/scenarios/eve-instrumentation/package.json @@ -0,0 +1,10 @@ +{ + "name": "eve-instrumentation-scenario", + "private": true, + "type": "module", + "dependencies": { + "@ai-sdk/openai": "4.0.0-beta.44", + "eve": "0.20.0", + "zod": "4.3.6" + } +} diff --git a/e2e/scenarios/eve-instrumentation/pnpm-lock.yaml b/e2e/scenarios/eve-instrumentation/pnpm-lock.yaml new file mode 100644 index 000000000..a229714ae --- /dev/null +++ b/e2e/scenarios/eve-instrumentation/pnpm-lock.yaml @@ -0,0 +1,737 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@ai-sdk/openai': + specifier: 4.0.0-beta.44 + version: 4.0.0-beta.44(zod@4.3.6) + eve: + specifier: 0.20.0 + version: 0.20.0(ai@7.0.15(zod@4.3.6)) + zod: + specifier: 4.3.6 + version: 4.3.6 + +packages: + + '@ai-sdk/gateway@4.0.12': + resolution: {integrity: sha512-Y7Fy8xJwPz7ZC0DhSQG3HIVk+drup42hrIj6yqKlib3CxwiR0F7nYyUI8+kPrEtbZEoyKoRstvT4/o0HEyFBHA==} + engines: {node: '>=22'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/openai@4.0.0-beta.44': + resolution: {integrity: sha512-kXdGA6vyh9SwL/B11UhODjbw0koFkDA/yD7hacBxMHSZx6l3T9hZLECEwOkn6OZDrQR+vno5Ka/pUyXrG82BkQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider-utils@5.0.0-beta.30': + resolution: {integrity: sha512-X3AxTsFJZr9Mw32SncWJA4/ShU8NA/XjhoallCYdcdQZ2gZZ/cIDyJJ6pNh1aBhicRNHv2ndgI6w/H2NGNIjOA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider-utils@5.0.5': + resolution: {integrity: sha512-oI0t3dvCoqWNV1I8o1Rybi2DXDvHES5r/TrwtJW90tuFLVepgJlftPxrcjh8vaSvjqC2diTuA2vXyjKAyHJm4A==} + engines: {node: '>=22'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider@4.0.0-beta.14': + resolution: {integrity: sha512-SMijtjVHs38n0dMkTcNuGrnStiF76OhAqS0R+ZX4iXUnuPPyTxQoVcF8Xuf2AoBB5rqndTId5FVT05bgqD5KsA==} + engines: {node: '>=18'} + + '@ai-sdk/provider@4.0.2': + resolution: {integrity: sha512-pfPoy9J1B1xV7cqJ8MYHOsDYrMv5tR3+EMNfI249OhkD2uRakvav3Fo7XpD2luuN/YNCBY7KfEQc7vEV7KEtyw==} + engines: {node: '>=22'} + + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@oxc-project/types@0.138.0': + resolution: {integrity: sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==} + + '@rolldown/binding-android-arm64@1.1.4': + resolution: {integrity: sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.1.4': + resolution: {integrity: sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.1.4': + resolution: {integrity: sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.1.4': + resolution: {integrity: sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.1.4': + resolution: {integrity: sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.1.4': + resolution: {integrity: sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.1.4': + resolution: {integrity: sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.1.4': + resolution: {integrity: sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.1.4': + resolution: {integrity: sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.1.4': + resolution: {integrity: sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.1.4': + resolution: {integrity: sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.1.4': + resolution: {integrity: sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.1.4': + resolution: {integrity: sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.1.4': + resolution: {integrity: sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.1.4': + resolution: {integrity: sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + + '@vercel/oidc@3.2.0': + resolution: {integrity: sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==} + engines: {node: '>= 20'} + + '@workflow/serde@4.1.0': + resolution: {integrity: sha512-pav4F2BoirECWR7Nf1TKt+2eETcBj7jj4cBefQ8VXQCA6NPkaKeLfj/zMgi+3zYV5ZIBT4GuUiphsj0/b9hPQQ==} + + ai@7.0.15: + resolution: {integrity: sha512-7406MUy9O5sIhwOgxEWuoj+td3XUGgG96SBt6pmBU4t4sQ2fpnDx5UnHaXT2HUTXl5GorbWz/MfCrSPRz0QJqw==} + engines: {node: '>=22'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + crossws@0.4.9: + resolution: {integrity: sha512-iWx+1OMSG2aOHpjyf9AESOzkwsVdS49cXM9dVrI2PDhxU5l2RIWE/KG56gk4BbAnsMoycvniJ9OnOxO9LRzHVA==} + peerDependencies: + srvx: '>=0.11.5' + peerDependenciesMeta: + srvx: + optional: true + + db0@0.3.4: + resolution: {integrity: sha512-RiXXi4WaNzPTHEOu8UPQKMooIbqOEyqA1t7Z6MsdxSCeb8iUC9ko3LcmsLmeUt2SM5bctfArZKkRQggKZz7JNw==} + peerDependencies: + '@electric-sql/pglite': '*' + '@libsql/client': '*' + better-sqlite3: '*' + drizzle-orm: '*' + mysql2: '*' + sqlite3: '*' + peerDependenciesMeta: + '@electric-sql/pglite': + optional: true + '@libsql/client': + optional: true + better-sqlite3: + optional: true + drizzle-orm: + optional: true + mysql2: + optional: true + sqlite3: + optional: true + + env-runner@0.1.16: + resolution: {integrity: sha512-2LRJM4P2KLX6J83QZZrMqvgCDt/D5ea7wPcI3yYiy5cG/9rX5QwdwZFx0D7ktWnjdRyZxYjttGGorb5nFqb1CA==} + hasBin: true + peerDependencies: + '@netlify/runtime': ^4.1.23 + '@vercel/queue': '>=0.2.0' + miniflare: ^4.20260515.0 + wrangler: ^4.0.0 + peerDependenciesMeta: + '@netlify/runtime': + optional: true + '@vercel/queue': + optional: true + miniflare: + optional: true + wrangler: + optional: true + + eve@0.20.0: + resolution: {integrity: sha512-O218jbwGCjg0ygTcBKuIy+c4UqL84/Zyala9qOAEhiMEVUpllUU6VZriQcnQJkkQscpOiOikF0rcWgUABBVrrw==} + engines: {node: '>=24'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.0.0 + ai: ^7.0.0 + braintrust: ^3.0.0 + just-bash: ^3.0.0 + microsandbox: ^0.5.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + braintrust: + optional: true + just-bash: + optional: true + microsandbox: + optional: true + + eventsource-parser@3.1.0: + resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==} + engines: {node: '>=18.0.0'} + + exsolve@1.1.0: + resolution: {integrity: sha512-D+42+T12DdIlJM3uepa55qGiL3sYdLBOxIl2ifQCzCHz4c7eiolaHsi3BIqEr7JxBzxv2pYZQX9kw16ziMcEmw==} + + h3@2.0.1-rc.22: + resolution: {integrity: sha512-Esv0DMIuPkCTSWCA0vO73vcTqwzH1wjSrAO1TXNu/K3up1sZHa9EKMapbmxCDYBeymC3fVTk4qxp7ogQWQ+KgA==} + engines: {node: '>=20.11.1'} + hasBin: true + peerDependencies: + crossws: ^0.4.1 + peerDependenciesMeta: + crossws: + optional: true + + hookable@6.1.1: + resolution: {integrity: sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==} + + httpxy@0.5.4: + resolution: {integrity: sha512-URfeibL0kTH6VuIxxaJDXWQWEk8fKr+9L8MGv6CuAiNy0fGnoVhWbXBvJR1mkdsvCDUxvhX9cW60k2AhtH5s6w==} + + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + + nf3@0.3.19: + resolution: {integrity: sha512-tfOXX/ivQBL+4km/fzxQ0HWMmp1Ewx/YpFLbya080gXVfm26bZy0n4a5K5PPnqTLyuAHu0FobVpXXUadIiIwIQ==} + + nitro@3.0.260610-beta: + resolution: {integrity: sha512-KPb4L5yaF/Rx/xoGMpgHRJvZhbhGiqbRKOwwPLCH9jKTKTsEUHLjnJas85AeCzaswqa8Wi52eQBtRsODC4PS0Q==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@vercel/queue': ^0.3.0 + dotenv: '*' + giget: '*' + jiti: ^2.7.0 + rollup: ^4.61.1 + vite: ^7 || ^8 + xml2js: ^0.6.2 + zephyr-agent: ^0.2.0 + peerDependenciesMeta: + '@vercel/queue': + optional: true + dotenv: + optional: true + giget: + optional: true + jiti: + optional: true + rollup: + optional: true + vite: + optional: true + xml2js: + optional: true + zephyr-agent: + optional: true + + ocache@0.1.5: + resolution: {integrity: sha512-kNNnkkVQup/QDvmTz8Q84wc2ntiyoVHDxa6eHWKt5qdGAmFRBIxy83rxgCYEjW0x06UJ9E3P6VgM2yY4rOBH4w==} + + ofetch@2.0.0-alpha.3: + resolution: {integrity: sha512-zpYTCs2byOuft65vI3z43Dd6iSdFbOZZLb9/d21aCpx2rGastVU9dOCv0lu4ykc1Ur1anAYjDi3SUvR0vq50JA==} + + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + rolldown@1.1.4: + resolution: {integrity: sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + rou3@0.8.1: + resolution: {integrity: sha512-ePa+XGk00/3HuCqrEnK3LxJW7I0SdNg6EFzKUJG73hMAdDcOUC/i/aSz7LSDwLrGr33kal/rqOGydzwl6U7zBA==} + + srvx@0.11.21: + resolution: {integrity: sha512-GWTHjKMeekX8CwJf4VU9Oo6mJpSGaflGMddbCvR+Cmmh9sslRMiGbAoqqZacE0r1ncARh6buCEETr2W52F8b1w==} + engines: {node: '>=20.16.0'} + hasBin: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + unenv@2.0.0-rc.24: + resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} + + unstorage@2.0.0-alpha.7: + resolution: {integrity: sha512-ELPztchk2zgFJnakyodVY3vJWGW9jy//keJ32IOJVGUMyaPydwcA1FtVvWqT0TNRch9H+cMNEGllfVFfScImog==} + peerDependencies: + '@azure/app-configuration': ^1.11.0 + '@azure/cosmos': ^4.9.1 + '@azure/data-tables': ^13.3.2 + '@azure/identity': ^4.13.0 + '@azure/keyvault-secrets': ^4.10.0 + '@azure/storage-blob': ^12.31.0 + '@capacitor/preferences': ^6 || ^7 || ^8 + '@deno/kv': '>=0.13.0' + '@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0 + '@planetscale/database': ^1.19.0 + '@upstash/redis': ^1.36.2 + '@vercel/blob': '>=0.27.3' + '@vercel/functions': ^2.2.12 || ^3.0.0 + '@vercel/kv': ^1.0.1 + aws4fetch: ^1.0.20 + chokidar: ^4 || ^5 + db0: '>=0.3.4' + idb-keyval: ^6.2.2 + ioredis: ^5.9.3 + lru-cache: ^11.2.6 + mongodb: ^6 || ^7 + ofetch: '*' + uploadthing: ^7.7.4 + peerDependenciesMeta: + '@azure/app-configuration': + optional: true + '@azure/cosmos': + optional: true + '@azure/data-tables': + optional: true + '@azure/identity': + optional: true + '@azure/keyvault-secrets': + optional: true + '@azure/storage-blob': + optional: true + '@capacitor/preferences': + optional: true + '@deno/kv': + optional: true + '@netlify/blobs': + optional: true + '@planetscale/database': + optional: true + '@upstash/redis': + optional: true + '@vercel/blob': + optional: true + '@vercel/functions': + optional: true + '@vercel/kv': + optional: true + aws4fetch: + optional: true + chokidar: + optional: true + db0: + optional: true + idb-keyval: + optional: true + ioredis: + optional: true + lru-cache: + optional: true + mongodb: + optional: true + ofetch: + optional: true + uploadthing: + optional: true + + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + +snapshots: + + '@ai-sdk/gateway@4.0.12(zod@4.3.6)': + dependencies: + '@ai-sdk/provider': 4.0.2 + '@ai-sdk/provider-utils': 5.0.5(zod@4.3.6) + '@vercel/oidc': 3.2.0 + zod: 4.3.6 + + '@ai-sdk/openai@4.0.0-beta.44(zod@4.3.6)': + dependencies: + '@ai-sdk/provider': 4.0.0-beta.14 + '@ai-sdk/provider-utils': 5.0.0-beta.30(zod@4.3.6) + zod: 4.3.6 + + '@ai-sdk/provider-utils@5.0.0-beta.30(zod@4.3.6)': + dependencies: + '@ai-sdk/provider': 4.0.0-beta.14 + '@standard-schema/spec': 1.1.0 + '@workflow/serde': 4.1.0 + eventsource-parser: 3.1.0 + zod: 4.3.6 + + '@ai-sdk/provider-utils@5.0.5(zod@4.3.6)': + dependencies: + '@ai-sdk/provider': 4.0.2 + '@standard-schema/spec': 1.1.0 + '@workflow/serde': 4.1.0 + eventsource-parser: 3.1.0 + zod: 4.3.6 + + '@ai-sdk/provider@4.0.0-beta.14': + dependencies: + json-schema: 0.4.0 + + '@ai-sdk/provider@4.0.2': + dependencies: + json-schema: 0.4.0 + + '@emnapi/core@1.11.1': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@oxc-project/types@0.138.0': {} + + '@rolldown/binding-android-arm64@1.1.4': + optional: true + + '@rolldown/binding-darwin-arm64@1.1.4': + optional: true + + '@rolldown/binding-darwin-x64@1.1.4': + optional: true + + '@rolldown/binding-freebsd-x64@1.1.4': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.1.4': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.1.4': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.1.4': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.1.4': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.1.4': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.1.4': + optional: true + + '@rolldown/binding-linux-x64-musl@1.1.4': + optional: true + + '@rolldown/binding-openharmony-arm64@1.1.4': + optional: true + + '@rolldown/binding-wasm32-wasi@1.1.4': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.1.4': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.1.4': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@standard-schema/spec@1.1.0': {} + + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@vercel/oidc@3.2.0': {} + + '@workflow/serde@4.1.0': {} + + ai@7.0.15(zod@4.3.6): + dependencies: + '@ai-sdk/gateway': 4.0.12(zod@4.3.6) + '@ai-sdk/provider': 4.0.2 + '@ai-sdk/provider-utils': 5.0.5(zod@4.3.6) + zod: 4.3.6 + + consola@3.4.2: {} + + crossws@0.4.9(srvx@0.11.21): + optionalDependencies: + srvx: 0.11.21 + + db0@0.3.4: {} + + env-runner@0.1.16: + dependencies: + crossws: 0.4.9(srvx@0.11.21) + exsolve: 1.1.0 + httpxy: 0.5.4 + srvx: 0.11.21 + + eve@0.20.0(ai@7.0.15(zod@4.3.6)): + dependencies: + ai: 7.0.15(zod@4.3.6) + nitro: 3.0.260610-beta + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@electric-sql/pglite' + - '@libsql/client' + - '@netlify/blobs' + - '@netlify/runtime' + - '@planetscale/database' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - '@vercel/queue' + - aws4fetch + - better-sqlite3 + - chokidar + - dotenv + - drizzle-orm + - giget + - idb-keyval + - ioredis + - jiti + - lru-cache + - miniflare + - mongodb + - mysql2 + - rollup + - sqlite3 + - uploadthing + - vite + - wrangler + - xml2js + - zephyr-agent + + eventsource-parser@3.1.0: {} + + exsolve@1.1.0: {} + + h3@2.0.1-rc.22(crossws@0.4.9(srvx@0.11.21)): + dependencies: + rou3: 0.8.1 + srvx: 0.11.21 + optionalDependencies: + crossws: 0.4.9(srvx@0.11.21) + + hookable@6.1.1: {} + + httpxy@0.5.4: {} + + json-schema@0.4.0: {} + + nf3@0.3.19: {} + + nitro@3.0.260610-beta: + dependencies: + consola: 3.4.2 + crossws: 0.4.9(srvx@0.11.21) + db0: 0.3.4 + env-runner: 0.1.16 + h3: 2.0.1-rc.22(crossws@0.4.9(srvx@0.11.21)) + hookable: 6.1.1 + nf3: 0.3.19 + ocache: 0.1.5 + ofetch: 2.0.0-alpha.3 + ohash: 2.0.11 + rolldown: 1.1.4 + srvx: 0.11.21 + unenv: 2.0.0-rc.24 + unstorage: 2.0.0-alpha.7(db0@0.3.4)(ofetch@2.0.0-alpha.3) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@electric-sql/pglite' + - '@libsql/client' + - '@netlify/blobs' + - '@netlify/runtime' + - '@planetscale/database' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - better-sqlite3 + - chokidar + - drizzle-orm + - idb-keyval + - ioredis + - lru-cache + - miniflare + - mongodb + - mysql2 + - sqlite3 + - uploadthing + - wrangler + + ocache@0.1.5: + dependencies: + ohash: 2.0.11 + + ofetch@2.0.0-alpha.3: {} + + ohash@2.0.11: {} + + pathe@2.0.3: {} + + rolldown@1.1.4: + dependencies: + '@oxc-project/types': 0.138.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.1.4 + '@rolldown/binding-darwin-arm64': 1.1.4 + '@rolldown/binding-darwin-x64': 1.1.4 + '@rolldown/binding-freebsd-x64': 1.1.4 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.4 + '@rolldown/binding-linux-arm64-gnu': 1.1.4 + '@rolldown/binding-linux-arm64-musl': 1.1.4 + '@rolldown/binding-linux-ppc64-gnu': 1.1.4 + '@rolldown/binding-linux-s390x-gnu': 1.1.4 + '@rolldown/binding-linux-x64-gnu': 1.1.4 + '@rolldown/binding-linux-x64-musl': 1.1.4 + '@rolldown/binding-openharmony-arm64': 1.1.4 + '@rolldown/binding-wasm32-wasi': 1.1.4 + '@rolldown/binding-win32-arm64-msvc': 1.1.4 + '@rolldown/binding-win32-x64-msvc': 1.1.4 + + rou3@0.8.1: {} + + srvx@0.11.21: {} + + tslib@2.8.1: + optional: true + + unenv@2.0.0-rc.24: + dependencies: + pathe: 2.0.3 + + unstorage@2.0.0-alpha.7(db0@0.3.4)(ofetch@2.0.0-alpha.3): + optionalDependencies: + db0: 0.3.4 + ofetch: 2.0.0-alpha.3 + + zod@4.3.6: {} diff --git a/e2e/scenarios/eve-instrumentation/scenario.test.ts b/e2e/scenarios/eve-instrumentation/scenario.test.ts new file mode 100644 index 000000000..677ed4838 --- /dev/null +++ b/e2e/scenarios/eve-instrumentation/scenario.test.ts @@ -0,0 +1,116 @@ +import { beforeAll, describe, expect, test } from "vitest"; +import { resolveFileSnapshotPath } from "../../helpers/file-snapshot"; +import type { CapturedLogEvent } from "../../helpers/mock-braintrust-server"; +import { + prepareScenarioDir, + resolveScenarioDir, + withScenarioHarness, +} from "../../helpers/scenario-harness"; +import { matchSpanTreeSnapshot } from "../../helpers/span-tree"; +import { + findChildSpans, + findLatestChildSpan, + findLatestSpan, +} from "../../helpers/trace-selectors"; + +const originalScenarioDir = resolveScenarioDir(import.meta.url); +const scenarioDir = await prepareScenarioDir({ + scenarioDir: originalScenarioDir, +}); +const spanTreeSnapshotPath = resolveFileSnapshotPath( + import.meta.url, + "eve-instrumentation.span-tree.json", +); +const TIMEOUT_MS = 120_000; + +describe("eve instrumentation", () => { + let events: CapturedLogEvent[] = []; + + beforeAll(async () => { + await withScenarioHarness( + async ({ events: harnessEvents, runScenarioDir }) => { + await runScenarioDir({ + entry: "scenario.ts", + env: { + NODE_ENV: "development", + }, + runContext: { + originalScenarioDir, + variantKey: "eve-v0-20-0", + }, + scenarioDir, + timeoutMs: TIMEOUT_MS, + }); + events = harnessEvents(); + }, + ); + }, TIMEOUT_MS); + + test("captures a flat Eve turn trace", async () => { + const root = findLatestSpan(events, "eve.turn"); + const steps = findChildSpans(events, "eve.step", root?.span.id).sort( + (left, right) => + Number(left.metadata?.["eve.step.index"]) - + Number(right.metadata?.["eve.step.index"]), + ); + const search = findLatestChildSpan(events, "search", root?.span.id); + const read = findLatestChildSpan(events, "read", root?.span.id); + + expect(root).toBeDefined(); + expect(root?.span.type).toBe("task"); + expect(root?.metadata).toMatchObject({ + "eve.agent.name": "eve-instrumentation-scenario", + "eve.channel.kind": "http", + "eve.model.id": expect.any(String), + model: "gpt-5.4-mini", + provider: "openai", + }); + expect(root?.metrics?.completion_tokens).toEqual(expect.any(Number)); + expect(root?.metrics?.prompt_tokens).toEqual(expect.any(Number)); + expect(root?.metrics?.tokens).toEqual(expect.any(Number)); + expect(root?.output).toContain("Final answer from read"); + + expect(steps).toHaveLength(3); + expect(steps.map((step) => step.span.type)).toEqual(["llm", "llm", "llm"]); + for (const step of steps) { + expect(step.span.parentIds).toEqual([root?.span.id]); + expect(step.input).toEqual(expect.arrayContaining([expect.anything()])); + expect(step.metadata).toMatchObject({ + "eve.model.id": expect.any(String), + model: "gpt-5.4-mini", + provider: "openai", + }); + } + + expect(search).toBeDefined(); + expect(search?.span.type).toBe("tool"); + expect(search?.span.parentIds).toEqual([root?.span.id]); + expect(search?.input).toMatchObject({ + query: expect.stringContaining("Braintrust Eve instrumentation"), + }); + expect(search?.output).toMatchObject({ + title: "Eve instrumentation", + }); + + expect(read).toBeDefined(); + expect(read?.span.type).toBe("tool"); + expect(read?.span.parentIds).toEqual([root?.span.id]); + expect(read?.input).toMatchObject({ + url: "https://eve.dev/docs/guides/instrumentation", + }); + expect(read?.output).toMatchObject({ + section: "Runtime context", + title: "Eve instrumentation", + }); + + await matchSpanTreeSnapshot(events, spanTreeSnapshotPath, { + normalize: { + additionalProviderIdKeys: [ + "eve.session.id", + "eve.tool.call_id", + "eve.turn.id", + ], + }, + }); + }); +}); diff --git a/e2e/scenarios/eve-instrumentation/scenario.ts b/e2e/scenarios/eve-instrumentation/scenario.ts new file mode 100644 index 000000000..4d1027b1e --- /dev/null +++ b/e2e/scenarios/eve-instrumentation/scenario.ts @@ -0,0 +1,208 @@ +import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; +import { once } from "node:events"; +import net from "node:net"; +import path from "node:path"; +import { runMain } from "../../helpers/scenario-runtime"; + +async function main() { + const eveBin = path.join( + process.cwd(), + "node_modules", + ".bin", + process.platform === "win32" ? "eve.cmd" : "eve", + ); + + await runProcess(eveBin, ["build"], 90_000); + + const port = await getFreePort(); + const server = spawn( + eveBin, + ["start", "--host", "127.0.0.1", "--port", String(port)], + { + cwd: process.cwd(), + env: process.env, + stdio: ["ignore", "pipe", "pipe"], + }, + ); + const output = captureOutput(server); + + try { + const baseUrl = `http://127.0.0.1:${port}`; + await waitForEve(baseUrl, server, output); + + const response = await fetch(`${baseUrl}/eve/v1/session`, { + body: JSON.stringify({ + message: "Run the Braintrust Eve instrumentation e2e scenario", + }), + headers: { "content-type": "application/json" }, + method: "POST", + }); + if (!response.ok) { + throw new Error( + `Eve session create failed with ${response.status}: ${await response.text()}`, + ); + } + + const body = (await response.json()) as { sessionId?: string }; + if (!body.sessionId) { + throw new Error(`Eve session create did not return a sessionId`); + } + + await streamUntilTurnCompleted(baseUrl, body.sessionId); + await new Promise((resolve) => setTimeout(resolve, 5000)); + } finally { + await stopServer(server); + } +} + +async function getFreePort(): Promise { + const server = net.createServer(); + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", () => resolve()); + }); + const address = server.address(); + await new Promise((resolve, reject) => { + server.close((error) => (error ? reject(error) : resolve())); + }); + if (!address || typeof address === "string") { + throw new Error("Could not allocate a port for eve"); + } + return address.port; +} + +async function runProcess( + command: string, + args: string[], + timeoutMs: number, +): Promise { + const child = spawn(command, args, { + cwd: process.cwd(), + env: process.env, + stdio: ["ignore", "pipe", "pipe"], + }); + const output = captureOutput(child); + const timeout = setTimeout(() => child.kill("SIGTERM"), timeoutMs); + try { + const [code] = (await once(child, "close")) as [number | null]; + if (code !== 0) { + throw new Error( + `${path.basename(command)} ${args.join(" ")} failed with code ${code}\n${output()}`, + ); + } + } finally { + clearTimeout(timeout); + } +} + +function captureOutput(child: ChildProcessWithoutNullStreams): () => string { + let stdout = ""; + let stderr = ""; + child.stdout.on("data", (chunk) => { + stdout += String(chunk); + }); + child.stderr.on("data", (chunk) => { + stderr += String(chunk); + }); + return () => `STDOUT:\n${stdout}\nSTDERR:\n${stderr}`; +} + +async function waitForEve( + baseUrl: string, + server: ChildProcessWithoutNullStreams, + output: () => string, +): Promise { + const startedAt = Date.now(); + while (Date.now() - startedAt < 45_000) { + if (server.exitCode !== null) { + throw new Error( + `eve start exited early with code ${server.exitCode}\n${output()}`, + ); + } + try { + const response = await fetch(`${baseUrl}/eve/v1/health`); + if (response.ok) { + return; + } + } catch { + // Keep polling until the server starts accepting connections. + } + await new Promise((resolve) => setTimeout(resolve, 250)); + } + throw new Error(`Timed out waiting for eve start\n${output()}`); +} + +async function streamUntilTurnCompleted( + baseUrl: string, + sessionId: string, +): Promise { + const controller = new AbortController(); + const response = await fetch( + `${baseUrl}/eve/v1/session/${sessionId}/stream`, + { + signal: controller.signal, + }, + ); + if (!response.ok || !response.body) { + throw new Error( + `Eve stream failed with ${response.status}: ${await response.text()}`, + ); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ""; + try { + while (true) { + const { done, value } = await reader.read(); + if (done) { + throw new Error("Eve stream ended before turn.completed"); + } + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split("\n"); + buffer = lines.pop() ?? ""; + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed) { + continue; + } + const event = JSON.parse(trimmed) as { + data?: { message?: string }; + type?: string; + }; + if ( + event.type === "step.failed" || + event.type === "turn.failed" || + event.type === "session.failed" + ) { + throw new Error( + `Eve emitted ${event.type}: ${event.data?.message ?? trimmed}`, + ); + } + if (event.type === "turn.completed") { + return; + } + } + } + } finally { + controller.abort(); + await reader.cancel().catch(() => undefined); + } +} + +async function stopServer( + server: ChildProcessWithoutNullStreams, +): Promise { + if (server.exitCode !== null) { + return; + } + server.kill("SIGTERM"); + const timeout = setTimeout(() => server.kill("SIGKILL"), 5_000); + try { + await once(server, "close"); + } finally { + clearTimeout(timeout); + } +} + +runMain(main); diff --git a/e2e/scenarios/eve-instrumentation/tsconfig.json b/e2e/scenarios/eve-instrumentation/tsconfig.json new file mode 100644 index 000000000..e334ac972 --- /dev/null +++ b/e2e/scenarios/eve-instrumentation/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "target": "ES2022" + } +} diff --git a/js/src/exports.ts b/js/src/exports.ts index 6ed3bf0e0..a2b0a38f6 100644 --- a/js/src/exports.ts +++ b/js/src/exports.ts @@ -175,6 +175,7 @@ export { BraintrustMiddleware, wrapAISDKModel, } from "./wrappers/ai-sdk"; +export { braintrustEveHook } from "./instrumentation/plugins/eve-plugin"; export { wrapAnthropic } from "./wrappers/anthropic"; export { BraintrustObservabilityExporter, diff --git a/js/src/instrumentation/index.ts b/js/src/instrumentation/index.ts index 385101d77..0c0d4f9db 100644 --- a/js/src/instrumentation/index.ts +++ b/js/src/instrumentation/index.ts @@ -23,6 +23,7 @@ export { braintrustFlueInstrumentation, braintrustFlueObserver, } from "./plugins/flue-plugin"; +export { braintrustEveHook } from "./plugins/eve-plugin"; // Re-export core types for external instrumentation packages export type { diff --git a/js/src/instrumentation/plugins/eve-plugin.test.ts b/js/src/instrumentation/plugins/eve-plugin.test.ts new file mode 100644 index 000000000..7acbfd685 --- /dev/null +++ b/js/src/instrumentation/plugins/eve-plugin.test.ts @@ -0,0 +1,353 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { configureNode } from "../../node/config"; +import { _exportsForTestingOnly, initLogger } from "../../logger"; +import { braintrustEveHook } from "./eve-plugin"; +import type { + EveHandleMessageStreamEvent, + EveHookContext, +} from "../../vendor-sdk-types/eve"; + +try { + configureNode(); +} catch { + // Best-effort initialization for test environments. +} + +describe("braintrustEveHook", () => { + let backgroundLogger: ReturnType< + typeof _exportsForTestingOnly.useTestBackgroundLogger + >; + + beforeAll(async () => { + await _exportsForTestingOnly.simulateLoginForTests(); + }); + + beforeEach(() => { + Reflect.deleteProperty(globalThis, Symbol.for("braintrust.eve.bridge")); + backgroundLogger = _exportsForTestingOnly.useTestBackgroundLogger(); + initLogger({ + projectName: "eve-plugin.test.ts", + projectId: "test-project-id", + }); + }); + + afterEach(() => { + Reflect.deleteProperty(globalThis, Symbol.for("braintrust.eve.bridge")); + _exportsForTestingOnly.clearTestBackgroundLogger(); + }); + + it("returns an Eve hook definition", () => { + const hook = braintrustEveHook(); + + expect(Object.keys(hook)).toEqual(["events"]); + expect(typeof hook.events?.["*"]).toBe("function"); + }); + + it("records a flat Eve turn with session model metadata", async () => { + const wildcard = braintrustEveHook({ + metadata: { + scenario: "eve-plugin-unit", + testRunId: "test-run-flat-tree", + }, + }).events?.["*"]; + expect(wildcard).toBeDefined(); + + const ctx: EveHookContext = { + agent: { name: "eve-test-agent" }, + channel: { kind: "http" }, + session: { id: "session-flat-tree" }, + }; + const emit = (event: EveHandleMessageStreamEvent) => wildcard?.(event, ctx); + const expectedModelMetadata = { + "eve.model.id": "eve-mock/braintrust-eve-mock", + model: "braintrust-eve-mock", + provider: "eve-mock", + }; + + emit({ + data: { + runtime: { + agentId: "agent-id", + agentName: "eve-test-agent", + eveVersion: "0.20.0", + modelId: "eve-mock/braintrust-eve-mock", + }, + }, + meta: { at: "2026-01-01T00:00:00.000Z" }, + type: "session.started", + }); + emit({ + data: { sequence: 0, turnId: "turn-flat-tree" }, + meta: { at: "2026-01-01T00:00:00.010Z" }, + type: "turn.started", + }); + emit({ + data: { + message: "Search then read", + sequence: 0, + turnId: "turn-flat-tree", + }, + meta: { at: "2026-01-01T00:00:00.020Z" }, + type: "message.received", + }); + emit({ + data: { sequence: 0, stepIndex: 0, turnId: "turn-flat-tree" }, + meta: { at: "2026-01-01T00:00:00.030Z" }, + type: "step.started", + }); + emit({ + data: { + actions: [ + { + callId: "call-search", + input: { query: "Eve instrumentation" }, + kind: "tool-call", + toolName: "search", + }, + ], + sequence: 0, + stepIndex: 0, + turnId: "turn-flat-tree", + }, + meta: { at: "2026-01-01T00:00:00.040Z" }, + type: "actions.requested", + }); + emit({ + data: { + error: undefined, + result: { + callId: "call-search", + kind: "tool-result", + output: { hits: ["eve.dev/docs"] }, + toolName: "search", + }, + sequence: 0, + status: "completed", + stepIndex: 0, + turnId: "turn-flat-tree", + }, + meta: { at: "2026-01-01T00:00:00.050Z" }, + type: "action.result", + }); + emit({ + data: { + finishReason: "tool-calls", + sequence: 0, + stepIndex: 0, + turnId: "turn-flat-tree", + usage: { + cacheReadTokens: 3, + cacheWriteTokens: 2, + costUsd: 0.001, + inputTokens: 10, + outputTokens: 5, + }, + }, + meta: { at: "2026-01-01T00:00:00.060Z" }, + type: "step.completed", + }); + emit({ + data: { sequence: 0, stepIndex: 1, turnId: "turn-flat-tree" }, + meta: { at: "2026-01-01T00:00:00.070Z" }, + type: "step.started", + }); + emit({ + data: { + finishReason: "stop", + message: "Here is the Eve instrumentation guide.", + sequence: 0, + stepIndex: 1, + turnId: "turn-flat-tree", + }, + meta: { at: "2026-01-01T00:00:00.080Z" }, + type: "message.completed", + }); + emit({ + data: { + finishReason: "stop", + sequence: 0, + stepIndex: 1, + turnId: "turn-flat-tree", + usage: { + inputTokens: 20, + outputTokens: 8, + }, + }, + meta: { at: "2026-01-01T00:00:00.090Z" }, + type: "step.completed", + }); + emit({ + data: { sequence: 0, turnId: "turn-flat-tree" }, + meta: { at: "2026-01-01T00:00:00.100Z" }, + type: "turn.completed", + }); + + const spans = (await backgroundLogger.drain()) as Array< + Record + >; + const root = spans.find( + (span) => span.span_attributes?.name === "eve.turn", + ); + const steps = spans + .filter((span) => + String(span.span_attributes?.name).startsWith("eve.step"), + ) + .sort( + (left, right) => + Number(left.metadata?.["eve.step.index"]) - + Number(right.metadata?.["eve.step.index"]), + ); + const tool = spans.find((span) => span.span_attributes?.name === "search"); + + expect(spans.map((span) => span.span_attributes?.name)).toEqual([ + "eve.turn", + "eve.step", + "search", + "eve.step", + ]); + expect(root).toMatchObject({ + input: [{ content: "Search then read", role: "user" }], + metadata: { + ...expectedModelMetadata, + "eve.agent.name": "eve-test-agent", + "eve.channel.kind": "http", + "eve.session.id": "session-flat-tree", + "eve.turn.id": "turn-flat-tree", + scenario: "eve-plugin-unit", + testRunId: "test-run-flat-tree", + }, + metrics: { + completion_tokens: 13, + estimated_cost: 0.001, + prompt_cached_tokens: 3, + prompt_cache_creation_tokens: 2, + prompt_tokens: 30, + tokens: 43, + }, + output: "Here is the Eve instrumentation guide.", + span_attributes: { + name: "eve.turn", + type: "task", + }, + }); + expect(steps).toHaveLength(2); + expect(steps.map((span) => span.span_attributes?.name)).toEqual([ + "eve.step", + "eve.step", + ]); + expect(steps.map((span) => span.span_attributes?.type)).toEqual([ + "llm", + "llm", + ]); + expect(steps.map((span) => span.span_parents)).toEqual([ + [root?.span_id], + [root?.span_id], + ]); + for (const [index, step] of steps.entries()) { + expect(step.metadata).toMatchObject({ + ...expectedModelMetadata, + "eve.step.index": index, + scenario: "eve-plugin-unit", + testRunId: "test-run-flat-tree", + }); + } + expect(steps[0]?.input).toEqual([ + { content: "Search then read", role: "user" }, + ]); + expect(steps[1]?.input).toMatchObject([ + { content: "Search then read", role: "user" }, + { + content: null, + role: "assistant", + tool_calls: [ + { + function: { + arguments: JSON.stringify({ query: "Eve instrumentation" }), + name: "search", + }, + id: "call-search", + type: "function", + }, + ], + }, + { + content: { hits: ["eve.dev/docs"] }, + name: "search", + role: "tool", + tool_call_id: "call-search", + }, + ]); + expect(tool).toMatchObject({ + input: { query: "Eve instrumentation" }, + metadata: { + "eve.step.index": 0, + "eve.tool.call_id": "call-search", + "eve.tool.name": "search", + }, + output: { hits: ["eve.dev/docs"] }, + span_attributes: { + name: "search", + type: "tool", + }, + span_parents: [root?.span_id], + }); + expect(tool?.metadata).not.toHaveProperty("eve.model.id"); + expect(tool?.metadata).not.toHaveProperty("model"); + expect(tool?.metadata).not.toHaveProperty("provider"); + expect(steps[0]?.output).toMatchObject({ + message: { + tool_calls: [ + { + function: { + arguments: JSON.stringify({ query: "Eve instrumentation" }), + name: "search", + }, + id: "call-search", + type: "function", + }, + ], + }, + }); + expect(steps[1]?.output).toMatchObject({ + finish_reason: "stop", + message: { + content: "Here is the Eve instrumentation guide.", + role: "assistant", + }, + }); + }); + + it("does not throw when Eve emits malformed events or failures", async () => { + const wildcard = braintrustEveHook().events?.["*"]; + expect(wildcard).toBeDefined(); + + expect(() => wildcard?.({ bad: true } as never, {})).not.toThrow(); + expect(() => + wildcard?.( + { + data: { + code: "boom", + message: "step failed", + sequence: 0, + stepIndex: 0, + turnId: "turn-missing-session", + }, + type: "step.failed", + }, + {}, + ), + ).not.toThrow(); + expect(() => + wildcard?.( + { + data: { runtime: { modelId: 123 } }, + type: "session.started", + } as never, + { session: { id: "session-malformed-runtime" } }, + ), + ).not.toThrow(); + + const spans = await backgroundLogger.drain(); + expect(spans).toEqual([]); + }); +}); diff --git a/js/src/instrumentation/plugins/eve-plugin.ts b/js/src/instrumentation/plugins/eve-plugin.ts new file mode 100644 index 000000000..e460daccb --- /dev/null +++ b/js/src/instrumentation/plugins/eve-plugin.ts @@ -0,0 +1,972 @@ +import { toLoggedError } from "../core"; +import { debugLogger } from "../../debug-logger"; +import { flush, logError, startSpan, withCurrent } from "../../logger"; +import type { Span, StartSpanArgs } from "../../logger"; +import { SpanTypeAttribute, isObject } from "../../../util/index"; +import type { + EveHandleMessageStreamEvent, + EveHookContext, + EveHookDefinition, + EveRuntimeToolCallActionRequest, + EveRuntimeToolResultActionResult, +} from "../../vendor-sdk-types/eve"; + +type SpanState = { + metadata: Record; + span: Span; +}; + +type StepState = SpanState & { + metrics: Record; + output?: unknown; + toolCalls: Array<{ + function: { + arguments: string; + name: string; + }; + id: string; + type: "function"; + }>; +}; + +type TurnState = SpanState & { + inputLogged: boolean; + messages: unknown[]; + metrics: Record; + output?: unknown; + stepsByIndex: Map; +}; + +type ToolState = SpanState & { + stepIndex?: number; + turnKey: string; +}; + +const EVE_BRIDGE = Symbol.for("braintrust.eve.bridge"); + +/** Manual hook instrumentation for eve runtime stream events. */ +export function braintrustEveHook( + options: { metadata?: Record } = {}, +): EveHookDefinition { + return { + events: { + "*": (event: EveHandleMessageStreamEvent, ctx: EveHookContext) => { + getEveBridge().handle(event, ctx, options.metadata); + }, + }, + }; +} + +function getEveBridge(): EveBridge { + const existing = Reflect.get(globalThis, EVE_BRIDGE); + if (existing instanceof EveBridge) { + return existing; + } + const bridge = new EveBridge(); + Reflect.set(globalThis, EVE_BRIDGE, bridge); + return bridge; +} + +class EveBridge { + private sessionsById = new Map< + string, + { metadata: Record } + >(); + private toolsByCallKey = new Map(); + private turnsByKey = new Map(); + + handle( + event: unknown, + ctx: unknown, + hookMetadata?: Record, + ): void { + if (!isObject(event)) { + return; + } + + try { + this.handleEvent(event as EveHandleMessageStreamEvent, ctx, hookMetadata); + } catch (error) { + logInstrumentationError("Eve hook event", error); + } + } + + private handleEvent( + event: EveHandleMessageStreamEvent, + ctx: unknown, + hookMetadata?: Record, + ): void { + switch (event.type) { + case "session.started": + this.handleSessionStarted(event, ctx); + return; + case "turn.started": + this.handleTurnStarted(event, ctx, hookMetadata); + return; + case "message.received": + this.handleMessageReceived(event, ctx, hookMetadata); + return; + case "step.started": + this.handleStepStarted(event, ctx, hookMetadata); + return; + case "message.completed": + this.handleMessageCompleted(event, ctx); + return; + case "result.completed": + this.handleResultCompleted(event, ctx); + return; + case "actions.requested": + this.handleActionsRequested(event, ctx, hookMetadata); + return; + case "action.result": + this.handleActionResult(event, ctx, hookMetadata); + return; + case "step.completed": + this.handleStepCompleted(event, ctx); + return; + case "step.failed": + this.handleStepFailed(event, ctx); + return; + case "turn.completed": + this.handleTurnCompleted(event, ctx); + return; + case "turn.failed": + this.handleTurnFailed(event, ctx); + return; + case "session.failed": + this.handleSessionFailed(event, ctx); + return; + case "session.completed": + this.handleSessionCompleted(ctx); + return; + default: + return; + } + } + + private handleSessionStarted( + event: Extract, + ctx: unknown, + ): void { + const sessionId = sessionIdFromContext(ctx); + const metadata = modelMetadataFromRuntime(event.data.runtime); + if (!sessionId || Object.keys(metadata).length === 0) { + return; + } + + this.sessionsById.set(sessionId, { metadata }); + for (const [key, turn] of this.turnsByKey) { + if (!key.startsWith(`${sessionId}:`)) { + continue; + } + + turn.metadata = { ...turn.metadata, ...metadata }; + safeLog(turn.span, { metadata: turn.metadata }); + for (const step of turn.stepsByIndex.values()) { + step.metadata = { ...step.metadata, ...metadata }; + safeLog(step.span, { metadata: step.metadata }); + } + } + } + + private handleTurnStarted( + event: Extract, + ctx: unknown, + hookMetadata?: Record, + ): void { + const sessionId = sessionIdFromContext(ctx); + if (!sessionId) { + return; + } + + const key = turnKey(sessionId, event.data.turnId); + const metadata = this.turnMetadata(sessionId, event, ctx, hookMetadata); + const existing = this.turnsByKey.get(key); + if (existing) { + existing.metadata = metadata; + safeLog(existing.span, { metadata }); + return; + } + + const span = startSpan({ + event: { metadata }, + name: "eve.turn", + spanAttributes: { type: SpanTypeAttribute.TASK }, + startTime: eventTime(event), + }); + safeLog(span, { metadata }); + this.turnsByKey.set(key, { + inputLogged: false, + messages: [], + metadata, + metrics: {}, + span, + stepsByIndex: new Map(), + }); + } + + private handleMessageReceived( + event: Extract, + ctx: unknown, + hookMetadata?: Record, + ): void { + const turn = this.ensureTurn(event, ctx, hookMetadata); + if (!turn) { + return; + } + + const input = [{ content: event.data.message, role: "user" }]; + turn.messages.push(...input); + if (!turn.inputLogged) { + turn.inputLogged = true; + safeLog(turn.span, { input }); + } + } + + private handleStepStarted( + event: Extract, + ctx: unknown, + hookMetadata?: Record, + ): void { + const turn = this.ensureTurn(event, ctx, hookMetadata); + if (!turn) { + return; + } + + const metadata = { + ...turn.metadata, + "eve.step.index": event.data.stepIndex, + }; + const span = startEveSpan(turn.span, { + event: { + input: turn.messages.length > 0 ? [...turn.messages] : undefined, + metadata, + }, + name: "eve.step", + spanAttributes: { type: SpanTypeAttribute.LLM }, + startTime: eventTime(event), + }); + safeLog(span, { + input: turn.messages.length > 0 ? [...turn.messages] : undefined, + metadata, + }); + + turn.stepsByIndex.set(event.data.stepIndex, { + metadata, + metrics: {}, + span, + toolCalls: [], + }); + } + + private handleMessageCompleted( + event: Extract, + ctx: unknown, + ): void { + const step = this.stepForEvent(event, ctx); + if (!step) { + return; + } + + step.output = { + finish_reason: event.data.finishReason, + message: { + content: event.data.message, + role: "assistant", + ...(step.toolCalls.length > 0 + ? { tool_calls: [...step.toolCalls] } + : {}), + }, + }; + + const turn = this.turnForEvent(event, ctx); + if (turn && event.data.finishReason !== "tool-calls") { + turn.output = event.data.message; + turn.messages.push({ + content: event.data.message, + role: "assistant", + }); + } + } + + private handleResultCompleted( + event: Extract, + ctx: unknown, + ): void { + const step = this.stepForEvent(event, ctx); + if (step) { + step.output = event.data.result; + } + + const turn = this.turnForEvent(event, ctx); + if (turn) { + turn.output = event.data.result; + } + } + + private handleActionsRequested( + event: Extract, + ctx: unknown, + hookMetadata?: Record, + ): void { + const turn = this.ensureTurn(event, ctx, hookMetadata); + const sessionId = sessionIdFromContext(ctx); + if (!turn || !sessionId) { + return; + } + + const step = turn.stepsByIndex.get(event.data.stepIndex); + const toolActions = event.data.actions.filter(isToolCallAction); + if (toolActions.length === 0) { + return; + } + + turn.messages.push({ + content: null, + role: "assistant", + tool_calls: toolActions.map(openAIToolCallFromAction), + }); + + for (const action of toolActions) { + step?.toolCalls.push(openAIToolCallFromAction(action)); + if (step) { + if (step.output && isObject(step.output)) { + const message = Reflect.get(step.output, "message"); + if (isObject(message)) { + Reflect.set(message, "tool_calls", [...step.toolCalls]); + } + } else { + step.output = { + message: { + content: null, + role: "assistant", + tool_calls: [...step.toolCalls], + }, + }; + } + } + + const key = toolKey(sessionId, action.callId); + if (this.toolsByCallKey.has(key)) { + continue; + } + + const metadata = { + ...withoutModelMetadata(turn.metadata), + "eve.step.index": event.data.stepIndex, + "eve.tool.call_id": action.callId, + "eve.tool.name": action.toolName, + }; + const span = startEveSpan(turn.span, { + event: { + input: action.input, + metadata, + }, + name: action.toolName, + spanAttributes: { type: SpanTypeAttribute.TOOL }, + startTime: eventTime(event), + }); + safeLog(span, { + input: action.input, + metadata, + }); + this.toolsByCallKey.set(key, { + metadata, + span, + stepIndex: event.data.stepIndex, + turnKey: turnKey(sessionId, event.data.turnId), + }); + } + } + + private handleActionResult( + event: Extract, + ctx: unknown, + hookMetadata?: Record, + ): void { + if (!isToolResult(event.data.result)) { + return; + } + + const sessionId = sessionIdFromContext(ctx); + if (!sessionId) { + return; + } + + const key = toolKey(sessionId, event.data.result.callId); + const tool = + this.toolsByCallKey.get(key) ?? + this.startSyntheticTool(event, ctx, event.data.result, hookMetadata); + if (!tool) { + return; + } + + const failed = + event.data.status === "failed" || + event.data.result.isError === true || + event.data.error !== undefined; + const metadata = { + ...tool.metadata, + ...(event.data.status ? { "eve.action.status": event.data.status } : {}), + }; + safeLog(tool.span, { + ...(failed + ? { + error: toLoggedError( + event.data.error?.message ?? event.data.result.output, + ), + } + : {}), + metadata, + output: event.data.result.output, + }); + + const turn = this.turnForEvent(event, ctx); + turn?.messages.push({ + content: event.data.result.output, + name: event.data.result.toolName, + role: "tool", + tool_call_id: event.data.result.callId, + }); + + safeEnd(tool.span, eventTime(event)); + this.toolsByCallKey.delete(key); + } + + private handleStepCompleted( + event: Extract, + ctx: unknown, + ): void { + const step = this.stepForEvent(event, ctx); + if (!step) { + return; + } + + const metrics = metricsFromUsage(event.data.usage); + step.metrics = { ...step.metrics, ...metrics }; + const metadata = { + ...step.metadata, + "eve.finish_reason": event.data.finishReason, + ...(event.data.providerMetadata?.gateway?.generationId + ? { + "eve.gateway.generation_id": + event.data.providerMetadata.gateway.generationId, + } + : {}), + }; + + safeLog(step.span, { + metadata, + metrics, + output: step.output, + }); + safeEnd(step.span, eventTime(event)); + + const turn = this.turnForEvent(event, ctx); + if (turn) { + addMetrics(turn.metrics, metrics); + turn.stepsByIndex.delete(event.data.stepIndex); + } + } + + private handleStepFailed( + event: Extract, + ctx: unknown, + ): void { + const step = this.stepForEvent(event, ctx); + if (step) { + logError( + step.span, + errorFromMessage( + event.data.message, + event.data.code, + event.data.details, + ), + ); + safeEnd(step.span, eventTime(event)); + } + + const turn = this.turnForEvent(event, ctx); + turn?.stepsByIndex.delete(event.data.stepIndex); + } + + private handleTurnCompleted( + event: Extract, + ctx: unknown, + ): void { + const turn = this.turnForEvent(event, ctx); + if (!turn) { + return; + } + + this.endOpenChildrenForTurn(turn, eventTime(event)); + safeLog(turn.span, { + metadata: turn.metadata, + metrics: turn.metrics, + output: turn.output, + }); + safeEnd(turn.span, eventTime(event)); + this.cleanupTurn(event, ctx); + + void flush().catch((error) => { + logInstrumentationError("Eve flush", error); + }); + } + + private handleTurnFailed( + event: Extract, + ctx: unknown, + ): void { + const turn = this.turnForEvent(event, ctx); + if (!turn) { + return; + } + + this.endOpenChildrenForTurn(turn, eventTime(event)); + logError( + turn.span, + errorFromMessage(event.data.message, event.data.code, event.data.details), + ); + safeEnd(turn.span, eventTime(event)); + this.cleanupTurn(event, ctx); + + void flush().catch((error) => { + logInstrumentationError("Eve flush", error); + }); + } + + private handleSessionFailed( + event: Extract, + ctx: unknown, + ): void { + const sessionId = event.data.sessionId || sessionIdFromContext(ctx); + if (!sessionId) { + return; + } + + for (const [key, turn] of this.turnsByKey) { + if (!key.startsWith(`${sessionId}:`)) { + continue; + } + this.endOpenChildrenForTurn(turn, eventTime(event)); + logError( + turn.span, + errorFromMessage( + event.data.message, + event.data.code, + event.data.details, + ), + ); + safeEnd(turn.span, eventTime(event)); + this.turnsByKey.delete(key); + } + + for (const [key, tool] of this.toolsByCallKey) { + if (key.startsWith(`${sessionId}:`)) { + safeEnd(tool.span, eventTime(event)); + this.toolsByCallKey.delete(key); + } + } + + this.sessionsById.delete(sessionId); + + void flush().catch((error) => { + logInstrumentationError("Eve flush", error); + }); + } + + private handleSessionCompleted(ctx: unknown): void { + const sessionId = sessionIdFromContext(ctx); + if (sessionId) { + this.sessionsById.delete(sessionId); + } + } + + private ensureTurn( + event: Extract< + EveHandleMessageStreamEvent, + { + data: { readonly sequence: number; readonly turnId: string }; + } + >, + ctx: unknown, + hookMetadata?: Record, + ): TurnState | undefined { + const sessionId = sessionIdFromContext(ctx); + if (!sessionId) { + return undefined; + } + + const key = turnKey(sessionId, event.data.turnId); + const existing = this.turnsByKey.get(key); + if (existing) { + return existing; + } + + const metadata = this.turnMetadata(sessionId, event, ctx, hookMetadata); + const span = startSpan({ + event: { metadata }, + name: "eve.turn", + spanAttributes: { type: SpanTypeAttribute.TASK }, + startTime: eventTime(event), + }); + safeLog(span, { metadata }); + const state = { + inputLogged: false, + messages: [], + metadata, + metrics: {}, + span, + stepsByIndex: new Map(), + }; + this.turnsByKey.set(key, state); + return state; + } + + private turnForEvent( + event: Extract< + EveHandleMessageStreamEvent, + { data: { readonly turnId: string } } + >, + ctx: unknown, + ): TurnState | undefined { + const sessionId = sessionIdFromContext(ctx); + return sessionId + ? this.turnsByKey.get(turnKey(sessionId, event.data.turnId)) + : undefined; + } + + private stepForEvent( + event: Extract< + EveHandleMessageStreamEvent, + { data: { readonly stepIndex: number; readonly turnId: string } } + >, + ctx: unknown, + ): StepState | undefined { + return this.turnForEvent(event, ctx)?.stepsByIndex.get( + event.data.stepIndex, + ); + } + + private startSyntheticTool( + event: Extract, + ctx: unknown, + result: EveRuntimeToolResultActionResult, + hookMetadata?: Record, + ): ToolState | undefined { + const turn = this.ensureTurn(event, ctx, hookMetadata); + const sessionId = sessionIdFromContext(ctx); + if (!turn || !sessionId) { + return undefined; + } + + const metadata = { + ...withoutModelMetadata(turn.metadata), + "eve.step.index": event.data.stepIndex, + "eve.tool.call_id": result.callId, + "eve.tool.name": result.toolName, + }; + const span = startEveSpan(turn.span, { + event: { metadata }, + name: result.toolName, + spanAttributes: { type: SpanTypeAttribute.TOOL }, + startTime: eventTime(event), + }); + safeLog(span, { metadata }); + const state = { + metadata, + span, + stepIndex: event.data.stepIndex, + turnKey: turnKey(sessionId, event.data.turnId), + }; + this.toolsByCallKey.set(toolKey(sessionId, result.callId), state); + return state; + } + + private endOpenChildrenForTurn(turn: TurnState, endTime: number | undefined) { + for (const step of turn.stepsByIndex.values()) { + safeEnd(step.span, endTime); + } + turn.stepsByIndex.clear(); + + for (const [key, tool] of this.toolsByCallKey) { + if (tool.turnKey !== this.keyForTurnState(turn)) { + continue; + } + safeEnd(tool.span, endTime); + this.toolsByCallKey.delete(key); + } + } + + private cleanupTurn( + event: Extract< + EveHandleMessageStreamEvent, + { data: { readonly turnId: string } } + >, + ctx: unknown, + ): void { + const sessionId = sessionIdFromContext(ctx); + if (!sessionId) { + return; + } + const key = turnKey(sessionId, event.data.turnId); + this.turnsByKey.delete(key); + } + + private keyForTurnState(turn: TurnState): string | undefined { + const sessionId = turn.metadata["eve.session.id"]; + const turnId = turn.metadata["eve.turn.id"]; + return typeof sessionId === "string" && typeof turnId === "string" + ? turnKey(sessionId, turnId) + : undefined; + } + + private turnMetadata( + sessionId: string, + event: Extract< + EveHandleMessageStreamEvent, + { data: { readonly sequence: number; readonly turnId: string } } + >, + ctx: unknown, + hookMetadata?: Record, + ): Record { + return { + ...baseMetadata({ + channelKind: channelKindFromContext(ctx), + sessionId, + turnId: event.data.turnId, + turnSequence: event.data.sequence, + }), + ...(hookMetadata ?? {}), + ...agentMetadataFromContext(ctx), + ...(this.sessionsById.get(sessionId)?.metadata ?? {}), + }; + } +} + +function baseMetadata(input: { + channelKind?: string; + sessionId: string; + turnId: string; + turnSequence: number; +}): Record { + return { + "eve.channel.kind": input.channelKind ?? "unknown", + "eve.session.id": input.sessionId, + "eve.turn.id": input.turnId, + "eve.turn.sequence": input.turnSequence, + }; +} + +function agentMetadataFromContext(ctx: unknown): Record { + if (!isObject(ctx)) { + return {}; + } + const agent = Reflect.get(ctx, "agent"); + if (!isObject(agent)) { + return {}; + } + const name = Reflect.get(agent, "name"); + const nodeId = Reflect.get(agent, "nodeId"); + return { + ...(typeof name === "string" ? { "eve.agent.name": name } : {}), + ...(typeof nodeId === "string" ? { "eve.agent.node_id": nodeId } : {}), + }; +} + +function modelMetadataFromRuntime(runtime: unknown): Record { + if (!isObject(runtime)) { + return {}; + } + const modelId = Reflect.get(runtime, "modelId"); + return typeof modelId === "string" ? modelMetadataFromModelId(modelId) : {}; +} + +function modelMetadataFromModelId(modelId: string): Record { + const normalized = modelId.trim(); + if (!normalized) { + return {}; + } + + const slashIndex = normalized.indexOf("/"); + if (slashIndex > 0 && slashIndex < normalized.length - 1) { + return { + "eve.model.id": normalized, + model: normalized.slice(slashIndex + 1), + provider: normalized.slice(0, slashIndex), + }; + } + + return { + "eve.model.id": normalized, + model: normalized, + }; +} + +function withoutModelMetadata( + metadata: Record, +): Record { + const { + ["eve.model.id"]: _modelId, + model: _model, + provider: _provider, + ...rest + } = metadata; + return rest; +} + +function sessionIdFromContext(ctx: unknown): string | undefined { + if (!isObject(ctx)) { + return undefined; + } + const session = Reflect.get(ctx, "session"); + if (!isObject(session)) { + return undefined; + } + const id = Reflect.get(session, "id"); + return typeof id === "string" ? id : undefined; +} + +function channelKindFromContext(ctx: unknown): string | undefined { + if (!isObject(ctx)) { + return undefined; + } + const channel = Reflect.get(ctx, "channel"); + if (!isObject(channel)) { + return undefined; + } + const kind = Reflect.get(channel, "kind"); + return typeof kind === "string" ? kind : undefined; +} + +function isToolCallAction( + action: unknown, +): action is EveRuntimeToolCallActionRequest { + return ( + isObject(action) && + Reflect.get(action, "kind") === "tool-call" && + typeof Reflect.get(action, "callId") === "string" && + typeof Reflect.get(action, "toolName") === "string" && + isObject(Reflect.get(action, "input")) + ); +} + +function isToolResult( + result: unknown, +): result is EveRuntimeToolResultActionResult { + return ( + isObject(result) && + Reflect.get(result, "kind") === "tool-result" && + typeof Reflect.get(result, "callId") === "string" && + typeof Reflect.get(result, "toolName") === "string" + ); +} + +function openAIToolCallFromAction(action: EveRuntimeToolCallActionRequest) { + return { + function: { + arguments: JSON.stringify(action.input), + name: action.toolName, + }, + id: action.callId, + type: "function" as const, + }; +} + +function metricsFromUsage( + usage: Extract< + EveHandleMessageStreamEvent, + { type: "step.completed" } + >["data"]["usage"], +): Record { + if (!usage) { + return {}; + } + + const total = + typeof usage.inputTokens === "number" && + typeof usage.outputTokens === "number" + ? usage.inputTokens + usage.outputTokens + : undefined; + return { + ...(typeof usage.inputTokens === "number" + ? { prompt_tokens: usage.inputTokens } + : {}), + ...(typeof usage.outputTokens === "number" + ? { completion_tokens: usage.outputTokens } + : {}), + ...(typeof total === "number" ? { tokens: total } : {}), + ...(typeof usage.cacheReadTokens === "number" + ? { prompt_cached_tokens: usage.cacheReadTokens } + : {}), + ...(typeof usage.cacheWriteTokens === "number" + ? { prompt_cache_creation_tokens: usage.cacheWriteTokens } + : {}), + ...(typeof usage.costUsd === "number" + ? { estimated_cost: usage.costUsd } + : {}), + }; +} + +function addMetrics( + target: Record, + metrics: Record, +): void { + for (const [key, value] of Object.entries(metrics)) { + target[key] = (target[key] ?? 0) + value; + } +} + +function errorFromMessage( + message: string, + code: string, + details?: unknown, +): Error { + const error = new Error(`${code}: ${message}`); + if (details !== undefined) { + error.cause = details; + } + return error; +} + +function eventTime(event: { + readonly meta?: { readonly at: string }; +}): number | undefined { + if (!event.meta?.at) { + return undefined; + } + const timestamp = Date.parse(event.meta.at); + return Number.isFinite(timestamp) ? timestamp / 1000 : undefined; +} + +function turnKey(sessionId: string, turnId: string): string { + return `${sessionId}:${turnId}`; +} + +function toolKey(sessionId: string, callId: string): string { + return `${sessionId}:${callId}`; +} + +function startEveSpan(parent: Span, args: StartSpanArgs): Span { + return withCurrent(parent, () => startSpan(args)); +} + +function safeLog(span: Span, event: Parameters[0]): void { + try { + span.log(event); + } catch (error) { + logInstrumentationError("Eve span log", error); + } +} + +function safeEnd(span: Span, endTime: number | undefined): void { + try { + span.end(endTime === undefined ? undefined : { endTime }); + } catch (error) { + logInstrumentationError("Eve span end", error); + } +} + +function logInstrumentationError(label: string, error: unknown): void { + debugLogger.warn(`Error in ${label} instrumentation:`, error); +} diff --git a/js/src/vendor-sdk-types/eve.ts b/js/src/vendor-sdk-types/eve.ts new file mode 100644 index 000000000..4d6decfbb --- /dev/null +++ b/js/src/vendor-sdk-types/eve.ts @@ -0,0 +1,257 @@ +/** + * Vendored types for eve's authored hook APIs. + * + * Keep this surface intentionally narrow. These types are not exported to SDK + * users and should only cover fields we read, correlate, or log. + */ + +export type EveJsonValue = + | null + | boolean + | number + | string + | EveJsonValue[] + | { readonly [key: string]: EveJsonValue }; + +export type EveJsonObject = { readonly [key: string]: EveJsonValue }; + +export interface EveHookContext { + readonly agent?: { + readonly name?: string; + readonly nodeId?: string; + }; + readonly channel?: { + readonly kind?: string; + readonly continuationToken?: string; + }; + readonly session?: { + readonly id?: string; + }; +} + +export type EveAssistantStepFinishReason = + | "content-filter" + | "error" + | "length" + | "other" + | "stop" + | "tool-calls"; + +export interface EveStreamEventMeta { + readonly at: string; +} + +export interface EveRuntimeToolCallActionRequest { + readonly callId: string; + readonly input: EveJsonObject; + readonly kind: "tool-call"; + readonly toolName: string; +} + +export interface EveRuntimeToolResultActionResult { + readonly callId: string; + readonly isError?: boolean; + readonly kind: "tool-result"; + readonly output: EveJsonValue; + readonly toolName: string; +} + +export type EveRuntimeActionRequest = + | EveRuntimeToolCallActionRequest + | { + readonly callId: string; + readonly input?: EveJsonObject; + readonly kind: "load-skill" | "remote-agent-call" | "subagent-call"; + readonly name?: string; + }; + +export type EveRuntimeActionResult = + | EveRuntimeToolResultActionResult + | { + readonly callId: string; + readonly isError?: boolean; + readonly kind: "load-skill-result" | "subagent-result"; + readonly output?: EveJsonValue; + readonly name?: string; + readonly subagentName?: string; + }; + +export type EveActionResultStatus = "completed" | "failed" | "rejected"; + +export interface EveActionResultError { + readonly code: string; + readonly message: string; +} + +export type EveHandleMessageStreamEvent = + | { + readonly data: { + readonly invocation?: unknown; + readonly runtime?: { + readonly agentId: string; + readonly agentName?: string; + readonly eveVersion: string; + readonly modelId: string; + }; + }; + readonly meta?: EveStreamEventMeta; + readonly type: "session.started"; + } + | { + readonly data: { + readonly sequence: number; + readonly turnId: string; + }; + readonly meta?: EveStreamEventMeta; + readonly type: "turn.started"; + } + | { + readonly data: { + readonly sequence: number; + readonly turnId: string; + }; + readonly meta?: EveStreamEventMeta; + readonly type: "turn.completed"; + } + | { + readonly data: { + readonly message: string; + readonly sequence: number; + readonly turnId: string; + }; + readonly meta?: EveStreamEventMeta; + readonly type: "message.received"; + } + | { + readonly data: { + readonly finishReason: EveAssistantStepFinishReason; + readonly message: string | null; + readonly sequence: number; + readonly stepIndex: number; + readonly turnId: string; + }; + readonly meta?: EveStreamEventMeta; + readonly type: "message.completed"; + } + | { + readonly data: { + readonly result: EveJsonValue; + readonly sequence: number; + readonly stepIndex: number; + readonly turnId: string; + }; + readonly meta?: EveStreamEventMeta; + readonly type: "result.completed"; + } + | { + readonly data: { + readonly sequence: number; + readonly stepIndex: number; + readonly turnId: string; + }; + readonly meta?: EveStreamEventMeta; + readonly type: "step.started"; + } + | { + readonly data: { + readonly finishReason: EveAssistantStepFinishReason; + readonly providerMetadata?: { + readonly gateway?: { + readonly generationId?: string; + }; + }; + readonly sequence: number; + readonly stepIndex: number; + readonly turnId: string; + readonly usage?: { + readonly cacheReadTokens?: number; + readonly cacheWriteTokens?: number; + readonly costUsd?: number; + readonly inputTokens?: number; + readonly outputTokens?: number; + }; + }; + readonly meta?: EveStreamEventMeta; + readonly type: "step.completed"; + } + | { + readonly data: { + readonly code: string; + readonly details?: EveJsonObject; + readonly message: string; + readonly sequence: number; + readonly stepIndex: number; + readonly turnId: string; + }; + readonly meta?: EveStreamEventMeta; + readonly type: "step.failed"; + } + | { + readonly data: { + readonly actions: readonly EveRuntimeActionRequest[]; + readonly sequence: number; + readonly stepIndex: number; + readonly turnId: string; + }; + readonly meta?: EveStreamEventMeta; + readonly type: "actions.requested"; + } + | { + readonly data: { + readonly error?: EveActionResultError; + readonly result: EveRuntimeActionResult; + readonly sequence: number; + readonly stepIndex: number; + readonly status: EveActionResultStatus; + readonly turnId: string; + }; + readonly meta?: EveStreamEventMeta; + readonly type: "action.result"; + } + | { + readonly data: { + readonly code: string; + readonly details?: EveJsonObject; + readonly message: string; + readonly sequence: number; + readonly turnId: string; + }; + readonly meta?: EveStreamEventMeta; + readonly type: "turn.failed"; + } + | { + readonly data: { + readonly code: string; + readonly details?: EveJsonObject; + readonly message: string; + readonly sessionId: string; + }; + readonly meta?: EveStreamEventMeta; + readonly type: "session.failed"; + } + | { + readonly data: { + readonly wait: "next-user-message"; + }; + readonly meta?: EveStreamEventMeta; + readonly type: "session.waiting"; + } + | { + readonly meta?: EveStreamEventMeta; + readonly type: "session.completed"; + }; + +export interface EveHookDefinition { + readonly events?: { + readonly "*"?: ( + event: EveHandleMessageStreamEvent, + ctx: EveHookContext, + ) => void | Promise; + readonly [eventType: string]: + | (( + event: EveHandleMessageStreamEvent, + ctx: EveHookContext, + ) => void | Promise) + | undefined; + }; +}