@mono-agent/agent-runtime 0.4.0 → 0.4.1

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 (119) hide show
  1. package/ARCHITECTURE.md +37 -16
  2. package/MIGRATION.md +185 -7
  3. package/README.md +24 -14
  4. package/package.json +103 -10
  5. package/src/agent/allowlists.js +3 -0
  6. package/src/agent/approval.js +3 -0
  7. package/src/agent/compaction.js +244 -1
  8. package/src/agent/prompt/skill-index.js +6 -0
  9. package/src/agent/sandbox-seam.js +227 -0
  10. package/src/agent/tool-bloat.js +8 -2
  11. package/src/agent/tools/bash.js +16 -8
  12. package/src/agent/tools/edit.js +7 -3
  13. package/src/agent/tools/glob.js +11 -5
  14. package/src/agent/tools/grep.js +11 -5
  15. package/src/agent/tools/pi-bridge.js +227 -45
  16. package/src/agent/tools/read.js +28 -3
  17. package/src/agent/tools/shared/output-truncation.js +8 -3
  18. package/src/agent/tools/shared/path-resolver.js +24 -18
  19. package/src/agent/tools/shared/ripgrep.js +33 -6
  20. package/src/agent/tools/shared/runtime-context.js +60 -50
  21. package/src/agent/tools/shared/tool-context.js +157 -0
  22. package/src/agent/tools/web-fetch.js +21 -10
  23. package/src/agent/tools/web-search.js +11 -4
  24. package/src/agent/tools/write.js +7 -3
  25. package/src/agent/transcript.js +16 -1
  26. package/src/ai/cost.js +128 -3
  27. package/src/ai/failure.js +96 -4
  28. package/src/ai/live-input-prompt.js +11 -1
  29. package/src/ai/providers/claude-cli.js +2 -2
  30. package/src/ai/providers/claude-sdk.js +55 -12
  31. package/src/ai/providers/codex-app.js +36 -23
  32. package/src/ai/providers/opencode-app.js +2 -2
  33. package/src/ai/providers/opencode-discovery.js +2 -2
  34. package/src/ai/providers/pi-events.js +5 -0
  35. package/src/ai/providers/pi-models.js +21 -4
  36. package/src/ai/providers/pi-native/compaction-driver.js +394 -0
  37. package/src/ai/providers/pi-native/result-builder.js +312 -0
  38. package/src/ai/providers/pi-native/session-lifecycle.js +438 -0
  39. package/src/ai/providers/pi-native/stream-subscriber.js +148 -0
  40. package/src/ai/providers/pi-native/structured-output.js +130 -0
  41. package/src/ai/providers/pi-native/turn-runner.js +278 -0
  42. package/src/ai/providers/pi-native.js +415 -1106
  43. package/src/ai/runtime/model-refs.js +30 -0
  44. package/src/ai/runtime/registry.js +29 -0
  45. package/src/ai/runtime/router.js +96 -12
  46. package/src/ai/runtime/session-liveness.js +110 -0
  47. package/src/ai/runtime/sessions.js +16 -0
  48. package/src/ai/types.js +252 -19
  49. package/src/index.js +1 -1
  50. package/src/pi-auth.js +147 -16
  51. package/src/runtime-brand.js +21 -1
  52. package/src/runtime.js +72 -8
  53. package/types/agent/allowlists.d.ts +25 -0
  54. package/types/agent/approval.d.ts +30 -0
  55. package/types/agent/compaction.d.ts +97 -0
  56. package/types/agent/index.d.ts +5 -0
  57. package/types/agent/prompt/skill-index.d.ts +19 -0
  58. package/types/agent/sandbox-seam.d.ts +148 -0
  59. package/types/agent/tool-bloat.d.ts +22 -0
  60. package/types/agent/tools/bash.d.ts +16 -0
  61. package/types/agent/tools/edit.d.ts +14 -0
  62. package/types/agent/tools/glob.d.ts +16 -0
  63. package/types/agent/tools/grep.d.ts +22 -0
  64. package/types/agent/tools/index.d.ts +10 -0
  65. package/types/agent/tools/pi-bridge.d.ts +144 -0
  66. package/types/agent/tools/read.d.ts +19 -0
  67. package/types/agent/tools/shared/constants.d.ts +13 -0
  68. package/types/agent/tools/shared/dedup.d.ts +8 -0
  69. package/types/agent/tools/shared/output-truncation.d.ts +22 -0
  70. package/types/agent/tools/shared/path-resolver.d.ts +6 -0
  71. package/types/agent/tools/shared/ripgrep.d.ts +38 -0
  72. package/types/agent/tools/shared/runtime-context.d.ts +27 -0
  73. package/types/agent/tools/shared/tool-context.d.ts +48 -0
  74. package/types/agent/tools/web-fetch.d.ts +13 -0
  75. package/types/agent/tools/web-search.d.ts +11 -0
  76. package/types/agent/tools/write.d.ts +12 -0
  77. package/types/agent/transcript.d.ts +45 -0
  78. package/types/ai/backend.d.ts +41 -0
  79. package/types/ai/cost.d.ts +96 -0
  80. package/types/ai/failure.d.ts +117 -0
  81. package/types/ai/file-change-stats.d.ts +75 -0
  82. package/types/ai/index.d.ts +7 -0
  83. package/types/ai/live-input-prompt.d.ts +6 -0
  84. package/types/ai/observer.d.ts +68 -0
  85. package/types/ai/providers/claude-cli.d.ts +211 -0
  86. package/types/ai/providers/claude-sdk.d.ts +66 -0
  87. package/types/ai/providers/claude-subagents.d.ts +2 -0
  88. package/types/ai/providers/codex-app.d.ts +151 -0
  89. package/types/ai/providers/opencode-app.d.ts +95 -0
  90. package/types/ai/providers/opencode-discovery.d.ts +4 -0
  91. package/types/ai/providers/pi-errors.d.ts +3 -0
  92. package/types/ai/providers/pi-events.d.ts +24 -0
  93. package/types/ai/providers/pi-messages.d.ts +5 -0
  94. package/types/ai/providers/pi-models.d.ts +57 -0
  95. package/types/ai/providers/pi-native/compaction-driver.d.ts +86 -0
  96. package/types/ai/providers/pi-native/result-builder.d.ts +168 -0
  97. package/types/ai/providers/pi-native/session-lifecycle.d.ts +62 -0
  98. package/types/ai/providers/pi-native/stream-subscriber.d.ts +50 -0
  99. package/types/ai/providers/pi-native/structured-output.d.ts +69 -0
  100. package/types/ai/providers/pi-native/turn-runner.d.ts +60 -0
  101. package/types/ai/providers/pi-native.d.ts +18 -0
  102. package/types/ai/registry.d.ts +1 -0
  103. package/types/ai/runtime/capabilities-used.d.ts +21 -0
  104. package/types/ai/runtime/capabilities.d.ts +33 -0
  105. package/types/ai/runtime/context-windows.d.ts +8 -0
  106. package/types/ai/runtime/fast-mode.d.ts +2 -0
  107. package/types/ai/runtime/model-refs.d.ts +35 -0
  108. package/types/ai/runtime/registry.d.ts +38 -0
  109. package/types/ai/runtime/router.d.ts +56 -0
  110. package/types/ai/runtime/session-liveness.d.ts +55 -0
  111. package/types/ai/runtime/sessions.d.ts +38 -0
  112. package/types/ai/streaming/codex-events.d.ts +30 -0
  113. package/types/ai/streaming/opencode-events.d.ts +41 -0
  114. package/types/ai/types.d.ts +702 -0
  115. package/types/index.d.ts +7 -0
  116. package/types/pi-auth.d.ts +28 -0
  117. package/types/runtime-brand.d.ts +30 -0
  118. package/types/runtime.d.ts +10 -0
  119. package/src/ai/providers/pi-sdk.js +0 -35
@@ -0,0 +1,130 @@
1
+ // @ts-check
2
+ // Structured-output enforcement for the pi-native bridge.
3
+ //
4
+ // Pure moves out of pi-native.js: the system-prompt instruction append, the
5
+ // finalization re-prompt text, the retry predicate, the retry diagnostics, and
6
+ // the single-re-prompt finalization executor. No session or run state lives
7
+ // here — the executor takes its harness/tool/warnings deps explicitly and the
8
+ // caller owns the `structuredResult` closure the tool populates.
9
+
10
+ /**
11
+ * Append the StructuredOutput usage instruction to the system prompt when an
12
+ * output schema is active. No schema → the prompt is returned unchanged. A host
13
+ * or run `prompts.structuredOutputInstruction(systemPrompt)` override, when
14
+ * supplied, replaces the default instruction text (it receives the raw system
15
+ * prompt and returns the augmented one); otherwise the default below is used.
16
+ * @param {string} systemPrompt
17
+ * @param {unknown} outputSchema
18
+ * @param {import('../../types.js').RuntimePromptOverrides} [prompts]
19
+ * @returns {string}
20
+ */
21
+ export function appendStructuredOutputInstruction(systemPrompt, outputSchema, prompts) {
22
+ if (!outputSchema) return systemPrompt;
23
+ if (typeof prompts?.structuredOutputInstruction === "function") {
24
+ return prompts.structuredOutputInstruction(systemPrompt);
25
+ }
26
+ return [
27
+ systemPrompt,
28
+ "",
29
+ "Structured output is available through the `StructuredOutput` tool.",
30
+ "When the final result is ready, call `StructuredOutput` with the complete JSON object matching the requested schema.",
31
+ "Do not also print the same JSON as prose unless tool calling is unavailable.",
32
+ ].join("\n");
33
+ }
34
+
35
+ /**
36
+ * The finalization re-prompt issued when a turn ended without submitting the
37
+ * required structured result. A `prompts.structuredOutputFinalization()`
38
+ * override, when supplied, replaces the default text.
39
+ * @param {import('../../types.js').RuntimePromptOverrides} [prompts]
40
+ * @returns {string}
41
+ */
42
+ export function structuredOutputFinalizationPrompt(prompts) {
43
+ if (typeof prompts?.structuredOutputFinalization === "function") {
44
+ return prompts.structuredOutputFinalization();
45
+ }
46
+ return [
47
+ "The previous assistant turn ended without submitting the required structured result.",
48
+ "Do not run tools, inspect files, or redo work.",
49
+ "Call only `StructuredOutput` once with the final object matching the requested schema, based on the completed transcript above.",
50
+ "Do not print prose before or after the tool call.",
51
+ ].join("\n");
52
+ }
53
+
54
+ /**
55
+ * Whether the run should re-prompt once for structured-output finalization: a
56
+ * schema is active, no structured result was produced, the turn ended with no
57
+ * text, the run was not aborted / did not hit max turns, and the stop reason is
58
+ * not an error/abort.
59
+ * @param {{outputSchema: unknown, structuredResult: unknown, finalText: unknown, stopReason: unknown, externalAbort: boolean, maxTurnsHit: boolean}} params
60
+ * @returns {boolean}
61
+ */
62
+ export function shouldRetryStructuredOutputFinalization({
63
+ outputSchema,
64
+ structuredResult,
65
+ finalText,
66
+ stopReason,
67
+ externalAbort,
68
+ maxTurnsHit,
69
+ }) {
70
+ if (!outputSchema) return false;
71
+ if (structuredResult !== null && structuredResult !== undefined) return false;
72
+ if (String(finalText || "").trim()) return false;
73
+ if (externalAbort || maxTurnsHit) return false;
74
+ return stopReason !== "error" && stopReason !== "aborted";
75
+ }
76
+
77
+ /**
78
+ * The structured-output finalization diagnostics spread into the run's
79
+ * diagnostics/errorDetails. Empty when no retry was attempted.
80
+ * @param {number} attempts
81
+ * @param {string|null} reason
82
+ * @param {boolean} failed
83
+ * @returns {Record<string, unknown>}
84
+ */
85
+ export function structuredOutputRetryDiagnostics(attempts, reason, failed) {
86
+ if (!attempts) return {};
87
+ return {
88
+ structured_output_finalization_retry_attempts: attempts,
89
+ structured_output_finalization_retry_reason: reason,
90
+ structured_output_finalization_retry_failed: !!failed,
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Run the single structured-output finalization re-prompt in the same session.
96
+ *
97
+ * The harness is idle after waitForIdle(), so re-prompt (not followUp, which
98
+ * only queues onto an active run) with only StructuredOutput active. This
99
+ * re-prompts ONCE in the same session, matching the legacy single
100
+ * agent.continue() finalization re-prompt. Tools are restored in finally.
101
+ *
102
+ * Returns the retry bookkeeping ({attempts, reason}); the caller computes
103
+ * `failed` from the (closure-owned) structuredResult after re-capturing state.
104
+ * @param {{harness: any, structuredTool: {name: string}|null, runtimeWarnings: Array<Record<string, unknown>>, prompts?: import('../../types.js').RuntimePromptOverrides}} deps
105
+ * @returns {Promise<{attempts: number, reason: string}>}
106
+ */
107
+ export async function runStructuredOutputFinalizationRetry({ harness, structuredTool, runtimeWarnings, prompts }) {
108
+ const reason = "empty_final_output";
109
+ runtimeWarnings.push({
110
+ warning_kind: "structured_output_finalization_retry",
111
+ source: "pi",
112
+ reason,
113
+ message: "Pi stopped without text or structured output; retrying once in the same session with only StructuredOutput enabled.",
114
+ });
115
+ const previousActive = harness.getActiveTools().map((/** @type {{name: string}} */ toolDef) => toolDef.name);
116
+ try {
117
+ await harness.setActiveTools(structuredTool ? [structuredTool.name] : []);
118
+ await harness.prompt(structuredOutputFinalizationPrompt(prompts));
119
+ await harness.waitForIdle();
120
+ } catch (err) {
121
+ runtimeWarnings.push({
122
+ warning_kind: "structured_output_finalization_retry_failed",
123
+ source: "pi",
124
+ message: (/** @type {any} */ (err))?.message || String(err),
125
+ });
126
+ } finally {
127
+ try { await harness.setActiveTools(previousActive); } catch { /* best-effort */ }
128
+ }
129
+ return { attempts: 1, reason };
130
+ }
@@ -0,0 +1,278 @@
1
+ // @ts-check
2
+ // Turn execution for the pi-native bridge.
3
+ //
4
+ // Pure moves out of pi-native.js: effort→thinkingLevel mapping, harness
5
+ // construction + stream-subscriber wiring + abort wiring, the live-input
6
+ // steering consumer, and the prompt/waitForIdle run. No module-level mutable
7
+ // state; run state (harness, removeAbortHandler, externalAbort) lives on the
8
+ // caller-owned runState.
9
+
10
+ import { AgentHarness } from "@earendil-works/pi-agent-core";
11
+ import { NodeExecutionEnv } from "@earendil-works/pi-agent-core/node";
12
+ import {
13
+ createStructuredOutputTool,
14
+ getPiBuiltinTools,
15
+ initPiMcpTools,
16
+ } from "../../../agent/tools/pi-bridge.js";
17
+ import { readToolRuntime } from "../../../agent/tools/shared/runtime-context.js";
18
+ import { formatLiveInputGuidance } from "../../live-input-prompt.js";
19
+ import { appendStructuredOutputInstruction } from "./structured-output.js";
20
+ import { createStreamSubscriber } from "./stream-subscriber.js";
21
+
22
+ /**
23
+ * Build the turn's tool set: the sandboxed/allowlisted/bloat-guarded builtin
24
+ * tools, the MCP tool bridge, and the StructuredOutput tool (whose callback
25
+ * writes runState.structuredResult). Surfaces MCP init/list failures to both the
26
+ * event stream and runtimeWarnings. Returns the assembled tools plus the MCP
27
+ * clients (closed by the caller's finally) and the structured tool.
28
+ * @param {any} runState
29
+ * @param {any} params
30
+ * @returns {Promise<{tools: any[], structuredTool: any, mcpClients: any[]}>}
31
+ */
32
+ export async function buildTurnTools(runState, {
33
+ options,
34
+ capabilities,
35
+ toolLimits,
36
+ approvalManager,
37
+ runtime,
38
+ resolved,
39
+ onEvent,
40
+ runtimeWarnings,
41
+ }) {
42
+ const onTruncate = (info) => {
43
+ try {
44
+ onEvent({
45
+ type: "runtime_warning",
46
+ warning_kind: "tool_payload_truncated",
47
+ source: "tool_bloat_guard",
48
+ ...info,
49
+ });
50
+ } catch { /* best-effort */ }
51
+ };
52
+ const persistArtifact = options.persistArtifact || null;
53
+ const qaOutputDir = options.qaOutputDir || options.runArtifactDir || null;
54
+
55
+ // Per-run sandbox override (item 7): precedence run impl > host impl >
56
+ // passthrough. When the run supplies its own RuntimeSandbox, thread a ctx copy
57
+ // whose `.sandbox` is that impl so this run's tools/MCP-stdio launcher enforce
58
+ // through it; otherwise the host/default ToolContext is used unchanged. The
59
+ // per-run policy DATA (sandboxPolicy) still merges monotonically inside
60
+ // resolveSandboxPolicy (I13) regardless of which impl is chosen.
61
+ const runCtx = options.sandbox
62
+ ? { ...(options.toolContext ?? readToolRuntime()), sandbox: options.sandbox }
63
+ : options.toolContext;
64
+ const sandboxEngine = options.sandboxEngine ?? runCtx?.sandboxEngine;
65
+
66
+ // REUSED custom pieces: built-in tool sandboxing + allowlist/bloat filter +
67
+ // approval gates. These are identical to the legacy bridge.
68
+ // Cast: pi-bridge's tool-builder option bags are not precisely typed (their
69
+ // no-default destructured keys drop out of structural inference), so the
70
+ // caller's full bag is passed through an `any` boundary — matching how the
71
+ // pre-split (non-@ts-check) orchestrator called these.
72
+ const builtIns = capabilities.tool_use === false
73
+ ? []
74
+ : getPiBuiltinTools(options.allowedTools, /** @type {any} */ ({
75
+ skillNames: (options.skills || []).map((/** @type {{name: string}} */ skill) => skill.name),
76
+ // Full skill objects so read_skill can honor pi's neutral Skill shape
77
+ // ({name, filePath, ...}) and derive each skill's root from its own
78
+ // filePath when no shared skillsRoot is threaded.
79
+ skills: options.skills || [],
80
+ // Progressive skill disclosure: when the harness threads the skills root
81
+ // (the directory holding `<name>/SKILL.md`) the read_skill tool resolves
82
+ // bodies directly from there. `dataDir` (skills under `<dataDir>/skills`)
83
+ // remains the back-compat fallback; a per-skill filePath is the third path.
84
+ skillsRoot: options.skillsRoot,
85
+ dataDir: options.dataDir,
86
+ cwd: options.cwd,
87
+ onEvent,
88
+ persistArtifact,
89
+ onTruncate,
90
+ toolLimits,
91
+ toolPayloadMaxBytes: toolLimits.toolPayloadMaxBytes,
92
+ imageInlineMaxBytes: toolLimits.imageInlineMaxBytes,
93
+ toolPolicy: options.toolPolicy,
94
+ sandboxPolicy: options.sandboxPolicy,
95
+ sandboxEngine,
96
+ approvalManager,
97
+ approvalModel: runtime.model?.id || runtime.model?.name || resolved.model,
98
+ ctx: runCtx,
99
+ }));
100
+
101
+ const structuredTool = createStructuredOutputTool(options.outputSchema, (value) => {
102
+ runState.structuredResult = value;
103
+ });
104
+ const reservedNames = new Set(builtIns.map((/** @type {{name: string}} */ toolDef) => toolDef.name));
105
+ if (structuredTool) reservedNames.add(structuredTool.name);
106
+
107
+ // REUSED MCP tool bridge: same initPiMcpTools sandboxing path (see the cast
108
+ // note above — the option bag crosses the same untyped pi-bridge boundary).
109
+ const mcpInit = capabilities.tool_use === false
110
+ ? { clients: [], tools: [], warnings: [] }
111
+ : await initPiMcpTools(options.mcpServers || {}, reservedNames, /** @type {any} */ ({
112
+ cwd: options.cwd,
113
+ persistArtifact,
114
+ qaOutputDir,
115
+ onTruncate,
116
+ limits: toolLimits,
117
+ toolPayloadMaxBytes: toolLimits.toolPayloadMaxBytes,
118
+ sandboxPolicy: options.sandboxPolicy,
119
+ sandboxEngine,
120
+ ctx: runCtx,
121
+ }));
122
+ // Surface MCP init/list failures BOTH to the live event stream and to runtimeWarnings, so a
123
+ // failed server (e.g. an stdio adapter-send child that closed on startup) lands in the run
124
+ // summary's runtimeWarnings instead of being buried as a transient event the summary drops.
125
+ for (const warning of mcpInit.warnings || []) {
126
+ onEvent(warning);
127
+ runtimeWarnings.push(warning);
128
+ }
129
+
130
+ const tools = [
131
+ ...builtIns,
132
+ ...mcpInit.tools,
133
+ ...(structuredTool ? [structuredTool] : []),
134
+ ];
135
+ return { tools, structuredTool, mcpClients: mcpInit.clients };
136
+ }
137
+
138
+ /**
139
+ * Map an effort level to the harness thinkingLevel, respecting model reasoning
140
+ * capability (no reasoning / reasoning_mode "none" → "off").
141
+ * @param {string} effort
142
+ * @param {any} capabilities
143
+ * @returns {string}
144
+ */
145
+ export function thinkingLevelForEffort(effort, capabilities) {
146
+ if (!capabilities?.reasoning || capabilities.reasoning_mode === "none") return "off";
147
+ if (effort === "none") return "off";
148
+ if (effort === "max") return "xhigh";
149
+ if (effort === "xhigh") return "xhigh";
150
+ if (effort === "high") return "high";
151
+ if (effort === "medium") return "medium";
152
+ return "low";
153
+ }
154
+
155
+ /**
156
+ * Construct the AgentHarness for this turn, subscribe the stream-subscriber, and
157
+ * wire the external abort handler. Sets runState.harness and
158
+ * runState.removeAbortHandler; the abort handler sets runState.externalAbort and
159
+ * aborts the harness. Returns the harness.
160
+ * @param {any} runState
161
+ * @param {any} params
162
+ * @returns {any}
163
+ */
164
+ export function buildTurnHarness(runState, {
165
+ cwd,
166
+ session,
167
+ piModels,
168
+ model,
169
+ thinkingLevel,
170
+ systemPrompt,
171
+ outputSchema,
172
+ tools,
173
+ maxRetries,
174
+ maxRetryDelayMs,
175
+ steeringMode,
176
+ onEvent,
177
+ options,
178
+ toolLimits,
179
+ }) {
180
+ const harness = new AgentHarness({
181
+ env: new NodeExecutionEnv({ cwd: cwd || process.cwd() }),
182
+ session,
183
+ models: piModels,
184
+ model,
185
+ thinkingLevel,
186
+ systemPrompt: appendStructuredOutputInstruction(systemPrompt, outputSchema, options.prompts),
187
+ tools,
188
+ streamOptions: { maxRetries, maxRetryDelayMs },
189
+ steeringMode,
190
+ followUpMode: steeringMode,
191
+ });
192
+ runState.harness = harness;
193
+
194
+ harness.subscribe(createStreamSubscriber(runState, { onEvent, options, toolLimits, harness }));
195
+
196
+ const abortHandler = () => {
197
+ runState.externalAbort = true;
198
+ harness.abort();
199
+ };
200
+ if (options.abortSignal) {
201
+ options.abortSignal.addEventListener("abort", abortHandler, { once: true });
202
+ runState.removeAbortHandler = () => options.abortSignal.removeEventListener?.("abort", abortHandler);
203
+ }
204
+ return harness;
205
+ }
206
+
207
+ /**
208
+ * Start the live-input steering consumer. Consumes follow-up messages and steers
209
+ * the harness mid-run; the consumer is tied to run completion (an internal
210
+ * runComplete flag) so it stops steering once the run finishes and does not
211
+ * swallow a follow-up meant for a later turn. Returns a `stop()` teardown.
212
+ * @param {{harness: any, options: any, onEvent: (event: any) => void}} deps
213
+ * @returns {{stop: () => Promise<void>}}
214
+ */
215
+ export function startLiveInput({ harness, options, onEvent }) {
216
+ if (!options.liveInput) return { stop: async () => {} };
217
+ const iterator = typeof options.liveInput[Symbol.asyncIterator] === "function"
218
+ ? options.liveInput[Symbol.asyncIterator]()
219
+ : options.liveInput;
220
+ let runComplete = false;
221
+ const task = (async () => {
222
+ try {
223
+ while (!runComplete && !options.abortSignal?.aborted) {
224
+ const next = await iterator.next();
225
+ if (next.done || runComplete || options.abortSignal?.aborted) break;
226
+ await harness.steer(formatLiveInputGuidance(next.value.body, options.prompts));
227
+ }
228
+ } catch (err) {
229
+ onEvent({
230
+ type: "runtime_warning",
231
+ warning_kind: "live_input_failed",
232
+ message: (/** @type {any} */ (err))?.message || String(err),
233
+ });
234
+ }
235
+ })();
236
+ return {
237
+ // The run is done: stop the live-steering consumer so it cannot steer a
238
+ // finished harness or swallow a follow-up meant for the next turn. We signal
239
+ // completion, then best-effort return() the iterator to unblock a pending
240
+ // next(). We do NOT await the task (it could block on next() if the source
241
+ // has no return()), but the runComplete guard prevents any further steering.
242
+ stop: async () => {
243
+ runComplete = true;
244
+ if (iterator && typeof iterator.return === "function") {
245
+ try { await iterator.return(); } catch { /* best-effort */ }
246
+ }
247
+ void task;
248
+ },
249
+ };
250
+ }
251
+
252
+ /**
253
+ * Run a single prompt on the harness and wait for it to go idle. A stream error
254
+ * surfaces on the harness (not a throw), so a thrown prompt is captured as
255
+ * runError; waitForIdle always runs afterward.
256
+ * @param {any} harness
257
+ * @param {string} promptText
258
+ * @param {Array<any>} promptImages
259
+ * @returns {Promise<{runError: any}>}
260
+ */
261
+ export async function runHarnessPrompt(harness, promptText, promptImages) {
262
+ let runError = null;
263
+ try {
264
+ // Pass structured images (when present) so multimodal input reaches the
265
+ // model as image blocks rather than stringified text. AgentHarness.prompt
266
+ // takes them under an options object (`{ images }`); a bare array would be
267
+ // read as `options` and silently dropped (options?.images === undefined).
268
+ if (Array.isArray(promptImages) && promptImages.length > 0) {
269
+ await harness.prompt(promptText, { images: promptImages });
270
+ } else {
271
+ await harness.prompt(promptText);
272
+ }
273
+ } catch (err) {
274
+ runError = err;
275
+ }
276
+ await harness.waitForIdle();
277
+ return { runError };
278
+ }