@mono-agent/agent-runtime 0.1.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.
- package/ARCHITECTURE.md +219 -0
- package/LICENSE +674 -0
- package/README.md +430 -0
- package/package.json +46 -0
- package/src/agent/allowlists.js +49 -0
- package/src/agent/approval.js +211 -0
- package/src/agent/compaction.js +752 -0
- package/src/agent/index.js +40 -0
- package/src/agent/prompt/skill-index.js +66 -0
- package/src/agent/tool-bloat.js +164 -0
- package/src/agent/tools/bash.js +156 -0
- package/src/agent/tools/edit.js +15 -0
- package/src/agent/tools/glob.js +71 -0
- package/src/agent/tools/grep.js +84 -0
- package/src/agent/tools/index.js +17 -0
- package/src/agent/tools/pi-bridge.js +638 -0
- package/src/agent/tools/read.js +39 -0
- package/src/agent/tools/shared/constants.js +21 -0
- package/src/agent/tools/shared/dedup.js +31 -0
- package/src/agent/tools/shared/output-truncation.js +54 -0
- package/src/agent/tools/shared/path-resolver.js +156 -0
- package/src/agent/tools/shared/ripgrep.js +130 -0
- package/src/agent/tools/shared/runtime-context.js +69 -0
- package/src/agent/tools/web-fetch.js +59 -0
- package/src/agent/tools/web-search.js +21 -0
- package/src/agent/tools/write.js +14 -0
- package/src/agent/transcript.js +227 -0
- package/src/ai/backend.js +17 -0
- package/src/ai/cost.js +164 -0
- package/src/ai/failure.js +165 -0
- package/src/ai/file-change-stats.js +234 -0
- package/src/ai/index.js +16 -0
- package/src/ai/live-input-prompt.js +15 -0
- package/src/ai/observer.js +233 -0
- package/src/ai/providers/claude-cli.js +694 -0
- package/src/ai/providers/claude-sdk.js +864 -0
- package/src/ai/providers/claude-subagents.js +67 -0
- package/src/ai/providers/codex-app.js +1045 -0
- package/src/ai/providers/opencode-app.js +356 -0
- package/src/ai/providers/opencode-discovery.js +39 -0
- package/src/ai/providers/pi-events.js +62 -0
- package/src/ai/providers/pi-messages.js +68 -0
- package/src/ai/providers/pi-models.js +111 -0
- package/src/ai/providers/pi-sdk.js +1310 -0
- package/src/ai/registry.js +5 -0
- package/src/ai/runtime/capabilities-used.js +56 -0
- package/src/ai/runtime/capabilities.js +44 -0
- package/src/ai/runtime/context-windows.js +38 -0
- package/src/ai/runtime/fast-mode.js +8 -0
- package/src/ai/runtime/model-refs.js +144 -0
- package/src/ai/runtime/registry.js +57 -0
- package/src/ai/runtime/router.js +214 -0
- package/src/ai/runtime/sessions.js +126 -0
- package/src/ai/streaming/codex-events.js +139 -0
- package/src/ai/streaming/opencode-events.js +54 -0
- package/src/ai/types.js +70 -0
- package/src/index.js +23 -0
- package/src/pi-auth.js +80 -0
- package/src/runtime-brand.js +32 -0
- package/src/runtime.js +104 -0
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { createInterface } from "node:readline";
|
|
6
|
+
import { getSkillAccessDirs } from "../../agent/prompt/skill-index.js";
|
|
7
|
+
import { normalizeCodexItemEvent } from "../streaming/codex-events.js";
|
|
8
|
+
import { createFileChangePayload } from "../file-change-stats.js";
|
|
9
|
+
import { estimateCost } from "../cost.js";
|
|
10
|
+
import { createStderrTail } from "../failure.js";
|
|
11
|
+
import { modelWithContextWindow } from "../runtime/context-windows.js";
|
|
12
|
+
import { readRuntimeBrand } from "../../agent/tools/shared/runtime-context.js";
|
|
13
|
+
import { buildCapabilitiesUsed } from "../runtime/capabilities-used.js";
|
|
14
|
+
import {
|
|
15
|
+
claudeNativeAgentDefinitions,
|
|
16
|
+
claudeToolsWithNativeSubagents,
|
|
17
|
+
} from "./claude-subagents.js";
|
|
18
|
+
|
|
19
|
+
const DORMANT_CLI_CAPABILITIES = {
|
|
20
|
+
streaming: true,
|
|
21
|
+
structured_output: true,
|
|
22
|
+
// intelligence-ramp Phase 5.1: claude-cli supports resume via `--resume` and
|
|
23
|
+
// surfaces session_id from init/result events; the bridge wraps the env-var
|
|
24
|
+
// hand-off the coordinator already populates on continuations.
|
|
25
|
+
supports_session_resume: true,
|
|
26
|
+
native_runtime_config: null,
|
|
27
|
+
supports_mcp: true,
|
|
28
|
+
supports_skills: true,
|
|
29
|
+
supports_builtin_tools: true,
|
|
30
|
+
supports_live_input: true,
|
|
31
|
+
supports_native_subagents: true,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function promptFromMessages(messages) {
|
|
35
|
+
return Array.isArray(messages)
|
|
36
|
+
? messages.map((message) => typeof message.content === "string" ? message.content : JSON.stringify(message.content)).join("\n\n")
|
|
37
|
+
: String(messages || "");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const CODEX_REASONING_ITEM_EVENTS = new Set(["item.started", "item.updated", "item.completed"]);
|
|
41
|
+
const CODEX_REASONING_EVENT_TYPES = new Set([
|
|
42
|
+
"agent_reasoning",
|
|
43
|
+
"agent_reasoning_delta",
|
|
44
|
+
"reasoning_content_delta",
|
|
45
|
+
"reasoning_summary_part_added",
|
|
46
|
+
"reasoning_summary_text_delta",
|
|
47
|
+
]);
|
|
48
|
+
const CODEX_RAW_REASONING_EVENT_TYPES = new Set([
|
|
49
|
+
"agent_reasoning_raw_content",
|
|
50
|
+
"agent_reasoning_raw_content_delta",
|
|
51
|
+
"reasoning_raw_content",
|
|
52
|
+
"reasoning_raw_content_delta",
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
function summaryTextFromValue(value) {
|
|
56
|
+
if (typeof value === "string") return value;
|
|
57
|
+
if (Array.isArray(value)) return value.map(summaryTextFromValue).filter(Boolean).join("");
|
|
58
|
+
if (!value || typeof value !== "object") return "";
|
|
59
|
+
if (value.type && !["summary_text", "reasoning_summary_text"].includes(value.type)) return "";
|
|
60
|
+
return summaryTextFromValue(value.text ?? value.delta ?? value.summary ?? value.content);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function codexReasoningSummaryText(raw) {
|
|
64
|
+
const item = raw?.item || {};
|
|
65
|
+
return [
|
|
66
|
+
raw?.delta,
|
|
67
|
+
raw?.text,
|
|
68
|
+
raw?.summary,
|
|
69
|
+
raw?.content,
|
|
70
|
+
item.delta,
|
|
71
|
+
item.text,
|
|
72
|
+
item.summary,
|
|
73
|
+
item.summaries,
|
|
74
|
+
].map(summaryTextFromValue).find((text) => text.trim()) || "";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isCodexReasoningEvent(raw) {
|
|
78
|
+
if (!raw || typeof raw !== "object") return false;
|
|
79
|
+
return CODEX_REASONING_EVENT_TYPES.has(raw.type)
|
|
80
|
+
|| CODEX_RAW_REASONING_EVENT_TYPES.has(raw.type)
|
|
81
|
+
|| (CODEX_REASONING_ITEM_EVENTS.has(raw.type) && raw.item?.type === "reasoning");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const ANTHROPIC_STREAM_EVENT_TYPES = new Set([
|
|
85
|
+
"message_start",
|
|
86
|
+
"message_delta",
|
|
87
|
+
"message_stop",
|
|
88
|
+
"content_block_start",
|
|
89
|
+
"content_block_delta",
|
|
90
|
+
"content_block_stop",
|
|
91
|
+
"ping",
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
// Claude Code CLI emits one `assistant` event per finalised content block in
|
|
95
|
+
// stream-json mode, but for `thinking` blocks the text is only ever sent via
|
|
96
|
+
// `thinking_delta` chunks under `--include-partial-messages`. The finalised
|
|
97
|
+
// block carries the signature but `thinking: ""`. The buffer accumulates the
|
|
98
|
+
// streamed thinking deltas and splices them back into the finalised assistant
|
|
99
|
+
// event before it is forwarded to the host.
|
|
100
|
+
export function createThinkingBuffer() {
|
|
101
|
+
let currentMessageId = null;
|
|
102
|
+
const byMessage = new Map();
|
|
103
|
+
|
|
104
|
+
function unwrap(raw) {
|
|
105
|
+
return raw?.type === "stream_event" && raw.event ? raw.event : raw;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isStreamShape(raw) {
|
|
109
|
+
if (!raw || typeof raw !== "object") return false;
|
|
110
|
+
if (raw.type === "stream_event") return true;
|
|
111
|
+
return ANTHROPIC_STREAM_EVENT_TYPES.has(raw.type);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function bufferFor(messageId) {
|
|
115
|
+
if (!messageId) return null;
|
|
116
|
+
let bucket = byMessage.get(messageId);
|
|
117
|
+
if (!bucket) {
|
|
118
|
+
bucket = new Map();
|
|
119
|
+
byMessage.set(messageId, bucket);
|
|
120
|
+
}
|
|
121
|
+
return bucket;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function onStreamEvent(raw) {
|
|
125
|
+
const inner = unwrap(raw);
|
|
126
|
+
if (!inner || typeof inner !== "object") return;
|
|
127
|
+
|
|
128
|
+
if (inner.type === "message_start") {
|
|
129
|
+
currentMessageId = inner.message?.id || null;
|
|
130
|
+
// Defensive: drop any stale state if the same id reappears.
|
|
131
|
+
if (currentMessageId) byMessage.delete(currentMessageId);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (inner.type === "content_block_start") {
|
|
136
|
+
if (inner.content_block?.type !== "thinking") return;
|
|
137
|
+
const bucket = bufferFor(currentMessageId);
|
|
138
|
+
if (bucket) bucket.set(inner.index, { text: "", consumed: false });
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (inner.type === "content_block_delta") {
|
|
143
|
+
const bucket = bufferFor(currentMessageId);
|
|
144
|
+
if (!bucket) return;
|
|
145
|
+
const entry = bucket.get(inner.index);
|
|
146
|
+
if (!entry) return;
|
|
147
|
+
if (inner.delta?.type === "thinking_delta" && typeof inner.delta.thinking === "string") {
|
|
148
|
+
entry.text += inner.delta.thinking;
|
|
149
|
+
}
|
|
150
|
+
// signature_delta is ignored: the signature already lands on the
|
|
151
|
+
// finalised assistant event we will rehydrate.
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function rehydrate(assistantRaw) {
|
|
156
|
+
if (!assistantRaw || typeof assistantRaw !== "object") return assistantRaw;
|
|
157
|
+
const messageId = assistantRaw.message?.id;
|
|
158
|
+
const content = assistantRaw.message?.content;
|
|
159
|
+
if (!messageId || !Array.isArray(content)) return assistantRaw;
|
|
160
|
+
const bucket = byMessage.get(messageId);
|
|
161
|
+
if (!bucket || bucket.size === 0) return assistantRaw;
|
|
162
|
+
|
|
163
|
+
const pending = [...bucket.entries()]
|
|
164
|
+
.filter(([, entry]) => !entry.consumed && entry.text)
|
|
165
|
+
.sort(([a], [b]) => (Number(a) || 0) - (Number(b) || 0));
|
|
166
|
+
if (pending.length === 0) return assistantRaw;
|
|
167
|
+
|
|
168
|
+
let mutated = null;
|
|
169
|
+
let cursor = 0;
|
|
170
|
+
for (let i = 0; i < content.length; i++) {
|
|
171
|
+
const block = content[i];
|
|
172
|
+
if (!block || block.type !== "thinking" || block.thinking) continue;
|
|
173
|
+
if (cursor >= pending.length) break;
|
|
174
|
+
const [, entry] = pending[cursor++];
|
|
175
|
+
entry.consumed = true;
|
|
176
|
+
if (!mutated) {
|
|
177
|
+
mutated = { ...assistantRaw, message: { ...assistantRaw.message, content: [...content] } };
|
|
178
|
+
}
|
|
179
|
+
mutated.message.content[i] = { ...block, thinking: entry.text };
|
|
180
|
+
}
|
|
181
|
+
return mutated || assistantRaw;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { isStreamShape, onStreamEvent, rehydrate };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function normalizeCliEvent(raw, context = {}) {
|
|
188
|
+
if (!raw || typeof raw !== "object") return { type: "cli_event", raw };
|
|
189
|
+
const thinkingBuffer = context.thinkingBuffer;
|
|
190
|
+
if (thinkingBuffer && thinkingBuffer.isStreamShape(raw)) {
|
|
191
|
+
thinkingBuffer.onStreamEvent(raw);
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
if (CODEX_RAW_REASONING_EVENT_TYPES.has(raw.type)) return null;
|
|
195
|
+
if (CODEX_REASONING_EVENT_TYPES.has(raw.type) || (CODEX_REASONING_ITEM_EVENTS.has(raw.type) && raw.item?.type === "reasoning")) {
|
|
196
|
+
const text = codexReasoningSummaryText(raw).trim();
|
|
197
|
+
return text
|
|
198
|
+
? { type: "assistant", message: { content: [{ type: "thinking", text }] } }
|
|
199
|
+
: null;
|
|
200
|
+
}
|
|
201
|
+
if (raw.type === "assistant") {
|
|
202
|
+
return thinkingBuffer ? thinkingBuffer.rehydrate(raw) : raw;
|
|
203
|
+
}
|
|
204
|
+
if (raw.type === "user" || raw.type === "result" || raw.type === "error") return raw;
|
|
205
|
+
if (raw.type === "message" && raw.message) return { type: "assistant", message: raw.message };
|
|
206
|
+
if (raw.type === "item.completed" && raw.item?.type === "agent_message" && typeof raw.item.text === "string") {
|
|
207
|
+
return { type: "assistant", message: { content: [{ type: "text", text: raw.item.text }] } };
|
|
208
|
+
}
|
|
209
|
+
const codexItem = normalizeCodexItemEvent(raw, {
|
|
210
|
+
fileChangePayload: (event) => createFileChangePayload(event, {
|
|
211
|
+
cwd: context.cwd || process.cwd(),
|
|
212
|
+
snapshots: context.fileChangeSnapshots || new Map(),
|
|
213
|
+
}),
|
|
214
|
+
});
|
|
215
|
+
if (codexItem) return codexItem;
|
|
216
|
+
if (raw.type === "tool_call") {
|
|
217
|
+
return { type: "assistant", message: { content: [{ type: "tool_use", id: raw.id, name: raw.name, input: raw.input || raw.arguments }] } };
|
|
218
|
+
}
|
|
219
|
+
if (raw.type === "tool_result") {
|
|
220
|
+
return { type: "user", message: { content: [{ type: "tool_result", tool_use_id: raw.id || raw.tool_use_id, content: raw.output || raw.result || "" }] } };
|
|
221
|
+
}
|
|
222
|
+
return { type: "cli_event", raw };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function textFromEvent(raw) {
|
|
226
|
+
if (typeof raw?.text === "string") return raw.text;
|
|
227
|
+
if (typeof raw?.item?.text === "string") return raw.item.text;
|
|
228
|
+
if (raw?.type === "result" && raw.result != null) {
|
|
229
|
+
return typeof raw.result === "string" ? raw.result : JSON.stringify(raw.result);
|
|
230
|
+
}
|
|
231
|
+
if (raw?.final_output != null) {
|
|
232
|
+
return typeof raw.final_output === "string" ? raw.final_output : JSON.stringify(raw.final_output);
|
|
233
|
+
}
|
|
234
|
+
if (typeof raw?.message?.content === "string") return raw.message.content;
|
|
235
|
+
if (Array.isArray(raw?.message?.content)) {
|
|
236
|
+
return raw.message.content.filter((part) => part?.type === "text").map((part) => part.text).join("");
|
|
237
|
+
}
|
|
238
|
+
return "";
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function stringifyError(value) {
|
|
242
|
+
if (!value) return "";
|
|
243
|
+
if (typeof value === "string") return value;
|
|
244
|
+
if (typeof value.message === "string") return value.message;
|
|
245
|
+
try { return JSON.stringify(value); } catch { return String(value); }
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function humanizeSubtype(subtype) {
|
|
249
|
+
return String(subtype || "").replace(/^error_/, "").replace(/_/g, " ").trim();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function resultEventError(raw, command) {
|
|
253
|
+
if (raw?.type !== "result") return null;
|
|
254
|
+
const subtype = typeof raw.subtype === "string" ? raw.subtype : "";
|
|
255
|
+
const errors = Array.isArray(raw.errors) ? raw.errors.filter(Boolean) : [];
|
|
256
|
+
const explicit = stringifyError(raw.error) || stringifyError(raw.message);
|
|
257
|
+
if (!raw.is_error && !subtype.startsWith("error_") && errors.length === 0 && !explicit) return null;
|
|
258
|
+
|
|
259
|
+
const runtime = command === "claude" ? "Claude Code" : command === "codex" ? "Codex" : command || "CLI";
|
|
260
|
+
const detail = explicit || errors.map(stringifyError).filter(Boolean).join("; ");
|
|
261
|
+
const label = humanizeSubtype(subtype);
|
|
262
|
+
const message = subtype === "error_max_turns"
|
|
263
|
+
? `${runtime} stopped before final output: max turns reached`
|
|
264
|
+
: `${runtime} result error${label ? ` (${label})` : ""}${detail ? `: ${detail}` : ""}`;
|
|
265
|
+
return {
|
|
266
|
+
message,
|
|
267
|
+
failureKind: subtype === "error_max_turns" ? "usage_limit" : "provider_unavailable",
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function pushUniqueText(texts, text) {
|
|
272
|
+
const value = typeof text === "string" ? text.trim() : "";
|
|
273
|
+
if (!value) return;
|
|
274
|
+
if (texts.some((existing) => existing.trim() === value)) return;
|
|
275
|
+
texts.push(value);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function parseJsonError(text) {
|
|
279
|
+
const raw = String(text || "").trim();
|
|
280
|
+
if (!raw) return null;
|
|
281
|
+
try {
|
|
282
|
+
const parsed = JSON.parse(raw);
|
|
283
|
+
return parsed?.error || parsed;
|
|
284
|
+
} catch {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function formatCliError(message, command) {
|
|
290
|
+
const raw = String(message || "").trim();
|
|
291
|
+
const parsed = parseJsonError(raw);
|
|
292
|
+
const code = parsed?.code || parsed?.error?.code;
|
|
293
|
+
const detail = parsed?.message || parsed?.error?.message;
|
|
294
|
+
const param = parsed?.param || parsed?.error?.param;
|
|
295
|
+
if (code === "invalid_json_schema" || /invalid_json_schema|Invalid schema/i.test(raw)) {
|
|
296
|
+
return `Invalid response schema${param ? ` (${param})` : ""}: ${detail || raw}`;
|
|
297
|
+
}
|
|
298
|
+
if (
|
|
299
|
+
command === "claude" &&
|
|
300
|
+
(/401|Unauthorized|OAuth token is invalid|Please run \/login|auth/i.test(raw))
|
|
301
|
+
) {
|
|
302
|
+
return "Claude Code authentication failed. Run `claude /login` or configure ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, or CLAUDE_CODE_OAUTH_TOKEN.";
|
|
303
|
+
}
|
|
304
|
+
return detail || raw;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function hasEntries(value) {
|
|
308
|
+
return value && typeof value === "object" && Object.keys(value).length > 0;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function shellList(values = []) {
|
|
312
|
+
return values.filter(Boolean).join(" ");
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function tomlValue(value) {
|
|
316
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
317
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
318
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
319
|
+
if (Array.isArray(value)) return `[${value.map(tomlValue).join(", ")}]`;
|
|
320
|
+
return JSON.stringify(value);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function codexMcpConfigArgs(mcpServers = {}) {
|
|
324
|
+
const args = [];
|
|
325
|
+
for (const [name, cfg] of Object.entries(mcpServers)) {
|
|
326
|
+
if (!/^[A-Za-z0-9_-]+$/.test(name)) continue;
|
|
327
|
+
const prefix = `mcp_servers.${name}`;
|
|
328
|
+
if (cfg.command) {
|
|
329
|
+
args.push("--config", `${prefix}.command=${tomlValue(cfg.command)}`);
|
|
330
|
+
if (Array.isArray(cfg.args) && cfg.args.length) args.push("--config", `${prefix}.args=${tomlValue(cfg.args)}`);
|
|
331
|
+
if (cfg.cwd && typeof cfg.cwd === "string") args.push("--config", `${prefix}.cwd=${tomlValue(cfg.cwd)}`);
|
|
332
|
+
if (cfg.env && typeof cfg.env === "object") {
|
|
333
|
+
for (const [key, value] of Object.entries(cfg.env)) {
|
|
334
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
335
|
+
args.push("--config", `${prefix}.env.${key}=${tomlValue(String(value))}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
} else if (cfg.url) {
|
|
340
|
+
args.push("--config", `${prefix}.url=${tomlValue(cfg.url)}`);
|
|
341
|
+
const headers = cfg.headers || {};
|
|
342
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
343
|
+
if (/^[A-Za-z0-9_-]+$/.test(key)) {
|
|
344
|
+
args.push("--config", `${prefix}.http_headers.${key}=${tomlValue(String(value))}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
args.push("--config", `${prefix}.enabled=true`);
|
|
349
|
+
args.push("--config", `${prefix}.required=false`);
|
|
350
|
+
}
|
|
351
|
+
return args;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function buildCliCommand({
|
|
355
|
+
sdk,
|
|
356
|
+
model,
|
|
357
|
+
effort,
|
|
358
|
+
cwd,
|
|
359
|
+
schemaPath,
|
|
360
|
+
outputSchema,
|
|
361
|
+
systemPrompt,
|
|
362
|
+
prompt,
|
|
363
|
+
mcpConfigPath,
|
|
364
|
+
mcpServers,
|
|
365
|
+
allowedTools,
|
|
366
|
+
disallowedTools,
|
|
367
|
+
permissionMode,
|
|
368
|
+
maxTurns,
|
|
369
|
+
skillDirs,
|
|
370
|
+
resumeSessionId,
|
|
371
|
+
nativeSubagents,
|
|
372
|
+
contextWindow,
|
|
373
|
+
}) {
|
|
374
|
+
// Effort is expected to be pre-normalized by core/ai.js#generateResponse
|
|
375
|
+
// before reaching this provider. Direct callers of buildCliCommand must
|
|
376
|
+
// pass an already-normalized reasoning level (low/medium/high/xhigh/none).
|
|
377
|
+
const normalizedEffort = typeof effort === "string" && effort.trim() ? effort : null;
|
|
378
|
+
if (sdk === "claude-code") {
|
|
379
|
+
const nativeAgents = claudeNativeAgentDefinitions(nativeSubagents);
|
|
380
|
+
const cliAllowedTools = claudeToolsWithNativeSubagents(allowedTools, nativeSubagents);
|
|
381
|
+
// intelligence-ramp Phase 5.1: when the coordinator hands us a parent
|
|
382
|
+
// session id (recovery continuation, R12), pass --resume so the host
|
|
383
|
+
// CLI can keep its own conversation cache warm. Otherwise stay
|
|
384
|
+
// ephemeral so unrelated runs never bleed into each other.
|
|
385
|
+
const resumeFlag = typeof resumeSessionId === "string" && resumeSessionId.trim().length > 0
|
|
386
|
+
? ["--resume", resumeSessionId.trim()]
|
|
387
|
+
: ["--no-session-persistence"];
|
|
388
|
+
const args = [
|
|
389
|
+
"-p",
|
|
390
|
+
"--output-format", "stream-json",
|
|
391
|
+
"--include-partial-messages",
|
|
392
|
+
"--verbose",
|
|
393
|
+
...(outputSchema ? ["--json-schema", JSON.stringify(outputSchema)] : []),
|
|
394
|
+
"--model", modelWithContextWindow(model, contextWindow),
|
|
395
|
+
"--append-system-prompt", systemPrompt,
|
|
396
|
+
...resumeFlag,
|
|
397
|
+
];
|
|
398
|
+
if (normalizedEffort) args.push("--effort", normalizedEffort);
|
|
399
|
+
if (permissionMode) args.push("--permission-mode", permissionMode);
|
|
400
|
+
if (Number.isFinite(Number(maxTurns)) && Number(maxTurns) > 0) args.push("--max-turns", String(Number(maxTurns)));
|
|
401
|
+
if (Array.isArray(skillDirs) && skillDirs.length) {
|
|
402
|
+
args.push("--add-dir", ...skillDirs);
|
|
403
|
+
}
|
|
404
|
+
if (nativeAgents) args.push("--agents", JSON.stringify(nativeAgents));
|
|
405
|
+
if (Array.isArray(cliAllowedTools) && cliAllowedTools.length) {
|
|
406
|
+
args.push("--tools", cliAllowedTools.join(","));
|
|
407
|
+
}
|
|
408
|
+
const autoAllowed = [
|
|
409
|
+
...(Array.isArray(cliAllowedTools) ? cliAllowedTools : []),
|
|
410
|
+
...Object.keys(mcpServers || {}).map((name) => `mcp__${name}__*`),
|
|
411
|
+
];
|
|
412
|
+
if (autoAllowed.length) args.push("--allowedTools", shellList(autoAllowed));
|
|
413
|
+
if (Array.isArray(disallowedTools) && disallowedTools.length) {
|
|
414
|
+
args.push("--disallowedTools", shellList(disallowedTools));
|
|
415
|
+
}
|
|
416
|
+
if (mcpConfigPath) args.push("--mcp-config", mcpConfigPath, "--strict-mcp-config");
|
|
417
|
+
args.push("--", prompt);
|
|
418
|
+
return { command: "claude", args, cwd };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const args = [
|
|
422
|
+
"exec",
|
|
423
|
+
"--json",
|
|
424
|
+
...(schemaPath ? ["--output-schema", schemaPath] : []),
|
|
425
|
+
"--model", model,
|
|
426
|
+
"--cd", cwd,
|
|
427
|
+
"--ephemeral",
|
|
428
|
+
"--skip-git-repo-check",
|
|
429
|
+
"--config", `service_tier=${tomlValue("fast")}`,
|
|
430
|
+
"--config", "features.fast_mode=true",
|
|
431
|
+
];
|
|
432
|
+
if (permissionMode === "bypassPermissions") args.push("--dangerously-bypass-approvals-and-sandbox");
|
|
433
|
+
else if (permissionMode === "acceptEdits" || permissionMode === "auto") args.push("--full-auto");
|
|
434
|
+
else if (permissionMode === "plan") args.push("--sandbox", "read-only");
|
|
435
|
+
if (normalizedEffort) args.push("--config", `model_reasoning_effort=${normalizedEffort}`);
|
|
436
|
+
if (normalizedEffort !== "none") args.push("--config", `model_reasoning_summary=${tomlValue("auto")}`);
|
|
437
|
+
if (hasEntries(mcpServers)) args.push(...codexMcpConfigArgs(mcpServers));
|
|
438
|
+
args.push([systemPrompt, prompt].filter((part) => String(part || "").trim()).join("\n\n"));
|
|
439
|
+
return { command: "codex", args, cwd };
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export async function generateCliResponse(systemPrompt, options = {}) {
|
|
443
|
+
const start = Date.now();
|
|
444
|
+
const resolved = options.model;
|
|
445
|
+
const prompt = promptFromMessages(options.messages);
|
|
446
|
+
const dir = mkdtempSync(join(tmpdir(), readRuntimeBrand().tempdirPrefix));
|
|
447
|
+
const schemaPath = options.outputSchema ? join(dir, "output-schema.json") : null;
|
|
448
|
+
if (schemaPath) writeFileSync(schemaPath, JSON.stringify(options.outputSchema));
|
|
449
|
+
const mcpServers = options.mcpServers || {};
|
|
450
|
+
const mcpConfigPath = hasEntries(mcpServers) && resolved.sdk === "claude-code"
|
|
451
|
+
? join(dir, "mcp.json")
|
|
452
|
+
: null;
|
|
453
|
+
if (mcpConfigPath) writeFileSync(mcpConfigPath, JSON.stringify({ mcpServers }, null, 2));
|
|
454
|
+
const reusableSessionId = (typeof options.sessionId === "string" && options.sessionId.trim())
|
|
455
|
+
|| (typeof options.providerSessionId === "string" && options.providerSessionId.trim())
|
|
456
|
+
|| null;
|
|
457
|
+
let providerSessionId = reusableSessionId || null;
|
|
458
|
+
const commandSpec = buildCliCommand({
|
|
459
|
+
sdk: resolved.sdk,
|
|
460
|
+
model: resolved.model,
|
|
461
|
+
effort: options.effort,
|
|
462
|
+
cwd: options.cwd || process.cwd(),
|
|
463
|
+
schemaPath,
|
|
464
|
+
outputSchema: options.outputSchema,
|
|
465
|
+
systemPrompt,
|
|
466
|
+
prompt,
|
|
467
|
+
mcpConfigPath,
|
|
468
|
+
mcpServers,
|
|
469
|
+
allowedTools: options.allowedTools,
|
|
470
|
+
disallowedTools: options.disallowedTools,
|
|
471
|
+
permissionMode: options.permissionMode,
|
|
472
|
+
maxTurns: options.maxTurns,
|
|
473
|
+
skillDirs: Array.isArray(options.skillDirs)
|
|
474
|
+
? options.skillDirs
|
|
475
|
+
: getSkillAccessDirs(options.skills || []),
|
|
476
|
+
resumeSessionId: reusableSessionId,
|
|
477
|
+
nativeSubagents: options.nativeSubagents,
|
|
478
|
+
contextWindow: options.contextWindow,
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
const events = [];
|
|
482
|
+
const texts = [];
|
|
483
|
+
let errorMessage = null;
|
|
484
|
+
let failureKind = null;
|
|
485
|
+
let usage = {};
|
|
486
|
+
// Claude Code returns structured output via a `StructuredOutput` tool_use
|
|
487
|
+
// block. We capture the latest one we see during the run; if `outputSchema`
|
|
488
|
+
// was supplied the bridge surfaces it as `structuredResult` so the host's
|
|
489
|
+
// result parser can validate it without re-walking the event stream.
|
|
490
|
+
let structuredResult;
|
|
491
|
+
function captureStructuredOutputFromRaw(raw) {
|
|
492
|
+
const blocks = raw?.message?.content || raw?.content;
|
|
493
|
+
if (!Array.isArray(blocks)) return;
|
|
494
|
+
for (const block of blocks) {
|
|
495
|
+
if (block?.type === "tool_use" && block?.name === "StructuredOutput" && block?.input !== undefined) {
|
|
496
|
+
structuredResult = block.input;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
const cliEventContext = {
|
|
501
|
+
cwd: commandSpec.cwd,
|
|
502
|
+
fileChangeSnapshots: new Map(),
|
|
503
|
+
thinkingBuffer: createThinkingBuffer(),
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
const stderrTail = createStderrTail({ limit: 8 * 1024 });
|
|
507
|
+
try {
|
|
508
|
+
const child = spawn(commandSpec.command, commandSpec.args, {
|
|
509
|
+
cwd: commandSpec.cwd,
|
|
510
|
+
env: process.env,
|
|
511
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
child.stderr.on("data", (chunk) => stderrTail.push(chunk));
|
|
515
|
+
|
|
516
|
+
const rl = createInterface({ input: child.stdout });
|
|
517
|
+
rl.on("line", (line) => {
|
|
518
|
+
if (!line.trim()) return;
|
|
519
|
+
let raw;
|
|
520
|
+
try {
|
|
521
|
+
raw = JSON.parse(line);
|
|
522
|
+
} catch {
|
|
523
|
+
const ev = { type: "cli_stdout", text: line };
|
|
524
|
+
events.push(ev);
|
|
525
|
+
options.onEvent?.(ev);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
const ev = normalizeCliEvent(raw, cliEventContext);
|
|
529
|
+
if (ev) {
|
|
530
|
+
events.push(ev);
|
|
531
|
+
options.onEvent?.(ev);
|
|
532
|
+
}
|
|
533
|
+
if (!isCodexReasoningEvent(raw)) {
|
|
534
|
+
const text = textFromEvent(raw);
|
|
535
|
+
pushUniqueText(texts, text);
|
|
536
|
+
}
|
|
537
|
+
captureStructuredOutputFromRaw(raw);
|
|
538
|
+
if (raw.usage) usage = raw.usage;
|
|
539
|
+
// intelligence-ramp Phase 5.1: capture session_id from CLI events so the
|
|
540
|
+
// coordinator can chain it on the next continuation. Claude Code emits
|
|
541
|
+
// session_id on the init system message and again on the result event.
|
|
542
|
+
const candidateSessionId = raw.session_id ?? raw.sessionId ?? raw.thread_id ?? null;
|
|
543
|
+
if (typeof candidateSessionId === "string" && candidateSessionId.trim().length > 0) {
|
|
544
|
+
providerSessionId = candidateSessionId.trim();
|
|
545
|
+
}
|
|
546
|
+
if (raw.type === "error") {
|
|
547
|
+
const rawError = raw.message || raw.error || "cli error";
|
|
548
|
+
errorMessage = typeof rawError === "string" ? rawError : JSON.stringify(rawError);
|
|
549
|
+
failureKind = "provider_unavailable";
|
|
550
|
+
}
|
|
551
|
+
const resultError = resultEventError(raw, commandSpec.command);
|
|
552
|
+
if (resultError) {
|
|
553
|
+
errorMessage = resultError.message;
|
|
554
|
+
failureKind = resultError.failureKind;
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
if (options.abortSignal) {
|
|
559
|
+
const abort = () => child.kill("SIGTERM");
|
|
560
|
+
if (options.abortSignal.aborted) abort();
|
|
561
|
+
else options.abortSignal.addEventListener("abort", abort, { once: true });
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const exitCode = await new Promise((resolve) => child.on("close", resolve));
|
|
565
|
+
const stderrText = stderrTail.toString().trim();
|
|
566
|
+
let cliErrorCode = null;
|
|
567
|
+
if (exitCode !== 0 && !errorMessage) errorMessage = stderrText || `${commandSpec.command} exited ${exitCode}`;
|
|
568
|
+
const text = texts[texts.length - 1] || "";
|
|
569
|
+
const hadPartialProgress = events.length > 0 || texts.length > 0;
|
|
570
|
+
if (exitCode === 0 && !errorMessage && !text.trim() && structuredResult === undefined) {
|
|
571
|
+
errorMessage = `${commandSpec.command} completed without final output`;
|
|
572
|
+
failureKind = failureKind || "provider_unavailable";
|
|
573
|
+
cliErrorCode = "cli_stream_terminated";
|
|
574
|
+
}
|
|
575
|
+
const reference = `${resolved.sdk}:${resolved.model}`;
|
|
576
|
+
const inputTokens = usage?.input_tokens ?? usage?.inputTokens ?? 0;
|
|
577
|
+
const outputTokens = usage?.output_tokens ?? usage?.outputTokens ?? 0;
|
|
578
|
+
const cachedTokens = usage?.cache_read_tokens ?? usage?.cache_read_input_tokens ?? 0;
|
|
579
|
+
const cacheCreationTokens = usage?.cache_creation_tokens ?? usage?.cache_creation_input_tokens ?? 0;
|
|
580
|
+
const costUsd = estimateCost({
|
|
581
|
+
resolveCustomPricing: options.resolveCustomPricing,
|
|
582
|
+
model: reference,
|
|
583
|
+
inputTokens,
|
|
584
|
+
outputTokens,
|
|
585
|
+
cachedTokens,
|
|
586
|
+
cacheWriteTokens: cacheCreationTokens,
|
|
587
|
+
});
|
|
588
|
+
const enrichedUsage = {
|
|
589
|
+
...usage,
|
|
590
|
+
input_tokens: inputTokens || null,
|
|
591
|
+
output_tokens: outputTokens || null,
|
|
592
|
+
cache_read_tokens: cachedTokens || null,
|
|
593
|
+
cache_creation_tokens: cacheCreationTokens || null,
|
|
594
|
+
cost_usd: costUsd,
|
|
595
|
+
};
|
|
596
|
+
return {
|
|
597
|
+
text,
|
|
598
|
+
structuredResult,
|
|
599
|
+
structuredResultSource: structuredResult === undefined ? null : "StructuredOutput",
|
|
600
|
+
events,
|
|
601
|
+
usage: enrichedUsage,
|
|
602
|
+
durationMs: Date.now() - start,
|
|
603
|
+
numTurns: texts.length || (events.length ? 1 : 0),
|
|
604
|
+
model: reference,
|
|
605
|
+
effort: options.effort || null,
|
|
606
|
+
sdk: resolved.sdk,
|
|
607
|
+
providerSessionId: providerSessionId || null,
|
|
608
|
+
provider_session_id: providerSessionId || null,
|
|
609
|
+
cancelled: !!options.abortSignal?.aborted,
|
|
610
|
+
error: errorMessage ? formatCliError(errorMessage, commandSpec.command) : null,
|
|
611
|
+
failureKind,
|
|
612
|
+
stderrTail: stderrText || null,
|
|
613
|
+
diagnostics: {
|
|
614
|
+
...(cliErrorCode ? { pi_error_code: cliErrorCode } : {}),
|
|
615
|
+
...(hadPartialProgress && failureKind === "provider_unavailable"
|
|
616
|
+
? { had_partial_progress: true }
|
|
617
|
+
: {}),
|
|
618
|
+
},
|
|
619
|
+
capabilitiesUsed: buildCapabilitiesUsed({
|
|
620
|
+
promptCacheActive: (cachedTokens || 0) > 0 || (cacheCreationTokens || 0) > 0,
|
|
621
|
+
thinkingEnabled: null,
|
|
622
|
+
structuredOutputEnforced: !!options.outputSchema,
|
|
623
|
+
subagentInvoked: null,
|
|
624
|
+
mcpServersUsed: Object.keys(options.mcpServers || {}),
|
|
625
|
+
nativeSubagentsUsed: [],
|
|
626
|
+
toolCompactionApplied: false,
|
|
627
|
+
contextCompactionApplied: null,
|
|
628
|
+
}),
|
|
629
|
+
};
|
|
630
|
+
} catch (err) {
|
|
631
|
+
return {
|
|
632
|
+
text: texts[texts.length - 1] || null,
|
|
633
|
+
structuredResult,
|
|
634
|
+
structuredResultSource: structuredResult === undefined ? null : "StructuredOutput",
|
|
635
|
+
events,
|
|
636
|
+
usage: {},
|
|
637
|
+
durationMs: Date.now() - start,
|
|
638
|
+
numTurns: texts.length || (events.length ? 1 : 0),
|
|
639
|
+
model: resolved?.reference || null,
|
|
640
|
+
effort: options.effort || null,
|
|
641
|
+
sdk: resolved?.sdk || "cli",
|
|
642
|
+
providerSessionId: providerSessionId || null,
|
|
643
|
+
provider_session_id: providerSessionId || null,
|
|
644
|
+
cancelled: !!options.abortSignal?.aborted,
|
|
645
|
+
error: err.message || String(err),
|
|
646
|
+
failureKind: failureKind || "provider_unavailable",
|
|
647
|
+
stderrTail: stderrTail.toString() || null,
|
|
648
|
+
diagnostics: {
|
|
649
|
+
...(err?.code ? { pi_error_code: String(err.code) } : {}),
|
|
650
|
+
...(events.length > 0 || texts.length > 0 ? { had_partial_progress: true } : {}),
|
|
651
|
+
},
|
|
652
|
+
capabilitiesUsed: buildCapabilitiesUsed({
|
|
653
|
+
promptCacheActive: null,
|
|
654
|
+
thinkingEnabled: null,
|
|
655
|
+
structuredOutputEnforced: !!options.outputSchema,
|
|
656
|
+
subagentInvoked: null,
|
|
657
|
+
mcpServersUsed: Object.keys(options.mcpServers || {}),
|
|
658
|
+
nativeSubagentsUsed: [],
|
|
659
|
+
toolCompactionApplied: false,
|
|
660
|
+
contextCompactionApplied: null,
|
|
661
|
+
}),
|
|
662
|
+
};
|
|
663
|
+
} finally {
|
|
664
|
+
try { rmSync(dir, { recursive: true, force: true }); } catch {}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
export const claudeCodeBackend = {
|
|
669
|
+
kind: "claude-code",
|
|
670
|
+
capabilities: { kind: "claude-code", runtime: "cli", ...DORMANT_CLI_CAPABILITIES },
|
|
671
|
+
execute: generateCliResponse,
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
export const codexCliBackend = {
|
|
675
|
+
kind: "codex-cli",
|
|
676
|
+
capabilities: { kind: "codex-cli", runtime: "cli", ...DORMANT_CLI_CAPABILITIES },
|
|
677
|
+
execute: generateCliResponse,
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
// CLI bridge for sdk='claude' agents that opt into execution_mode='cli'.
|
|
681
|
+
// generateCliResponse internally branches on resolved.sdk; the SDK shape
|
|
682
|
+
// from parseModelReference uses 'claude', the CLI builder expects
|
|
683
|
+
// 'claude-code', so we shim the model ref here rather than scattering
|
|
684
|
+
// translation logic across the registry.
|
|
685
|
+
export const claudeCodeRuntimeBridge = {
|
|
686
|
+
id: "claude-code",
|
|
687
|
+
kind: "claude-code",
|
|
688
|
+
capabilities: { kind: "claude-code", runtime: "cli", ...DORMANT_CLI_CAPABILITIES },
|
|
689
|
+
supports: (ref, options) => ref?.sdk === "claude" && options?.executionMode === "cli",
|
|
690
|
+
execute: (systemPrompt, options) => generateCliResponse(systemPrompt, {
|
|
691
|
+
...options,
|
|
692
|
+
model: { ...options.model, sdk: "claude-code" },
|
|
693
|
+
}),
|
|
694
|
+
};
|