@diousk/pi-subagents-fast 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/CHANGELOG.md +808 -0
  2. package/CONTRIBUTING.md +72 -0
  3. package/LICENSE +21 -0
  4. package/README.md +1034 -0
  5. package/SECURITY.md +95 -0
  6. package/dist/abortable.d.ts +12 -0
  7. package/dist/abortable.js +42 -0
  8. package/dist/agent-color.d.ts +35 -0
  9. package/dist/agent-color.js +123 -0
  10. package/dist/agent-file-toggle.d.ts +125 -0
  11. package/dist/agent-file-toggle.js +260 -0
  12. package/dist/agent-manager.d.ts +472 -0
  13. package/dist/agent-manager.js +1338 -0
  14. package/dist/agent-runner.d.ts +312 -0
  15. package/dist/agent-runner.js +1034 -0
  16. package/dist/agent-types.d.ts +119 -0
  17. package/dist/agent-types.js +286 -0
  18. package/dist/child-context.d.ts +2 -0
  19. package/dist/child-context.js +12 -0
  20. package/dist/context.d.ts +12 -0
  21. package/dist/context.js +56 -0
  22. package/dist/cross-extension-rpc.d.ts +66 -0
  23. package/dist/cross-extension-rpc.js +138 -0
  24. package/dist/custom-agents.d.ts +54 -0
  25. package/dist/custom-agents.js +316 -0
  26. package/dist/default-agents.d.ts +7 -0
  27. package/dist/default-agents.js +122 -0
  28. package/dist/enabled-models.d.ts +49 -0
  29. package/dist/enabled-models.js +145 -0
  30. package/dist/env.d.ts +6 -0
  31. package/dist/env.js +28 -0
  32. package/dist/group-join.d.ts +32 -0
  33. package/dist/group-join.js +116 -0
  34. package/dist/index.d.ts +50 -0
  35. package/dist/index.js +3682 -0
  36. package/dist/invocation-config.d.ts +107 -0
  37. package/dist/invocation-config.js +83 -0
  38. package/dist/memory.d.ts +53 -0
  39. package/dist/memory.js +165 -0
  40. package/dist/mention-clone.d.ts +87 -0
  41. package/dist/mention-clone.js +153 -0
  42. package/dist/mention.d.ts +81 -0
  43. package/dist/mention.js +131 -0
  44. package/dist/model-resolver.d.ts +36 -0
  45. package/dist/model-resolver.js +95 -0
  46. package/dist/model-scope.d.ts +49 -0
  47. package/dist/model-scope.js +48 -0
  48. package/dist/nested-tools.d.ts +55 -0
  49. package/dist/nested-tools.js +299 -0
  50. package/dist/output-file.d.ts +43 -0
  51. package/dist/output-file.js +142 -0
  52. package/dist/prompts.d.ts +55 -0
  53. package/dist/prompts.js +91 -0
  54. package/dist/schedule-store.d.ts +38 -0
  55. package/dist/schedule-store.js +155 -0
  56. package/dist/schedule.d.ts +109 -0
  57. package/dist/schedule.js +359 -0
  58. package/dist/settings.d.ts +360 -0
  59. package/dist/settings.js +251 -0
  60. package/dist/skill-loader.d.ts +24 -0
  61. package/dist/skill-loader.js +93 -0
  62. package/dist/status-note.d.ts +61 -0
  63. package/dist/status-note.js +85 -0
  64. package/dist/structured-output.d.ts +61 -0
  65. package/dist/structured-output.js +112 -0
  66. package/dist/types.d.ts +371 -0
  67. package/dist/types.js +5 -0
  68. package/dist/ui/agent-mention.d.ts +82 -0
  69. package/dist/ui/agent-mention.js +187 -0
  70. package/dist/ui/agent-widget.d.ts +219 -0
  71. package/dist/ui/agent-widget.js +592 -0
  72. package/dist/ui/conversation-viewer.d.ts +120 -0
  73. package/dist/ui/conversation-viewer.js +578 -0
  74. package/dist/ui/fleet-list.d.ts +195 -0
  75. package/dist/ui/fleet-list.js +471 -0
  76. package/dist/ui/schedule-menu.d.ts +16 -0
  77. package/dist/ui/schedule-menu.js +94 -0
  78. package/dist/ui/select-item.d.ts +27 -0
  79. package/dist/ui/select-item.js +34 -0
  80. package/dist/ui/viewer-keys.d.ts +20 -0
  81. package/dist/ui/viewer-keys.js +17 -0
  82. package/dist/ui/workflow-card.d.ts +175 -0
  83. package/dist/ui/workflow-card.js +332 -0
  84. package/dist/ui/workflow-dialog.d.ts +305 -0
  85. package/dist/ui/workflow-dialog.js +843 -0
  86. package/dist/ui/workflow-menu.d.ts +60 -0
  87. package/dist/ui/workflow-menu.js +147 -0
  88. package/dist/usage.d.ts +135 -0
  89. package/dist/usage.js +120 -0
  90. package/dist/workflow/collisions.d.ts +95 -0
  91. package/dist/workflow/collisions.js +88 -0
  92. package/dist/workflow/entry.d.ts +32 -0
  93. package/dist/workflow/entry.js +29 -0
  94. package/dist/workflow/host.d.ts +62 -0
  95. package/dist/workflow/host.js +362 -0
  96. package/dist/workflow/journal.d.ts +97 -0
  97. package/dist/workflow/journal.js +120 -0
  98. package/dist/workflow/json-schema.d.ts +51 -0
  99. package/dist/workflow/json-schema.js +111 -0
  100. package/dist/workflow/meta.d.ts +67 -0
  101. package/dist/workflow/meta.js +317 -0
  102. package/dist/workflow/progress.d.ts +224 -0
  103. package/dist/workflow/progress.js +361 -0
  104. package/dist/workflow/runtime.d.ts +334 -0
  105. package/dist/workflow/runtime.js +830 -0
  106. package/dist/workflow/saved.d.ts +90 -0
  107. package/dist/workflow/saved.js +203 -0
  108. package/dist/workflow/task.d.ts +136 -0
  109. package/dist/workflow/task.js +207 -0
  110. package/dist/workflow/tool-description.d.ts +38 -0
  111. package/dist/workflow/tool-description.js +199 -0
  112. package/dist/workflow/worker-source.d.ts +47 -0
  113. package/dist/workflow/worker-source.js +778 -0
  114. package/dist/worktree.d.ts +52 -0
  115. package/dist/worktree.js +164 -0
  116. package/dist/xml.d.ts +10 -0
  117. package/dist/xml.js +12 -0
  118. package/docs/rpc.md +183 -0
  119. package/docs/workflows.md +437 -0
  120. package/examples/agent-tool-description.md +42 -0
  121. package/examples/workflows/compose.js +51 -0
  122. package/examples/workflows/fan-out-audit.js +47 -0
  123. package/examples/workflows/gated-fix.js +60 -0
  124. package/examples/workflows/lib/count-child.js +27 -0
  125. package/examples/workflows/review-panel.js +63 -0
  126. package/examples/workflows/structured-findings.js +78 -0
  127. package/package.json +68 -0
  128. package/src/abortable.ts +43 -0
  129. package/src/agent-color.ts +161 -0
  130. package/src/agent-file-toggle.ts +270 -0
  131. package/src/agent-manager.ts +1581 -0
  132. package/src/agent-runner.ts +1286 -0
  133. package/src/agent-types.ts +346 -0
  134. package/src/child-context.ts +15 -0
  135. package/src/context.ts +58 -0
  136. package/src/cross-extension-rpc.ts +198 -0
  137. package/src/custom-agents.ts +333 -0
  138. package/src/default-agents.ts +126 -0
  139. package/src/enabled-models.ts +180 -0
  140. package/src/env.ts +33 -0
  141. package/src/group-join.ts +141 -0
  142. package/src/index.ts +3991 -0
  143. package/src/invocation-config.ts +155 -0
  144. package/src/memory.ts +179 -0
  145. package/src/mention-clone.ts +196 -0
  146. package/src/mention.ts +141 -0
  147. package/src/model-resolver.ts +118 -0
  148. package/src/model-scope.ts +70 -0
  149. package/src/nested-tools.ts +422 -0
  150. package/src/output-file.ts +155 -0
  151. package/src/prompts.ts +142 -0
  152. package/src/schedule-store.ts +153 -0
  153. package/src/schedule.ts +386 -0
  154. package/src/settings.ts +587 -0
  155. package/src/skill-loader.ts +102 -0
  156. package/src/status-note.ts +90 -0
  157. package/src/structured-output.ts +130 -0
  158. package/src/types.ts +384 -0
  159. package/src/ui/agent-mention.ts +216 -0
  160. package/src/ui/agent-widget.ts +664 -0
  161. package/src/ui/conversation-viewer.ts +589 -0
  162. package/src/ui/fleet-list.ts +543 -0
  163. package/src/ui/schedule-menu.ts +105 -0
  164. package/src/ui/select-item.ts +45 -0
  165. package/src/ui/viewer-keys.ts +39 -0
  166. package/src/ui/workflow-card.ts +470 -0
  167. package/src/ui/workflow-dialog.ts +1115 -0
  168. package/src/ui/workflow-menu.ts +193 -0
  169. package/src/usage.ts +167 -0
  170. package/src/workflow/collisions.ts +123 -0
  171. package/src/workflow/entry.ts +47 -0
  172. package/src/workflow/host.ts +403 -0
  173. package/src/workflow/journal.ts +164 -0
  174. package/src/workflow/json-schema.ts +128 -0
  175. package/src/workflow/meta.ts +325 -0
  176. package/src/workflow/progress.ts +550 -0
  177. package/src/workflow/runtime.ts +1219 -0
  178. package/src/workflow/saved.ts +217 -0
  179. package/src/workflow/task.ts +302 -0
  180. package/src/workflow/tool-description.ts +200 -0
  181. package/src/workflow/worker-source.ts +781 -0
  182. package/src/worktree.ts +205 -0
  183. package/src/xml.ts +13 -0
@@ -0,0 +1,102 @@
1
+ /**
2
+ * skill-loader.ts — Preload named skills.
3
+ *
4
+ * Roots, in precedence order:
5
+ * - <cwd>/.pi/skills (project, Pi's standard)
6
+ * - <cwd>/.agents/skills (project, cross-tool Agent Skills spec — https://agentskills.io)
7
+ * - getAgentDir()/skills (user, default ~/.pi/agent/skills — Pi's standard)
8
+ * - ~/.agents/skills (user, cross-tool Agent Skills spec)
9
+ * - ~/.pi/skills (legacy global, pre-Pi)
10
+ *
11
+ * Layout per root:
12
+ * - <root>/<name>.md (flat file at the top level)
13
+ * - <root>/.../<name>/SKILL.md (directory skill, may be nested — Pi's standard)
14
+ *
15
+ * Recursion skips dotfile entries and node_modules. A directory that itself contains
16
+ * SKILL.md is a skill — we don't descend into it (Pi: skills don't nest).
17
+ *
18
+ * Symlinks are rejected for security (deviation from Pi, which follows them).
19
+ */
20
+
21
+ import type { Dirent } from "node:fs";
22
+ import { existsSync, readdirSync } from "node:fs";
23
+ import { homedir } from "node:os";
24
+ import { join } from "node:path";
25
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
26
+ import { isSymlink, isUnsafeName, safeReadFile } from "./memory.js";
27
+
28
+ export interface PreloadedSkill {
29
+ name: string;
30
+ content: string;
31
+ }
32
+
33
+ export function preloadSkills(skillNames: string[], cwd: string): PreloadedSkill[] {
34
+ return skillNames.map((name) => ({ name, content: loadSkillContent(name, cwd) }));
35
+ }
36
+
37
+ function loadSkillContent(name: string, cwd: string): string {
38
+ if (isUnsafeName(name)) {
39
+ return `(Skill "${name}" skipped: name contains path traversal characters)`;
40
+ }
41
+ const roots = [
42
+ join(cwd, ".pi", "skills"), // project — Pi standard
43
+ join(cwd, ".agents", "skills"), // project — Agent Skills spec
44
+ join(getAgentDir(), "skills"), // user — Pi standard
45
+ join(homedir(), ".agents", "skills"), // user — Agent Skills spec
46
+ join(homedir(), ".pi", "skills"), // legacy global, pre-Pi
47
+ ];
48
+ for (const root of roots) {
49
+ const content = findInRoot(root, name);
50
+ if (content !== undefined) return content;
51
+ }
52
+ return `(Skill "${name}" not found in .pi/skills/, .agents/skills/, or global skill locations)`;
53
+ }
54
+
55
+ function findInRoot(root: string, name: string): string | undefined {
56
+ if (isSymlink(root)) return undefined; // reject symlinked roots entirely
57
+ const flat = safeReadFile(join(root, `${name}.md`))?.trim();
58
+ if (flat !== undefined) return flat;
59
+ return findSkillDirectory(root, name);
60
+ }
61
+
62
+ /** BFS under `root` for a directory named `name` containing `SKILL.md`. Pi-conforming filters. */
63
+ function findSkillDirectory(root: string, name: string): string | undefined {
64
+ if (!existsSync(root)) return undefined;
65
+ const queue: string[] = [root];
66
+
67
+ while (queue.length > 0) {
68
+ const current = queue.shift();
69
+ if (current === undefined) continue;
70
+
71
+ let entries: Dirent<string>[];
72
+ try {
73
+ entries = readdirSync(current, { withFileTypes: true });
74
+ } catch {
75
+ continue;
76
+ }
77
+
78
+ // Deterministic byte-order traversal — locale-independent.
79
+ entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
80
+
81
+ for (const entry of entries) {
82
+ if (!entry.isDirectory()) continue;
83
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
84
+
85
+ // Symlinked dirs already filtered by entry.isDirectory() — Dirent uses lstat semantics.
86
+ const path = join(current, entry.name);
87
+ const skillMd = join(path, "SKILL.md");
88
+ const isSkillDir = existsSync(skillMd);
89
+
90
+ if (isSkillDir) {
91
+ if (entry.name === name) {
92
+ const content = safeReadFile(skillMd)?.trim();
93
+ if (content !== undefined) return content;
94
+ }
95
+ continue; // Pi rule: skills don't nest — don't descend into a skill dir
96
+ }
97
+
98
+ queue.push(path);
99
+ }
100
+ }
101
+ return undefined;
102
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * status-note.ts — Honest framing for an agent result: the parenthetical status
3
+ * note for a non-normal outcome, and the salvaged partial output of a failure.
4
+ *
5
+ * Lives here rather than in an index.ts closure because both entry points need
6
+ * it — the top-level tools and the nested delegation tools, which can't import
7
+ * from index.ts (that is the extension entry, and it already reaches these tools
8
+ * through agent-runner).
9
+ */
10
+
11
+ import type { AgentRecord } from "./types.js";
12
+
13
+ /**
14
+ * Explicit parenthetical note for a non-normal terminal outcome, so the parent
15
+ * agent can't mistake partial output for a completed result. Empty string for a
16
+ * clean completion (and any unknown/non-terminal status).
17
+ *
18
+ * `stopped` (a human aborted it) is deliberately distinct from `aborted` (the
19
+ * turn limit was hit) — the parent should treat human intervention differently
20
+ * from a budget cutoff.
21
+ */
22
+ export function getStatusNote(status: string): string {
23
+ switch (status) {
24
+ case "stopped":
25
+ return " (STOPPED BY THE USER before completion — output is partial; the task was NOT finished)";
26
+ case "aborted":
27
+ return " (aborted — hit the turn limit before completion; output may be incomplete)";
28
+ case "steered":
29
+ return " (wrapped up at the turn limit — output may be partial)";
30
+ default:
31
+ return "";
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Foreground variant of `getStatusNote`. A foreground caller is in a different
37
+ * position from a background one, so it needs different text:
38
+ *
39
+ * - It already holds the agent's ENTIRE output inline, whereas the background
40
+ * notification carries a 500-char preview. So only here can we truthfully
41
+ * say there is nothing more to fetch — which is the whole point, because
42
+ * - it has no agent id. The id travels in the tool result's renderer
43
+ * `details`, which is never serialized to the model. A parent that reads
44
+ * "output may be partial" as "truncated, go retrieve the rest" therefore
45
+ * has nothing valid to call `get_subagent_result` with, and will invent an
46
+ * id (#174).
47
+ *
48
+ * Only the lead clause varies between the three, and each variation carries
49
+ * information: `wrapped up` vs `aborted` tells the parent whether the output is
50
+ * a considered final answer or a fragment, and `stopped` shouts because a human
51
+ * intervening outranks everything else in the string. Only `steered` hedges on
52
+ * completion — it was told to wrap up and did, so it may well have finished at
53
+ * the limit; an aborted run blew through its grace turns while still working,
54
+ * and `stopped` can only fire on a running agent, so neither ever delivered a
55
+ * final answer. Identical confidence gets identical wording: phrasing one fact
56
+ * two ways invites a hunt for a distinction that isn't there.
57
+ *
58
+ * Every clause is a statement about state, never an instruction to act, and
59
+ * `get_subagent_result` is never named — naming the tool we steer away from only
60
+ * raises its salience. Two instructions were tried here and cut: "re-spawn with
61
+ * a higher max_turns" (pushes a fresh multi-minute run to save one wasted tool
62
+ * call) and, on `stopped`, "ask before restarting it" (restates the lead, and
63
+ * presumes someone is present to ask — false under `pi -p`, in scheduled jobs,
64
+ * and in any background-driven run). Nothing here can measure whether wording
65
+ * improves parent behavior, so removing a false cue (which cannot induce new
66
+ * behavior) and adding an instruction (which can) are not equally safe bets.
67
+ * Don't add either back without a way to measure it.
68
+ */
69
+ export function getForegroundOutcomeNote(status: string): string {
70
+ switch (status) {
71
+ case "stopped":
72
+ return " (STOPPED BY THE USER — everything the agent produced is above; the task is unfinished)";
73
+ case "aborted":
74
+ return " (aborted at the turn limit — everything the agent produced is above; the task is unfinished)";
75
+ case "steered":
76
+ return " (wrapped up at the turn limit — everything the agent produced is above; the task may be unfinished)";
77
+ default:
78
+ return "";
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Salvaged partial output of a failed run, as a labeled suffix for the error
84
+ * surfaces (or "" if the run produced nothing). `record.result` is bounded to
85
+ * the run's own turns, so this is never a stale earlier answer (#144).
86
+ */
87
+ export function partialOutputSuffix(record: AgentRecord): string {
88
+ const partial = record.result?.trim();
89
+ return partial ? `\n\nPartial output before the failure:\n${partial}` : "";
90
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * structured-output.ts — the synthetic tool behind `agent(prompt, { schema })`.
3
+ *
4
+ * A workflow script that passes a `schema` wants an *object* back, not prose it
5
+ * has to parse. Claude Code does this by giving the child a `StructuredOutput`
6
+ * tool whose input schema is the caller's schema, so the provider fills the
7
+ * fields, and returning the validated payload as the agent's result.
8
+ *
9
+ * We do the same, with one gap named up front: Claude Code *forces* the call,
10
+ * and we cannot. `toolChoice` exists in pi-ai's provider layer but is not
11
+ * plumbed through `AgentSession`, so an extension has no way to require a
12
+ * particular tool. What we have instead is three softer pressures —
13
+ *
14
+ * 1. `constrainedSampling`, so providers that support it hold the payload to
15
+ * the schema at sampling time;
16
+ * 2. the tool's description, snippet and guideline, which say the answer must
17
+ * come through this call;
18
+ * 3. validation here, answering a bad payload with `isError` so the model
19
+ * sees what was wrong and calls again inside the same run.
20
+ *
21
+ * — and, when all three fail, one more prompt from `runAgent`. See
22
+ * {@link structuredRetryPrompt}.
23
+ *
24
+ * The name matches Claude Code's exactly, so a ported prompt that mentions
25
+ * `StructuredOutput` is still telling the truth.
26
+ */
27
+
28
+ import { defineTool, type ToolDefinition } from "@earendil-works/pi-coding-agent";
29
+ import type { CompiledSchema } from "./workflow/json-schema.js";
30
+
31
+ /**
32
+ * Deliberately NOT added to `SUBAGENT_TOOL_NAMES`: that list becomes
33
+ * `EXCLUDED_TOOL_NAMES`, which is exactly the denial this tool has to avoid.
34
+ * Nor to `BUILTIN_TOOL_NAMES` — it is ours to inject, never a name a user may
35
+ * ask for in an agent's `tools:` frontmatter.
36
+ */
37
+ export const STRUCTURED_OUTPUT_TOOL_NAME = "StructuredOutput";
38
+
39
+ /** What the child produced, filled in as the tool is called. */
40
+ export interface StructuredCapture {
41
+ /** The last payload that validated, canonicalised. Absent until one does. */
42
+ json?: string;
43
+ /** Why the most recent attempt was rejected, for the retry prompt. */
44
+ lastError?: string;
45
+ /** Whether the tool was called at all — "never tried" reads differently. */
46
+ called: boolean;
47
+ }
48
+
49
+ export function createStructuredCapture(): StructuredCapture {
50
+ return { called: false };
51
+ }
52
+
53
+ /**
54
+ * Build the tool for one child.
55
+ *
56
+ * `capture` is the box the caller reads afterwards. It is passed in rather than
57
+ * returned so `runAgent` owns its lifetime and can consult it on every exit
58
+ * path, including the ones where the tool was never reached.
59
+ */
60
+ export function createStructuredOutputTool(
61
+ compiled: CompiledSchema,
62
+ capture: StructuredCapture,
63
+ ): ToolDefinition {
64
+ return defineTool({
65
+ name: STRUCTURED_OUTPUT_TOOL_NAME,
66
+ label: "Structured Output",
67
+ description:
68
+ "Report your final answer. Call this exactly once, with the complete result, and put everything the "
69
+ + "caller needs inside the arguments — text written outside this call is discarded. If a call is "
70
+ + "rejected for not matching the schema, fix the reported fields and call it again.",
71
+ promptSnippet: "Report your final answer as structured data",
72
+ promptGuidelines: [
73
+ "Your final answer MUST be reported by calling StructuredOutput. Prose outside that call is discarded.",
74
+ ],
75
+ // The caller's schema *is* the tool's input schema, verbatim — that is what
76
+ // makes the provider fill the fields. pi types this as TypeBox's `TSchema`,
77
+ // which v1 defines as an open interface, so a plain JSON Schema satisfies
78
+ // it without a cast at runtime or a conversion at author time.
79
+ parameters: compiled.schema as never,
80
+ // "prefer", not "require": a provider that cannot constrain sampling should
81
+ // fall through to validation-and-retry rather than fail the call outright.
82
+ constrainedSampling: { type: "json_schema", strict: "prefer" },
83
+ // Models occasionally send the whole payload as one JSON string instead of
84
+ // an object. Recovering that costs nothing and saves a whole retry.
85
+ prepareArguments: (args: unknown) => {
86
+ if (typeof args !== "string") return args as never;
87
+ try {
88
+ return JSON.parse(args) as never;
89
+ } catch {
90
+ return args as never;
91
+ }
92
+ },
93
+ execute: async (_toolCallId, params) => {
94
+ capture.called = true;
95
+ const verdict = compiled.check(params);
96
+ if (verdict !== true) {
97
+ capture.lastError = verdict;
98
+ // `isError` puts the reason in front of the model as a tool result, so
99
+ // it can correct itself inside this same run. This is where most
100
+ // mismatches are resolved; the prompt-level retry is the backstop.
101
+ return {
102
+ content: [{
103
+ type: "text",
104
+ text: `StructuredOutput did not match the required schema:\n${verdict}\nCall it again with a corrected value.`,
105
+ }],
106
+ isError: true,
107
+ details: {},
108
+ };
109
+ }
110
+ // Last valid call wins: a model that calls twice meant the second one.
111
+ capture.json = JSON.stringify(params);
112
+ capture.lastError = undefined;
113
+ return { content: [{ type: "text", text: "Recorded." }], details: {} };
114
+ },
115
+ }) as ToolDefinition;
116
+ }
117
+
118
+ /**
119
+ * The one extra prompt sent when a run ended with nothing captured.
120
+ *
121
+ * Distinguishes "never called it" from "called it wrongly" — the two need
122
+ * different corrections, and telling a model it got the shape wrong when it
123
+ * never answered at all sends it looking for a mistake it did not make.
124
+ */
125
+ export function structuredRetryPrompt(capture: StructuredCapture): string {
126
+ const reason = capture.called && capture.lastError !== undefined
127
+ ? `Your last ${STRUCTURED_OUTPUT_TOOL_NAME} call did not match the required schema: ${capture.lastError}`
128
+ : `You did not call ${STRUCTURED_OUTPUT_TOOL_NAME}, so your answer was not recorded.`;
129
+ return `${reason}\n\nCall ${STRUCTURED_OUTPUT_TOOL_NAME} now with your complete final answer. Do not reply with prose.`;
130
+ }