diff --git a/packages/coding-agent/src/extensions/codemode/tool.ts b/packages/coding-agent/src/extensions/codemode/tool.ts index b55a117a7..e28ea999f 100644 --- a/packages/coding-agent/src/extensions/codemode/tool.ts +++ b/packages/coding-agent/src/extensions/codemode/tool.ts @@ -271,8 +271,8 @@ export function createCodemodeDescription( /** * The field names of an object schema, `{ output, exit_code, full_output_path? }`, with the fields of - * object-typed fields `depth` levels deep: `{ content, structuredContent?: { results } }`. Undefined - * for other schemas. + * object-typed fields `depth` levels deep: `{ content, structuredContent?: { results } }`, and the + * value of `const` fields: `{ action: "start", id }`. Undefined for other schemas. */ function describeFields(schema: unknown, depth: number): string | undefined { if (typeof schema !== "object" || schema === null) return undefined; @@ -280,7 +280,13 @@ function describeFields(schema: unknown, depth: number): string | undefined { if (type !== "object" || typeof properties !== "object" || properties === null) return undefined; const requiredNames = new Set(Array.isArray(required) ? required : []); const fields = Object.entries(properties).map(([name, field]) => { - const nested = depth > 0 ? describeFields(field, depth - 1) : undefined; + const constValue = (field as Record | null)?.const; + const nested = + constValue !== undefined + ? JSON.stringify(constValue) + : depth > 0 + ? describeFields(field, depth - 1) + : undefined; return `${name}${requiredNames.has(name) ? "" : "?"}${nested ? `: ${nested}` : ""}`; }); return `{ ${fields.join(", ")} }`; @@ -288,13 +294,30 @@ function describeFields(schema: unknown, depth: number): string | undefined { /** * What a script call resolves to, in one line: `a string`, the field names of an object (see - * {@link describeFields}), or the rendered type for anything else. + * {@link describeFields}), the field names of each object in a union, the rendered type when it + * fits on one line, or a pointer to `describeTool()` for anything longer. The full type of a large + * schema would otherwise be sent with every request (#11501). */ -function describeOutput(schema: CodemodeJsonSchema | undefined): string { +function describeOutput(name: string, schema: CodemodeJsonSchema | undefined): string { const type = renderToolOutputType(schema); if (type === "string") return "a string"; - const fields = describeFields(schema, 1); - return `\`${fields ?? type.replace(/\s+/g, " ")}\``; + const fields = describeFields(schema, 1) ?? describeUnionFields(schema); + if (fields) return `\`${fields}\``; + if (!type.includes("\n")) return `\`${type}\``; + return `a value; \`await describeTool(${JSON.stringify(name)})\` shows its type`; +} + +/** + * The field names of each variant of an `anyOf`/`oneOf` whose variants are all objects, joined by + * `|`. + */ +function describeUnionFields(schema: unknown): string | undefined { + if (typeof schema !== "object" || schema === null) return undefined; + const { anyOf, oneOf } = schema as Record; + const variants = anyOf ?? oneOf; + if (!Array.isArray(variants) || variants.length === 0) return undefined; + const described = variants.map((variant) => describeFields(variant, 0)); + return described.every((fields) => fields !== undefined) ? described.join(" | ") : undefined; } /** @@ -302,7 +325,7 @@ function describeOutput(schema: CodemodeJsonSchema | undefined): string { * arguments are the tool's declared parameters, so they are not repeated. */ function describeScriptCall(tool: AgentTool): string { - return `${tool.description.trim()}\n\nCodemode: \`tools.${toCodemodeIdentifier(tool.name)}(args)\` resolves to ${describeOutput(toCodemodeDeclaration(tool).outputSchema)}.`; + return `${tool.description.trim()}\n\nCodemode: \`tools.${toCodemodeIdentifier(tool.name)}(args)\` resolves to ${describeOutput(tool.name, toCodemodeDeclaration(tool).outputSchema)}.`; } /** diff --git a/packages/coding-agent/test/suite/agent-session-codemode.test.ts b/packages/coding-agent/test/suite/agent-session-codemode.test.ts index 4aeeb9940..ff39f1cd6 100644 --- a/packages/coding-agent/test/suite/agent-session-codemode.test.ts +++ b/packages/coding-agent/test/suite/agent-session-codemode.test.ts @@ -184,6 +184,49 @@ describe("AgentSession codemode tool", () => { expect(description("echo")).toBe("Echo text back.\n\nSecond paragraph."); }); + // #11501 + it("summarizes union and multi-line output types instead of inlining them", async () => { + const outputTool = (name: string, outputSchema: AgentTool["outputSchema"]): AgentTool => ({ + name, + label: name, + description: `${name} tool`, + parameters: Type.Object({}), + outputSchema, + execute: async () => ({ content: [], details: {} }), + }); + const process = outputTool( + "process", + Type.Union([ + Type.Object({ + action: Type.Literal("start"), + process: Type.Object({ id: Type.String({ description: "Process id" }), pid: Type.Integer() }), + }), + Type.Object({ + action: Type.Literal("list"), + processes: Type.Array(Type.Object({ id: Type.String() })), + truncated: Type.Optional(Type.Boolean()), + }), + ]), + ); + const entries = outputTool("entries", Type.Array(Type.Object({ id: Type.String(), size: Type.Integer() }))); + const count = outputTool("count", Type.Number()); + const harness = await setup([ + (pi: ExtensionAPI) => { + for (const tool of [process, entries, count]) pi.registerTool(createToolDefinitionFromAgentTool(tool)); + }, + ]); + harness.session.setActiveToolsByName(["process", "entries", "count", "codemode"]); + const description = (name: string) => + harness.session.agent.state.tools.find((tool) => tool.name === name)?.description ?? ""; + expect(description("process")).toBe( + 'process tool\n\nCodemode: `tools.process(args)` resolves to `{ action: "start", process } | { action: "list", processes, truncated? }`.', + ); + expect(description("entries")).toBe( + 'entries tool\n\nCodemode: `tools.entries(args)` resolves to a value; `await describeTool("entries")` shows its type.', + ); + expect(description("count")).toBe("count tool\n\nCodemode: `tools.count(args)` resolves to `number`."); + }); + // #10343 it("shows the guidelines of tools that do not fit the inline budget through describeTool()", async () => { const harness = await setup();