#!/usr/bin/env node // @effect-diagnostics nodeBuiltinImport:off /** * Replays small fake threads on a Claude or Codex model to check T3's MCP tool use. A trimmed * harness capture supplies the prompt and native tools; a cheap model plays the shell. See * README.md for how to run it and add cases. */ import * as NodeChildProcess from "node:child_process"; import * as NodeCrypto from "node:crypto"; import * as NodeUtil from "node:util"; import * as NodeVM from "node:vm"; import * as NodeZlib from "node:zlib"; import * as NodeRuntime from "@effect/platform-node/NodeRuntime"; import * as PlatformNodeCrypto from "@effect/platform-node/NodeCrypto"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { ComposerContextRecord, DeviceOperationError, DeviceSummary, OrchestratorMcpTaskRole, ProviderInstanceId, } from "@t3tools/contracts"; import * as Cause from "effect/Cause"; import * as Context from "effect/Context"; import * as Config from "effect/Config"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Path from "effect/Path"; import * as Schedule from "effect/Schedule"; import * as Schema from "effect/Schema"; import * as Semaphore from "effect/Semaphore"; import { Tool } from "effect/ai"; import * as McpSchema from "effect/ai/McpSchema"; import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/http"; import { taskPrompt } from "../../apps/server/src/mcp/delegatedTaskModes.ts"; import * as McpInvocationContext from "../../apps/server/src/mcp/McpInvocationContext.ts"; import { advertisedJsonSchema } from "../../apps/server/src/mcp/advertisedJsonSchema.ts"; import * as Contracts from "@t3tools/contracts"; import { EnvironmentId, ORCHESTRATOR_MCP_RUNNING_TASK_NEXT, PullRequestState, ThreadId, } from "@t3tools/contracts"; import { projectComposerContextForProvider } from "@t3tools/shared/composerContextReferences"; import { formatThreadLink } from "@t3tools/shared/threadLinks"; import { buildCodexAdditionalContext } from "../../apps/server/src/provider/CodexDeveloperInstructions.ts"; import { makeSideChatContext } from "../../apps/server/src/orchestration-v2/ContextHandoffService.ts"; import { withInlineContext } from "../../apps/server/src/orchestration-v2/ContextHandoffDelivery.ts"; import { AttachmentToolkit } from "../../apps/server/src/mcp/toolkits/attachment/tools.ts"; import { DeviceToolkit } from "../../apps/server/src/mcp/toolkits/device/tools.ts"; import { EnvironmentToolkit } from "../../apps/server/src/mcp/toolkits/environment/tools.ts"; import { HtmlToolkit } from "../../apps/server/src/mcp/toolkits/html/tools.ts"; import { OrchestratorToolkit } from "../../apps/server/src/mcp/toolkits/orchestrator/tools.ts"; import { PreviewToolkit } from "../../apps/server/src/mcp/toolkits/preview/tools.ts"; import { PreviewControlsToolkit } from "../../apps/server/src/mcp/toolkits/previewControls/tools.ts"; import { ProjectToolkit } from "../../apps/server/src/mcp/toolkits/project/tools.ts"; import { CHILD_PULL_REQUEST_REPORT_NEXT, PullRequestsToolkit, } from "../../apps/server/src/mcp/toolkits/pullRequests/tools.ts"; import { ThreadToolkit } from "../../apps/server/src/mcp/toolkits/thread/tools.ts"; import { WorktreeToolkit } from "../../apps/server/src/mcp/toolkits/worktree/tools.ts"; import { CLAUDE_PROVIDER, makeClaudeQueryOptions, } from "../../apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts"; import { proxyConfig } from "../shared/proxy.ts"; import { changeRequestsCreatedBy } from "../../apps/server/src/orchestration-v2/PullRequestSyncReactor.ts"; import { DeviceExpect, gradeDevice } from "./device-grading.ts"; import { AGENT_DEVICE_SHELL, deviceFailure, type DeviceTools, isDeviceTool, launcherHelp, makeDeviceTools, } from "./devices.ts"; import { argvForm, gradeSecret, scriptSources, SecretExpect, secretDiagnostics, } from "./secret-grading.ts"; /** Agent models by id, with the display name a harness gives them. */ const MODELS = { "claude-sonnet-5-5": "Sonnet 5.5", "claude-opus-5-5": "Opus 5.5", "gpt-6.1-sol": "GPT-6.1 Sol", "gpt-6-astra": "GPT-6 Astra", } as const; type Model = keyof typeof MODELS; /** GPT models replay a Codex capture through CLIProxyAPI, the rest a Claude Code capture. */ const isCodex = (model: Model): model is Extract => model.startsWith("gpt-"); const EFFORTS = ["low", "medium", "high", "xhigh", "max"] as const; // USD per million tokens. Cache writes use the 5-minute rate. const PRICES = { "claude-sonnet-5-5": { input: 2, output: 10, cacheWrite: 2.5, cacheRead: 0.2 }, "claude-haiku-5-5": { input: 0.1, output: 0.5, cacheWrite: 0.125, cacheRead: 0.01 }, "claude-opus-5-5": { input: 4, output: 20, cacheWrite: 5, cacheRead: 0.2 }, }; type PricedModel = keyof typeof PRICES; /** GPT models that can play the shell through CLIProxyAPI, when no Anthropic key is at hand. */ const PROXY_SHELL_MODELS = ["gpt-6-luna", "gpt-6.1-terra"] as const; type ShellModel = PricedModel | (typeof PROXY_SHELL_MODELS)[number]; const isPricedModel = (model: ShellModel): model is PricedModel => model in PRICES; const T3_PREFIX = "mcp__t3-code__"; const RUNTIME_MARKER = ""; const TOOL_LOADED = "Tool loaded."; class EvalToolCallError extends Schema.TaggedError()("EvalToolCallError", { tool: Schema.String, cause: Schema.Defect(), }) { override get message(): string { return `The ${this.tool} tool call failed.`; } } const DEFERRED_LIST = /(The following deferred tools are now available via ToolSearch\.[^\n]*\n)((?:[^\n]+\n)*)/; const MODEL_LINE = /You are powered by the model named .+?\. The exact model ID is [\w.-]+\./; // API keys can be billed differently for requests that open with Claude Code's identity line, // so every copy of it becomes a neutral line. The rest of the captured prompt is unchanged. const IDENTITY = /You are Claude Code, Anthropic's official CLI for Claude(?:, running within the Claude Agent SDK)?\./g; const NEUTRAL_IDENTITY = "You are an AI coding agent running in the user's terminal."; type Block = { readonly type: string; readonly [key: string]: unknown }; type ApiMessage = { readonly role: "user" | "assistant"; readonly content: ReadonlyArray }; type ApiTool = { readonly name: string; readonly [key: string]: unknown }; type Usage = Record; type Call = { readonly step: number; readonly tool: string; readonly input: unknown; /** The tool_use id, when the call was a tool use of its own. */ readonly id?: string; /** Once answered: whether the agent saw an error, and the result text it saw. */ failed?: boolean; output?: string; }; export type SampleRun = { readonly calls: ReadonlyArray; text: string; stopReason: string; /** The agent's final answer: its last response's text, if it ended its turn. */ readonly finalText?: string; /** PRs linked to the thread when the sample ends, by the agent or by T3. */ readonly linked?: ReadonlyArray; /** The `agent-device` commands T3's device_open returned, prior turns included. */ readonly launchers?: ReadonlyArray; }; const Rest = [Schema.Record(Schema.String, Schema.Unknown)] as const; const PriorToolCall = Schema.Struct({ tool: Schema.String, input: Schema.Unknown, /** For a device tool, `"default"` runs the call on the world's devices for T3's result. */ result: Schema.Unknown, /** Repository-relative file whose current contents supply this tool result. */ resultFile: Schema.optional(Schema.String), }); /** * One expected call. `args` match partially: `~text` is a case-insensitive substring, * `"<=N"` / `">=N"` bound a number, `null` matches a missing key, and keys may be dotted paths. */ const McpToolUseCall = Schema.Struct({ tool: Schema.String, args: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), }); type McpToolUseCall = typeof McpToolUseCall.Type; /** Grading rules. Tool names are bare: `delegate_task`, `Bash`, `Agent`. */ const McpToolUseExpect = Schema.Struct({ /** Each entry needs one matching call. */ calls: Schema.optional(Schema.Array(McpToolUseCall)), /** Each entry matches a different call, and no call matches two entries; tool names are exact. */ distinctCalls: Schema.optional(Schema.Array(McpToolUseCall)), /** No call may match any entry. */ callsMustNot: Schema.optional(Schema.Array(McpToolUseCall)), notCalls: Schema.optional(Schema.Array(Schema.String)), /** First calls of these tools happen in this order. */ order: Schema.optional(Schema.Array(Schema.String)), minCalls: Schema.optional(Schema.Record(Schema.String, Schema.Number)), maxCalls: Schema.optional(Schema.Record(Schema.String, Schema.Number)), /** * One `args`-style value per new top-level thread the agent asks for, by `t3_thread_launch` or a * `create_threads` entry. Each matches a different thread's request; no other thread is asked for. */ newThreads: Schema.optional(Schema.Array(Schema.Unknown)), /** The last step with tool calls includes this tool, then the turn ends. */ endsTurnAfter: Schema.optional(Schema.String), /** * Regexes that no shell command may match. In a case with `secret` checks, a command also carries * the source of the scripts it runs that the agent wrote earlier in the sample. */ shellMustNot: Schema.optional(Schema.Array(Schema.String)), /** Regexes that some shell command must match, each. */ shellMust: Schema.optional(Schema.Array(Schema.String)), /** Case-sensitive regexes on the agent's visible text. */ textMust: Schema.optional(Schema.Array(Schema.String)), textMustNot: Schema.optional(Schema.Array(Schema.String)), /** Case-sensitive regexes on the final answer: the last response's text, once the turn ended. */ finalTextMust: Schema.optional(Schema.Array(Schema.String)), /** Each entry matches a PR URL linked at the end, as `args` values match. */ linked: Schema.optional(Schema.Array(Schema.String)), /** The PR numbers linked at the end, exactly. */ linkedNumbers: Schema.optional(Schema.Array(Schema.Number)), /** Device manager checks: child briefs, simulator UI over the shell, failures, recordings. */ device: Schema.optional(DeviceExpect), /** * Secret checks: chat asks in text and question tools, where fixture values went, and how the * files request_secret returned were read. See `SecretExpect` in secret-grading.ts. */ secret: Schema.optional(SecretExpect), }); /** * The agent's machine, as the shell simulator and the gitStatus reminder see it. Only these keys * are typed; others (files, diff, prs, pages) are free-form facts for the simulator. */ const McpToolUseWorld = Schema.StructWithRest( Schema.Struct({ /** GitHub `owner/name`. Defaults to `acme/app`. */ repo: Schema.optional(Schema.String), branch: Schema.optional(Schema.String), /** `git status --short` lines. */ status: Schema.optional(Schema.String), /** Newest first. On a branch other than main, these are the commits main lacks. */ commits: Schema.optional( Schema.Array( Schema.StructWithRest(Schema.Struct({ sha: Schema.String, subject: Schema.String }), Rest), ), ), /** Simulators and emulators. With them, T3's device tools answer from this world. */ devices: Schema.optional(Schema.Array(DeviceSummary)), /** Labels of the SSH device hosts in `devices`, by id. The local host always exists. */ deviceHosts: Schema.optional(Schema.Record(Schema.String, Schema.String)), /** * agent-device sessions that prior turns started, by device id, with the app `open` put in * each. Otherwise a device's session starts at its launcher's first `open` in the sample. */ agentDeviceSessions: Schema.optional( Schema.Record(Schema.String, Schema.Struct({ app: Schema.optional(Schema.String) })), ), /** * Hosts whose hub fails to list devices, by id, with the error, such as F1's * `{"_tag": "DeviceOperationError", "operation": "list", "reason": "request_failed", "cause": null}`. * T3 marks each one failed on every call and lists none of its devices. */ deviceDiscoveryFailures: Schema.optional(Schema.Record(Schema.String, DeviceOperationError)), /** false: agent device access is off. T3 denies every device call and drops its device text. */ deviceAgentAccess: Schema.optional(Schema.Boolean), /** Every capture reaches T3 with adb screencap's warning ahead of the PNG: T3 reports 0×0. */ deviceInvalidCapture: Schema.optional(Schema.Boolean), /** Devices another thread's agent-device session holds, by device id: that thread's id. */ deviceOwners: Schema.optional(Schema.Record(Schema.String, Schema.String)), /** PRs already linked to the thread, with the state T3 last saw. */ linkedPullRequests: Schema.optional( Schema.Array( Schema.Struct({ number: Schema.Number, state: PullRequestState, title: Schema.String }), ), ), }), Rest, ); /** One fake thread, one JSON object per line in a case file. */ export const McpToolUseCase = Schema.Struct({ id: Schema.String, area: Schema.String, kind: Schema.Literals(["bad", "guard", "control"]), pattern: Schema.String, experience: Schema.String, rule: Schema.optional(Schema.String), privacy: Schema.optional(Schema.Literals(["commit-safe", "private"])), setup: Schema.Struct({ role: Schema.Literals(["top", "delegated-child", "native-subagent"]), /** Runs only on this harness, for cases whose right answer depends on it. */ harness: Schema.optional(Schema.Literals(["claude", "codex"])), /** The Linked environments beta is on, so T3 lists its link tools. */ linkedEnvironments: Schema.optional(Schema.Boolean), /** delegate_task `role` for a delegated child. T3 builds the child's first message from it. */ delegateRole: Schema.optional(OrchestratorMcpTaskRole), context: Schema.optional(Schema.String), priorTurns: Schema.optional( Schema.Array( Schema.Struct({ role: Schema.Literals(["user", "assistant"]), text: Schema.optional(Schema.String), toolCalls: Schema.optional(Schema.Array(PriorToolCall)), }), ), ), world: Schema.optional(McpToolUseWorld), /** * Canned tool results by bare name, T3 or native. `"default"` uses the runner's result. For a * device tool, a T3 `DeviceError` (`{"_tag": ..., "cause": null}`) fails it inside T3. */ mocks: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), /** Regexes on one part of a shell command. A match wins over the shell simulator. */ shellMocks: Schema.optional( Schema.Array(Schema.Struct({ match: Schema.String, output: Schema.String })), ), /** The thread is a side chat's first turn: T3 puts the parent's digest ahead of `prompt`. */ sideChat: Schema.optional( Schema.Struct({ parentThreadId: Schema.String, digest: Schema.Array(Schema.String) }), ), }), /** Empty to resume a top-level thread without side-chat context after its last tool result. */ prompt: Schema.String, /** Records for the context chips `prompt` links, projected as T3 sends them. */ promptContext: Schema.optional(Schema.Struct({ records: Schema.Array(ComposerContextRecord) })), expect: McpToolUseExpect, /** Model calls this case may need. Overrides `--max-steps`. */ maxSteps: Schema.optional(Schema.Number), source: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), notes: Schema.optional(Schema.String), }); export type McpToolUseCase = typeof McpToolUseCase.Type; const AnyBlock = Schema.StructWithRest( Schema.Struct({ type: Schema.String, text: Schema.optional(Schema.String) }), Rest, ); const Content = Schema.Union([Schema.String, Schema.Array(AnyBlock)]); const MessagesRequest = Schema.Struct({ system: Schema.optional(Content), messages: Schema.Array(Schema.Struct({ role: Schema.String, content: Content })), tools: Schema.Array(Schema.StructWithRest(Schema.Struct({ name: Schema.String }), Rest)), }); // A CLIProxyAPI capture keeps the Anthropic body at request.provider_native. const Capture = Schema.Struct({ request: Schema.Struct({ provider_native: MessagesRequest }) }); const ResponsesRequest = Schema.StructWithRest( Schema.Struct({ input: Schema.Array(AnyBlock), reasoning: Schema.StructWithRest(Schema.Struct({ effort: Schema.String }), Rest), }), Rest, ); const CodexCapture = Schema.Struct({ request: Schema.Struct({ provider_native: ResponsesRequest }), }); const ToolUse = Schema.Struct({ type: Schema.Literal("tool_use"), id: Schema.String, name: Schema.String, input: Schema.Unknown, }); const isToolUse = Schema.is(ToolUse); const isText = Schema.is(Schema.Struct({ type: Schema.Literal("text"), text: Schema.String })); const isRecord = (value: unknown): value is Readonly> => typeof value === "object" && value !== null && !Array.isArray(value); const Count = Schema.optional(Schema.NullOr(Schema.Number)); const ApiResponse = Schema.Struct({ content: Schema.Array(AnyBlock), stop_reason: Schema.NullOr(Schema.String), usage: Schema.Struct({ input_tokens: Schema.Number, output_tokens: Schema.Number, cache_creation_input_tokens: Count, cache_read_input_tokens: Count, }), }); const emptyUsage = (): Usage => ({ input: 0, output: 0, cacheWrite: 0, cacheRead: 0 }); const addUsage = (total: Usage, usage: typeof ApiResponse.Type.usage) => { total.input += usage.input_tokens; total.output += usage.output_tokens; total.cacheWrite += usage.cache_creation_input_tokens ?? 0; total.cacheRead += usage.cache_read_input_tokens ?? 0; }; const text = (value: string): Block => ({ type: "text", text: value }); const reminder = (value: string) => text(`\n${value}\n`); const valueText = (value: unknown) => typeof value === "string" ? value : (JSON.stringify(value) ?? ""); const toolResult = (id: string, content: unknown): Block => ({ type: "tool_result", tool_use_id: id, content: valueText(content), }); const texts = (content: typeof Content.Type) => typeof content === "string" ? [content] : content.flatMap((block) => block.text ?? []); const isT3Name = (name: string) => name.startsWith(T3_PREFIX) || name.startsWith("mcp__t3_code__"); const hasTool = (tools: ReadonlyArray, name: string) => tools.some((tool) => tool.name === name); /** Grading name: T3 tools lose their MCP prefix, native tools keep the part after the last `_`. */ const CODEX_NAMES: Readonly> = { exec_command: "Bash", shell_command: "Bash", shell: "Bash", spawn_agent: "Agent", apply_patch: "Edit", request_user_input: "AskUserQuestion", request_user_input_async: "AskUserQuestion", write_stdin: "write_stdin", web__run: "WebFetch", }; export const normalizeToolName = (name: string) => { const bare = name.slice(name.lastIndexOf(".") + 1); return isT3Name(bare) ? bare.slice(T3_PREFIX.length) : (CODEX_NAMES[bare] ?? bare.slice(bare.lastIndexOf("_") + 1)); }; export const normalizeToolInput = (name: string, input: unknown) => { const decode = t3InputDecoders.get(normalizeToolName(name)); if (decode !== undefined) return Option.getOrElse(decode(input), () => input); return normalizeToolName(name) === "Bash" && isRecord(input) ? { ...input, command: input.command ?? input.cmd } : typeof input === "string" && normalizeToolName(name) === "Edit" ? { patch: input } : input; }; // McpHttpServer advertises each tool's JSON schema, and Claude Code passes the MCP name, // description, and schema through unchanged. const t3SourceTools: ReadonlyArray = [ OrchestratorToolkit, ThreadToolkit, AttachmentToolkit, ProjectToolkit, EnvironmentToolkit, PreviewControlsToolkit, WorktreeToolkit, PullRequestsToolkit, PreviewToolkit, DeviceToolkit, HtmlToolkit, ].flatMap((toolkit): ReadonlyArray => Object.values(toolkit.tools)); // McpHttpServer decodes arguments with each tool's JSON codec, which drops undeclared keys. const t3InputDecoders = new Map( t3SourceTools.map((tool) => [ tool.name, Schema.decodeUnknownOption( Schema.toCodecJson(tool.parametersSchema as Schema.Codec), ), ]), ); const t3Tools: ReadonlyArray = t3SourceTools.map((tool) => ({ name: `${T3_PREFIX}${tool.name}`, description: Tool.getDescription(tool) ?? "", input_schema: advertisedJsonSchema(tool), })); const enabledWhen = (predicate: () => boolean) => new Set( t3SourceTools .filter( (tool) => Context.getOrUndefined(tool.annotations, McpSchema.EnabledWhen) === predicate, ) .map((tool) => `${T3_PREFIX}${tool.name}`), ); // Claude Code loads a tool whose `_meta` sets anthropic/alwaysLoad up front instead of deferring it. const alwaysLoaded = new Set( t3SourceTools .filter((tool) => { const meta = Context.getOrUndefined(tool.annotations, Tool.Meta); return meta !== undefined && meta["anthropic/alwaysLoad"] === true; }) .map((tool) => `${T3_PREFIX}${tool.name}`), ); // Refs whose request_secret takes `value` save a secret the user already pasted, with no card. export const savesPastedSecrets = t3Tools.some( (tool) => tool.name === `${T3_PREFIX}request_secret` && isRecord(tool.input_schema) && isRecord(tool.input_schema.properties) && "value" in tool.input_schema.properties, ); /** A request_secret call that hands T3 a pasted secret, on a ref that takes one. */ const pastedSecret = (name: string, input: Readonly>) => savesPastedSecrets && name === "request_secret" && typeof input.value === "string"; // T3 does not offer these to a delegated child, so its harness never lists them. const hiddenFromSubagents = enabledWhen(McpInvocationContext.hiddenFromSubagents); // The Linked environments beta starts off, and T3 lists its tools only while it is on. const hiddenWhileLinksOff = enabledWhen(McpInvocationContext.hiddenWhileLinksOff); const visibleT3Tools = (options: { readonly subagent?: boolean; readonly links?: boolean }) => t3Tools.filter( (tool) => (options.links === true || !hiddenWhileLinksOff.has(tool.name)) && !(options.subagent === true && hiddenFromSubagents.has(tool.name)), ); /** * T3's Claude Code options for an eval thread: its system prompt append and its hooks. `device` says * whether the thread holds the `device` capability, as agent device access decides. */ const claudeQueryOptions = (model: Model, device = true) => { // Refs whose device text follows the capability read deviceToolsAvailable; older ones ignore it. const input: Parameters[0] & { readonly deviceToolsAvailable?: boolean; } = { modelSelection: { instanceId: ProviderInstanceId.make(CLAUDE_PROVIDER), model }, nativeThreadId: "mcp-tool-use-eval", resume: false, cwd: null, mcpServers: { "t3-code": { type: "http", url: "http://127.0.0.1/mcp" } }, deviceToolsAvailable: device, }; return makeClaudeQueryOptions(input); }; /** Text T3 appends to Claude Code's system prompt. Delegated children get the same text. */ export const t3SystemAppend = (model: Model, device = true) => { const { systemPrompt } = claudeQueryOptions(model, device); if (isRecord(systemPrompt) && systemPrompt.type === "preset" && systemPrompt.append) { return systemPrompt.append; } throw new Error("makeClaudeQueryOptions no longer appends T3 text to a preset prompt."); }; /** * The context T3's SubagentStart hooks give a native subagent, which gets none of the append. * Empty when T3 registers no such hook. */ export const t3SubagentContext = async (model: Model, device = true) => { const hooks = (claudeQueryOptions(model, device).hooks?.SubagentStart ?? []).flatMap( (m) => m.hooks, ); const input = { hook_event_name: "SubagentStart" as const, session_id: "mcp-tool-use-eval", transcript_path: "", cwd: "", agent_id: "mcp-tool-use-eval", agent_type: "general-purpose", }; const outputs = await Promise.all( hooks.map((hook) => hook(input, undefined, { signal: AbortSignal.timeout(10_000) })), ); return outputs .flatMap((output) => "hookSpecificOutput" in output && output.hookSpecificOutput?.hookEventName === "SubagentStart" ? [output.hookSpecificOutput.additionalContext ?? ""] : [], ) .filter(Boolean) .join("\n\n"); }; /** * Size of the context T3 adds: the system text every Claude thread gets, and the tool * descriptions and schemas (loaded on demand here, sent up front by harnesses without * deferred tools). Tokens are estimated at 4 characters each. */ const t3ContextReport = (model: Model) => { const size = (chars: number) => `${chars} chars (~${Math.round(chars / 4)} tokens)`; const descriptions = t3Tools.reduce((sum, tool) => sum + valueText(tool.description).length, 0); const schemas = JSON.stringify(t3Tools).length; return [ `- T3 context: system text ${size((isCodex(model) ? JSON.stringify(buildCodexAdditionalContext({ model }, { browser: true, device: true })) : t3SystemAppend(model)).length)}`, `tool descriptions ${size(descriptions)}`, `full tool schemas ${size(schemas)} across ${t3Tools.length} tools.`, ].join(", "); }; const stringProperty = (description: string) => ({ type: "string", description }); const NATIVE_SCHEMAS: Readonly>> = { WebFetch: { description: "Fetches content from a URL and answers the prompt about it.", input_schema: { type: "object", properties: { url: stringProperty("The URL to fetch"), prompt: stringProperty("What to extract"), }, required: ["url", "prompt"], }, }, WebSearch: { description: "Searches the web and returns result titles, links, and snippets.", input_schema: { type: "object", properties: { query: stringProperty("The search query") }, required: ["query"], }, }, }; /** Rebuilds the parts of a captured request that every case shares. */ export const buildBase = ( template: typeof MessagesRequest.Type, options: { readonly model: Model; readonly keepUserInstructions: boolean; readonly subagent?: boolean; readonly links?: boolean; /** A native subagent starts from its task: T3 appends nothing to its system prompt. */ readonly nativeSubagent?: boolean; /** false: the thread has no agent device access, so T3's text leaves devices out. */ readonly deviceAccess?: boolean; }, ) => { const t3 = visibleT3Tools(options); const firstAssistant = template.messages.findIndex((message) => message.role === "assistant"); const head = template.messages.slice(0, firstAssistant === -1 ? undefined : firstAssistant); const systemTexts = [ template.system ?? [], ...head.flatMap((m) => (m.role === "system" ? [m.content] : [])), ] .flatMap(texts) .filter((value) => !value.startsWith("x-anthropic-billing-header:")); const deferredList = DEFERRED_LIST.exec(systemTexts.join("\n"))?.[2]; if (systemTexts.filter((value) => value.includes(RUNTIME_MARKER)).length !== 1 || !deferredList) { throw new Error(`The template needs one ${RUNTIME_MARKER} block and a deferred tool list.`); } const nativeDeferred = deferredList.split("\n").filter((name) => name && !isT3Name(name)); const names = [ ...nativeDeferred, ...t3.filter((tool) => !alwaysLoaded.has(tool.name)).map((tool) => tool.name), ] .toSorted() .join("\n"); const { model } = options; const append = options.nativeSubagent ? "" : t3SystemAppend(model, options.deviceAccess); const modelLine = `You are powered by the model named ${MODELS[model]}. The exact model ID is ${model}.`; const joined = systemTexts.join("\n"); // The capture's home directory names its user. Cases use `/home/user` paths instead. const cwd = /Primary working directory: (\S+)/.exec(joined)?.[1] ?? "/home/user/app"; const home = /^\/(?:home|Users)\/[^/]+/.exec(cwd)?.[0]; const anonymize = (value: string) => home === undefined ? value : value.replaceAll(home, home.replace(/[^/]+$/, "user")); const system = systemTexts.map((value) => { // T3's append is the tail of its block: runtime info, PR linking, then orchestration. const at = value.indexOf(RUNTIME_MARKER); return anonymize(at === -1 ? value : value.slice(0, at) + append) .replace(DEFERRED_LIST, (_, header: string) => `${header}${names}\n`) .replace(MODEL_LINE, modelLine) .replace(IDENTITY, NEUTRAL_IDENTITY); }); const [first] = template.messages; // The last block is the captured prompt. const reminders = (first?.role === "user" ? texts(first.content).slice(0, -1) : []).filter( (value) => options.keepUserInstructions || !value.includes("Codebase and user instructions are"), ); // gitStatus names a real repo state. Each case gets one built from its fake world instead. const gitStatusAt = reminders.findIndex((value) => value.includes("# gitStatus")); // Claude Code sends no schema for tools it only names. Web tools get their usual one, the // rest load open. const open = { type: "object", additionalProperties: true }; const nativeSchema = (name: string) => NATIVE_SCHEMAS[name] ?? { description: name, input_schema: open }; return { model, system: system.map(text), reminders: reminders.filter((_, i) => i !== gitStatusAt).map((value) => text(anonymize(value))), gitStatusAt: gitStatusAt === -1 ? reminders.length : gitStatusAt, /** The shell simulator's cwd and OS follow the template's environment section. */ environment: { cwd: anonymize(cwd), platform: /Platform: (\w+)/.exec(joined)?.[1] ?? "linux", date: /Today's date is ([\d-]+)/.exec(reminders.join("\n"))?.[1], }, nativeTools: [ ...template.tools.filter((tool) => !isT3Name(tool.name)), ...t3.filter((tool) => alwaysLoaded.has(tool.name)), ], /** Tools ToolSearch loads: T3 tools from source, and native tools the template only names. */ t3, deferred: [...t3, ...nativeDeferred.map((name) => ({ name, ...nativeSchema(name) }))], }; }; type Base = ReturnType & { readonly codex?: typeof ResponsesRequest.Type; /** What T3's SubagentStart hooks give a native subagent. */ readonly subagentContext?: string; }; /** Keep the captured harness prompt and tools; replace app-owned context and the real user. */ const buildCodexBase = ( template: typeof ResponsesRequest.Type, options: { readonly model: Model; readonly keepUserInstructions: boolean; readonly effort?: string | undefined; readonly subagent?: boolean; readonly links?: boolean; /** false: the thread has no agent device access, so T3's text leaves devices out. */ readonly deviceAccess?: boolean; }, ): Base => { const t3 = visibleT3Tools(options); const contentText = (item: Block) => Array.isArray(item.content) ? item.content.map((block: Block) => valueText(block.text)).join("\n") : valueText(item.content); const joined = template.input.map(contentText).join("\n"); if (!joined.includes("")) throw new Error("Codex template needs full T3 context."); const cwd = /(.*?)<\/cwd>/.exec(joined)?.[1] ?? "/home/user/app"; const home = /^\/(?:home|Users)\/[^/]+/.exec(cwd)?.[0]; const clean = (s: string) => (home ? s.replaceAll(home, home.replace(/[^/]+$/, "user")) : s); const effort = options.effort ?? template.reasoning.effort; const message = (value: string, role = "developer") => ({ type: "message", role, content: [{ type: "input_text", text: value }], }); // The captured message stays: Codex supplies its own mode text natively. const input = template.input.flatMap((item) => { const content = contentText(item); if (content.includes("[\s\S]*?<\/environment_context>/.exec(content)?.[0]; return env ? [message(clean(options.keepUserInstructions ? content : env), "user")] : []; } // Only harness messages and tools. A captured turn's answers and tool calls would leak into // every case. if (item.type !== "message" && item.type !== "additional_tools") return []; if (item.role === "assistant") return []; return [JSON.parse(clean(JSON.stringify(item))) as Block]; }); const context = buildCodexAdditionalContext( { model: options.model, reasoningEffort: effort }, { browser: true, device: options.deviceAccess ?? true }, ); input.push( ...Object.entries(context).map(([key, entry]) => message(`<${key}>${entry.value}`)), ); const nativeTools = input .flatMap((item) => item.type === "additional_tools" && Array.isArray(item.tools) ? item.tools.flatMap((ns: unknown) => isRecord(ns) && Array.isArray(ns.tools) ? ns.tools : [], ) : [], ) .filter((tool): tool is ApiTool => isRecord(tool) && typeof tool.name === "string"); if (!nativeTools.some((tool) => tool.name === "exec")) throw new Error("Codex template needs code-mode exec."); return { model: options.model, system: [], reminders: [], gitStatusAt: 0, environment: { cwd: clean(cwd), platform: cwd.startsWith("/Users/") ? "darwin" : "linux", date: /(.*?)<\/current_date>/.exec(joined)?.[1], }, nativeTools, t3, deferred: [...t3], codex: { ...template, input, reasoning: { ...template.reasoning, effort } }, }; }; /** Replay prior calls in Codex's wire format, including Claude-shaped file operations. */ const codexCall = (name: string, input: unknown) => { const args = isRecord(input) ? input : {}; if (name === "Bash") return `text(await tools.exec_command(${JSON.stringify({ ...args, cmd: args.command })}));`; if (["Read", "Write", "Edit"].includes(name)) { const file = valueText(args.file_path); const command = name === "Read" ? `cat ${JSON.stringify(file)}` : name === "Write" ? `cat > ${JSON.stringify(file)} <<'EVAL_EOF'\n${valueText(args.content)}\nEVAL_EOF` : `python3 -c ${JSON.stringify(`from pathlib import Path; p=Path(${JSON.stringify(file)}); p.write_text(p.read_text().replace(${JSON.stringify(args.old_string)}, ${JSON.stringify(args.new_string)}))`)}`; return `text(await tools.exec_command(${JSON.stringify({ cmd: command })}));`; } return `text(await tools.${name.replace("mcp__t3-code__", "mcp__t3_code__")}(${JSON.stringify(input)}));`; }; const codexMessages = (messages: ReadonlyArray): Array => messages.flatMap((message) => message.content.map((block): Block => block.type === "tool_use" ? { type: "custom_tool_call", call_id: block.id, name: "exec", input: codexCall(valueText(block.name), block.input), } : block.type === "tool_result" ? { type: "custom_tool_call_output", call_id: block.tool_use_id, output: valueText(block.content), } : { type: "message", role: message.role, content: [ { type: message.role === "user" ? "input_text" : "output_text", text: valueText(block.text), }, ], }, ), ); /** One tool call the shell simulator has answered, or one from the case's prior turns. */ type ShellEntry = { readonly tool: string; readonly input: unknown; readonly output: string }; /** PRs T3 links by itself because a shell command created them, as `gh pr create` does. */ const createdPullRequests = ({ tool, input, output }: ShellEntry) => tool === "Bash" && isRecord(input) && typeof input.command === "string" ? changeRequestsCreatedBy(input.command, output).map(({ url }) => url) : []; /** * One sample's state. ToolSearch adds loaded schemas to `tools`, like Claude Code. `shell` is the * history the simulator keeps consistent with; `linked`, `nextTask` and `threads` do the same for * T3 results. */ type Sample = { readonly testCase: McpToolUseCase; readonly base: Base; readonly tools: Array; readonly repo: string; readonly shell: Array; readonly shellUsage: Usage; readonly linked: Set; readonly codeStore: Record; nextTask: number; /** Threads the agent launched in this sample, by id, so reading one shows its own brief. */ readonly threads: Map; /** Files T3's request_secret saved in this sample, in call order. */ readonly secretFiles: Array; secretRequests: number; /** T3's device tools, once `withDevices` builds them for a world with devices. */ devices?: DeviceTools; /** T3's results for prior device calls whose case result is "default". */ readonly priorResults: Map; }; const SHELL_TOOLS = new Set(["Bash", "Read", "Edit", "Write", "WebFetch"]); export const makeSample = (testCase: McpToolUseCase, base: Base): Sample => { const prior = testCase.setup.priorTurns ?? []; const priorCalls = prior.flatMap((turn) => turn.toolCalls ?? []); const repo = testCase.setup.world?.repo ?? "acme/app"; const urls = [ ...(testCase.setup.world?.linkedPullRequests ?? []).map( ({ number }) => pullRequest(repo, { number }).url, ), ...priorCalls.flatMap((call) => ["link_pull_request", "watch_pull_request"].includes(call.tool) && isRecord(call.input) ? [pullRequest(repo, call.input).url] : call.tool === "link_pull_requests" && isRecord(call.input) && Array.isArray(call.input.urls) ? call.input.urls .filter((url) => PULL_REQUEST_URL.test(valueText(url))) .map((url) => pullRequest(repo, { url }).url) : [], ), ]; const taskNumbers = [...valueText(prior).matchAll(/task-(\d+)/g)].map((match) => Number(match[1]), ); const shell = priorCalls .filter((call) => SHELL_TOOLS.has(call.tool)) .map(({ tool, input, result }) => ({ tool, input, output: valueText(result) })); return { testCase, base, tools: [...base.nativeTools], repo: testCase.setup.world?.repo ?? "acme/app", shell, shellUsage: emptyUsage(), linked: new Set([...urls, ...shell.flatMap(createdPullRequests)]), codeStore: {}, nextTask: Math.max(0, ...taskNumbers) + 1, threads: new Map(), secretFiles: [], secretRequests: 0, priorResults: new Map(), }; }; /** * Builds T3's device tools for a world with devices. Prior device calls whose result is "default" * run first, in order and without the case's mocks, so their results and sessions are T3's. As in * the real flow, device_open starts no agent-device session: one starts at a launcher `open` in the * sample's shell history, prior turns included, or where `world.agentDeviceSessions` declares it. */ export const withDevices = Effect.fn("evaluateMcpToolUse.withDevices")(function* (sample: Sample) { const world = sample.testCase.setup.world; if (world?.devices === undefined) return; sample.devices = yield* makeDeviceTools( { ...world, devices: world.devices }, sample.base.environment, ); for (const call of (sample.testCase.setup.priorTurns ?? []).flatMap((t) => t.toolCalls ?? [])) { if (call.result !== "default" || !isDeviceTool(call.tool)) continue; sample.priorResults.set(call, (yield* sample.devices.call(call.tool, call.input)).text); } }); // Main's history under every case's world. const MAIN_COMMITS = [ { sha: "9f2c1ab", subject: "feat(web): show the model name in the thread header" }, { sha: "4e7d2c0", subject: "fix(server): retry provider start once on ENOENT" }, { sha: "b81f3d9", subject: "chore: update dependencies" }, ]; /** * The gitStatus Claude Code shows when a session starts, built from the case's world. Commits and * a branch that the prior turns made (their tool results show it) came later, so they are left out. */ const gitStatus = ({ setup }: McpToolUseCase) => { const prior = valueText(setup.priorTurns ?? []); const branch = setup.world?.branch ?? "main"; const commits = [...(setup.world?.commits ?? []), ...MAIN_COMMITS].filter( (commit) => !prior.includes(` ${commit.sha}] `), ); return [ "As you answer the user's questions, you can use the following context:", "# gitStatus", "This is the git status at the start of the conversation. Note that this status is a snapshot in time, and will not update during the conversation.", "", `Current branch: ${prior.includes(`Switched to a new branch '${branch}'`) ? "main" : branch}`, "", "Main branch (you will usually use this for PRs): main", "", "Status:", setup.world?.status || "(clean)", "", "Recent commits:", ...commits.slice(0, 5).map((commit) => `${commit.sha} ${commit.subject}`), "", "Claude Code attached this context automatically; it isn't part of the user's message. It describes the user's own account and workspace, so they don't need it reported back.", ].join("\n"); }; const findTool = (base: Base, bare: string) => [...base.nativeTools, ...base.deferred].find((tool) => normalizeToolName(tool.name) === bare) ?? (base.codex ? { name: bare } : undefined); /** Adds deferred schemas to `tools` and returns the ToolSearch result Claude Code sends. */ const loadTools = (tools: Array, id: string, found: ReadonlyArray): Block => { for (const tool of found) { if (!hasTool(tools, tool.name)) tools.push({ ...tool, defer_loading: true }); } const content = found.map((tool) => ({ type: "tool_reference", tool_name: tool.name })); return { type: "tool_result", tool_use_id: id, content }; }; /** Messages for a case: reminders, context, prior turns with real tool blocks, then the prompt. */ export const buildMessages = (sample: Sample) => { const { testCase, base, tools } = sample; const messages: Array = []; const push = (role: ApiMessage["role"], ...blocks: ReadonlyArray) => { const last = messages.at(-1); if (last?.role !== role) messages.push({ role, content: blocks }); else messages[messages.length - 1] = { role, content: [...last.content, ...blocks] }; }; const { role, context, delegateRole, priorTurns = [] } = testCase.setup; const sideChat = testCase.setup.sideChat; if (testCase.prompt === "" && (role !== "top" || sideChat !== undefined)) { throw new Error( `${testCase.id}: Empty-prompt replay requires a top-level thread without side-chat context.`, ); } const sideChatContext = sideChat === undefined ? undefined : makeSideChatContext({ parentThreadId: ThreadId.make(sideChat.parentThreadId), digest: sideChat.digest, }); const native = role === "native-subagent"; const notes = [ native ? "You are a subagent started by the parent agent's Agent tool. Your final message goes to the parent agent, not to the user." : undefined, context, ]; const reminders = base.reminders.toSpliced(base.gitStatusAt, 0, reminder(gitStatus(testCase))); push("user", ...reminders, ...notes.flatMap((note) => (note ? [reminder(note)] : []))); const toolSearch = findTool(base, "ToolSearch")?.name ?? "ToolSearch"; let nextId = 0; for (const turn of priorTurns) { const calls = (turn.toolCalls ?? []).map((call) => { const tool = findTool(base, call.tool); if (tool === undefined) throw new Error(`${testCase.id}: unknown prior tool ${call.tool}.`); if (call.result === "default" && isDeviceTool(call.tool) && !sample.priorResults.has(call)) { throw new Error(`${testCase.id}: prior ${call.tool} needs T3's result; run withDevices.`); } return { ...call, id: `toolu_prior_${nextId++}`, name: tool.name, tool, prior: call }; }); // Prior T3 calls load their schemas first, as in a real deferred session. const unloaded = [...new Set(calls.map((call) => call.tool))].filter( (tool) => !hasTool(tools, tool.name), ); if (unloaded.length > 0 && !base.codex) { const id = `toolu_prior_${nextId++}`; const query = `select:${unloaded.map((tool) => tool.name).join(",")}`; const input = { query, max_results: unloaded.length }; push("assistant", { type: "tool_use", id, name: toolSearch, input }); push("user", loadTools(tools, id, unloaded), text(TOOL_LOADED)); } // One call per step, as most real turns run them. for (const { id, name, input, result, tool, prior } of calls) { push("assistant", { type: "tool_use", id, name, input }); const replayed = normalizeToolName(tool.name) === "orchestrator_capabilities" ? capabilities(sample, result) : (sample.priorResults.get(prior) ?? result); push("user", toolResult(id, replayed)); } if (turn.text) push(turn.role, text(turn.text)); } const userText = testCase.promptContext === undefined ? testCase.prompt : projectComposerContextForProvider({ text: testCase.prompt, ...testCase.promptContext }); const prompt = sideChatContext === undefined ? userText : withInlineContext(sideChatContext, userText); const childPrompt = () => taskPrompt({ task: prompt, role: delegateRole }); if (testCase.prompt !== "") push("user", text(role === "delegated-child" ? childPrompt() : prompt)); // Claude Code gives a subagent its SubagentStart hooks' context as a reminder after the task. if (native && base.subagentContext) { push("user", reminder(`SubagentStart hook additional context: ${base.subagentContext}`)); } return messages; }; // Results for T3 tools a case does not mock. The model ids are public in this repo's catalog. const claudeModel = (id: string) => ({ id, options: [{ id: "effort", values: EFFORTS }] }); const codexModel = (id: string) => ({ id, options: [{ id: "reasoningEffort", values: EFFORTS }] }); // Mirrors the real catalog's main models, so a case's named model is not missing by accident. const PROVIDERS = [ { providerInstanceId: "claudeAgent", models: ["claude-opus-5-5", "claude-sonnet-5-5", "claude-fable-5-1", "claude-haiku-5-5"].map( claudeModel, ), }, { providerInstanceId: "codex", models: ["gpt-6.1-sol", "gpt-6-astra", "gpt-6-luna"].map(codexModel), }, { providerInstanceId: "muse", models: ["muse-wonder-full", "muse-wonder"].map(codexModel) }, { providerInstanceId: "grok", models: ["grok-4.7"].map(codexModel) }, ]; const uuid = (n: number) => `00000000-0000-4000-8000-${String(n).padStart(12, "0")}`; const DEFAULT_THREAD = { title: "Fix the flaky reconnect test", status: "idle", items: [ { position: 1, createdBy: "user", text: "The reconnect test in src/session.test.ts fails about 1 in 10 runs. Fix it.", }, { position: 2, createdBy: "assistant", text: "Fixed. The test now waits for the socket close event before it reconnects. 20 runs in a row pass.", }, ], }; const HTML_RENDERED = "Shown to the reader above your reply. Don't mention or describe the page; reply with only what it doesn't already say."; const PULL_REQUEST_URL = /\/pull\/\d+\/?$/; /** Pull request identity from a `url` or `repository` plus `number` input, like T3's results. */ const pullRequest = (repo: string, input: Readonly>) => { const url = typeof input.url === "string" ? input.url : ""; const repository = /github\.com\/([^/]+\/[^/]+)\/pull/.exec(url)?.[1] ?? valueText(input.repository ?? repo); const number = Number(/(\d+)\/?$/.exec(url)?.[1] ?? input.number); return { host: "github.com", repository, number, url: url || `https://github.com/${repository}/pull/${number}`, }; }; /** A linked PR as `list_thread_pull_requests` shows it. Links made in the sample are open. */ const linkedEntry = (sample: Sample, url: string) => { const pr = pullRequest(sample.repo, { url }); const seeded = sample.testCase.setup.world?.linkedPullRequests?.find( ({ number }) => number === pr.number && pr.repository === sample.repo, ); return { ...pr, state: seeded?.state ?? "open", title: seeded?.title ?? null }; }; /** A believable T3 result for a tool the case does not mock. */ export const t3Default = ( sample: Sample, name: string, input: Readonly>, ) => { const pr = () => pullRequest(sample.repo, input); const newThread = (request: unknown, brief: unknown) => { const n = sample.nextTask++; const title = isRecord(request) && typeof request.title === "string" ? request.title : "New thread"; const text = typeof brief === "string" ? brief.trim() : ""; sample.threads.set(uuid(n), { title, brief: text }); return { threadId: uuid(n), title, ...(text ? { runId: `run-${n}`, status: "starting" } : { status: "idle" }), }; }; switch (name) { case "t3_thread_launch": return newThread(input, input.message); case "create_threads": return { threads: (Array.isArray(input.threads) ? input.threads : []).map((request) => { const target = isRecord(request) && isRecord(request.target) ? request.target : {}; return { ...newThread(request, isRecord(request) ? request.prompt : undefined), createdBy: "agent", creationSource: "mcp", providerInstanceId: target.providerInstanceId ?? (sample.base.codex ? "codex" : "claudeAgent"), model: target.model ?? sample.base.model, }; }), }; case "orchestrator_capabilities": return { inheritedProviderInstanceId: "claudeAgent", inheritedModel: sample.base.model, providers: PROVIDERS, }; case "delegate_task": { const n = sample.nextTask++; const target = isRecord(input.target) ? input.target : {}; return { taskId: `task-${n}`, childThreadId: uuid(n), childRunId: `run-${n}`, status: "running", workState: "working", hasPendingChildRuns: false, providerInstanceId: target.providerInstanceId ?? (sample.base.codex ? "codex" : "claudeAgent"), model: target.model ?? sample.base.model, summary: null, hasPendingQuestion: false, pendingApproval: null, }; } case "task_status": return { taskId: input.taskId, status: "running", workState: "working", hasPendingChildRuns: false, summary: null, }; case "task_cancel": return { taskId: input.taskId, status: "cancel_requested" }; case "link_pull_request": { const alreadyLinked = sample.linked.has(pr().url); sample.linked.add(pr().url); return { ...pr(), alreadyLinked }; } case "link_pull_requests": { const urls = Array.isArray(input.urls) ? input.urls.map(valueText) : []; const invalidUrls = urls.filter((url) => !PULL_REQUEST_URL.test(url)); const targets = [...new Set(urls.filter((url) => PULL_REQUEST_URL.test(url)))].map((url) => pullRequest(sample.repo, { url }), ); const fresh = targets.filter(({ url }) => !sample.linked.has(url)); for (const { url } of targets) sample.linked.add(url); const byRepository = (prs: ReadonlyArray>) => [...Map.groupBy(prs, (pr) => pr.repository).values()].map((group) => ({ host: "github.com", repository: group[0]!.repository, numbers: group.map((pr) => pr.number), })); return { linked: byRepository(fresh), alreadyLinked: byRepository(targets.filter((pr) => !fresh.includes(pr))), linkedCount: targets.length, ...(invalidUrls.length > 0 ? { invalidUrls } : {}), }; } case "request_secret": { // T3 names a ref and a file with 32 hex characters, new for each request, in its secrets dir. const id = NodeCrypto.createHash("sha256") .update(`${sample.testCase.id}:${sample.secretRequests++}`) .digest("hex") .slice(0, 32); const home = /^\/(?:home|Users)\/[^/]+/.exec(sample.base.environment.cwd)?.[0] ?? "/home/user"; return input.delivery === "file" || pastedSecret(name, input) ? { status: "saved", file: `${home}/.t3/userdata/secrets/files/${id}` } : { status: "saved", secretRef: `secret-ref:${id}` }; } case "unlink_pull_request": return { ...pr(), wasLinked: sample.linked.delete(pr().url) }; case "unlink_pull_requests": { const states = Array.isArray(input.states) ? input.states : []; const unlinked = [...sample.linked] .map((url) => linkedEntry(sample, url)) .filter((entry) => states.includes(entry.state)); for (const { url } of unlinked) sample.linked.delete(url); const groups = Map.groupBy(unlinked, (entry) => `${entry.repository}/${entry.state}`); return { unlinked: [...groups.values()].map((entries) => ({ host: "github.com", repository: entries[0]!.repository, state: entries[0]!.state, numbers: entries.map((entry) => entry.number), })), unlinkedCount: unlinked.length, remaining: sample.linked.size, }; } case "watch_pull_request": sample.linked.add(pr().url); return { ...pr(), watching: true, wasWatching: false }; case "unwatch_pull_request": return { ...pr(), watching: false, wasWatching: true }; case "list_thread_pull_requests": return { pullRequests: [...sample.linked].map((url) => ({ ...linkedEntry(sample, url), source: "agent", watching: false, })), chains: [], }; case "t3_thread_read": { const threadId = valueText(input.threadId); const launched = sample.threads.get(threadId); if (launched !== undefined) { const items = launched.brief ? [{ position: 1, createdBy: "agent", creationSource: "mcp", text: launched.brief }] : []; const thread = { threadId, title: launched.title, createdBy: "agent", creationSource: "mcp", status: launched.brief ? "running" : "idle", }; return { thread, items, nextPosition: items.length + 1, hasMore: false }; } const { title, status, items } = DEFAULT_THREAD; return { thread: { threadId, title, status }, items, nextPosition: 3, hasMore: false }; } case "html_preview": return { width: 728, contentHeight: 560, capturedHeight: 560, consoleMessages: [] }; case "html_render": return { htmlRender: { attachmentId: "att-1", title: input.title ?? "Visual", height: 560 }, message: HTML_RENDERED, }; case "preview_status": return { available: false, visible: false, tabId: null, url: null, title: null, loading: false, }; default: return { ok: true }; } }; /** The machine the fake threads live on. */ const EVAL_ENVIRONMENT_ID = EnvironmentId.make("eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee"); // The note T3 adds when a secret card ends unsaved, on refs that have one (looked up by name so // the runner still builds on refs without it). const unsavedSecretNote = (Contracts as Record).SECRET_REQUEST_UNSAVED_NOTE as | Readonly> | undefined; /** Fields T3 adds to real thread and task results, applied to mocks and defaults alike. */ const withProductFields = (name: string, value: unknown, child: boolean): unknown => { if (!isRecord(value)) return value; const linked = (thread: unknown, idKey = "threadId") => { if (!isRecord(thread) || thread.link !== undefined) return thread; const id = thread[idKey] ?? thread.id; return typeof id === "string" ? { ...thread, link: formatThreadLink({ environmentId: EVAL_ENVIRONMENT_ID, threadId: ThreadId.make(id), }), } : thread; }; const listed = (key: string) => Array.isArray(value[key]) ? { ...value, [key]: value[key].map((item) => linked(item)) } : value; switch (name) { case "request_secret": { const note = typeof value.status === "string" ? unsavedSecretNote?.[value.status] : undefined; return note === undefined || value.note !== undefined ? value : { ...value, note }; } case "delegate_task": case "task_status": { const task = linked(value, "childThreadId") as Record; const running = task.status === "running" && task.next === undefined; return running ? { ...task, next: ORCHESTRATOR_MCP_RUNNING_TASK_NEXT } : task; } case "t3_thread_read": return { ...value, thread: linked(value.thread) }; case "t3_thread_launch": case "t3_thread_send": case "run_scheduled_task_now": return linked(value); case "t3_thread_list": case "create_threads": return listed("threads"); case "t3_thread_search": return listed("matches"); case "link_pull_request": case "link_pull_requests": return child ? { ...value, next: CHILD_PULL_REQUEST_REPORT_NEXT } : value; default: return value; } }; /** Cases mock a Claude caller, so the inherited fields follow the model under test. */ const capabilities = ({ base }: Sample, value: unknown) => isRecord(value) ? { ...value, inheritedModel: base.model, ...(base.codex ? { inheritedProviderInstanceId: "codex", inheritedOptions: [{ id: "reasoningEffort", value: base.codex.reasoning.effort }], } : {}), } : value; /** ToolSearch: `select:a,b` loads those tools. Other queries load tools whose name has a word. */ const searchTools = (base: Base, query: string, max: number) => { if (query.startsWith("select:")) { const wanted = query.slice("select:".length).split(","); return base.deferred.filter((tool) => wanted.some((name) => [tool.name, normalizeToolName(tool.name)].includes(name.trim())), ); } const words = query .toLowerCase() .split(/[\s+]+/) .filter(Boolean); const hits = (tool: ApiTool) => words.filter((word) => tool.name.toLowerCase().includes(word)); const found = base.deferred.filter((tool) => hits(tool).length > 0); return found.toSorted((a, b) => hits(b).length - hits(a).length).slice(0, max); }; const SUBAGENT_WATCH_ERROR = "This thread is a subagent, so it cannot watch pull requests. Its parent thread owns the pull request: finish your task and report back instead."; /** Result for a native tool the case does not mock, other than shell, file, and web tools. */ const nativeResult = (name: string, input: Readonly>) => { const skill = valueText(input.skill); const results: Readonly> = { Agent: "The agent finished. Summary: the task is done and nothing else needs attention.", Skill: `Launching skill: ${skill}\n\nBase directory for this skill: ~/.claude/skills/${skill}\n\nThis skill adds no steps for this task. Do it with your usual tools.`, WebSearch: "No results found.", }; return results[name] ?? "OK"; }; const SHELL_PROMPT = `You play the computer a coding agent works on. You get a description of the machine, the agent's tool calls so far with their outputs, and one new call. Reply in JSON. In "facts", note briefly what the output must agree with. In "output", put only the call's output, exactly as the tool prints it: no notes, headings, code fences, or corrections. The agent sees "output" unchanged. - Bash: what the command prints in a real terminal (git, gh, ls, cat, grep, package managers). If it fails, start with "Exit code N", then the error. If it prints nothing, reply "(Bash completed with no output)". - Read: the file with numbered lines, like \`cat -n\`. Write: "File created successfully at: ". Edit: "The file has been updated successfully." Edit may instead supply a Codex apply_patch patch; apply its additions, deletions and hunks. A missing file, or an Edit whose old_string is not in the file: "reason". - WebFetch: the page's text. Stay consistent with the machine and with earlier outputs: edits, commits, pushes, and new PRs show in later output. Where the description is silent, invent plausible small details that fit it (file contents, SHAs, test names) rather than printing nothing. Commands succeed unless the description says otherwise, including git push, gh pr create, installs, builds, and tests. A new PR gets the next number after the highest one in the description or the history, or 124 if there is none.`; const ShellReply = Schema.Struct({ facts: Schema.String, output: Schema.String }); const SHELL_REPLY_SCHEMA = { ...Schema.toJsonSchemaDocument(ShellReply).schema, additionalProperties: false, }; const decodeShellReply = Schema.decodeUnknownOption(Schema.fromJsonString(ShellReply)); const callText = ({ tool, input }: Omit) => `${tool} ${valueText(input)}`; /** * Absolute paths the case's context or files name exist, wherever the template's working * directory is. Without this the simulator denies a checkout the agent was told to work in. */ export const namedPathsLine = ({ setup }: McpToolUseCase) => { const absolute = /(?:^|[\s(`'"])((?:\/home|\/Users|\/tmp)\/[^\s`'",;:)]+)/g; const fromContext = [...(setup.context ?? "").matchAll(absolute)].flatMap(([, path]) => path ? [path] : [], ); const fromFiles = Object.keys(isRecord(setup.world?.files) ? setup.world.files : {}).filter( (path) => path.startsWith("/"), ); const paths = [ ...new Set([...fromContext, ...fromFiles].map((path) => path.replace(/[.]+$/, ""))), ]; return paths.length > 0 ? [ `These paths exist on this machine, with the contents the world gives: ${paths.join(", ")}. The working directory above is only where the shell starts; the agent may cd into them.`, ] : []; }; /** The simulator's request: the machine, this sample's earlier calls, then the new call. */ export const shellRequest = (sample: Sample, call: Omit, model: string) => { const { testCase, base, repo, shell, devices } = sample; const { world, context, shellMocks = [] } = testCase.setup; const { cwd, platform, date } = base.environment; const command = call.tool === "Bash" && isRecord(call.input) ? call.input.command : undefined; const machine = [ `Working directory: ${cwd} on ${platform}, a git checkout of github.com/${repo}.${date ? ` Today is ${date}.` : ""}`, gitStatus(testCase), `World: ${valueText(world ?? {})}`, ...(shellMocks.length > 0 ? [`Commands that match these regexes print exactly this output: ${valueText(shellMocks)}`] : []), // Which device each launcher and session drives, and which of this call's opens are refused. ...(devices?.describe(shell, typeof command === "string" ? command : undefined) ?? []), ...(world?.deviceInvalidCapture ? [ "T3's device_screenshot gets each capture with adb screencap's multi-display warning ahead of the PNG, so it reports 0×0. agent-device's own screenshot finds the PNG and is unaffected.", ] : []), ...(sample.secretFiles.length > 0 ? [ `T3's request_secret saved these files in this sample: ${sample.secretFiles.join(", ")}. Each exists, is readable only by the user, and holds exactly the value the user gave, typed into T3's private card or pasted in chat and saved by the agent, the right one for the task, with no trailing newline. A program that reads it, as in "$(cat )", < or a --*-file flag, gets that value. What appears in the output is whatever that program prints: one that prints or logs its input shows the value. No other file in that directory exists.`, ] : []), ...(context ? [`The agent was told: ${context}`] : []), `The agent's task: ${testCase.prompt}`, ...namedPathsLine(testCase), ]; const history = shell.map((entry) => `${callText(entry)}\n\n${entry.output}\n`); return { model, max_tokens: 8_000, // The "facts" field keeps the simulator's reasoning out of the output the agent sees. output_config: { effort: "low", format: { type: "json_schema", schema: SHELL_REPLY_SCHEMA } }, // A case with devices also gets agent-device's own CLI, so T3's launchers answer as it does. system: devices ? `${SHELL_PROMPT}\n\n${AGENT_DEVICE_SHELL}` : SHELL_PROMPT, messages: [ { role: "user", content: [machine, [...history, `New call:\n${callText(call)}`]].map((part) => text(part.join("\n\n")), ), }, ], }; }; /** The same simulator request in Responses form, for a GPT shell model through the proxy. */ export const shellResponsesRequest = (body: ReturnType) => ({ model: body.model, stream: true, store: false, reasoning: { effort: "low" }, instructions: body.system, text: { format: { type: "json_schema", name: "shell_reply", schema: SHELL_REPLY_SCHEMA, strict: true }, }, input: body.messages.map((message) => ({ role: message.role, content: message.content.map((block) => ({ type: "input_text", text: block.text })), })), }); /** The tool output in the simulator's JSON reply, or its raw text if that is not JSON. */ export const shellOutput = (content: ReadonlyArray) => { const reply = content.flatMap((block) => (isText(block) ? [block.text] : [])).join(""); return Option.match(decodeShellReply(reply), { onNone: () => reply.trim(), onSome: ({ output }) => output.trim(), }); }; /** A shell mock's output, when the command is one simple command that the mock matches. */ export const mockOutput = (sample: Sample, command: string) => /&&|\|\||[;|\n]/.test(command.trim()) ? undefined : sample.testCase.setup.shellMocks?.find((mock) => new RegExp(mock.match).test(command)) ?.output; type Config = { readonly apiKey: string; readonly model: Model; readonly proxy?: { readonly url: string; readonly headers: Readonly> }; readonly effort: string; readonly maxSteps: number; readonly shellModel: ShellModel; }; /** * Output for a command that needs no simulator: a matching shell mock, else, for a plain command * on a launcher T3 returned, agent-device's help or its error for a session not yet started. */ export const cannedOutput = (sample: Sample, command: string) => mockOutput(sample, command) ?? launcherHelp(sample.devices?.launchers ?? [], command) ?? sample.devices?.sessionError(sample.shell, command); /** Output for a shell, file, or web tool: canned output, else the shell simulator. */ const simulate = Effect.fn("evaluateMcpToolUse.simulate")(function* ( sample: Sample, config: Config, call: Omit, ) { const command = call.tool === "Bash" && isRecord(call.input) ? call.input.command : undefined; let output = typeof command === "string" ? cannedOutput(sample, command) : undefined; if (output === undefined) { const body = shellRequest(sample, call, config.shellModel); const response = isPricedModel(config.shellModel) ? yield* callModel(config.apiKey, body, config.apiKey ? undefined : config.proxy) : yield* callCodex(config, shellResponsesRequest(body)); addUsage(sample.shellUsage, response.usage); output = shellOutput(response.content); } if (call.tool === "Bash" && output === "") output = "(Bash completed with no output)"; const entry = { ...call, output }; sample.shell.push(entry); for (const url of createdPullRequests(entry)) sample.linked.add(url); return output; }); /** Tells the shell simulator about a file request_secret saved, so it exists for later commands. */ const savedSecretFile = (sample: Sample, result: unknown) => { if (isRecord(result) && typeof result.file === "string") sample.secretFiles.push(result.file); }; /** The result for one tool call. */ export const answerToolCall = Effect.fn("evaluateMcpToolUse.answerToolCall")(function* ( sample: Sample, config: Config, call: typeof ToolUse.Type, ) { const { testCase, base, tools } = sample; let name = normalizeToolName(call.name); let input = isRecord(call.input) ? call.input : {}; if (call.name === "web__run") { const open = Array.isArray(input.open) ? input.open[0] : undefined; const search = Array.isArray(input.search_query) ? input.search_query[0] : undefined; name = open ? "WebFetch" : "WebSearch"; input = isRecord(open) ? { url: open.ref_id, prompt: "Return the page content." } : isRecord(search) ? { query: search.q } : input; } const failed = (error: string): Block => ({ ...toolResult(call.id, error), is_error: true }); if (!base.codex && !hasTool(tools, call.name)) { const error = `InputValidationError: ${call.name} is deferred. Load it with ToolSearch first.`; return failed(`${error}`); } if (name === "ToolSearch") { const max = typeof input.max_results === "number" ? input.max_results : 5; const found = searchTools(base, valueText(input.query), max); if (found.length === 0) return toolResult(call.id, "No matching deferred tools found"); return loadTools(tools, call.id, found); } const mock = testCase.setup.mocks?.[name]; // T3 answers device calls from the world. A mock that is a T3 device error fails inside T3. const failure = deviceFailure(mock); const fromWorld = mock === undefined || mock === "default" || failure !== undefined; if (sample.devices && isT3Name(call.name) && isDeviceTool(name) && fromWorld) { const result = yield* sample.devices.call(name, input, failure); return result.isError ? failed(result.text) : toolResult(call.id, result.text); } // A pasted secret is saved without a card, so a mocked card outcome does not apply. if (mock !== undefined && mock !== "default" && !pastedSecret(name, input)) { if (name === "request_secret") savedSecretFile(sample, mock); return toolResult( call.id, name === "orchestrator_capabilities" ? capabilities(sample, mock) : withProductFields(name, mock, testCase.setup.role === "delegated-child"), ); } if (isT3Name(call.name)) { // T3 refuses a watch from a delegated child. if (name === "watch_pull_request" && testCase.setup.role === "delegated-child") { return failed(SUBAGENT_WATCH_ERROR); } const value = t3Default(sample, name, input); if (name === "request_secret") savedSecretFile(sample, value); return toolResult( call.id, name === "orchestrator_capabilities" ? capabilities(sample, value) : withProductFields(name, value, testCase.setup.role === "delegated-child"), ); } if (call.name === "clock__curr_time") return toolResult(call.id, { current_time: `${base.environment.date ?? "2026-01-01"} 12:00:00 UTC`, }); if (!SHELL_TOOLS.has(name)) return toolResult(call.id, nativeResult(name, input)); const output = yield* simulate(sample, config, { tool: name, input }); // Claude Code marks a failed command or file call as an error. return /^(Exit code [1-9]|)/.test(output) ? failed(output) : toolResult(call.id, output); }); const BOUND = /^([<>]=)(-?\d+(?:\.\d+)?)$/; /** * Partial match: `~text` is a case-insensitive substring, `<=N` / `>=N` bound a number, and `null` * matches a missing or null value. */ const matches = (expected: unknown, actual: unknown): boolean => { if (expected === null) return actual === undefined || actual === null; const bound = typeof expected === "string" ? BOUND.exec(expected) : null; if (bound !== null) { const limit = Number(bound[2]); return typeof actual === "number" && (bound[1] === "<=" ? actual <= limit : actual >= limit); } if (typeof expected === "string" && expected.startsWith("~")) { return valueText(actual).toLowerCase().includes(expected.slice(1).toLowerCase()); } if (Array.isArray(expected)) { return ( Array.isArray(actual) && expected.every((item) => actual.some((at) => matches(item, at))) ); } if (!isRecord(expected)) return expected === actual; const at = (path: string) => path .split(".") .reduce((value, key) => (isRecord(value) ? value[key] : undefined), actual); return Object.entries(expected).every(([path, value]) => matches(value, at(path))); }; /** Each new top-level thread request in a run; a failed call created nothing. */ const newThreadRequests = (calls: ReadonlyArray) => calls.flatMap((call): ReadonlyArray<{ request: unknown; brief: unknown }> => { if (call.failed === true || !isRecord(call.input)) return []; if (call.tool === "t3_thread_launch") return [{ request: call.input, brief: call.input.message }]; if (call.tool !== "create_threads" || !Array.isArray(call.input.threads)) return []; return call.input.threads.map((request: unknown) => ({ request, brief: isRecord(request) ? request.prompt : undefined, })); }); /** Whether every expected value can take a different request it matches. */ const matchesDistinct = ( expected: ReadonlyArray, requests: ReturnType, taken: ReadonlySet = new Set(), ): boolean => { const [first, ...rest] = expected; if (expected.length === 0) return true; return requests.some( ({ request, brief }, i) => !taken.has(i) && matches(first, typeof first === "string" ? brief : request) && matchesDistinct(rest, requests, new Set([...taken, i])), ); }; /** delegate_task takes `target.options` as an object or `[{id, value}]`. Grade both as an object. */ const withOptionsObject = (input: unknown) => { if (!isRecord(input) || !isRecord(input.target) || !Array.isArray(input.target.options)) { return input; } const options = Object.fromEntries( input.target.options.flatMap((option) => isRecord(option) ? [[valueText(option.id), option.value]] : [], ), ); return { ...input, target: { ...input.target, options } }; }; /** What the user wrote in the case's thread: its context, prior user turns and the prompt. */ export const pastedText = ({ prompt, setup }: Pick) => [ setup.context ?? "", ...(setup.priorTurns ?? []).flatMap((turn) => (turn.role === "user" ? [turn.text ?? ""] : [])), prompt, ].join("\n"); /** `pasted` is the thread's user text (`pastedText`), for request_secret's `value`. */ export const gradeSample = (expect: McpToolUseCase["expect"], run: SampleRun, pasted = "") => { const checks: Array<{ readonly check: string; readonly pass: boolean }> = []; const add = (check: string, pass: boolean) => checks.push({ check, pass }); const calls = run.calls.map((call) => ({ ...call, input: withOptionsObject(call.input) })); const count = (tool: string) => calls.filter((call) => call.tool === tool).length; // `tool` may list alternatives, as in "link_pull_request|watch_pull_request". const found = ({ tool, args = {} }: McpToolUseCall) => calls.some((call) => tool.split("|").includes(call.tool) && matches(args, call.input)); const label = ({ tool, args }: McpToolUseCall) => (args ? `${tool} ${valueText(args)}` : tool); for (const call of expect.calls ?? []) add(`calls ${label(call)}`, found(call)); if (expect.distinctCalls !== undefined) { const expected = expect.distinctCalls.map(({ tool, args }) => ({ tool, input: args ?? {} })); add( "distinctCalls", calls.every((call) => expected.filter((entry) => matches(entry, call)).length <= 1) && matchesDistinct( expected, calls.map((request) => ({ request, brief: "" })), ), ); } for (const call of expect.callsMustNot ?? []) add(`callsMustNot ${label(call)}`, !found(call)); for (const tool of expect.notCalls ?? []) add(`notCalls ${tool}`, count(tool) === 0); if (expect.order !== undefined) { const firsts = expect.order.map((tool) => calls.findIndex((call) => call.tool === tool)); const inOrder = firsts.every((at, i) => at > (firsts[i - 1] ?? -1)); add(`order ${expect.order.join(" > ")}`, inOrder); } for (const [t, n] of Object.entries(expect.minCalls ?? {})) add(`minCalls ${t} >= ${n}`, count(t) >= n); for (const [t, n] of Object.entries(expect.maxCalls ?? {})) add(`maxCalls ${t} <= ${n}`, count(t) <= n); if (expect.newThreads !== undefined) { const requests = newThreadRequests(calls); add( `newThreads ${valueText(expect.newThreads)}`, requests.length === expect.newThreads.length && matchesDistinct(expect.newThreads, requests), ); } if (expect.endsTurnAfter !== undefined) { const lastStep = Math.max(-1, ...calls.map((call) => call.step)); const last = calls.filter((call) => call.step === lastStep).map((call) => call.tool); const ended = run.stopReason === "end_turn" && last.includes(expect.endsTurnAfter); add(`endsTurnAfter ${expect.endsTurnAfter}`, ended); } // Bash and Monitor both take a shell `command`. Secret cases also check, one by one, the scripts // the agent wrote and a command ran: `node run.mjs signup.mjs` after a Write of signup.mjs. const sources = expect.secret === undefined ? [] : scriptSources(calls); const shell = calls.flatMap((call, i) => isRecord(call.input) && typeof call.input.command === "string" ? [call.input.command, ...(sources[i] ?? [])] : [], ); const test = (pattern: string, value: string) => new RegExp(pattern).test(value); for (const p of expect.shellMustNot ?? []) add(`shellMustNot /${p}/`, !shell.some((c) => test(p, c))); // A positive check also reads argv lists as command lines: spawnSync('pnpm', ['exec', …]). for (const p of expect.shellMust ?? []) add( `shellMust /${p}/`, shell.some((c) => test(p, c) || test(p, argvForm(c))), ); for (const p of expect.textMust ?? []) add(`textMust /${p}/`, test(p, run.text)); for (const p of expect.textMustNot ?? []) add(`textMustNot /${p}/`, !test(p, run.text)); for (const p of expect.finalTextMust ?? []) add(`finalTextMust /${p}/`, run.stopReason === "end_turn" && test(p, run.finalText ?? "")); const isLinked = (url: string) => (run.linked ?? []).some((at) => matches(url, at)); for (const url of expect.linked ?? []) add(`linked ${url}`, isLinked(url)); if (expect.linkedNumbers !== undefined) { const numbers = (run.linked ?? []).map((url) => Number(/(\d+)\/?$/.exec(url)?.[1])); const exact = valueText(numbers.toSorted((a, b) => a - b)); add("linkedNumbers", exact === valueText(expect.linkedNumbers.toSorted((a, b) => a - b))); } checks.push(...gradeDevice(expect.device, run)); checks.push(...gradeSecret(expect.secret, run, pasted)); return checks; }; // Checks that can only go from pass to fail as a sample goes on. const SETTLED = /^(?:(?:notCalls|callsMustNot|shellMustNot|maxCalls|textMustNot) |secret\.(?:chatAsk|askTools|values|fileUse|noCardDemand|afterDecline) )/; /** True once the sample has failed for good, so the rest of it cannot change the result. */ export const failedEarly = (expect: McpToolUseCase["expect"], run: SampleRun, pasted = "") => gradeSample(expect, run, pasted).some((check) => !check.pass && SETTLED.test(check.check)); const requestBody = ({ base, tools }: Sample, effort: string, messages: unknown) => ({ model: base.model, max_tokens: 16_000, thinking: { type: "adaptive", display: "summarized" }, output_config: { effort }, cache_control: { type: "ephemeral" }, // Like Claude Code, cache the shared system prompt so every sample reuses it. system: base.system.map((block, i) => i === base.system.length - 1 ? { ...block, cache_control: { type: "ephemeral" } } : block, ), tools, messages, }); const codexRequest = (base: Base, input: ReadonlyArray) => ({ model: base.model, stream: true, store: false, parallel_tool_calls: true, reasoning: base.codex?.reasoning, instructions: base.codex?.instructions, text: base.codex?.text, tool_choice: base.codex?.tool_choice, include: ["reasoning.encrypted_content"], input: [...(base.codex?.input ?? []), ...input], }); /** The bridge only accepts serialized data. Model code never receives host objects/functions. */ export const executeCodex = async ( code: string, catalog: ReadonlyArray<{ name: string; description: string }>, answer: (name: string, input: unknown) => Promise, saved: Record = {}, ) => { const pending: Array<{ name: string; input: string; resolve: (value: string) => void }> = []; const bridge = Object.setPrototypeOf( (name: string, input: string, resolve: (value: string) => void) => pending.push({ name, input, resolve }), null, ); const context = NodeVM.createContext( { __bridge: bridge, __catalog: JSON.stringify(catalog), __storeJson: JSON.stringify(saved) }, { codeGeneration: { strings: false, wasm: false }, microtaskMode: "afterEvaluate" }, ); const run = (source: string) => NodeVM.runInContext(source, context, { timeout: 250 }); run(`((bridge, catalog) => { delete globalThis.__bridge; delete globalThis.__catalog; globalThis.ALL_TOOLS = catalog; globalThis.tools = Object.fromEntries(catalog.map(({name}) => [name, (input) => new Promise(resolve => bridge(name, JSON.stringify(input ?? {}), value => resolve(JSON.parse(value))))])); globalThis.__output = []; globalThis.__done = false; globalThis.text = globalThis.notify = value => __output.push(typeof value === 'string' ? value : JSON.stringify(value)); globalThis.__stored = JSON.parse(__storeJson); delete globalThis.__storeJson; globalThis.store = (k,v) => __stored[k] = v; globalThis.load = k => __stored[k]; })(__bridge, JSON.parse(__catalog)); (async () => { ${code}\n })().then(() => { __done = true; }, e => { text(String(e)); __done = true; });`); // Unawaited calls still run, as they do in Codex, so they are answered and graded too. for (let count = 0; !context.__done || pending.length > 0; count++) { if (count === 128) throw new Error("Code cell exceeded 128 tool calls."); const job = pending.shift(); if (!job) throw new Error("Code cell stalled without a tool call."); const result = await answer(job.name, JSON.parse(job.input)); context.__resolve = job.resolve; context.__result = JSON.stringify(result ?? null); run("__resolve(__result); delete globalThis.__resolve; delete globalThis.__result;"); } Object.assign(saved, JSON.parse(run("JSON.stringify(__stored)"))); return valueText(context.__output.join("\n")); }; const codexCatalog = (base: Base) => [ ...base.t3.map((tool) => ({ name: tool.name.replace("mcp__t3-code__", "mcp__t3_code__"), description: `${valueText(tool.description)}\nInput JSON schema: ${JSON.stringify(tool.input_schema)}`, })), ...[ ...valueText(base.nativeTools.find((tool) => tool.name === "exec")?.description).matchAll( /### `([^`]+)`/g, ), ].map((match) => ({ name: match[1]!, description: match[1]! })), ]; const CodexResponse = Schema.Struct({ output: Schema.Array(AnyBlock), usage: Schema.Struct({ input_tokens: Schema.Number, output_tokens: Schema.Number, input_tokens_details: Schema.Struct({ cached_tokens: Schema.Number }), }), }); const decodeCodexResponse = Schema.decodeUnknownEffect(CodexResponse); const decodeJson = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Unknown)); const callCodex = Effect.fn("evaluateMcpToolUse.callCodex")(function* ( config: Config, body: unknown, ) { const raw = yield* postModel("", body, config.proxy, true); const completed = raw .split("\n") .filter((line) => line.startsWith("data: ")) .map((line) => JSON.parse(line.slice(6)) as Record) .find((event) => event.type === "response.completed"); if (!completed) return yield* Effect.die(new Error("Responses stream ended without response.completed.")); const response = yield* decodeCodexResponse(completed.response); return { raw: response.output, content: response.output.flatMap((item): Array => { if (item.type === "message" && Array.isArray(item.content)) return item.content.map((part: Block) => text(valueText(part.text))); if (item.type === "function_call" || item.type === "custom_tool_call") return [ { type: "tool_use", id: item.call_id, name: item.name, input: item.type === "custom_tool_call" ? item.input : JSON.parse(valueText(item.arguments)), }, ]; return []; }), stop_reason: response.output.some( (item) => item.type.endsWith("tool_call") || item.type === "function_call", ) ? "tool_use" : "end_turn", usage: { ...response.usage, input_tokens: response.usage.input_tokens - response.usage.input_tokens_details.cached_tokens, cache_read_input_tokens: response.usage.input_tokens_details.cached_tokens, }, }; }); // Concurrent runs share an organization's rate limits. Backing off for up to about 4 minutes // lets a sample wait out a busy spell, with jitter so samples do not retry in step. const RETRY = Schedule.min([Schedule.exponential("2 seconds"), Schedule.spaced("1 minute")]).pipe( Schedule.jittered, ); const postModel = Effect.fn("evaluateMcpToolUse.postModel")(function* ( apiKey: string, body: unknown, proxy?: Config["proxy"], responses = false, ) { const client = (yield* HttpClient.HttpClient).pipe( // Anthropic answers 529 when it is overloaded; retryTransient already retries 429 and 5xx. HttpClient.filterStatus((status) => status !== 529), HttpClient.retryTransient({ retryOn: "errors-and-responses", while: (error) => error.reason._tag === "StatusCodeError" && error.reason.response.status === 529, schedule: RETRY, times: 8, }), ); const response = yield* HttpClientRequest.post( `${proxy?.url ?? "https://api.anthropic.com/v1"}/${responses ? "responses" : "messages"}`, ).pipe( HttpClientRequest.setHeaders({ ...(apiKey ? { "x-api-key": apiKey } : {}), "anthropic-version": "2023-06-01", // Claude Code sends this beta for `defer_loading` tools and `tool_reference` results. "anthropic-beta": "advanced-tool-use-2025-11-20", ...proxy?.headers, }), HttpClientRequest.bodyJson(body), Effect.flatMap(client.execute), ); if (response.status !== 200) { const body = yield* response.text; return yield* Effect.die(new Error(`Model API ${response.status}: ${body}`)); } return yield* response.text; }); const callModel = (apiKey: string, body: unknown, proxy?: Config["proxy"]) => postModel(apiKey, body, proxy).pipe( Effect.flatMap(Schema.decodeUnknownEffect(Schema.fromJsonString(ApiResponse))), ); /** A tool call as graded. Claude Code runs an `Agent` in the background unless told otherwise. */ const recordedCall = (step: number, use: typeof ToolUse.Type): Call => { const tool = normalizeToolName(use.name); const input = normalizeToolInput(use.name, use.input); const claudeAgent = tool === "Agent" && !use.name.endsWith("spawn_agent") && isRecord(input); return { step, tool, input: claudeAgent ? { run_in_background: true, ...input } : input, id: use.id, }; }; /** Records on a call what the agent saw, so graders can count failed attempts. */ const settle = (call: Call | undefined, result: Block) => { if (call !== undefined) { call.failed = result.is_error === true; call.output = valueText(result.content); } return result; }; /** Calls that ran. Claude Code rejects a deferred tool that was not loaded, so it never runs. */ export const ranCalls = ( { base, tools }: Sample, step: number, uses: ReadonlyArray, ) => uses .filter((use) => base.codex !== undefined || hasTool(tools, use.name)) .map((use) => recordedCall(step, use)); /** * Runs one sample until the model ends its turn, uses up its steps, or fails a check that no * later step can undo. Tokens add up in `spent`, so a sample that errors still reports them. */ const runSample = Effect.fn("evaluateMcpToolUse.runSample")(function* ( testCase: McpToolUseCase, config: Config, base: Base, spent: { readonly usage: Usage; readonly shellUsage: Usage }, ) { const sample = { ...makeSample(testCase, base), shellUsage: spent.shellUsage }; yield* withDevices(sample); const runPromise = Effect.runPromiseWith(yield* Effect.context()); const messages = buildMessages(sample); const input = base.codex ? codexMessages(messages) : undefined; const transcript: Array = []; const start = messages.length; const calls: Array = []; const said: Array = []; const { usage } = spent; let stopReason = "max_steps"; let finalText = ""; // Codex's code-mode cells take more model calls, so a case's own budget grows for it too. const extra = isCodex(config.model) ? 4 : 0; const maxSteps = testCase.maxSteps === undefined ? config.maxSteps : testCase.maxSteps + extra; for (let step = 0; step < maxSteps; step++) { const response = input ? yield* callCodex(config, codexRequest(base, input)) : yield* callModel(config.apiKey, requestBody(sample, config.effort, messages)); addUsage(usage, response.usage); if ("raw" in response) { input?.push(...response.raw); transcript.push(...response.raw); } messages.push({ role: "assistant", content: response.content }); const texts = response.content.filter(isText).map((block) => block.text); said.push(...texts); const uses = response.content.filter(isToolUse); calls.push(...ranCalls(sample, step, uses)); if (response.stop_reason !== "tool_use") { stopReason = response.stop_reason ?? "unknown"; finalText = texts.join("\n\n"); break; } if ( failedEarly( testCase.expect, { calls, text: said.join("\n\n"), stopReason }, pastedText(testCase), ) ) { stopReason = "failed_early"; break; } const results = yield* Effect.forEach(uses, (use) => Effect.gen(function* () { const own = calls.find((call) => call.id === use.id); if (!input) { return settle(own, yield* answerToolCall(sample, config, use)); } const answer = (name: string, args: unknown, record = true) => { const tool = normalizeToolName(name); const normalized = normalizeToolInput(name, args); // A code cell's calls are recorded here; a direct function call already was. let recorded = own; if (record) { recorded = { step, tool, input: normalized }; calls.push(recorded); } const call = { ...use, name, input: normalized }; return answerToolCall(sample, config, call).pipe( Effect.map((result) => settle(recorded, result)), Effect.map((result) => { const output = valueText(result.content); if (isT3Name(name)) return { content: [text(output)], ...(!result.is_error ? { structuredContent: Option.getOrElse(decodeJson(output), () => output) } : { isError: true }), }; return tool === "Bash" ? { output, exit_code: /^Exit code [1-9]/.test(output) ? 1 : 0, wall_time_seconds: 0, } : output; }), runPromise, ); }; return yield* Effect.tryPromise({ try: async () => { const output = use.name === "exec" ? await executeCodex( valueText(use.input), codexCatalog(base), answer, sample.codeStore, ) : await answer(use.name, use.input, false); const item = { type: use.name === "exec" ? "custom_tool_call_output" : "function_call_output", call_id: use.id, output: valueText(output), }; input.push(item); transcript.push(item); return toolResult(use.id, output); }, catch: (cause) => new EvalToolCallError({ tool: use.name, cause }), }); }), ); // Claude Code adds this text after a ToolSearch that loaded tools. const loaded = results.some((result) => Array.isArray(result.content)); messages.push({ role: "user", content: [...results, ...(loaded ? [text(TOOL_LOADED)] : [])] }); if ( failedEarly( testCase.expect, { calls, text: said.join("\n\n"), stopReason }, pastedText(testCase), ) ) { stopReason = "failed_early"; break; } } // The transcript (model turns and tool results) shows what the agent saw when a case fails. return { calls, text: said.join("\n\n"), stopReason, finalText, linked: [...sample.linked], ...(sample.devices ? { launchers: [...sample.devices.launchers] } : {}), usage, shellUsage: sample.shellUsage, transcript: input ? transcript : messages.slice(start), }; }, Effect.scoped); type ResultRow = Pick & { readonly role: string; readonly pass: boolean; /** Set when the sample could not run to the end, even after retries. It is not graded. */ readonly error?: string; readonly usage: Usage; readonly shellUsage: Usage; }; /** The share of samples that passed. Samples that errored are left out: NaN if all did. */ const passRate = ( rows: ReadonlyArray<{ readonly pass: boolean; readonly error?: string | undefined }>, ) => { const finished = rows.filter((row) => row.error === undefined); return finished.filter((row) => row.pass).length / finished.length; }; const percent = (rate: number) => (Number.isNaN(rate) ? "-" : `${Math.round(rate * 100)}%`); const groups = (rows: ReadonlyArray, key: (row: A) => string) => [...Map.groupBy(rows, key)].toSorted(([left], [right]) => left.localeCompare(right)); const USAGE_KEYS = ["input", "output", "cacheWrite", "cacheRead"] as const; /** Token totals and their cost in USD at one model's prices. Proxy runs have no price. */ const spend = (usages: ReadonlyArray, model: ShellModel | undefined) => { const total = (key: keyof Usage) => usages.reduce((sum, usage) => sum + usage[key], 0); const price = (key: keyof Usage) => (model && isPricedModel(model) ? PRICES[model][key] : 0); const cost = USAGE_KEYS.reduce((sum, key) => sum + total(key) * price(key), 0) / 1e6; return { cost, tokens: USAGE_KEYS.map((key) => `${key} ${total(key)}`).join(", ") }; }; /** Markdown report. With a baseline, each case shows its earlier pass rate and the change. */ export const summarize = ( rows: Array, options: { readonly header: string; readonly shellModel: ShellModel; readonly model: Model }, before?: Map, ) => { const codex = isCodex(options.model); const agent = spend( rows.map((row) => row.usage), codex ? undefined : options.model, ); const shell = spend( rows.map((row) => row.shellUsage), options.shellModel, ); const cost = agent.cost + shell.cost; const table = (title: string, key: (row: ResultRow) => string, compare = false) => [ `## ${title}\n`, compare ? "| Group | Pass | Baseline | Delta |" : "| Group | Pass |", compare ? "| --- | --- | --- | --- |" : "| --- | --- |", ...groups(rows, key).map(([group, members]) => { const now = passRate(members); const was = before?.get(group); const errored = members.filter((row) => row.error !== undefined).length; const delta = was === undefined ? "new | |" : `${percent(was)} | ${Number.isNaN(now) ? "" : `${Math.round((now - was) * 100)} pts`} |`; const pass = `${percent(now)} of ${members.length - errored}${errored > 0 ? `, ${errored} errored` : ""}`; return `| ${group} | ${pass} |${compare ? ` ${delta}` : ""}`; }), "", ]; const errors = groups( rows.filter((row) => row.error !== undefined), (row) => row.id, ); const errorNote = `- Errored: ${errors.map(([id, failed]) => `${id} ×${failed.length}`).join(", ")}. These samples did not finish, even after retries, so pass rates leave them out; tokens they spent are counted.`; const nativeNote = "- native-subagent cases are approximate: the harness gives subagents their own system prompt, which this runner does not rebuild. T3's append is left out and its SubagentStart hook context added after the task."; const identityNote = `- The captured Claude Code identity line is replaced with "${NEUTRAL_IDENTITY}"`; const finished = rows.filter((row) => row.error === undefined).length; return [ "# MCP tool-use eval\n", `- ${options.header}`, `- Pass: ${percent(passRate(rows))} of ${finished} samples.`, ...(errors.length > 0 ? [errorNote] : []), codex ? "- Cost: the agent is not priced. `vibeproxy` in `~/.codex/config.toml` may use a subscription or bill an API key per token; agent tokens are below." : `- Estimated cost $${cost.toFixed(2)}, $${(cost / rows.length).toFixed(3)} per sample.`, `- Agent tokens: ${agent.tokens}${codex ? "." : ` ($${agent.cost.toFixed(2)}).`}`, `- Shell simulator (${options.shellModel}) tokens: ${shell.tokens} (API-price estimate $${shell.cost.toFixed(2)}).`, ...(codex ? [ "- Code-mode tools use case mocks and a simulated machine. Native subagent prompts and asynchronous notifications are approximate.", ] : [identityNote]), ...(rows.some((row) => row.role === "native-subagent") ? [nativeNote] : []), "", ...table("Cases", (row) => row.id, before !== undefined), ...table("By kind", (row) => row.kind), ...table("By pattern", (row) => row.pattern), ].join("\n"); }; const PositiveInt = Schema.FiniteFromString.check(Schema.isInt(), Schema.isGreaterThan(0)); const PositiveIntOrZero = Schema.FiniteFromString.check( Schema.isInt(), Schema.isGreaterThanOrEqualTo(0), ); const Flags = Schema.Struct({ model: Schema.Literals(Object.keys(MODELS) as Array), template: Schema.String, cases: Schema.optional(Schema.Array(Schema.String)), out: Schema.optional(Schema.String), baseline: Schema.optional(Schema.String), case: Schema.optional(Schema.Array(Schema.String)), samples: Schema.optional(PositiveInt), concurrency: PositiveInt, "max-steps": Schema.optional(PositiveInt), effort: Schema.optional(Schema.Literals(EFFORTS)), "shell-model": Schema.Literals([ ...(Object.keys(PRICES) as Array), ...PROXY_SHELL_MODELS, ]), "user-instructions": Schema.Literals(["keep", "drop"]), "dry-run": Schema.Boolean, "stop-on-regression": Schema.Boolean, "regression-budget": Schema.optional(PositiveIntOrZero), context: Schema.Boolean, }); const decodeFlags = Schema.decodeUnknownEffect(Flags); const decodeCapture = Schema.decodeUnknownEffect(Schema.fromJsonString(Capture)); const decodeCodexCapture = Schema.decodeUnknownEffect(Schema.fromJsonString(CodexCapture)); const decodeCase = Schema.decodeUnknownEffect(Schema.fromJsonString(McpToolUseCase)); const decodeBaselineRow = Schema.decodeUnknownEffect( Schema.fromJsonString( Schema.Struct({ id: Schema.String, pass: Schema.Boolean, error: Schema.optional(Schema.String), }), ), ); const encodeJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); const string = { type: "string" } as const; const main = Effect.gen(function* () { const { values } = NodeUtil.parseArgs({ options: { model: { ...string, default: "claude-sonnet-5-5" }, template: string, cases: { ...string, multiple: true }, out: string, baseline: string, case: { ...string, multiple: true }, // Unset: 2 samples per case, and a 3rd when those 2 disagree. samples: string, // Samples in flight at once, across all cases. concurrency: { ...string, default: "8" }, // Unset: 6 for Claude, 10 for Codex, whose code-mode cells take more steps. "max-steps": string, effort: string, "shell-model": { ...string, default: "claude-haiku-5-5" }, // T3's text must work for every user, so the captured CLAUDE.md is dropped by default. "user-instructions": { ...string, default: "drop" }, "dry-run": { type: "boolean", default: false }, "stop-on-regression": { type: "boolean", default: false }, "regression-budget": string, // Prints the size of T3's text and tool schemas, then exits. Needs no template or key. context: { type: "boolean", default: false }, }, }); const flags = yield* decodeFlags({ template: "", ...values }); if (flags.context) { process.stdout.write(`${t3ContextReport(flags.model)}\n`); return; } if (flags["stop-on-regression"] && flags.baseline === undefined) { return yield* Effect.die(new Error("--stop-on-regression needs --baseline.")); } const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const readNdjson = (file: string, decode: (line: string) => Effect.Effect) => fs.readFileString(file).pipe( Effect.map((content) => content.split("\n").filter((line) => line.trim() !== "")), Effect.flatMap((lines) => Effect.forEach(lines, (line) => decode(line))), ); const casesDir = path.join(import.meta.dirname, "cases"); const caseFiles = flags.cases ?? (yield* fs.readDirectory(casesDir)) .filter((name) => name.endsWith(".ndjson")) .toSorted() .map((name) => path.join(casesDir, name)); const cases = (yield* Effect.forEach(caseFiles, (file) => readNdjson(file, decodeCase))).flat(); const wanted = flags.case?.flatMap((ids) => ids.split(",")); const codex = isCodex(flags.model); const harness = codex ? "codex" : "claude"; const selectedCases = cases.filter( ({ id, setup }) => (wanted === undefined || wanted.includes(id)) && (setup.harness === undefined || setup.harness === harness), ); if (selectedCases.length === 0) return yield* Effect.die(new Error("No case matches --case.")); const selected = yield* Effect.forEach(selectedCases, (testCase) => Effect.gen(function* () { const priorTurns = yield* Effect.forEach(testCase.setup.priorTurns ?? [], (turn) => Effect.gen(function* () { const toolCalls = yield* Effect.forEach(turn.toolCalls ?? [], (call) => call.resultFile === undefined ? Effect.succeed(call) : fs .readFileString(path.resolve(import.meta.dirname, "../..", call.resultFile)) .pipe(Effect.map((result) => ({ ...call, result }))), ); return { ...turn, toolCalls }; }), ); return { ...testCase, setup: { ...testCase.setup, priorTurns, }, }; }), ); // Trimmed captures without user data ship beside the cases. `--template` also takes a raw // CLIProxyAPI capture. const templatePath = flags.template || path.join(import.meta.dirname, "templates", codex ? "codex.json.gz" : "claude.json.gz"); const templateBytes = yield* fs.readFile(templatePath); const template = new TextDecoder().decode( templatePath.endsWith(".gz") ? NodeZlib.gunzipSync(templateBytes) : templateBytes, ); const keepUserInstructions = flags["user-instructions"] === "keep"; const buildFor = ( role: McpToolUseCase["setup"]["role"], deviceAccess: boolean, links: boolean, ): Effect.Effect => codex ? decodeCodexCapture(template).pipe( Effect.map((capture) => buildCodexBase(capture.request.provider_native, { model: flags.model, keepUserInstructions, effort: flags.effort, subagent: role === "delegated-child", links, deviceAccess, }), ), ) : decodeCapture(template).pipe( Effect.flatMap((capture) => { const built = buildBase(capture.request.provider_native, { model: flags.model, keepUserInstructions, subagent: role === "delegated-child", nativeSubagent: role === "native-subagent", links, deviceAccess, }); return role === "native-subagent" ? Effect.promise(() => t3SubagentContext(flags.model, deviceAccess)).pipe( Effect.map((subagentContext): Base => ({ ...built, subagentContext })), ) : Effect.succeed(built); }), ); // Codex has no native subagents; its cases with that role run as top-level threads. const baseKey = ({ setup }: McpToolUseCase) => `${codex && setup.role === "native-subagent" ? "top" : setup.role}:${setup.world?.deviceAgentAccess !== false}:${setup.linkedEnvironments === true}`; const base = yield* buildFor("top", true, false); const bases = new Map([["top:true:false", base]]); for (const testCase of selected) { const key = baseKey(testCase); if (bases.has(key)) continue; const [role = "top", access, links] = key.split(":") as [ McpToolUseCase["setup"]["role"], string, string, ]; bases.set(key, yield* buildFor(role, access === "true", links === "true")); } const baseFor = (testCase: McpToolUseCase) => bases.get(baseKey(testCase)) ?? base; const effort = flags.effort ?? base.codex?.reasoning.effort ?? "high"; if (flags["dry-run"]) { // One JSON line per case. Without --case this also checks that every case builds. for (const testCase of selected) { const sample = makeSample(testCase, baseFor(testCase)); yield* Effect.scoped(withDevices(sample)); const messages = buildMessages(sample); const body = sample.base.codex ? codexRequest(sample.base, codexMessages(messages)) : requestBody(sample, effort, messages); process.stdout.write(`${yield* encodeJson(body)}\n`); } return; } const apiKey = yield* Config.String("ANTHROPIC_API_KEY").pipe(Config.withDefault("")); const out = flags.out; if ((!apiKey && !codex) || out === undefined) { const message = "Set ANTHROPIC_API_KEY and pass --out . ANTHROPIC_AUTH_TOKEN and ANTHROPIC_BASE_URL are never used."; return yield* Effect.die(new Error(message)); } const config: Config = { apiKey, model: flags.model, ...(codex || !isPricedModel(flags["shell-model"]) ? { proxy: proxyConfig() } : {}), effort, maxSteps: flags["max-steps"] ?? (codex ? 10 : 6), shellModel: flags["shell-model"], }; const previous = flags.baseline === undefined ? [] : yield* readNdjson(flags.baseline, decodeBaselineRow); const baseline = new Map( groups(previous, (row) => row.id) .map(([id, r]) => [id, passRate(r)] as const) .filter(([, rate]) => !Number.isNaN(rate)), ); // Only a case that passed every baseline sample can stop a run; flaky ones are just reported. const passedBefore = (id: string) => baseline.get(id) === 1; // Before the paid runs, so a missing git cannot lose their results. const commit = yield* Effect.sync(() => Option.getOrElse( Option.liftThrowable(() => NodeChildProcess.execFileSync("git", ["describe", "--always", "--dirty"], { encoding: "utf8", }).trim(), )(), () => "unknown", ), ); const inFlight = yield* Semaphore.make(flags.concurrency); const runOne = (testCase: McpToolUseCase) => { const { id, kind, pattern } = testCase; const identity = { id, kind, pattern, role: testCase.setup.role }; const usage = { usage: emptyUsage(), shellUsage: emptyUsage() }; const privateValues = [config.apiKey, ...Object.values(config.proxy?.headers ?? {})] .flatMap((value) => [value, value.replace(/^Bearer /i, "")]) .filter(Boolean); return runSample(testCase, config, baseFor(testCase), usage).pipe( inFlight.withPermits(1), Effect.matchCause({ // Not graded: a sample that errors is left out of pass rates, with its tokens so far. onFailure: (cause) => ({ ...identity, ...usage, pass: false, error: privateValues.reduce( (message, value) => message.replaceAll(value, "[redacted]"), Cause.pretty(cause), ), }), onSuccess: (sample) => { const checks = gradeSample(testCase.expect, sample, pastedText(testCase)); const { secret } = testCase.expect; return { ...identity, pass: checks.every((check) => check.pass), checks, // Not graded: how the agent handled a secret it needed, for a person reading the run. ...(secret ? { secret: secretDiagnostics(sample, secret) } : {}), ...sample, }; }, }), Effect.tap((row) => Effect.log(`${row.id}: ${"error" in row ? "ERROR" : row.pass ? "pass" : "FAIL"}`), ), ); }; // Without --samples, a case whose 2 samples disagree gets a 3rd to settle it. const runCase = Effect.fn("evaluateMcpToolUse.runCase")(function* (testCase: McpToolUseCase) { const samples = Array.from({ length: flags.samples ?? 2 }, () => testCase); const first = yield* Effect.forEach(samples, runOne, { concurrency: "unbounded" }); const split = flags.samples === undefined && new Set(first.map((row) => row.pass)).size > 1; return split ? [...first, yield* runOne(testCase)] : first; }); // --stop-on-regression runs cases that passed in the baseline first and stops once more than // --regression-budget of them fail, so a copy change that breaks passing cases fails fast. // A budget lets a quirky model miss a case or two without ending the run. const stopEarly = flags["stop-on-regression"]; const budget = flags["regression-budget"] ?? 0; const ordered = stopEarly ? selected.toSorted((a, b) => Number(passedBefore(b.id)) - Number(passedBefore(a.id))) : selected; const rows: Array>> = []; const regressions: Array = []; yield* Effect.forEach( ordered, (testCase) => runCase(testCase).pipe( Effect.flatMap((caseRows) => { rows.push(...caseRows); // A case whose samples all errored has no rate, so it is no regression either. if (!stopEarly || !passedBefore(testCase.id) || !(passRate(caseRows) <= 0.5)) { return Effect.void; } regressions.push(testCase.id); return regressions.length > budget ? Effect.fail(testCase.id) : Effect.void; }), ), { concurrency: flags.concurrency, discard: true }, ).pipe(Effect.ignore); const samples = flags.samples === undefined ? "2-3 (adaptive)" : flags.samples; const header = `Commit \`${commit}\`, ${flags.model} at ${effort} effort, user instructions ${flags["user-instructions"]}, ${samples} samples per case, template \`${path.basename(templatePath)}\``; const finished = new Set(rows.map((row) => row.id)).size; const stopped = regressions.length === 0 ? [] : [ `- Regressions (passed every baseline sample, now fail): ${regressions.map((id) => `\`${id}\``).join(", ")}.${regressions.length > budget ? ` Stopped with ${finished} of ${selected.length} cases finished.` : ""}`, ]; const summary = summarize( rows, { header: [header, t3ContextReport(flags.model), ...stopped].join("\n"), shellModel: config.shellModel, model: config.model, }, flags.baseline === undefined ? undefined : baseline, ); yield* fs.makeDirectory(out, { recursive: true }); const lines = yield* Effect.forEach(rows, (row) => encodeJson(row)); yield* fs.writeFileString(path.join(out, "results.jsonl"), `${lines.join("\n")}\n`); yield* fs.writeFileString(path.join(out, "summary.md"), summary); yield* Effect.log(`Wrote ${rows.length} samples to ${out}.\n\n${summary}`); }); if (import.meta.main) { main.pipe( Effect.provide( Layer.mergeAll(NodeServices.layer, PlatformNodeCrypto.layer, FetchHttpClient.layer), ), NodeRuntime.runMain, ); }