mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
fix(coding-agent): summarize union and multi-line codemode output types in tool descriptions (#11513)
With `codemode.mode: "on"`, each declared tool gets a description suffix
that says what a script call resolves to. For an object output schema,
`describeOutput` shortens this to field names. Any other schema fell
back to the whole rendered TypeScript type, collapsed onto one line. A
tool with a union output schema therefore put its full output type,
nested fields and comments included, into every request. The reporter's
`process` tool, a union of 7 variants, added about 8,000 characters.
Collapsing the type onto one line also left `//` comments in the middle
of the text.
This change:
- **Unions:** for `anyOf`/`oneOf` schemas where every variant is an
object, the suffix lists each variant's field names, joined by ` | `.
- **`const` fields:** `describeFields` now shows the value of `const`
fields, so a discriminator like `action` stays visible. For example: ``
`{ action: "start", process } | { action: "list", processes, truncated?
}` ``.
- **Other multi-line types**, such as an array of objects: instead of
the inlined type, the suffix says `` a value; `await
describeTool("name")` shows its type ``. `describeTool()` already
returns the full declaration.
- **Single-line types** such as `number` are unchanged.
Testing:
- I added a test in `test/suite/agent-session-codemode.test.ts` that
checks the suffix for three tools: one with a union output, one with an
array-of-objects output, and one with a `number` output. It fails
without the change and passes with it.
- `node ../../node_modules/vitest/dist/cli.js --run
test/suite/agent-session-codemode.test.ts
test/suite/agent-session-mcp.test.ts` passes, including the existing MCP
description assertions.
- `npm run check` is clean.
Fixes #11501
Requested by @badlogic
Co-authored-by: triagent <triagent@localhost>
This commit is contained in:
parent
7391dbb0f8
commit
f6ee9ce9c1
2 changed files with 74 additions and 8 deletions
|
|
@ -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<string, unknown> | 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<string, unknown>;
|
||||
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<any>): 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)}.`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue