feat(durable): nested tool calls, structured output, callers; storage errors end the Session

Nested tool calls: ToolExecutionApi.executeTool() runs another tool as its own pi.tool task owned by the caller,
keyed for replay-safe reattachment. Results for programs live in the caller's NestedResultDoc until it settles;
pi.live.nestedTools shows each call with the arguments it runs with and a summary. TaskOptions.abandonOnRestart
aborts work its creator awaited only in memory before it runs again after a restart.

Structured output: structuredOutputSchema and structuredOutput, validated; a tool without a schema gives programs its
bounded output (one text as a string, one image as itself, else the list). ToolExecutionResult.content is now output.
callers and modelTools decide who may call a tool.

Storage errors: any error a Storage method throws fails the Session, which closes itself; later calls and waits reject
with SessionFailed, watches end with session_failed, and Session.closed settles. Nothing retries in the Harness.
StorageRejected is removed; StorageRequestError marks invalid reads that fail only their call. Listener and onReport
errors are contained and reported.

Also updates scripts/issues/triage.ts and the vacation experiment for the content -> output rename.
This commit is contained in:
Mario Zechner 2026-10-09 17:12:05 +00:00
parent 5a10492690
commit eba8497395
70 changed files with 6137 additions and 671 deletions

View file

@ -290,7 +290,7 @@ export class Triage {
replay: "safe",
execute: async (args, api) => {
const hits = corpus.search(args.query, issueFor(api.conversationId));
return { content: [{ type: "text", text: hits.length ? hits.map(line).join("\n") : "No matches." }] };
return { output: [{ type: "text", text: hits.length ? hits.map(line).join("\n") : "No matches." }] };
},
});
@ -301,9 +301,9 @@ export class Triage {
replay: "safe",
execute: async (args) => {
const issue = corpus.get(args.number);
if (!issue) return { content: [{ type: "text", text: `#${args.number} is not in the local corpus.` }] };
if (!issue) return { output: [{ type: "text", text: `#${args.number} is not in the local corpus.` }] };
const text = `#${issue.number} [${issueStatus(issue)}] ${issue.title}\nLabels: ${issue.labels.join(", ") || "none"}. Created ${issue.createdAt}.\n\n${issue.body.slice(0, 8000)}`;
return { content: [{ type: "text", text }] };
return { output: [{ type: "text", text }] };
},
});
@ -329,7 +329,7 @@ export class Triage {
replay: "safe",
execute: async (args, api, callContext) => {
const number = issueFor(api.conversationId);
if (number < 0) return { content: [{ type: "text", text: "Unknown conversation." }], isError: true };
if (number < 0) return { output: [{ type: "text", text: "Unknown conversation." }], isError: true };
const result: TriageResult = {
...args,
duplicates: args.duplicates.filter((dup) => dup.number !== number).slice(0, 5),
@ -344,7 +344,7 @@ export class Triage {
}),
callContext,
);
return { content: [{ type: "text", text: "Recorded." }], control: { terminate: true } };
return { output: [{ type: "text", text: "Recorded." }], control: { terminate: true } };
},
});