@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.
- package/ARCHITECTURE.md +37 -16
- package/MIGRATION.md +185 -7
- package/README.md +24 -14
- package/package.json +103 -10
- package/src/agent/allowlists.js +3 -0
- package/src/agent/approval.js +3 -0
- package/src/agent/compaction.js +244 -1
- package/src/agent/prompt/skill-index.js +6 -0
- package/src/agent/sandbox-seam.js +227 -0
- package/src/agent/tool-bloat.js +8 -2
- package/src/agent/tools/bash.js +16 -8
- package/src/agent/tools/edit.js +7 -3
- package/src/agent/tools/glob.js +11 -5
- package/src/agent/tools/grep.js +11 -5
- package/src/agent/tools/pi-bridge.js +227 -45
- package/src/agent/tools/read.js +28 -3
- package/src/agent/tools/shared/output-truncation.js +8 -3
- package/src/agent/tools/shared/path-resolver.js +24 -18
- package/src/agent/tools/shared/ripgrep.js +33 -6
- package/src/agent/tools/shared/runtime-context.js +60 -50
- package/src/agent/tools/shared/tool-context.js +157 -0
- package/src/agent/tools/web-fetch.js +21 -10
- package/src/agent/tools/web-search.js +11 -4
- package/src/agent/tools/write.js +7 -3
- package/src/agent/transcript.js +16 -1
- package/src/ai/cost.js +128 -3
- package/src/ai/failure.js +96 -4
- package/src/ai/live-input-prompt.js +11 -1
- package/src/ai/providers/claude-cli.js +2 -2
- package/src/ai/providers/claude-sdk.js +55 -12
- package/src/ai/providers/codex-app.js +36 -23
- package/src/ai/providers/opencode-app.js +2 -2
- package/src/ai/providers/opencode-discovery.js +2 -2
- package/src/ai/providers/pi-events.js +5 -0
- package/src/ai/providers/pi-models.js +21 -4
- package/src/ai/providers/pi-native/compaction-driver.js +394 -0
- package/src/ai/providers/pi-native/result-builder.js +312 -0
- package/src/ai/providers/pi-native/session-lifecycle.js +438 -0
- package/src/ai/providers/pi-native/stream-subscriber.js +148 -0
- package/src/ai/providers/pi-native/structured-output.js +130 -0
- package/src/ai/providers/pi-native/turn-runner.js +278 -0
- package/src/ai/providers/pi-native.js +415 -1106
- package/src/ai/runtime/model-refs.js +30 -0
- package/src/ai/runtime/registry.js +29 -0
- package/src/ai/runtime/router.js +96 -12
- package/src/ai/runtime/session-liveness.js +110 -0
- package/src/ai/runtime/sessions.js +16 -0
- package/src/ai/types.js +252 -19
- package/src/index.js +1 -1
- package/src/pi-auth.js +147 -16
- package/src/runtime-brand.js +21 -1
- package/src/runtime.js +72 -8
- package/types/agent/allowlists.d.ts +25 -0
- package/types/agent/approval.d.ts +30 -0
- package/types/agent/compaction.d.ts +97 -0
- package/types/agent/index.d.ts +5 -0
- package/types/agent/prompt/skill-index.d.ts +19 -0
- package/types/agent/sandbox-seam.d.ts +148 -0
- package/types/agent/tool-bloat.d.ts +22 -0
- package/types/agent/tools/bash.d.ts +16 -0
- package/types/agent/tools/edit.d.ts +14 -0
- package/types/agent/tools/glob.d.ts +16 -0
- package/types/agent/tools/grep.d.ts +22 -0
- package/types/agent/tools/index.d.ts +10 -0
- package/types/agent/tools/pi-bridge.d.ts +144 -0
- package/types/agent/tools/read.d.ts +19 -0
- package/types/agent/tools/shared/constants.d.ts +13 -0
- package/types/agent/tools/shared/dedup.d.ts +8 -0
- package/types/agent/tools/shared/output-truncation.d.ts +22 -0
- package/types/agent/tools/shared/path-resolver.d.ts +6 -0
- package/types/agent/tools/shared/ripgrep.d.ts +38 -0
- package/types/agent/tools/shared/runtime-context.d.ts +27 -0
- package/types/agent/tools/shared/tool-context.d.ts +48 -0
- package/types/agent/tools/web-fetch.d.ts +13 -0
- package/types/agent/tools/web-search.d.ts +11 -0
- package/types/agent/tools/write.d.ts +12 -0
- package/types/agent/transcript.d.ts +45 -0
- package/types/ai/backend.d.ts +41 -0
- package/types/ai/cost.d.ts +96 -0
- package/types/ai/failure.d.ts +117 -0
- package/types/ai/file-change-stats.d.ts +75 -0
- package/types/ai/index.d.ts +7 -0
- package/types/ai/live-input-prompt.d.ts +6 -0
- package/types/ai/observer.d.ts +68 -0
- package/types/ai/providers/claude-cli.d.ts +211 -0
- package/types/ai/providers/claude-sdk.d.ts +66 -0
- package/types/ai/providers/claude-subagents.d.ts +2 -0
- package/types/ai/providers/codex-app.d.ts +151 -0
- package/types/ai/providers/opencode-app.d.ts +95 -0
- package/types/ai/providers/opencode-discovery.d.ts +4 -0
- package/types/ai/providers/pi-errors.d.ts +3 -0
- package/types/ai/providers/pi-events.d.ts +24 -0
- package/types/ai/providers/pi-messages.d.ts +5 -0
- package/types/ai/providers/pi-models.d.ts +57 -0
- package/types/ai/providers/pi-native/compaction-driver.d.ts +86 -0
- package/types/ai/providers/pi-native/result-builder.d.ts +168 -0
- package/types/ai/providers/pi-native/session-lifecycle.d.ts +62 -0
- package/types/ai/providers/pi-native/stream-subscriber.d.ts +50 -0
- package/types/ai/providers/pi-native/structured-output.d.ts +69 -0
- package/types/ai/providers/pi-native/turn-runner.d.ts +60 -0
- package/types/ai/providers/pi-native.d.ts +18 -0
- package/types/ai/registry.d.ts +1 -0
- package/types/ai/runtime/capabilities-used.d.ts +21 -0
- package/types/ai/runtime/capabilities.d.ts +33 -0
- package/types/ai/runtime/context-windows.d.ts +8 -0
- package/types/ai/runtime/fast-mode.d.ts +2 -0
- package/types/ai/runtime/model-refs.d.ts +35 -0
- package/types/ai/runtime/registry.d.ts +38 -0
- package/types/ai/runtime/router.d.ts +56 -0
- package/types/ai/runtime/session-liveness.d.ts +55 -0
- package/types/ai/runtime/sessions.d.ts +38 -0
- package/types/ai/streaming/codex-events.d.ts +30 -0
- package/types/ai/streaming/opencode-events.d.ts +41 -0
- package/types/ai/types.d.ts +702 -0
- package/types/index.d.ts +7 -0
- package/types/pi-auth.d.ts +28 -0
- package/types/runtime-brand.d.ts +30 -0
- package/types/runtime.d.ts +10 -0
- 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
|
+
}
|