@tangle-network/sdk-provider-cli-base 0.13.16 → 0.13.18
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/dist/adapters/amp/index.d.mts +250 -0
- package/dist/adapters/amp/index.mjs +233 -0
- package/dist/adapters/factory-droids/index.d.mts +200 -0
- package/dist/adapters/factory-droids/index.mjs +356 -0
- package/dist/adapters/grok/index.d.mts +27 -0
- package/dist/adapters/grok/index.mjs +56 -0
- package/dist/adapters/hermes/index.d.mts +148 -0
- package/dist/adapters/hermes/index.mjs +244 -0
- package/dist/adapters/nanoclaw/index.d.mts +50 -0
- package/dist/adapters/nanoclaw/index.mjs +116 -0
- package/dist/adapters/openclaw/index.d.mts +68 -0
- package/dist/adapters/openclaw/index.mjs +240 -0
- package/dist/adapters/prime/index.d.mts +126 -0
- package/dist/adapters/prime/index.mjs +446 -0
- package/dist/conformance/index.d.mts +1 -1
- package/dist/index-BpO3fvbf.d.mts +1254 -0
- package/dist/index.d.mts +3 -1254
- package/dist/index.mjs +1 -4585
- package/dist/{prompt-input-BHn_1z69.d.mts → prompt-input-CD7okX8T.d.mts} +1 -1
- package/dist/src-Fn8Pu1xq.mjs +4586 -0
- package/package.json +35 -6
package/dist/index.mjs
CHANGED
|
@@ -1,4586 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { CODEX_DISABLED_FEATURE_ARGS, HARNESS_LAUNCH_MATERIALIZE_OPTIONS, UNAVAILABLE_AGENT_PROFILE_SECRET_PROVIDER, applyWorkspacePlanWithSecretProvider, bindLaunchToolBinDirs, bindSecretSessionView, codexDeveloperInstructionArgs, enforceWorkspacePlanSupport, ensurePublicConcurrentProfileRoots, joinSystemPromptAdditions, keepsHarnessSystemPrompt, materializeProfile, modelIdsMatch, parseSandboxToolBinDirs, preparePublicConcurrentProfileRevision, prepareSecretConcurrentProfileRevision, resolveAgentProfileResources, secretSessionViewWorkspace } from "@tangle-network/agent-profile-materialize";
|
|
3
|
-
import { BridgeClosedError, InvalidAttachmentError, JsonRpcError, RpcRequestTimeoutError, execCli, launchCliProcess, resolveRawCapturePath, spawnCliProcess, spawnRpcBridge } from "@tangle-network/sdk-cli-runner";
|
|
4
|
-
import { createHash, createHmac, randomUUID } from "node:crypto";
|
|
5
|
-
import { AgentExecutionError, AgentExecutionOutcomeSchema, AgentRuntimeAttachmentsSchema, InteractionExecutionBindingSchema, InteractionKind, REASONING_EFFORTS, agentProfileSchema, nativeReasoningControl, normalizeInputParts } from "@tangle-network/agent-interface";
|
|
6
|
-
import { HarnessSessionContractError, assertArgvHonoursSessionContract, assertFlagsHonourSessionContract, assertStoreLocationHonoursSessionContract, compileSessionStoreGlob } from "@tangle-network/cli-agent-registry/session-contract";
|
|
7
|
-
import { MAX_SECRET_PROFILE_REPLAY_CHARS, RESTORED_SESSION_CONTEXT_PREAMBLE, RESTORED_SESSION_CONTEXT_TAG, buildCompleteForkContextBlock, buildForkContextBlock, createMessageStore, createSessionStore, forkSessionWithMessages, readProviderMessageIds } from "@tangle-network/sdk-session-persistence";
|
|
8
|
-
import { mkdir, open, readdir, writeFile } from "node:fs/promises";
|
|
9
|
-
import path, { basename, dirname, isAbsolute, join, normalize, resolve } from "node:path";
|
|
10
|
-
import * as fs$1 from "node:fs";
|
|
11
|
-
import fs, { constants, lstatSync, readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
12
|
-
import { execFile } from "node:child_process";
|
|
13
|
-
import { isDeepStrictEqual, promisify } from "node:util";
|
|
14
|
-
import { messageStorageDirOf } from "@tangle-network/cli-agent-registry";
|
|
15
|
-
import { NATIVE_SESSION_CONTRACTS, NATIVE_SESSION_CONTRACTS as NATIVE_SESSION_CONTRACTS$1, configDirHomeEnv } from "@tangle-network/cli-agent-registry/native-session-contracts";
|
|
16
|
-
import * as W3C from "@tangle-network/agent-trace-contract";
|
|
17
|
-
import { z } from "zod";
|
|
18
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
19
|
-
import { DatabaseSync } from "node:sqlite";
|
|
20
|
-
import { StringDecoder } from "node:string_decoder";
|
|
21
|
-
import { DEFAULT_INTERACTION_MCP_SERVER_NAME, InteractionMcpServer as InteractionMcpServer$1, interactionMcpToolName } from "@tangle-network/agent-core/interactions/mcp";
|
|
22
|
-
import { isRecord, isRecord as isPlainRecord } from "@tangle-network/agent-core";
|
|
23
|
-
//#region src/acp-process-launch.ts
|
|
24
|
-
/** Spawn an ACP agent over NDJSON stdio from `env` alone, never the sidecar's own environment. The adapter owns abort, so no signal. */
|
|
25
|
-
function spawnAcpBridge(request) {
|
|
26
|
-
return spawnRpcBridge({
|
|
27
|
-
...request,
|
|
28
|
-
inheritEnv: false
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* A JSON-RPC error with the cause the agent put in `data`: hermes sends
|
|
33
|
-
* `data.details`, codex-acp `data.message`.
|
|
34
|
-
*/
|
|
35
|
-
function jsonRpcErrorText(err) {
|
|
36
|
-
const data = err.data && typeof err.data === "object" ? err.data : void 0;
|
|
37
|
-
const cause = [data?.details, data?.message].find((value) => typeof value === "string" && value.length > 0);
|
|
38
|
-
return cause && cause !== err.message ? `${err.message}: ${cause}` : err.message;
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* An ACP process env: the spec's keys with the agent environment over them,
|
|
42
|
-
* unset keys dropped. The agent environment is the whole inherited part.
|
|
43
|
-
*/
|
|
44
|
-
function withAgentEnvironment(env, agent) {
|
|
45
|
-
const merged = {};
|
|
46
|
-
for (const [key, value] of Object.entries(env ?? {})) if (value !== void 0) merged[key] = value;
|
|
47
|
-
for (const [key, value] of Object.entries(agent)) if (value !== void 0) merged[key] = value;
|
|
48
|
-
return merged;
|
|
49
|
-
}
|
|
50
|
-
//#endregion
|
|
51
|
-
//#region src/acp-protocol.ts
|
|
52
|
-
/** The ACP protocol version this client speaks, and the oldest it accepts. */
|
|
53
|
-
const ACP_PROTOCOL_VERSION = 1;
|
|
54
|
-
const ACP_MIN_PROTOCOL_VERSION = 1;
|
|
55
|
-
/** Text carried by ACP content blocks, including tool-call content. */
|
|
56
|
-
function acpContentText(content) {
|
|
57
|
-
if (!content) return "";
|
|
58
|
-
const blocks = Array.isArray(content) ? content : [content];
|
|
59
|
-
const parts = [];
|
|
60
|
-
for (const block of blocks) {
|
|
61
|
-
if (!block || typeof block !== "object") continue;
|
|
62
|
-
if (block.type === "text" && typeof block.text === "string") parts.push(block.text);
|
|
63
|
-
else if (block.type === "content" && block.content?.type === "text" && typeof block.content.text === "string") parts.push(block.content.text);
|
|
64
|
-
}
|
|
65
|
-
return parts.join("");
|
|
66
|
-
}
|
|
67
|
-
//#endregion
|
|
68
|
-
//#region src/reasoning-parts.ts
|
|
69
|
-
/**
|
|
70
|
-
* Assigns a stable part id and an append-only delta to a reasoning block
|
|
71
|
-
* emission, absorbing provider replay/growth patterns:
|
|
72
|
-
*
|
|
73
|
-
* - text exactly matches an already-tracked block → `null` (duplicate
|
|
74
|
-
* delivery, e.g. a final assistant message replaying streamed content —
|
|
75
|
-
* emit nothing). Byte-identical distinct blocks therefore dedupe to one
|
|
76
|
-
* part; safer than duplicate-part spam.
|
|
77
|
-
* - text extends the most recent block → same part id, delta = the suffix
|
|
78
|
-
* (cumulative re-emission of a growing block).
|
|
79
|
-
* - otherwise → a new block: fresh part id, delta = the full text, so
|
|
80
|
-
* consumers can be pure appenders (prev text for a fresh id is empty).
|
|
81
|
-
*/
|
|
82
|
-
function trackReasoningBlock(state, blockText) {
|
|
83
|
-
if (blockText === "") return null;
|
|
84
|
-
state.reasoningParts ??= [];
|
|
85
|
-
const parts = state.reasoningParts;
|
|
86
|
-
if (parts.some((p) => p.text === blockText)) return null;
|
|
87
|
-
const last = parts[parts.length - 1];
|
|
88
|
-
if (last && last.text !== "" && blockText.startsWith(last.text)) {
|
|
89
|
-
const delta = blockText.slice(last.text.length);
|
|
90
|
-
last.text = blockText;
|
|
91
|
-
return {
|
|
92
|
-
partId: last.id,
|
|
93
|
-
text: blockText,
|
|
94
|
-
delta,
|
|
95
|
-
isNewBlock: false
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
const id = randomUUID();
|
|
99
|
-
parts.push({
|
|
100
|
-
id,
|
|
101
|
-
text: blockText
|
|
102
|
-
});
|
|
103
|
-
return {
|
|
104
|
-
partId: id,
|
|
105
|
-
text: blockText,
|
|
106
|
-
delta: blockText,
|
|
107
|
-
isNewBlock: true
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
//#endregion
|
|
111
|
-
//#region src/stream-parts.ts
|
|
112
|
-
/**
|
|
113
|
-
* The text, reasoning and tool part emissions every stream-json harness
|
|
114
|
-
* shares. A provider's event mapper decodes its own wire shape, then calls
|
|
115
|
-
* these so part ids, buffers, tool timing and processing phases stay one
|
|
116
|
-
* implementation across claude-code, codex, kimi, amp and factory-droids.
|
|
117
|
-
*/
|
|
118
|
-
/**
|
|
119
|
-
* Records the CLI's native session id on the turn and announces it: the
|
|
120
|
-
* generic `session.updated` plus the provider-attributed observation the
|
|
121
|
-
* turn observer keys evidence on.
|
|
122
|
-
*/
|
|
123
|
-
function sessionObservedEvents(state, provider, sessionId) {
|
|
124
|
-
state.sessionId = sessionId;
|
|
125
|
-
return [{
|
|
126
|
-
type: "session.updated",
|
|
127
|
-
sessionId
|
|
128
|
-
}, {
|
|
129
|
-
type: "native.session.observed",
|
|
130
|
-
provider,
|
|
131
|
-
nativeSessionId: sessionId
|
|
132
|
-
}];
|
|
133
|
-
}
|
|
134
|
-
/** Correlation fields every emitted part carries. */
|
|
135
|
-
function streamPartBase(state, partId) {
|
|
136
|
-
return {
|
|
137
|
-
id: partId ?? randomUUID(),
|
|
138
|
-
sessionID: state.sessionId ?? "",
|
|
139
|
-
messageID: state.messageId ?? ""
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
function textPartEvent(state, delta) {
|
|
143
|
-
state.textPartId ??= randomUUID();
|
|
144
|
-
return {
|
|
145
|
-
type: "message.part.updated",
|
|
146
|
-
part: {
|
|
147
|
-
...streamPartBase(state, state.textPartId),
|
|
148
|
-
type: "text",
|
|
149
|
-
text: state.textBuffer
|
|
150
|
-
},
|
|
151
|
-
delta
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Appends a text delta to the turn's single text part. The first text of a
|
|
156
|
-
* turn also signals the `generating` phase.
|
|
157
|
-
*/
|
|
158
|
-
function textDeltaEvents(state, delta) {
|
|
159
|
-
const first = state.textBuffer.length === 0;
|
|
160
|
-
state.textBuffer += delta;
|
|
161
|
-
const update = textPartEvent(state, delta);
|
|
162
|
-
return first ? [{
|
|
163
|
-
type: "model-processing",
|
|
164
|
-
phase: "generating"
|
|
165
|
-
}, update] : [update];
|
|
166
|
-
}
|
|
167
|
-
/** The whole reply as one text part, for harnesses that do not stream. */
|
|
168
|
-
function finalTextEvent(state, text) {
|
|
169
|
-
state.textBuffer = text;
|
|
170
|
-
return textPartEvent(state, text);
|
|
171
|
-
}
|
|
172
|
-
/**
|
|
173
|
-
* Applies a cumulative text frame (the whole assistant text so far). Only the
|
|
174
|
-
* suffix past the buffer is emitted; a frame that does not extend the buffer
|
|
175
|
-
* replaces it and is emitted whole.
|
|
176
|
-
*/
|
|
177
|
-
function textSnapshotEvents(state, text) {
|
|
178
|
-
const delta = text.startsWith(state.textBuffer) ? text.slice(state.textBuffer.length) : text;
|
|
179
|
-
if (!delta) return [];
|
|
180
|
-
state.textBuffer = text;
|
|
181
|
-
return [textPartEvent(state, delta)];
|
|
182
|
-
}
|
|
183
|
-
/**
|
|
184
|
-
* Records a thinking block under a stable part id (see trackReasoningBlock).
|
|
185
|
-
* With `signalPhase`, the turn's first block also signals `thinking`.
|
|
186
|
-
*/
|
|
187
|
-
function reasoningEvents(state, thinking, options) {
|
|
188
|
-
const tracked = trackReasoningBlock(state, thinking);
|
|
189
|
-
if (!tracked) return [];
|
|
190
|
-
if (tracked.isNewBlock) state.reasoningBuffer.push(tracked.text);
|
|
191
|
-
else state.reasoningBuffer[state.reasoningBuffer.length - 1] = tracked.text;
|
|
192
|
-
const update = {
|
|
193
|
-
type: "message.part.updated",
|
|
194
|
-
part: {
|
|
195
|
-
...streamPartBase(state, tracked.partId),
|
|
196
|
-
type: "reasoning",
|
|
197
|
-
text: tracked.text
|
|
198
|
-
},
|
|
199
|
-
delta: tracked.delta
|
|
200
|
-
};
|
|
201
|
-
return options.signalPhase && tracked.isNewBlock && state.reasoningBuffer.length === 1 ? [{
|
|
202
|
-
type: "model-processing",
|
|
203
|
-
phase: "thinking"
|
|
204
|
-
}, update] : [update];
|
|
205
|
-
}
|
|
206
|
-
/** Registers a running tool call and emits its running part. */
|
|
207
|
-
function toolStartEvents(state, call) {
|
|
208
|
-
const now = Date.now();
|
|
209
|
-
state.toolCalls.set(call.id, {
|
|
210
|
-
id: call.id,
|
|
211
|
-
name: call.name,
|
|
212
|
-
input: call.input,
|
|
213
|
-
startTime: now,
|
|
214
|
-
status: "running"
|
|
215
|
-
});
|
|
216
|
-
const toolState = {
|
|
217
|
-
status: "running",
|
|
218
|
-
input: call.input ?? {},
|
|
219
|
-
time: { start: now }
|
|
220
|
-
};
|
|
221
|
-
return [{
|
|
222
|
-
type: "message.part.updated",
|
|
223
|
-
part: {
|
|
224
|
-
...streamPartBase(state, call.id),
|
|
225
|
-
type: "tool",
|
|
226
|
-
callID: call.id,
|
|
227
|
-
tool: call.name,
|
|
228
|
-
state: toolState
|
|
229
|
-
}
|
|
230
|
-
}];
|
|
231
|
-
}
|
|
232
|
-
/**
|
|
233
|
-
* Completes a tracked tool call. A successful result emits `afterSuccess`
|
|
234
|
-
* (file parts the provider detected) and then the `tool-result` phase. A
|
|
235
|
-
* result for an untracked call is a warning.
|
|
236
|
-
*/
|
|
237
|
-
function toolResultEvents(state, result) {
|
|
238
|
-
const toolCall = state.toolCalls.get(result.id);
|
|
239
|
-
if (!toolCall) return [{
|
|
240
|
-
type: "warning",
|
|
241
|
-
code: "UNKNOWN_TOOL_RESULT",
|
|
242
|
-
message: `Received result for unknown tool call: ${result.id}`
|
|
243
|
-
}];
|
|
244
|
-
toolCall.status = result.isError ? "failed" : "completed";
|
|
245
|
-
toolCall.output = result.content;
|
|
246
|
-
const now = Date.now();
|
|
247
|
-
const input = toolCall.input ?? {};
|
|
248
|
-
const time = {
|
|
249
|
-
start: toolCall.startTime,
|
|
250
|
-
end: now
|
|
251
|
-
};
|
|
252
|
-
const metadata = result.metadata ? { metadata: result.metadata } : {};
|
|
253
|
-
const toolState = result.isError ? {
|
|
254
|
-
status: "failed",
|
|
255
|
-
input,
|
|
256
|
-
error: typeof result.content === "string" ? result.content : void 0,
|
|
257
|
-
output: result.content,
|
|
258
|
-
...metadata,
|
|
259
|
-
time
|
|
260
|
-
} : {
|
|
261
|
-
status: "completed",
|
|
262
|
-
input,
|
|
263
|
-
output: result.content,
|
|
264
|
-
...metadata,
|
|
265
|
-
time
|
|
266
|
-
};
|
|
267
|
-
const events = [{
|
|
268
|
-
type: "message.part.updated",
|
|
269
|
-
part: {
|
|
270
|
-
...streamPartBase(state, result.id),
|
|
271
|
-
type: "tool",
|
|
272
|
-
callID: result.id,
|
|
273
|
-
tool: toolCall.name,
|
|
274
|
-
state: toolState
|
|
275
|
-
}
|
|
276
|
-
}];
|
|
277
|
-
if (!result.isError) events.push(...result.afterSuccess?.(toolCall) ?? [], {
|
|
278
|
-
type: "model-processing",
|
|
279
|
-
phase: "tool-result",
|
|
280
|
-
toolName: toolCall.name,
|
|
281
|
-
elapsedMs: now - toolCall.startTime
|
|
282
|
-
});
|
|
283
|
-
return events;
|
|
284
|
-
}
|
|
285
|
-
//#endregion
|
|
286
|
-
//#region src/acp-session-updates.ts
|
|
287
|
-
/** An agent message or thought chunk: text extends the message part, thought joins reasoning. */
|
|
288
|
-
function acpMessageChunkEvents(kind, update, state) {
|
|
289
|
-
const text = acpContentText(update.content);
|
|
290
|
-
if (!text) return [];
|
|
291
|
-
if (kind === "agent_message_chunk") return textDeltaEvents(state, text);
|
|
292
|
-
state.reasoningBuffer.push(text);
|
|
293
|
-
return [];
|
|
294
|
-
}
|
|
295
|
-
/** A new tool call: recorded as running and reported as its tool part. */
|
|
296
|
-
function acpToolCallEvents(update, state) {
|
|
297
|
-
const call = update;
|
|
298
|
-
const name = call.title ?? call.kind ?? "tool";
|
|
299
|
-
return toolStartEvents(state, {
|
|
300
|
-
id: call.toolCallId,
|
|
301
|
-
name,
|
|
302
|
-
input: call.rawInput
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
|
-
/**
|
|
306
|
-
* Fold one tool_call_update into the call's record. Tool output arrives as
|
|
307
|
-
* content blocks; rawOutput is the fallback. Returns the update's text output.
|
|
308
|
-
*/
|
|
309
|
-
function applyAcpToolCallUpdate(existing, mapped, change) {
|
|
310
|
-
const outputText = acpContentText(change.content);
|
|
311
|
-
if (mapped === "running" && outputText) existing.output = typeof existing.output === "string" ? existing.output + outputText : outputText;
|
|
312
|
-
existing.status = mapped;
|
|
313
|
-
if (mapped === "completed" || mapped === "failed") {
|
|
314
|
-
if (outputText) existing.output = typeof existing.output === "string" && existing.output.length > 0 ? existing.output + outputText : outputText;
|
|
315
|
-
else if (change.rawOutput !== void 0) existing.output = change.rawOutput;
|
|
316
|
-
}
|
|
317
|
-
return outputText;
|
|
318
|
-
}
|
|
319
|
-
/** The events a call's current record reports: its settled result, or its open part. */
|
|
320
|
-
function acpToolCallUpdateEvents(existing, state) {
|
|
321
|
-
if (existing.status === "completed" || existing.status === "failed") return toolResultEvents(state, {
|
|
322
|
-
id: existing.id,
|
|
323
|
-
content: existing.output,
|
|
324
|
-
isError: existing.status === "failed"
|
|
325
|
-
});
|
|
326
|
-
const input = existing.input ?? {};
|
|
327
|
-
return [{
|
|
328
|
-
type: "message.part.updated",
|
|
329
|
-
part: {
|
|
330
|
-
...streamPartBase(state, existing.id),
|
|
331
|
-
type: "tool",
|
|
332
|
-
tool: existing.name,
|
|
333
|
-
callID: existing.id,
|
|
334
|
-
state: existing.status === "running" ? {
|
|
335
|
-
status: "running",
|
|
336
|
-
input,
|
|
337
|
-
time: { start: existing.startTime }
|
|
338
|
-
} : {
|
|
339
|
-
status: "pending",
|
|
340
|
-
input
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
}];
|
|
344
|
-
}
|
|
345
|
-
/**
|
|
346
|
-
* Plan entries are the agent's step list. Fold their text into the
|
|
347
|
-
* reasoning record, bounded per notification and per turn so a
|
|
348
|
-
* misbehaving agent cannot grow turn memory or storage without limit.
|
|
349
|
-
*/
|
|
350
|
-
function acpPlanEvents(update, state, provider) {
|
|
351
|
-
const MAX_ENTRIES_PER_UPDATE = 256;
|
|
352
|
-
const MAX_REASONING_ENTRIES_PER_TURN = 1024;
|
|
353
|
-
const MAX_ENTRY_LENGTH = 8 * 1024;
|
|
354
|
-
const raw = update.entries;
|
|
355
|
-
const entries = (Array.isArray(raw) ? raw : []).slice(0, MAX_ENTRIES_PER_UPDATE);
|
|
356
|
-
for (const entry of entries) {
|
|
357
|
-
if (state.reasoningBuffer.length >= MAX_REASONING_ENTRIES_PER_TURN) break;
|
|
358
|
-
if (typeof entry.content === "string" && entry.content.length > 0) state.reasoningBuffer.push(entry.content.slice(0, MAX_ENTRY_LENGTH));
|
|
359
|
-
}
|
|
360
|
-
return [{
|
|
361
|
-
type: "raw",
|
|
362
|
-
backend: provider,
|
|
363
|
-
event: {
|
|
364
|
-
sessionUpdate: "plan",
|
|
365
|
-
entries
|
|
366
|
-
}
|
|
367
|
-
}];
|
|
368
|
-
}
|
|
369
|
-
//#endregion
|
|
370
|
-
//#region src/provider-config.ts
|
|
371
|
-
/**
|
|
372
|
-
* Render a value as a digest that does not change with key order. The type
|
|
373
|
-
* parameter keeps the caller's own type at the call site, so the digest of a
|
|
374
|
-
* configuration cannot silently accept a value of an unrelated shape.
|
|
375
|
-
*/
|
|
376
|
-
function stableSerialize(value) {
|
|
377
|
-
if (value === null) return "null";
|
|
378
|
-
if (value === void 0) return "undefined";
|
|
379
|
-
if (typeof value === "string") return JSON.stringify(value);
|
|
380
|
-
if (typeof value === "number") {
|
|
381
|
-
if (Number.isNaN(value)) return "number:NaN";
|
|
382
|
-
if (Object.is(value, -0)) return "number:-0";
|
|
383
|
-
return `number:${value}`;
|
|
384
|
-
}
|
|
385
|
-
if (typeof value === "boolean") return value ? "true" : "false";
|
|
386
|
-
if (typeof value === "bigint") return `bigint:${value.toString()}`;
|
|
387
|
-
if (Array.isArray(value)) return `[${value.map((entry) => stableSerialize(entry)).join(",")}]`;
|
|
388
|
-
if (typeof value === "object") return `{${Object.entries(value).filter(([, entry]) => entry !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => `${JSON.stringify(key)}:${stableSerialize(entry)}`).join(",")}}`;
|
|
389
|
-
return `${typeof value}:${String(value)}`;
|
|
390
|
-
}
|
|
391
|
-
function cloneConfig(config) {
|
|
392
|
-
return structuredClone(config);
|
|
393
|
-
}
|
|
394
|
-
function isInteractionObject(value) {
|
|
395
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
396
|
-
}
|
|
397
|
-
function mergeCliProviderConfig(current, next) {
|
|
398
|
-
const merged = {
|
|
399
|
-
...current,
|
|
400
|
-
...next
|
|
401
|
-
};
|
|
402
|
-
const currentInteractions = current.interactions;
|
|
403
|
-
const nextInteractions = next.interactions;
|
|
404
|
-
if (isInteractionObject(currentInteractions) && isInteractionObject(nextInteractions)) merged.interactions = {
|
|
405
|
-
...currentInteractions,
|
|
406
|
-
...nextInteractions
|
|
407
|
-
};
|
|
408
|
-
return merged;
|
|
409
|
-
}
|
|
410
|
-
//#endregion
|
|
411
|
-
//#region src/adapter-initialization.ts
|
|
412
|
-
/**
|
|
413
|
-
* The in-flight and settled initialization of one adapter, and the pending
|
|
414
|
-
* shutdown: which `initialize()` call is current, which configuration it
|
|
415
|
-
* carries, and the generation that a `shutdown()` bumps to cancel it.
|
|
416
|
-
*/
|
|
417
|
-
var InitializationTracker = class {
|
|
418
|
-
generation = 0;
|
|
419
|
-
promise = null;
|
|
420
|
-
configKey = null;
|
|
421
|
-
cancellation = null;
|
|
422
|
-
initializedConfigKey = null;
|
|
423
|
-
shutdownPromise;
|
|
424
|
-
providerTails = /* @__PURE__ */ new Set();
|
|
425
|
-
constructor(provider) {
|
|
426
|
-
this.provider = provider;
|
|
427
|
-
}
|
|
428
|
-
assertCurrent(generation, cancellation) {
|
|
429
|
-
if (generation !== this.generation || cancellation.cancelled) throw new Error(`${this.provider()} provider initialization cancelled by shutdown`);
|
|
430
|
-
}
|
|
431
|
-
/** Forget `promise` once it settles, unless a newer one replaced it. */
|
|
432
|
-
clear(promise) {
|
|
433
|
-
if (this.promise !== promise) return;
|
|
434
|
-
this.promise = null;
|
|
435
|
-
this.configKey = null;
|
|
436
|
-
this.cancellation = null;
|
|
437
|
-
}
|
|
438
|
-
/** Cancel the in-flight initialization and invalidate its generation. */
|
|
439
|
-
cancel() {
|
|
440
|
-
this.generation += 1;
|
|
441
|
-
if (this.cancellation) this.cancellation.cancelled = true;
|
|
442
|
-
this.promise = null;
|
|
443
|
-
this.configKey = null;
|
|
444
|
-
this.cancellation = null;
|
|
445
|
-
this.initializedConfigKey = null;
|
|
446
|
-
}
|
|
447
|
-
};
|
|
448
|
-
/**
|
|
449
|
-
* The adapter configuration an `initialize()` call produces: the initial
|
|
450
|
-
* configuration overlaid with the host's runtime values. Throws when a
|
|
451
|
-
* trusted-path or identity value is malformed.
|
|
452
|
-
*/
|
|
453
|
-
function overlayProviderConfig(provider, initialConfig, config) {
|
|
454
|
-
const nextConfig = cloneConfig(initialConfig);
|
|
455
|
-
const runtimeAttachments = config.runtimeAttachments;
|
|
456
|
-
if (runtimeAttachments !== void 0) nextConfig.runtimeAttachments = structuredClone(runtimeAttachments);
|
|
457
|
-
const runtimeSecrets = config.runtimeSecrets;
|
|
458
|
-
if (runtimeSecrets !== void 0) nextConfig.runtimeSecrets = { ...runtimeSecrets };
|
|
459
|
-
const runtimeHome = config.runtimeHome;
|
|
460
|
-
if (runtimeHome !== void 0) {
|
|
461
|
-
if (typeof runtimeHome !== "string" || !path.isAbsolute(runtimeHome)) throw new Error(`${provider} runtimeHome must be an absolute filesystem path`);
|
|
462
|
-
nextConfig.runtimeHome = runtimeHome;
|
|
463
|
-
}
|
|
464
|
-
const agentHome = config.agentHome;
|
|
465
|
-
if (agentHome !== void 0) {
|
|
466
|
-
if (typeof agentHome !== "string" || !path.isAbsolute(agentHome)) throw new Error(`${provider} agentHome must be an absolute filesystem path`);
|
|
467
|
-
nextConfig.agentHome = agentHome;
|
|
468
|
-
}
|
|
469
|
-
const subprocessUser = config.subprocessUser;
|
|
470
|
-
if (subprocessUser !== void 0) {
|
|
471
|
-
if (process.getuid?.() !== 0 || !Number.isSafeInteger(subprocessUser.uid) || subprocessUser.uid <= 0 || !Number.isSafeInteger(subprocessUser.gid) || subprocessUser.gid <= 0) throw new Error(`${provider}: invalid trusted subprocess identity`);
|
|
472
|
-
nextConfig.subprocessUser = { ...subprocessUser };
|
|
473
|
-
if (config.stableIsolatedIdentity === true) nextConfig.stableIsolatedIdentity = true;
|
|
474
|
-
}
|
|
475
|
-
const publicProfileRevisionRoot = config.publicProfileRevisionRoot;
|
|
476
|
-
if (publicProfileRevisionRoot !== void 0) {
|
|
477
|
-
if (typeof publicProfileRevisionRoot !== "string" || !path.isAbsolute(publicProfileRevisionRoot)) throw new Error(`${provider} publicProfileRevisionRoot must be an absolute filesystem path`);
|
|
478
|
-
nextConfig.publicProfileRevisionRoot = publicProfileRevisionRoot;
|
|
479
|
-
}
|
|
480
|
-
const privateRevision = config;
|
|
481
|
-
if (privateRevision.secretProfileRevisionRoot !== void 0 || privateRevision.secretProfileNamingKeyPath !== void 0) {
|
|
482
|
-
if (!subprocessUser || publicProfileRevisionRoot !== void 0 || !privateRevision.secretProfileRevisionRoot || !path.isAbsolute(privateRevision.secretProfileRevisionRoot) || !privateRevision.secretProfileNamingKeyPath || !path.isAbsolute(privateRevision.secretProfileNamingKeyPath)) throw new Error(`${provider}: invalid secret profile revision root`);
|
|
483
|
-
nextConfig.secretProfileRevisionRoot = privateRevision.secretProfileRevisionRoot;
|
|
484
|
-
nextConfig.secretProfileNamingKeyPath = privateRevision.secretProfileNamingKeyPath;
|
|
485
|
-
}
|
|
486
|
-
const sandboxedRuntime = config.sandboxedRuntime;
|
|
487
|
-
if (sandboxedRuntime !== void 0) nextConfig.sandboxedRuntime = sandboxedRuntime === true;
|
|
488
|
-
const nativeAuthEnv = config.nativeAuthEnv;
|
|
489
|
-
if (nativeAuthEnv !== void 0) {
|
|
490
|
-
nextConfig.nativeAuthEnv = { ...nativeAuthEnv };
|
|
491
|
-
nextConfig.nativeAuthSecretEnv = config.nativeAuthSecretEnv;
|
|
492
|
-
}
|
|
493
|
-
if (config.model?.model !== void 0) nextConfig.model = config.model.model;
|
|
494
|
-
if (config.model?.provider !== void 0) nextConfig.provider = config.model.provider;
|
|
495
|
-
if (config.model?.apiKey !== void 0) nextConfig.apiKey = config.model.apiKey;
|
|
496
|
-
if (config.model?.baseUrl !== void 0) nextConfig.baseUrl = config.model.baseUrl;
|
|
497
|
-
if (config.model?.authMode !== void 0) nextConfig.authMode = config.model.authMode;
|
|
498
|
-
if (config.model?.authFiles !== void 0) nextConfig.authFiles = config.model.authFiles.map((file) => ({ ...file }));
|
|
499
|
-
return nextConfig;
|
|
500
|
-
}
|
|
501
|
-
//#endregion
|
|
502
|
-
//#region src/execution-lifecycle.ts
|
|
503
|
-
const lifecycles = /* @__PURE__ */ new WeakMap();
|
|
504
|
-
function executionLifecycle(owner) {
|
|
505
|
-
let lifecycle = lifecycles.get(owner);
|
|
506
|
-
if (!lifecycle) {
|
|
507
|
-
lifecycle = {
|
|
508
|
-
inFlightExecutions: /* @__PURE__ */ new Set(),
|
|
509
|
-
shuttingDown: false
|
|
510
|
-
};
|
|
511
|
-
lifecycles.set(owner, lifecycle);
|
|
512
|
-
}
|
|
513
|
-
return lifecycle;
|
|
514
|
-
}
|
|
515
|
-
function beginExecution(owner, callerAbortSignal) {
|
|
516
|
-
const lifecycle = executionLifecycle(owner);
|
|
517
|
-
if (lifecycle.shuttingDown) throw new Error(`${owner.provider} provider is shutting down`);
|
|
518
|
-
let resolveSettled;
|
|
519
|
-
const execution = {
|
|
520
|
-
controller: new AbortController(),
|
|
521
|
-
settled: new Promise((resolve) => {
|
|
522
|
-
resolveSettled = resolve;
|
|
523
|
-
}),
|
|
524
|
-
resolveSettled: () => resolveSettled()
|
|
525
|
-
};
|
|
526
|
-
lifecycle.inFlightExecutions.add(execution);
|
|
527
|
-
if (callerAbortSignal) {
|
|
528
|
-
const abortFromCaller = () => execution.controller.abort();
|
|
529
|
-
execution.callerAbortSignal = callerAbortSignal;
|
|
530
|
-
execution.callerAbortListener = abortFromCaller;
|
|
531
|
-
if (callerAbortSignal.aborted) execution.controller.abort();
|
|
532
|
-
else callerAbortSignal.addEventListener("abort", abortFromCaller, { once: true });
|
|
533
|
-
}
|
|
534
|
-
return execution;
|
|
535
|
-
}
|
|
536
|
-
function finishExecution(owner, execution) {
|
|
537
|
-
executionLifecycle(owner).inFlightExecutions.delete(execution);
|
|
538
|
-
if (execution.callerAbortSignal && execution.callerAbortListener) execution.callerAbortSignal.removeEventListener("abort", execution.callerAbortListener);
|
|
539
|
-
execution.resolveSettled();
|
|
540
|
-
}
|
|
541
|
-
//#endregion
|
|
542
|
-
//#region src/writable-home.ts
|
|
543
|
-
/**
|
|
544
|
-
* The nearest ancestor of `target` that exists, or null when even the root of
|
|
545
|
-
* the path is absent. A directory that does not exist yet is usable only if
|
|
546
|
-
* something above it can be written to, so the writability question always
|
|
547
|
-
* resolves against an ancestor that is really there.
|
|
548
|
-
*/
|
|
549
|
-
function nearestExistingAncestor(target) {
|
|
550
|
-
let current = path.resolve(target);
|
|
551
|
-
for (;;) {
|
|
552
|
-
if (fs.existsSync(current)) return current;
|
|
553
|
-
const parent = path.dirname(current);
|
|
554
|
-
if (parent === current) return null;
|
|
555
|
-
current = parent;
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
function isWritableTarget(target) {
|
|
559
|
-
const ancestor = nearestExistingAncestor(target);
|
|
560
|
-
if (!ancestor) return false;
|
|
561
|
-
try {
|
|
562
|
-
fs.accessSync(ancestor, fs.constants.W_OK);
|
|
563
|
-
return true;
|
|
564
|
-
} catch {
|
|
565
|
-
return false;
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
/** `/root` belongs to the calling process, never to the agent subprocess. */
|
|
569
|
-
const READ_ONLY_ROOT_HOME = "/root";
|
|
570
|
-
function isRootHomePath(value) {
|
|
571
|
-
return value === READ_ONLY_ROOT_HOME || value.startsWith(`${READ_ONLY_ROOT_HOME}/`);
|
|
572
|
-
}
|
|
573
|
-
/**
|
|
574
|
-
* Whether an inherited path must not reach the CLI, given whether it can be
|
|
575
|
-
* written. Two independent reasons, kept apart from the filesystem probe so
|
|
576
|
-
* each is decidable on its own:
|
|
577
|
-
*
|
|
578
|
-
* - It sits under `/root`. The agent subprocess runs as its own user, so
|
|
579
|
-
* that path is wrong even while the calling process can write there.
|
|
580
|
-
* - Nothing can create it. A read-only mount refuses `mkdir` for every user,
|
|
581
|
-
* including root.
|
|
582
|
-
*/
|
|
583
|
-
function shouldReplacePath(value, writable) {
|
|
584
|
-
return isRootHomePath(value) || !writable;
|
|
585
|
-
}
|
|
586
|
-
function mustReplace(value) {
|
|
587
|
-
return shouldReplacePath(value, isWritableTarget(value));
|
|
588
|
-
}
|
|
589
|
-
/**
|
|
590
|
-
* Point `HOME` and the XDG directories at somewhere a CLI can actually write.
|
|
591
|
-
*
|
|
592
|
-
* An inherited value is kept whenever it is usable, so an operator can still
|
|
593
|
-
* aim a cache at a large volume. An unusable value is replaced by the
|
|
594
|
-
* equivalent path under the writable home, because keeping it only moves the
|
|
595
|
-
* failure into the CLI, where it surfaces as a run that emits nothing.
|
|
596
|
-
*/
|
|
597
|
-
function applyWritableHomeDefaults(env, workspaceRoot) {
|
|
598
|
-
const next = { ...env };
|
|
599
|
-
const writableHome = next.SIDECAR_AGENT_HOME?.trim() || next.AGENT_WORKSPACE_ROOT?.trim() || workspaceRoot;
|
|
600
|
-
const currentHome = next.HOME?.trim();
|
|
601
|
-
const home = currentHome && !mustReplace(currentHome) ? currentHome : writableHome;
|
|
602
|
-
for (const [key, fallback] of Object.entries(homeXdgEnv(home))) {
|
|
603
|
-
const current = next[key]?.trim();
|
|
604
|
-
if (key === "HOME" || !current || mustReplace(current)) next[key] = fallback;
|
|
605
|
-
}
|
|
606
|
-
return next;
|
|
607
|
-
}
|
|
608
|
-
/** HOME and the XDG base directories a CLI resolves under it. */
|
|
609
|
-
function homeXdgEnv(home) {
|
|
610
|
-
return {
|
|
611
|
-
HOME: home,
|
|
612
|
-
XDG_CONFIG_HOME: path.join(home, ".config"),
|
|
613
|
-
XDG_CACHE_HOME: path.join(home, ".cache"),
|
|
614
|
-
XDG_DATA_HOME: path.join(home, ".local", "share"),
|
|
615
|
-
XDG_STATE_HOME: path.join(home, ".local", "state")
|
|
616
|
-
};
|
|
617
|
-
}
|
|
618
|
-
//#endregion
|
|
619
|
-
//#region src/adapter-lifecycle.ts
|
|
620
|
-
const DEFAULT_CONTEXT_WINDOW = 2e5;
|
|
621
|
-
const execFileAsync = promisify(execFile);
|
|
622
|
-
/** Console-based logger fallback. */
|
|
623
|
-
const consoleLogger = {
|
|
624
|
-
debug: (msg, meta) => console.debug(`[cli-provider] ${msg}`, meta ?? ""),
|
|
625
|
-
info: (msg, meta) => console.info(`[cli-provider] ${msg}`, meta ?? ""),
|
|
626
|
-
warn: (msg, meta) => console.warn(`[cli-provider] ${msg}`, meta ?? ""),
|
|
627
|
-
error: (msg, meta) => console.error(`[cli-provider] ${msg}`, meta ?? "")
|
|
628
|
-
};
|
|
629
|
-
/** Owns configuration, initialization, cancellation, and subprocess identity. */
|
|
630
|
-
var CliAdapterLifecycle = class {
|
|
631
|
-
/** The message log directory when the caller names none. */
|
|
632
|
-
defaultStorageDir;
|
|
633
|
-
inheritProcessEnv;
|
|
634
|
-
config;
|
|
635
|
-
initialConfig;
|
|
636
|
-
logger;
|
|
637
|
-
sessionStore = null;
|
|
638
|
-
messageStore = null;
|
|
639
|
-
storagePath;
|
|
640
|
-
_storageDir;
|
|
641
|
-
agentEnvironment;
|
|
642
|
-
/** The consumer's executor for turn processes; a local spawn when absent. */
|
|
643
|
-
launcher;
|
|
644
|
-
/** Whether a turn's leftover process-group members are stopped at exit. */
|
|
645
|
-
ownProcessGroup;
|
|
646
|
-
initialized = false;
|
|
647
|
-
activeProcesses = /* @__PURE__ */ new Set();
|
|
648
|
-
initialization = new InitializationTracker(() => this.provider);
|
|
649
|
-
constructor(options = {}) {
|
|
650
|
-
executionLifecycle(this);
|
|
651
|
-
this.initialConfig = cloneConfig(options.config ?? {});
|
|
652
|
-
this.config = cloneConfig(this.initialConfig);
|
|
653
|
-
this.logger = options.logger ?? consoleLogger;
|
|
654
|
-
this.storagePath = options.storagePath ?? process.cwd();
|
|
655
|
-
this._storageDir = options.storageDir;
|
|
656
|
-
this.agentEnvironment = options.agentEnvironment;
|
|
657
|
-
this.inheritProcessEnv = options.agentEnvironment === void 0;
|
|
658
|
-
this.launcher = options.launcher;
|
|
659
|
-
this.ownProcessGroup = options.ownProcessGroup ?? false;
|
|
660
|
-
}
|
|
661
|
-
get storageDir() {
|
|
662
|
-
return this._storageDir ?? this.defaultStorageDir ?? messageStorageDirOf(this.provider);
|
|
663
|
-
}
|
|
664
|
-
/** Bind a live interaction to the exact provider execution that originated it. */
|
|
665
|
-
interactionExecutionBinding(input, sessionId) {
|
|
666
|
-
if (!input.interactionBinding) throw new Error(`${this.provider} interactions require AgentExecutionInput.interactionBinding`);
|
|
667
|
-
const binding = InteractionExecutionBindingSchema.parse(input.interactionBinding);
|
|
668
|
-
if (binding.provider !== this.provider) throw new Error(`Interaction binding provider "${binding.provider}" does not match adapter "${this.provider}"`);
|
|
669
|
-
if (binding.sessionId !== sessionId) throw new Error(`Interaction binding session "${binding.sessionId}" does not match adapter session "${sessionId}"`);
|
|
670
|
-
return binding;
|
|
671
|
-
}
|
|
672
|
-
/** The uid/gid every CLI process this adapter spawns runs as, or undefined when the host process is already unprivileged. */
|
|
673
|
-
subprocessIdentity() {
|
|
674
|
-
const assigned = this.config.subprocessUser;
|
|
675
|
-
if (assigned) {
|
|
676
|
-
if (process.getuid?.() !== 0 || !Number.isSafeInteger(assigned.uid) || assigned.uid <= 0 || !Number.isSafeInteger(assigned.gid) || assigned.gid <= 0) throw new Error(`${this.provider}: invalid trusted subprocess identity`);
|
|
677
|
-
return assigned;
|
|
678
|
-
}
|
|
679
|
-
if (process.getuid?.() !== 0) return void 0;
|
|
680
|
-
const rawUid = process.env.AGENT_SUBPROCESS_UID ?? "1000";
|
|
681
|
-
const rawGid = process.env.AGENT_SUBPROCESS_GID ?? "1000";
|
|
682
|
-
const uid = Number(rawUid);
|
|
683
|
-
const gid = Number(rawGid);
|
|
684
|
-
if (!Number.isInteger(uid) || uid < 0 || !Number.isInteger(gid) || gid < 0) throw new Error(`${this.provider}: AGENT_SUBPROCESS_UID/AGENT_SUBPROCESS_GID must be non-negative integers (got ${rawUid}/${rawGid})`);
|
|
685
|
-
return {
|
|
686
|
-
uid,
|
|
687
|
-
gid
|
|
688
|
-
};
|
|
689
|
-
}
|
|
690
|
-
/** `subprocessIdentity` shaped for `spawn`'s options bag. */
|
|
691
|
-
subprocessSpawnOptions() {
|
|
692
|
-
return this.subprocessIdentity() ?? {};
|
|
693
|
-
}
|
|
694
|
-
/** Run a one-shot CLI command under this adapter's subprocess identity. */
|
|
695
|
-
runCli(config) {
|
|
696
|
-
return execCli({
|
|
697
|
-
...config,
|
|
698
|
-
inheritEnv: this.inheritProcessEnv && config.inheritEnv !== false,
|
|
699
|
-
rawCapturePath: void 0,
|
|
700
|
-
...this.config.subprocessUser ? { isolatedIdentity: this.config.subprocessUser } : {},
|
|
701
|
-
spawnOptions: {
|
|
702
|
-
...this.subprocessSpawnOptions(),
|
|
703
|
-
...config.spawnOptions
|
|
704
|
-
}
|
|
705
|
-
});
|
|
706
|
-
}
|
|
707
|
-
/** Run a builtin-only Node script under this adapter's subprocess identity and return its stdout. */
|
|
708
|
-
async runNodeAsSubprocess(script, args, input = "") {
|
|
709
|
-
try {
|
|
710
|
-
const pending = execFileAsync(process.execPath, [
|
|
711
|
-
"-e",
|
|
712
|
-
script,
|
|
713
|
-
"--",
|
|
714
|
-
...args
|
|
715
|
-
], {
|
|
716
|
-
...this.subprocessSpawnOptions(),
|
|
717
|
-
cwd: "/",
|
|
718
|
-
env: {},
|
|
719
|
-
timeout: 3e4,
|
|
720
|
-
maxBuffer: 64 * 1024 * 1024
|
|
721
|
-
});
|
|
722
|
-
pending.child.stdin?.on("error", () => {});
|
|
723
|
-
pending.child.stdin?.end(input);
|
|
724
|
-
return (await pending).stdout;
|
|
725
|
-
} catch (error) {
|
|
726
|
-
const stderr = String(error.stderr ?? "").trim();
|
|
727
|
-
throw new Error((stderr || (error instanceof Error ? error.message : String(error))).slice(-500));
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
getCapabilities() {
|
|
731
|
-
return {
|
|
732
|
-
streaming: true,
|
|
733
|
-
toolUse: true,
|
|
734
|
-
reasoning: true,
|
|
735
|
-
multimodal: false,
|
|
736
|
-
contextWindow: DEFAULT_CONTEXT_WINDOW
|
|
737
|
-
};
|
|
738
|
-
}
|
|
739
|
-
initialize(config) {
|
|
740
|
-
const attachments = config.runtimeAttachments === void 0 ? void 0 : AgentRuntimeAttachmentsSchema.safeParse(config.runtimeAttachments);
|
|
741
|
-
if (attachments && !attachments.success) return Promise.reject(/* @__PURE__ */ new Error("Invalid runtime MCP attachments"));
|
|
742
|
-
const capturedConfig = attachments?.success ? {
|
|
743
|
-
...config,
|
|
744
|
-
runtimeAttachments: attachments.data
|
|
745
|
-
} : config;
|
|
746
|
-
const configKey = stableSerialize(capturedConfig);
|
|
747
|
-
if (this.initialization.promise) {
|
|
748
|
-
if (configKey === this.initialization.configKey) return this.initialization.promise;
|
|
749
|
-
return Promise.reject(/* @__PURE__ */ new Error(`${this.provider} provider cannot start a conflicting initialize() while initialization is in flight; concurrent calls must use the same configuration`));
|
|
750
|
-
}
|
|
751
|
-
if (this.initialized) {
|
|
752
|
-
if (configKey === this.initialization.initializedConfigKey) return Promise.resolve();
|
|
753
|
-
return Promise.reject(/* @__PURE__ */ new Error(`${this.provider} provider is already initialized with a different configuration; call shutdown() before reinitializing`));
|
|
754
|
-
}
|
|
755
|
-
const pendingShutdown = this.initialization.shutdownPromise;
|
|
756
|
-
const generation = this.initialization.generation;
|
|
757
|
-
const cancellation = { cancelled: false };
|
|
758
|
-
const initialization = Promise.resolve().then(async () => {
|
|
759
|
-
if (pendingShutdown) await pendingShutdown;
|
|
760
|
-
this.initialization.assertCurrent(generation, cancellation);
|
|
761
|
-
executionLifecycle(this).shuttingDown = false;
|
|
762
|
-
await this.initializeAtGeneration(capturedConfig, configKey, generation, cancellation);
|
|
763
|
-
});
|
|
764
|
-
this.initialization.promise = initialization;
|
|
765
|
-
this.initialization.configKey = configKey;
|
|
766
|
-
this.initialization.cancellation = cancellation;
|
|
767
|
-
initialization.then(() => this.initialization.clear(initialization), () => this.initialization.clear(initialization));
|
|
768
|
-
return initialization;
|
|
769
|
-
}
|
|
770
|
-
async initializeAtGeneration(config, configKey, generation, cancellation) {
|
|
771
|
-
this.initialization.assertCurrent(generation, cancellation);
|
|
772
|
-
const nextStoragePath = config.workspace?.rootPath || this.storagePath;
|
|
773
|
-
const nextConfig = overlayProviderConfig(this.provider, this.initialConfig, config);
|
|
774
|
-
const configuredProfile = config.profile ?? nextConfig.profile;
|
|
775
|
-
if (configuredProfile !== void 0) {
|
|
776
|
-
nextConfig.profile = await resolveAgentProfileResources(agentProfileSchema.parse(configuredProfile));
|
|
777
|
-
this.initialization.assertCurrent(generation, cancellation);
|
|
778
|
-
}
|
|
779
|
-
const contract = this.sessionContract();
|
|
780
|
-
if (contract) assertFlagsHonourSessionContract(this.provider, contract, nextConfig.extraFlags ?? [], "extraFlags");
|
|
781
|
-
const persistenceConfig = {
|
|
782
|
-
storagePath: nextStoragePath,
|
|
783
|
-
storageDir: this.storageDir
|
|
784
|
-
};
|
|
785
|
-
const nextSessionStore = createSessionStore(persistenceConfig);
|
|
786
|
-
const nextMessageStore = createMessageStore(persistenceConfig);
|
|
787
|
-
this.initialization.assertCurrent(generation, cancellation);
|
|
788
|
-
this.storagePath = nextStoragePath;
|
|
789
|
-
this.config = nextConfig;
|
|
790
|
-
this.sessionStore = nextSessionStore;
|
|
791
|
-
this.messageStore = nextMessageStore;
|
|
792
|
-
const providerTail = Promise.resolve().then(() => this.afterInitialize(config));
|
|
793
|
-
this.initialization.providerTails.add(providerTail);
|
|
794
|
-
try {
|
|
795
|
-
await providerTail;
|
|
796
|
-
this.initialization.assertCurrent(generation, cancellation);
|
|
797
|
-
this.initialized = true;
|
|
798
|
-
this.initialization.initializedConfigKey = configKey;
|
|
799
|
-
} catch (error) {
|
|
800
|
-
if (generation === this.initialization.generation) {
|
|
801
|
-
this.config = cloneConfig(this.initialConfig);
|
|
802
|
-
this.sessionStore = null;
|
|
803
|
-
this.messageStore = null;
|
|
804
|
-
this.initialized = false;
|
|
805
|
-
this.initialization.initializedConfigKey = null;
|
|
806
|
-
}
|
|
807
|
-
throw error;
|
|
808
|
-
} finally {
|
|
809
|
-
this.initialization.providerTails.delete(providerTail);
|
|
810
|
-
}
|
|
811
|
-
this.logger.info(`${this.provider} provider initialized`, {
|
|
812
|
-
storagePath: this.storagePath,
|
|
813
|
-
storageDir: this.storageDir,
|
|
814
|
-
model: this.config.model
|
|
815
|
-
});
|
|
816
|
-
}
|
|
817
|
-
/** Auth/config files the adapter needs on disk that do NOT arrive through `config.model.authFiles` — e.g. */
|
|
818
|
-
getManagedAuthFiles() {
|
|
819
|
-
return [];
|
|
820
|
-
}
|
|
821
|
-
/** Provider-specific initialization runs inside the tracked lifecycle. */
|
|
822
|
-
async afterInitialize(_config) {}
|
|
823
|
-
/** The harness session contract this adapter honours (cli-agent-registry NATIVE_SESSION_CONTRACTS), or null for a provider that declares none. */
|
|
824
|
-
sessionContract() {
|
|
825
|
-
return NATIVE_SESSION_CONTRACTS$1[this.provider] ?? null;
|
|
826
|
-
}
|
|
827
|
-
async withExecution(signal, run) {
|
|
828
|
-
const execution = beginExecution(this, signal);
|
|
829
|
-
try {
|
|
830
|
-
return await run(execution.controller.signal);
|
|
831
|
-
} catch (error) {
|
|
832
|
-
execution.controller.abort();
|
|
833
|
-
throw error;
|
|
834
|
-
} finally {
|
|
835
|
-
finishExecution(this, execution);
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
async shutdown() {
|
|
839
|
-
this.initialization.cancel();
|
|
840
|
-
this.initialized = false;
|
|
841
|
-
executionLifecycle(this).shuttingDown = true;
|
|
842
|
-
this.config = cloneConfig(this.initialConfig);
|
|
843
|
-
if (this.initialization.shutdownPromise) return this.initialization.shutdownPromise;
|
|
844
|
-
const executions = [...executionLifecycle(this).inFlightExecutions];
|
|
845
|
-
const processes = [...this.activeProcesses];
|
|
846
|
-
const providerInitializationTails = [...this.initialization.providerTails];
|
|
847
|
-
for (const execution of executions) execution.controller.abort();
|
|
848
|
-
const trackedShutdown = (async () => {
|
|
849
|
-
await Promise.allSettled([
|
|
850
|
-
...providerInitializationTails,
|
|
851
|
-
...processes.map((process) => terminateTurnProcess(process, this.activeProcesses)),
|
|
852
|
-
...executions.map((execution) => execution.settled)
|
|
853
|
-
]);
|
|
854
|
-
this.sessionStore = null;
|
|
855
|
-
this.messageStore = null;
|
|
856
|
-
executionLifecycle(this).shuttingDown = false;
|
|
857
|
-
this.logger.info(`${this.provider} provider shut down`);
|
|
858
|
-
})().finally(() => {
|
|
859
|
-
if (this.initialization.shutdownPromise === trackedShutdown) this.initialization.shutdownPromise = void 0;
|
|
860
|
-
});
|
|
861
|
-
this.initialization.shutdownPromise = trackedShutdown;
|
|
862
|
-
return trackedShutdown;
|
|
863
|
-
}
|
|
864
|
-
async healthCheck() {
|
|
865
|
-
try {
|
|
866
|
-
return (await this.runAuxiliaryCli(["--version"], 5e3)).exit.success;
|
|
867
|
-
} catch {
|
|
868
|
-
return false;
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
/** Run this CLI's own one-shot command (version, MCP) in the workspace. */
|
|
872
|
-
runAuxiliaryCli(args, timeoutMs) {
|
|
873
|
-
return this.runCli({
|
|
874
|
-
command: this.getCliPath(),
|
|
875
|
-
args,
|
|
876
|
-
cwd: this.storagePath,
|
|
877
|
-
env: this.applyWritableHomeDefaults(this.buildNativeExecutionEnvironment(this.config), this.storagePath),
|
|
878
|
-
inheritEnv: this.inheritProcessEnv,
|
|
879
|
-
timeoutMs
|
|
880
|
-
});
|
|
881
|
-
}
|
|
882
|
-
async updateConfig(config) {
|
|
883
|
-
const resolvedConfig = { ...config };
|
|
884
|
-
if (config.profile !== void 0) resolvedConfig.profile = await resolveAgentProfileResources(agentProfileSchema.parse(config.profile));
|
|
885
|
-
this.config = mergeCliProviderConfig(this.config, resolvedConfig);
|
|
886
|
-
return this.config;
|
|
887
|
-
}
|
|
888
|
-
/** Apply private native credentials to every CLI subprocess through one path. */
|
|
889
|
-
buildNativeExecutionEnvironment(config) {
|
|
890
|
-
const env = this.buildExecEnv(config);
|
|
891
|
-
if (config.nativeAuthSecretEnv) env[config.nativeAuthSecretEnv] = void 0;
|
|
892
|
-
return {
|
|
893
|
-
...env,
|
|
894
|
-
...config.nativeAuthEnv
|
|
895
|
-
};
|
|
896
|
-
}
|
|
897
|
-
getCliPath() {
|
|
898
|
-
return this.config.cliPath ?? this.defaultCliPath;
|
|
899
|
-
}
|
|
900
|
-
ensureInitialized() {
|
|
901
|
-
if (executionLifecycle(this).shuttingDown) throw new Error(`${this.provider} provider is shutting down`);
|
|
902
|
-
if (!this.initialized) throw new Error(`${this.provider} provider not initialized. Call initialize() first.`);
|
|
903
|
-
}
|
|
904
|
-
getSessionStore() {
|
|
905
|
-
if (!this.sessionStore) throw new Error("Session store not initialized");
|
|
906
|
-
return this.sessionStore;
|
|
907
|
-
}
|
|
908
|
-
getMessageStore() {
|
|
909
|
-
if (!this.messageStore) throw new Error("Message store not initialized");
|
|
910
|
-
return this.messageStore;
|
|
911
|
-
}
|
|
912
|
-
/** The agent environment the host supplied (CliProviderOptions.agentEnvironment). */
|
|
913
|
-
agentEnvironmentValues() {
|
|
914
|
-
const secretProfile = !!this.config.secretProfileRevisionRoot;
|
|
915
|
-
return this.agentEnvironment?.({ secretProfile }) ?? (secretProfile ? {} : { ...process.env });
|
|
916
|
-
}
|
|
917
|
-
applyWritableHomeDefaults(adapterEnv, workspaceRoot) {
|
|
918
|
-
const env = {
|
|
919
|
-
...this.agentEnvironmentValues(),
|
|
920
|
-
...adapterEnv
|
|
921
|
-
};
|
|
922
|
-
const runtimeHome = this.config.runtimeHome?.trim();
|
|
923
|
-
if (!runtimeHome) return applyWritableHomeDefaults(env, workspaceRoot);
|
|
924
|
-
const agentHome = this.config.agentHome?.trim();
|
|
925
|
-
const configDir = agentHome ? configDirHomeEnv(this.provider, runtimeHome, agentHome) : void 0;
|
|
926
|
-
if (configDir) {
|
|
927
|
-
const adapterStores = Object.keys(configDir).flatMap((name) => {
|
|
928
|
-
if (name === "HOME" || name.startsWith("XDG_")) return [];
|
|
929
|
-
const value = adapterEnv[name]?.trim();
|
|
930
|
-
return value && value !== process.env[name] ? [[name, value]] : [];
|
|
931
|
-
});
|
|
932
|
-
return applyWritableHomeDefaults({
|
|
933
|
-
...env,
|
|
934
|
-
...configDir,
|
|
935
|
-
...Object.fromEntries(adapterStores)
|
|
936
|
-
}, workspaceRoot);
|
|
937
|
-
}
|
|
938
|
-
return applyWritableHomeDefaults({
|
|
939
|
-
...env,
|
|
940
|
-
...homeXdgEnv(runtimeHome)
|
|
941
|
-
}, workspaceRoot);
|
|
942
|
-
}
|
|
943
|
-
};
|
|
944
|
-
async function terminateTurnProcess(process, activeProcesses) {
|
|
945
|
-
process.kill();
|
|
946
|
-
const escalation = setTimeout(() => process.kill("SIGKILL"), 1e3);
|
|
947
|
-
escalation.unref();
|
|
948
|
-
try {
|
|
949
|
-
await process.exitPromise;
|
|
950
|
-
} finally {
|
|
951
|
-
clearTimeout(escalation);
|
|
952
|
-
activeProcesses.delete(process);
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
//#endregion
|
|
956
|
-
//#region src/execution-request-headers.ts
|
|
957
|
-
const headerValue = z.string().regex(/^[\x21-\x2b\x2d-\x7e]{1,256}$/);
|
|
958
|
-
const EXECUTION_ID_HEADER = "X-Tangle-Execution-ID";
|
|
959
|
-
const CORRELATION_FIELDS = {
|
|
960
|
-
"X-Tangle-Environment-ID": "environmentId",
|
|
961
|
-
"X-Tangle-Session-ID": "sessionId",
|
|
962
|
-
[EXECUTION_ID_HEADER]: "executionId"
|
|
963
|
-
};
|
|
964
|
-
/** Router column that joins a model request to the turn trace that caused it. */
|
|
965
|
-
const EXECUTION_TRACE_HEADER = "X-Tangle-Trace-ID";
|
|
966
|
-
const MANAGED_QUOTE_VERSION_HEADER = "X-Tangle-Managed-Quote-Version";
|
|
967
|
-
/** Every header a turn's correlation may set, lowercased. */
|
|
968
|
-
const EXECUTION_CORRELATION_HEADERS = [
|
|
969
|
-
...Object.keys(CORRELATION_FIELDS),
|
|
970
|
-
EXECUTION_TRACE_HEADER,
|
|
971
|
-
MANAGED_QUOTE_VERSION_HEADER,
|
|
972
|
-
"traceparent"
|
|
973
|
-
].map((name) => name.toLowerCase());
|
|
974
|
-
const managedQuoteVersion = z.string().regex(/^mmq_[a-f0-9]{40}$/);
|
|
975
|
-
/**
|
|
976
|
-
* Capture diagnostic identity without forwarding caller-supplied transport headers.
|
|
977
|
-
* `traceId` is the turn's trace id; a value no header can carry is omitted,
|
|
978
|
-
* because a trace is diagnostic and must never refuse the run.
|
|
979
|
-
*/
|
|
980
|
-
function executionCorrelationHeaders(binding, traceId, quoteVersion) {
|
|
981
|
-
const quote = managedQuoteVersion.optional().parse(quoteVersion);
|
|
982
|
-
if (quote !== void 0 && binding === void 0) throw new Error("Managed quote requires an exact execution binding");
|
|
983
|
-
const trace = headerValue.safeParse(traceId);
|
|
984
|
-
const headers = {};
|
|
985
|
-
if (trace.success) {
|
|
986
|
-
headers[EXECUTION_TRACE_HEADER] = trace.data;
|
|
987
|
-
const span = W3C.toW3CSpanId(`${trace.data}:sidecar`);
|
|
988
|
-
headers.traceparent = `00-${W3C.toW3CTraceId(trace.data)}-${span}-01`;
|
|
989
|
-
}
|
|
990
|
-
if (binding === void 0) return trace.success ? Object.freeze(headers) : void 0;
|
|
991
|
-
if (quote !== void 0) headers[MANAGED_QUOTE_VERSION_HEADER] = quote;
|
|
992
|
-
for (const [name, field] of Object.entries(CORRELATION_FIELDS)) {
|
|
993
|
-
const value = headerValue.safeParse(binding[field]);
|
|
994
|
-
if (!value.success) throw new Error(`Execution correlation ${field} must contain 1–256 printable ASCII characters without spaces or commas`);
|
|
995
|
-
headers[name] = value.data;
|
|
996
|
-
}
|
|
997
|
-
return Object.freeze(headers);
|
|
998
|
-
}
|
|
999
|
-
//#endregion
|
|
1000
|
-
//#region src/profile-config.ts
|
|
1001
|
-
/**
|
|
1002
|
-
* Per-turn credential delivery wins over host-env resolution; the host box
|
|
1003
|
-
* environment stays the fallback for references the turn did not deliver.
|
|
1004
|
-
* With no turn secrets the host provider is returned unchanged, so the
|
|
1005
|
-
* overlay adds no behavior to executions that never asked for it.
|
|
1006
|
-
*/
|
|
1007
|
-
function runtimeSecretsOverlay(runtimeSecrets, fallback) {
|
|
1008
|
-
if (!runtimeSecrets || Object.keys(runtimeSecrets).length === 0) return fallback;
|
|
1009
|
-
return { get: async (key) => runtimeSecrets[key] ?? await fallback.get(key) };
|
|
1010
|
-
}
|
|
1011
|
-
function hasPortableProfileControls(profile) {
|
|
1012
|
-
return profile.prompt !== void 0 || profile.model !== void 0 || profile.harness !== void 0 || profile.permissions !== void 0 || profile.tools !== void 0 || profile.mcp !== void 0 || profile.connections !== void 0 || profile.subagents !== void 0 || profile.resources !== void 0 || profile.hooks !== void 0 || profile.modes !== void 0 || profile.confidential !== void 0 || profile.extensions !== void 0;
|
|
1013
|
-
}
|
|
1014
|
-
function systemPromptConflict(request, profile) {
|
|
1015
|
-
return /* @__PURE__ */ new Error(`execution system prompt (${request.length} chars) conflicts with the materialized AgentProfile prompt.systemPrompt (${profile.length} chars); send no systemPrompt, or move the addition to appendSystemPrompt`);
|
|
1016
|
-
}
|
|
1017
|
-
/** A blank profile prompt declares none, so the request prompt stands. */
|
|
1018
|
-
function resolveSystemPrompt(requestPrompt, profilePrompt) {
|
|
1019
|
-
if (!profilePrompt?.trim()) return requestPrompt;
|
|
1020
|
-
if (requestPrompt && requestPrompt !== profilePrompt) throw systemPromptConflict(requestPrompt, profilePrompt);
|
|
1021
|
-
return profilePrompt;
|
|
1022
|
-
}
|
|
1023
|
-
function profileForExecution(profile, requestPrompt) {
|
|
1024
|
-
const profilePrompt = profile?.prompt?.systemPrompt;
|
|
1025
|
-
if (!profile || !profilePrompt?.trim() || !requestPrompt || requestPrompt === profilePrompt) return profile;
|
|
1026
|
-
if (!requestPrompt.startsWith(`${profilePrompt}\n\n`)) throw systemPromptConflict(requestPrompt, profilePrompt);
|
|
1027
|
-
return {
|
|
1028
|
-
...profile,
|
|
1029
|
-
prompt: {
|
|
1030
|
-
...profile.prompt,
|
|
1031
|
-
systemPrompt: requestPrompt
|
|
1032
|
-
}
|
|
1033
|
-
};
|
|
1034
|
-
}
|
|
1035
|
-
/**
|
|
1036
|
-
* Refuse to dispatch an execution against a model the profile did not
|
|
1037
|
-
* materialize — a caller must not silently get a different model than the one
|
|
1038
|
-
* the workspace was built for.
|
|
1039
|
-
*
|
|
1040
|
-
* The request and the profile carry the same model at different qualification
|
|
1041
|
-
* levels: the request is the id the caller sent, the profile default is that id
|
|
1042
|
-
* qualified with its provider. Both go through the one shared canonicalization
|
|
1043
|
-
* so only a genuinely different model is a conflict.
|
|
1044
|
-
*/
|
|
1045
|
-
function resolveModel(requestModel, profileModel, configuredModel, profileProvider) {
|
|
1046
|
-
if (profileModel !== void 0 && requestModel !== void 0 && !modelIdsMatch(requestModel, profileModel, profileProvider)) throw new Error("execution model conflicts with the materialized AgentProfile");
|
|
1047
|
-
return profileModel ?? requestModel ?? configuredModel;
|
|
1048
|
-
}
|
|
1049
|
-
//#endregion
|
|
1050
|
-
//#region src/profile-revision-receipts.ts
|
|
1051
|
-
/** Receipts belong to their adapter; resolved secrets never enter them. */
|
|
1052
|
-
var ProfileRevisionReceipts = class {
|
|
1053
|
-
public = /* @__PURE__ */ new WeakMap();
|
|
1054
|
-
secret = /* @__PURE__ */ new WeakMap();
|
|
1055
|
-
};
|
|
1056
|
-
const receiptsByAdapter = /* @__PURE__ */ new WeakMap();
|
|
1057
|
-
function profileRevisionReceipts(owner) {
|
|
1058
|
-
let receipts = receiptsByAdapter.get(owner);
|
|
1059
|
-
if (!receipts) {
|
|
1060
|
-
receipts = new ProfileRevisionReceipts();
|
|
1061
|
-
receiptsByAdapter.set(owner, receipts);
|
|
1062
|
-
}
|
|
1063
|
-
return receipts;
|
|
1064
|
-
}
|
|
1065
|
-
function assertExecutionProfileRevision(owner, getConfig, input, profilePlan) {
|
|
1066
|
-
const config = getConfig();
|
|
1067
|
-
const receipts = profileRevisionReceipts(owner);
|
|
1068
|
-
const actualPublic = profilePlan ? receipts.public.get(profilePlan) : void 0;
|
|
1069
|
-
const actualSecret = profilePlan ? receipts.secret.get(profilePlan) : void 0;
|
|
1070
|
-
for (const [root, receipt, kind] of [[
|
|
1071
|
-
config.publicProfileRevisionRoot,
|
|
1072
|
-
actualPublic,
|
|
1073
|
-
"public"
|
|
1074
|
-
], [
|
|
1075
|
-
config.secretProfileRevisionRoot,
|
|
1076
|
-
actualSecret,
|
|
1077
|
-
"secret"
|
|
1078
|
-
]]) if (root && profilePlan && !receipt) throw new Error(`Provider added ${kind} profile launch controls after immutable revision preparation`);
|
|
1079
|
-
const { expectedSecretProfileReceipt, expectedPublicProfileRevision } = input;
|
|
1080
|
-
if (config.secretProfileRevisionRoot) {
|
|
1081
|
-
if (!expectedSecretProfileReceipt || !actualSecret || actualSecret.executorReceipt !== expectedSecretProfileReceipt) throw new Error("Secret native profile receipt mismatch before native spawn");
|
|
1082
|
-
} else if (expectedSecretProfileReceipt !== void 0) throw new Error("Secret native profile execution is unavailable");
|
|
1083
|
-
assertPublicReceipt(actualPublic, expectedPublicProfileRevision);
|
|
1084
|
-
}
|
|
1085
|
-
function assertPublicReceipt(actual, expected) {
|
|
1086
|
-
if (expected !== void 0 && (!actual || [
|
|
1087
|
-
"workspacePlanDigest",
|
|
1088
|
-
"materializedDigest",
|
|
1089
|
-
"launchDigest"
|
|
1090
|
-
].some((key) => actual[key] !== expected[key]))) throw new Error("Public native profile revision mismatch before native spawn");
|
|
1091
|
-
}
|
|
1092
|
-
//#endregion
|
|
1093
|
-
//#region src/errors.ts
|
|
1094
|
-
/**
|
|
1095
|
-
* A session's persisted transcript exists but could not be read for replay
|
|
1096
|
-
* into the fresh provider session this turn starts. The turn fails instead
|
|
1097
|
-
* of serving an empty conversation under the same session id. The sidecar's
|
|
1098
|
-
* client-visible whitelist matches on `code`; the wire contract is the
|
|
1099
|
-
* duck-typed field, not class identity.
|
|
1100
|
-
*/
|
|
1101
|
-
var SessionRehydrationError = class extends Error {
|
|
1102
|
-
code = "session_rehydration_failed";
|
|
1103
|
-
constructor(message, cause) {
|
|
1104
|
-
super(message, { cause });
|
|
1105
|
-
this.name = "SessionRehydrationError";
|
|
1106
|
-
}
|
|
1107
|
-
};
|
|
1108
|
-
const NOT_LAUNCHED = Symbol.for("@tangle-network/sdk-provider-cli-base.not-launched");
|
|
1109
|
-
/**
|
|
1110
|
-
* Marks a turn failure the adapter raised before it planned the launch:
|
|
1111
|
-
* session preparation or profile materialization refused, so no native
|
|
1112
|
-
* process received this turn and it left no native transcript to copy.
|
|
1113
|
-
* Only the adapter marks, on the error object it caught; error text never
|
|
1114
|
-
* does, so a provider message that reads the same is not marked.
|
|
1115
|
-
*/
|
|
1116
|
-
function rejectNotLaunched(error) {
|
|
1117
|
-
if (error instanceof Error && !Object.isFrozen(error)) Object.defineProperty(error, NOT_LAUNCHED, { value: true });
|
|
1118
|
-
throw error;
|
|
1119
|
-
}
|
|
1120
|
-
/** Whether this error is one the adapter raised before launching anything. */
|
|
1121
|
-
function wasNeverLaunched(error) {
|
|
1122
|
-
return Object.getOwnPropertyDescriptor(error, NOT_LAUNCHED)?.value === true;
|
|
1123
|
-
}
|
|
1124
|
-
//#endregion
|
|
1125
|
-
//#region src/file-parts.ts
|
|
1126
|
-
/** Extension → MIME type, keyed by lowercased extension without the dot. */
|
|
1127
|
-
const EXTENSION_TO_MEDIA_TYPE = {
|
|
1128
|
-
png: "image/png",
|
|
1129
|
-
jpg: "image/jpeg",
|
|
1130
|
-
jpeg: "image/jpeg",
|
|
1131
|
-
gif: "image/gif",
|
|
1132
|
-
webp: "image/webp",
|
|
1133
|
-
svg: "image/svg+xml",
|
|
1134
|
-
pdf: "application/pdf",
|
|
1135
|
-
mp3: "audio/mpeg",
|
|
1136
|
-
wav: "audio/wav",
|
|
1137
|
-
mp4: "video/mp4",
|
|
1138
|
-
webm: "video/webm",
|
|
1139
|
-
txt: "text/plain",
|
|
1140
|
-
md: "text/markdown",
|
|
1141
|
-
csv: "text/csv",
|
|
1142
|
-
json: "application/json",
|
|
1143
|
-
html: "text/html",
|
|
1144
|
-
markdown: "text/markdown",
|
|
1145
|
-
htm: "text/html",
|
|
1146
|
-
xml: "application/xml",
|
|
1147
|
-
yaml: "application/yaml",
|
|
1148
|
-
yml: "application/yaml",
|
|
1149
|
-
js: "text/javascript",
|
|
1150
|
-
ts: "application/typescript",
|
|
1151
|
-
py: "text/x-python"
|
|
1152
|
-
};
|
|
1153
|
-
/**
|
|
1154
|
-
* Inverse of the table above, for synthesizing a filename from a media type.
|
|
1155
|
-
* Built once from the first extension seen per media type (e.g. image/jpeg
|
|
1156
|
-
* → "jpg", not "jpeg") so the mapping is stable across calls.
|
|
1157
|
-
*/
|
|
1158
|
-
const MEDIA_TYPE_TO_EXTENSION = Object.entries(EXTENSION_TO_MEDIA_TYPE).reduce((acc, [ext, mediaType]) => {
|
|
1159
|
-
if (!(mediaType in acc)) acc[mediaType] = ext;
|
|
1160
|
-
return acc;
|
|
1161
|
-
}, {});
|
|
1162
|
-
function sha1Hex(value) {
|
|
1163
|
-
return createHash("sha1").update(value).digest("hex");
|
|
1164
|
-
}
|
|
1165
|
-
/** Infers a MIME type from a filename's extension. Unknown/no extension → undefined. */
|
|
1166
|
-
function inferMediaType(filename) {
|
|
1167
|
-
const dot = filename.lastIndexOf(".");
|
|
1168
|
-
if (dot < 0 || dot === filename.length - 1) return void 0;
|
|
1169
|
-
return EXTENSION_TO_MEDIA_TYPE[filename.slice(dot + 1).toLowerCase()];
|
|
1170
|
-
}
|
|
1171
|
-
/**
|
|
1172
|
-
* Builds the canonical URL for a file part. A data URI passes through
|
|
1173
|
-
* verbatim; a path is returned unchanged if it already carries a recognized
|
|
1174
|
-
* scheme, else converted to a percent-encoded `file://` URL.
|
|
1175
|
-
*
|
|
1176
|
-
* Callers must pass an already-resolved absolute path — `trackFilePart`
|
|
1177
|
-
* resolves relative paths (or drops the observation when it can't) before
|
|
1178
|
-
* ever reaching here, so a relative path arriving at this layer means a
|
|
1179
|
-
* caller bypassed that resolution. We throw rather than mint a malformed
|
|
1180
|
-
* `file://` URL (e.g. `file://./chart.png`, whose authority is `.`).
|
|
1181
|
-
*/
|
|
1182
|
-
function buildFileUrl(input) {
|
|
1183
|
-
if (input.dataUri !== void 0) return input.dataUri;
|
|
1184
|
-
if (input.path === void 0) throw new Error("buildFileUrl requires either `path` or `dataUri` to build a file part URL");
|
|
1185
|
-
if (input.path.startsWith("file://") || input.path.startsWith("http://") || input.path.startsWith("https://") || input.path.startsWith("data:")) return input.path;
|
|
1186
|
-
if (!isAbsolute(input.path)) throw new Error(`buildFileUrl requires an absolute path to build a file:// URL, got "${input.path}" — resolve it against a workspace root before calling buildFileUrl`);
|
|
1187
|
-
return pathToFileURL(input.path).href;
|
|
1188
|
-
}
|
|
1189
|
-
/** Deterministic, stable part id for a file announced within one message. */
|
|
1190
|
-
function deterministicFilePartId(messageId, key) {
|
|
1191
|
-
return `file-${sha1Hex(`${messageId}|${key}`).slice(0, 16)}`;
|
|
1192
|
-
}
|
|
1193
|
-
function deriveFilePart(input) {
|
|
1194
|
-
const normalized = normalize(input.path);
|
|
1195
|
-
let resolvedPath;
|
|
1196
|
-
if (isAbsolute(normalized)) resolvedPath = normalized;
|
|
1197
|
-
else if (input.workspaceRoot !== void 0) resolvedPath = resolve(input.workspaceRoot, normalized);
|
|
1198
|
-
else return null;
|
|
1199
|
-
const filename = basename(resolvedPath);
|
|
1200
|
-
return {
|
|
1201
|
-
id: deterministicFilePartId(input.messageId, resolvedPath),
|
|
1202
|
-
key: resolvedPath,
|
|
1203
|
-
filename,
|
|
1204
|
-
mediaType: inferMediaType(filename),
|
|
1205
|
-
url: buildFileUrl({ path: resolvedPath })
|
|
1206
|
-
};
|
|
1207
|
-
}
|
|
1208
|
-
function trackFilePart(state, args) {
|
|
1209
|
-
if (args.changeKind === "delete") return null;
|
|
1210
|
-
if (args.path === void 0 && args.dataUri === void 0) return null;
|
|
1211
|
-
if (state.messageId === void 0) throw new Error("trackFilePart requires state.messageId (set by createExecutionState) to derive a stable part id");
|
|
1212
|
-
let derived = null;
|
|
1213
|
-
if (args.path !== void 0) {
|
|
1214
|
-
derived = deriveFilePart({
|
|
1215
|
-
path: args.path,
|
|
1216
|
-
messageId: state.messageId,
|
|
1217
|
-
workspaceRoot: state.workspaceRoot
|
|
1218
|
-
});
|
|
1219
|
-
if (derived === null) return null;
|
|
1220
|
-
}
|
|
1221
|
-
state.fileParts ??= [];
|
|
1222
|
-
const parts = state.fileParts;
|
|
1223
|
-
const key = derived !== null ? derived.key : `data:${sha1Hex(args.dataUri)}`;
|
|
1224
|
-
const url = derived !== null ? derived.url : buildFileUrl({ dataUri: args.dataUri });
|
|
1225
|
-
const existing = parts.find((p) => p.key === key);
|
|
1226
|
-
let filename = args.filename ?? derived?.filename ?? existing?.filename;
|
|
1227
|
-
const mediaType = args.mediaType ?? (filename !== void 0 ? inferMediaType(filename) : void 0) ?? existing?.mediaType;
|
|
1228
|
-
if (filename === void 0 && args.path === void 0) {
|
|
1229
|
-
const priorDataUriEntries = parts.filter((p) => p.key.startsWith("data:")).length;
|
|
1230
|
-
const ext = mediaType ? MEDIA_TYPE_TO_EXTENSION[mediaType] ?? "bin" : "bin";
|
|
1231
|
-
filename = `attachment-${priorDataUriEntries + 1}.${ext}`;
|
|
1232
|
-
}
|
|
1233
|
-
if (existing) {
|
|
1234
|
-
existing.filename = filename;
|
|
1235
|
-
existing.mediaType = mediaType;
|
|
1236
|
-
existing.url = url;
|
|
1237
|
-
return {
|
|
1238
|
-
id: existing.id,
|
|
1239
|
-
filename: existing.filename,
|
|
1240
|
-
mediaType: existing.mediaType,
|
|
1241
|
-
url: existing.url
|
|
1242
|
-
};
|
|
1243
|
-
}
|
|
1244
|
-
const id = derived !== null ? derived.id : deterministicFilePartId(state.messageId, key);
|
|
1245
|
-
parts.push({
|
|
1246
|
-
id,
|
|
1247
|
-
key,
|
|
1248
|
-
filename,
|
|
1249
|
-
mediaType,
|
|
1250
|
-
url
|
|
1251
|
-
});
|
|
1252
|
-
return {
|
|
1253
|
-
id,
|
|
1254
|
-
filename,
|
|
1255
|
-
mediaType,
|
|
1256
|
-
url
|
|
1257
|
-
};
|
|
1258
|
-
}
|
|
1259
|
-
//#endregion
|
|
1260
|
-
//#region src/prompt-input.ts
|
|
1261
|
-
/** Raw-byte ceiling for inlining one image as base64 (~1.33x expansion in
|
|
1262
|
-
* memory and on the wire); larger images keep only their `path` and the
|
|
1263
|
-
* adapter falls back to path-based delivery. */
|
|
1264
|
-
const MAX_INLINE_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
1265
|
-
/** Aggregate raw-byte budget for inlined images across one request's parts —
|
|
1266
|
-
* the request schema admits up to 64 parts, so a per-image cap alone would
|
|
1267
|
-
* still let one request hold hundreds of MB of base64 until execution. */
|
|
1268
|
-
const MAX_TOTAL_INLINE_IMAGE_BYTES = 32 * 1024 * 1024;
|
|
1269
|
-
async function readImageBase64(filePath, wroteFile, budgetBytes) {
|
|
1270
|
-
const limit = Math.min(MAX_INLINE_IMAGE_BYTES, budgetBytes);
|
|
1271
|
-
if (limit <= 0) return void 0;
|
|
1272
|
-
let handle;
|
|
1273
|
-
try {
|
|
1274
|
-
handle = await open(filePath, constants.O_RDONLY | constants.O_NONBLOCK);
|
|
1275
|
-
const stats = await handle.stat();
|
|
1276
|
-
if (!stats.isFile() || stats.size > limit) return void 0;
|
|
1277
|
-
const buffer = Buffer.alloc(stats.size + 1);
|
|
1278
|
-
let offset = 0;
|
|
1279
|
-
while (offset < buffer.length) {
|
|
1280
|
-
const { bytesRead } = await handle.read(buffer, offset, buffer.length - offset, offset);
|
|
1281
|
-
if (bytesRead === 0) break;
|
|
1282
|
-
offset += bytesRead;
|
|
1283
|
-
}
|
|
1284
|
-
if (offset > stats.size) return void 0;
|
|
1285
|
-
return {
|
|
1286
|
-
base64: buffer.subarray(0, offset).toString("base64"),
|
|
1287
|
-
rawBytes: offset
|
|
1288
|
-
};
|
|
1289
|
-
} catch (error) {
|
|
1290
|
-
if (wroteFile) throw error;
|
|
1291
|
-
return;
|
|
1292
|
-
} finally {
|
|
1293
|
-
await handle?.close();
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
function inferExtension(mediaType) {
|
|
1297
|
-
if (!mediaType) return "bin";
|
|
1298
|
-
if (mediaType === "application/pdf") return "pdf";
|
|
1299
|
-
if (mediaType.startsWith("image/")) return mediaType.split("/")[1] || "img";
|
|
1300
|
-
if (mediaType.startsWith("audio/")) return mediaType.split("/")[1] || "audio";
|
|
1301
|
-
if (mediaType.startsWith("video/")) return mediaType.split("/")[1] || "video";
|
|
1302
|
-
return mediaType.includes("/") ? mediaType.split("/")[1] || "bin" : "bin";
|
|
1303
|
-
}
|
|
1304
|
-
function sanitizeFilename(filename) {
|
|
1305
|
-
return filename.replace(/[^a-zA-Z0-9._-]/g, "-") || `attachment-${randomUUID()}`;
|
|
1306
|
-
}
|
|
1307
|
-
const TEXT_MEDIA_TYPES = new Set([
|
|
1308
|
-
"application/json",
|
|
1309
|
-
"application/xml",
|
|
1310
|
-
"application/yaml",
|
|
1311
|
-
"application/x-yaml",
|
|
1312
|
-
"application/javascript",
|
|
1313
|
-
"application/csv"
|
|
1314
|
-
]);
|
|
1315
|
-
function isTextMediaType(mediaType) {
|
|
1316
|
-
if (!mediaType) return true;
|
|
1317
|
-
const essence = (mediaType.split(";")[0] ?? "").trim().toLowerCase();
|
|
1318
|
-
if (essence.startsWith("text/")) return true;
|
|
1319
|
-
if (TEXT_MEDIA_TYPES.has(essence)) return true;
|
|
1320
|
-
return essence.endsWith("+json") || essence.endsWith("+xml");
|
|
1321
|
-
}
|
|
1322
|
-
async function materializeAttachmentPart(part, workspaceRoot, sessionId, index) {
|
|
1323
|
-
const attachmentDir = path.join(workspaceRoot, ".agent-inputs", sessionId);
|
|
1324
|
-
await mkdir(attachmentDir, { recursive: true });
|
|
1325
|
-
let dataUriMatch;
|
|
1326
|
-
if (part.url?.startsWith("data:")) {
|
|
1327
|
-
const match = part.url.match(/^data:([^;,]+)?;base64,(.*)$/s);
|
|
1328
|
-
if (!match) throw new InvalidAttachmentError(`Invalid data URI for ${part.type} attachment`);
|
|
1329
|
-
dataUriMatch = match;
|
|
1330
|
-
}
|
|
1331
|
-
const mediaType = part.mediaType || dataUriMatch?.[1] || void 0;
|
|
1332
|
-
const filename = sanitizeFilename(part.filename || (part.path ? path.basename(part.path) : void 0) || `${part.type}-${index + 1}.${inferExtension(mediaType)}`);
|
|
1333
|
-
const targetPath = path.join(attachmentDir, filename);
|
|
1334
|
-
let referencePath = part.path ? path.resolve(workspaceRoot, part.path) : targetPath;
|
|
1335
|
-
let wroteFile = false;
|
|
1336
|
-
const content = "content" in part ? part.content : void 0;
|
|
1337
|
-
const payload = dataUriMatch ? Buffer.from(dataUriMatch[2], "base64") : typeof content === "string" ? content : void 0;
|
|
1338
|
-
if (payload !== void 0) {
|
|
1339
|
-
if (!dataUriMatch && !isTextMediaType(part.mediaType)) throw new InvalidAttachmentError(`Cannot write binary attachment "${filename}" (mediaType: ${part.mediaType}) via "content" — deliver it as a base64 data: URL (data:${part.mediaType};base64,...) or via "path"/"file://" url instead.`);
|
|
1340
|
-
const partDirectory = path.join(attachmentDir, `part-${randomUUID()}`);
|
|
1341
|
-
await mkdir(partDirectory);
|
|
1342
|
-
referencePath = path.join(partDirectory, filename);
|
|
1343
|
-
await writeFile(referencePath, payload, { flag: "wx" });
|
|
1344
|
-
wroteFile = true;
|
|
1345
|
-
} else if (part.url?.startsWith("file://")) try {
|
|
1346
|
-
referencePath = fileURLToPath(part.url);
|
|
1347
|
-
} catch (err) {
|
|
1348
|
-
throw new InvalidAttachmentError(`Invalid file:// URL for ${part.type} attachment: ${JSON.stringify(part.url)} (${err instanceof Error ? err.message : String(err)})`);
|
|
1349
|
-
}
|
|
1350
|
-
else if (part.url && /^https?:\/\//i.test(part.url)) referencePath = part.url;
|
|
1351
|
-
const isHttpReference = referencePath.startsWith("http://") || referencePath.startsWith("https://");
|
|
1352
|
-
return {
|
|
1353
|
-
reference: referencePath,
|
|
1354
|
-
filename,
|
|
1355
|
-
...mediaType ? { mediaType } : {},
|
|
1356
|
-
wroteFile,
|
|
1357
|
-
persistedPart: {
|
|
1358
|
-
type: "file",
|
|
1359
|
-
filename,
|
|
1360
|
-
mediaType,
|
|
1361
|
-
url: isHttpReference ? referencePath : buildFileUrl({ path: referencePath })
|
|
1362
|
-
}
|
|
1363
|
-
};
|
|
1364
|
-
}
|
|
1365
|
-
async function preparePromptInputParts(parts, workspaceRoot, sessionId, options) {
|
|
1366
|
-
const nativeKinds = new Set(options?.nativeAttachmentKinds ?? []);
|
|
1367
|
-
const promptSections = [];
|
|
1368
|
-
const attachmentRefs = [];
|
|
1369
|
-
const persistedParts = [];
|
|
1370
|
-
const attachments = [];
|
|
1371
|
-
const inlineImages = nativeKinds.has("image");
|
|
1372
|
-
let inlineBudget = options?.inlineImageBudgetBytes ?? 33554432;
|
|
1373
|
-
for (const [index, part] of parts.entries()) {
|
|
1374
|
-
if (part.type === "text") {
|
|
1375
|
-
if (!part.text) continue;
|
|
1376
|
-
promptSections.push(part.text);
|
|
1377
|
-
persistedParts.push({
|
|
1378
|
-
type: "text",
|
|
1379
|
-
text: part.text
|
|
1380
|
-
});
|
|
1381
|
-
continue;
|
|
1382
|
-
}
|
|
1383
|
-
const materialized = await materializeAttachmentPart(part, workspaceRoot, sessionId, index);
|
|
1384
|
-
const marker = `[${part.type === "image" ? "Image" : "File"}: ${materialized.reference}]`;
|
|
1385
|
-
if (!nativeKinds.has(part.type)) attachmentRefs.push(marker);
|
|
1386
|
-
persistedParts.push(materialized.persistedPart);
|
|
1387
|
-
const isRemote = /^https?:\/\//i.test(materialized.reference);
|
|
1388
|
-
const attachment = {
|
|
1389
|
-
kind: part.type,
|
|
1390
|
-
filename: materialized.filename,
|
|
1391
|
-
marker,
|
|
1392
|
-
...materialized.mediaType ? { mediaType: materialized.mediaType } : {},
|
|
1393
|
-
...isRemote ? { url: materialized.reference } : { path: materialized.reference }
|
|
1394
|
-
};
|
|
1395
|
-
if (part.type === "image" && attachment.path && inlineImages) {
|
|
1396
|
-
const inlined = await readImageBase64(attachment.path, materialized.wroteFile, inlineBudget);
|
|
1397
|
-
if (inlined) {
|
|
1398
|
-
attachment.base64 = inlined.base64;
|
|
1399
|
-
inlineBudget -= inlined.rawBytes;
|
|
1400
|
-
}
|
|
1401
|
-
}
|
|
1402
|
-
attachments.push(attachment);
|
|
1403
|
-
}
|
|
1404
|
-
if (attachmentRefs.length > 0) promptSections.push(`Attached files:\n${attachmentRefs.join("\n")}`);
|
|
1405
|
-
return {
|
|
1406
|
-
promptText: promptSections.join("\n\n").trim(),
|
|
1407
|
-
persistedParts,
|
|
1408
|
-
attachments
|
|
1409
|
-
};
|
|
1410
|
-
}
|
|
1411
|
-
/**
|
|
1412
|
-
* Partition a turn's image attachments into the ones the adapter will
|
|
1413
|
-
* forward natively and the "stranded" rest, folding each stranded image's
|
|
1414
|
-
* `[Image: ...]` marker back into the message text. Once an adapter's
|
|
1415
|
-
* nativeAttachmentKinds() claims "image", the base prompt renderer omits
|
|
1416
|
-
* every image marker — so any image the mechanism can't carry (no base64
|
|
1417
|
-
* for a stdin block, no local path for an argv flag, beyond a single-path
|
|
1418
|
-
* flag's capacity) MUST re-enter the prompt here or the model never sees
|
|
1419
|
-
* a reference to it. Uses the same "Attached files:" block format the
|
|
1420
|
-
* renderer itself emits for non-native kinds.
|
|
1421
|
-
*/
|
|
1422
|
-
function partitionImageAttachments(options) {
|
|
1423
|
-
const { message, attachments, qualifies, maxNative } = options;
|
|
1424
|
-
const native = [];
|
|
1425
|
-
const strandedMarkers = [];
|
|
1426
|
-
for (const attachment of attachments ?? []) {
|
|
1427
|
-
if (attachment.kind !== "image") continue;
|
|
1428
|
-
if ((maxNative === void 0 || native.length < maxNative) && qualifies(attachment)) native.push(attachment);
|
|
1429
|
-
else strandedMarkers.push(attachment.marker);
|
|
1430
|
-
}
|
|
1431
|
-
return {
|
|
1432
|
-
native,
|
|
1433
|
-
message: strandedMarkers.length > 0 ? `${message}\n\nAttached files:\n${strandedMarkers.join("\n")}` : message
|
|
1434
|
-
};
|
|
1435
|
-
}
|
|
1436
|
-
/** Separates the sections a harness receives as one user message. */
|
|
1437
|
-
const PROMPT_SECTION_SEPARATOR = "\n\n---\n\n";
|
|
1438
|
-
/**
|
|
1439
|
-
* Puts `section` (a system prompt or replayed context) ahead of the message
|
|
1440
|
-
* for harnesses that take no separate channel for it.
|
|
1441
|
-
*/
|
|
1442
|
-
function prependPromptSection(section, body) {
|
|
1443
|
-
return section ? `${section}${PROMPT_SECTION_SEPARATOR}${body}` : body;
|
|
1444
|
-
}
|
|
1445
|
-
//#endregion
|
|
1446
|
-
//#region src/types.ts
|
|
1447
|
-
function interactionToggleEnabled(config, kind) {
|
|
1448
|
-
return config === true || typeof config === "object" && config !== null && config[kind] === true;
|
|
1449
|
-
}
|
|
1450
|
-
function enabledInteractionKinds(config, supported) {
|
|
1451
|
-
if (config === true) return [...supported];
|
|
1452
|
-
if (typeof config !== "object" || config === null) return [];
|
|
1453
|
-
return supported.filter((kind) => config[kind] === true);
|
|
1454
|
-
}
|
|
1455
|
-
function readEphemeralContext(input) {
|
|
1456
|
-
const context = input.ephemeralContext;
|
|
1457
|
-
if (context === void 0 || context === null) return void 0;
|
|
1458
|
-
if (typeof context !== "string") throw new TypeError(`ephemeralContext must be a string, received ${typeof context}`);
|
|
1459
|
-
return context || void 0;
|
|
1460
|
-
}
|
|
1461
|
-
/** Compose the model-facing prompt for a turn: the caller's own text, then this turn's ephemeral context. */
|
|
1462
|
-
function withEphemeralContext(promptText, input) {
|
|
1463
|
-
const context = readEphemeralContext(input);
|
|
1464
|
-
if (!context) return promptText;
|
|
1465
|
-
return promptText ? `${promptText}\n\n${context}` : context;
|
|
1466
|
-
}
|
|
1467
|
-
function turnInteractionEnabled(input, latched, kind) {
|
|
1468
|
-
const perTurn = input.interactions;
|
|
1469
|
-
return interactionToggleEnabled(perTurn !== void 0 ? perTurn : latched, kind);
|
|
1470
|
-
}
|
|
1471
|
-
/** Overlay this turn's interactions onto a latched config, returning a NEW object (no shared mutation — safe when. */
|
|
1472
|
-
function withTurnInteractions(config, input) {
|
|
1473
|
-
if (input.interactions === void 0) return config;
|
|
1474
|
-
return {
|
|
1475
|
-
...config,
|
|
1476
|
-
interactions: input.interactions
|
|
1477
|
-
};
|
|
1478
|
-
}
|
|
1479
|
-
/**
|
|
1480
|
-
* The one precedence every engine names a failed turn's end with (#16404): an
|
|
1481
|
-
* end the abort, the timeout or the harness named is theirs; only an
|
|
1482
|
-
* unexplained death is the link's, the memory limit's or the harness's.
|
|
1483
|
-
*/
|
|
1484
|
-
function turnEndReason(end) {
|
|
1485
|
-
if (end.aborted) return "abort";
|
|
1486
|
-
if (end.timedOut) return "timeout";
|
|
1487
|
-
if (end.harnessFailed) return "failed";
|
|
1488
|
-
if (end.linkLost) return "gateway_disconnect";
|
|
1489
|
-
if (end.memoryKilled) return "memory_limit";
|
|
1490
|
-
return end.signal ? "harness_exit" : "failed";
|
|
1491
|
-
}
|
|
1492
|
-
//#endregion
|
|
1493
|
-
//#region src/adapter-profile.ts
|
|
1494
|
-
/** Revisions that already passed the native health check in this process, by the verifier's scope and the revision's materialized digest. */
|
|
1495
|
-
const verifiedSecretProfileRevisions = /* @__PURE__ */ new Set();
|
|
1496
|
-
const VERIFIED_SECRET_PROFILE_REVISIONS_MAX = 1024;
|
|
1497
|
-
function rememberVerifiedSecretProfileRevision(key) {
|
|
1498
|
-
verifiedSecretProfileRevisions.delete(key);
|
|
1499
|
-
verifiedSecretProfileRevisions.add(key);
|
|
1500
|
-
if (verifiedSecretProfileRevisions.size > VERIFIED_SECRET_PROFILE_REVISIONS_MAX) {
|
|
1501
|
-
const oldest = verifiedSecretProfileRevisions.values().next().value;
|
|
1502
|
-
if (oldest !== void 0) verifiedSecretProfileRevisions.delete(oldest);
|
|
1503
|
-
}
|
|
1504
|
-
}
|
|
1505
|
-
/** The executor's naming key, refused unless root owns it alone. */
|
|
1506
|
-
function readSecretProfileNamingKey(keyPath) {
|
|
1507
|
-
if (!keyPath) throw new Error("Secret profile naming key is unavailable");
|
|
1508
|
-
const keyStat = lstatSync(keyPath);
|
|
1509
|
-
if (!keyStat.isFile() || keyStat.isSymbolicLink() || keyStat.uid !== 0 || (keyStat.mode & 511) !== 384 || keyStat.size !== 32) throw new Error("Secret profile naming key is not root-owned");
|
|
1510
|
-
return readFileSync(keyPath);
|
|
1511
|
-
}
|
|
1512
|
-
/** Owns native profile revisions, launch controls, and prompt composition. */
|
|
1513
|
-
var CliAdapterProfile = class extends CliAdapterLifecycle {
|
|
1514
|
-
/** Native profile lowering explicitly owned by this adapter, never inferred from a path. */
|
|
1515
|
-
profileHarness = void 0;
|
|
1516
|
-
/** Trusted implementation capabilities; never read from caller-owned configuration. */
|
|
1517
|
-
supportsSecretProfileRevisions = false;
|
|
1518
|
-
profileSecretProvider;
|
|
1519
|
-
secretProfileRuntimeKeyProvider;
|
|
1520
|
-
/** Private native preparation state; never part of a returned receipt. */
|
|
1521
|
-
secretProfileRevisions = /* @__PURE__ */ new WeakMap();
|
|
1522
|
-
constructor(options = {}) {
|
|
1523
|
-
super(options);
|
|
1524
|
-
this.profileSecretProvider = options.profileSecretProvider ?? UNAVAILABLE_AGENT_PROFILE_SECRET_PROVIDER;
|
|
1525
|
-
this.secretProfileRuntimeKeyProvider = options.secretProfileRuntimeKeyProvider ?? UNAVAILABLE_AGENT_PROFILE_SECRET_PROVIDER;
|
|
1526
|
-
}
|
|
1527
|
-
/** The exact child environment a turn uses; shared with prewarm paths. */
|
|
1528
|
-
buildTurnEnvironment(executionConfig, profilePlan, workspaceRoot, correlationHeaders) {
|
|
1529
|
-
const merged = this.mergeProfilePlanEnvironment(this.buildNativeExecutionEnvironment(executionConfig), profilePlan, executionConfig.nativeAuthEnv !== void 0 || (executionConfig.authFiles?.length ?? 0) > 0);
|
|
1530
|
-
return this.bindExecutionCorrelation(this.applyWritableHomeDefaults(this.bindProfileToolBinDirs(merged, profilePlan), workspaceRoot), correlationHeaders);
|
|
1531
|
-
}
|
|
1532
|
-
/** A profile's declared executable tools, sealed in its revision, stand in for the app's own tool dirs on this launch's PATH tail. */
|
|
1533
|
-
bindProfileToolBinDirs(env, profilePlan) {
|
|
1534
|
-
const toolBinDirs = profilePlan?.spawn.toolBinDirs;
|
|
1535
|
-
if (!toolBinDirs?.length) return env;
|
|
1536
|
-
return bindLaunchToolBinDirs("PATH" in env || !this.inheritProcessEnv || process.env.PATH === void 0 ? env : {
|
|
1537
|
-
...env,
|
|
1538
|
-
PATH: process.env.PATH
|
|
1539
|
-
}, toolBinDirs, [...parseSandboxToolBinDirs(env.SANDBOX_TOOL_BIN_DIRS), ...parseSandboxToolBinDirs(process.env.SANDBOX_TOOL_BIN_DIRS)]);
|
|
1540
|
-
}
|
|
1541
|
-
bindExecutionCorrelation(environment, correlationHeaders) {
|
|
1542
|
-
if (correlationHeaders?.["X-Tangle-Execution-ID"] !== void 0) this.logger.warn("Native model request correlation is unsupported", { provider: this.provider });
|
|
1543
|
-
return environment;
|
|
1544
|
-
}
|
|
1545
|
-
/** Launcher controls this adapter OWNS on the spawn it starts; the pre-box verdict reads the same table. */
|
|
1546
|
-
profileMaterializationBinds() {
|
|
1547
|
-
const harness = this.profileHarness;
|
|
1548
|
-
return (harness && HARNESS_LAUNCH_MATERIALIZE_OPTIONS[harness]?.binds) ?? [];
|
|
1549
|
-
}
|
|
1550
|
-
/** Optional native instruction root. */
|
|
1551
|
-
profileAgentDirectory(_workspaceRoot) {}
|
|
1552
|
-
/** Provider transport bound into the private immutable revision and receipt. */
|
|
1553
|
-
secretProfileProviderConfig() {}
|
|
1554
|
-
/** The values behind a secret revision's `{env:NAME}` config references, for an executor that serves those references itself. */
|
|
1555
|
-
secretProfileReferences(application) {
|
|
1556
|
-
return application ? this.secretProfileRevisions.get(application)?.references : void 0;
|
|
1557
|
-
}
|
|
1558
|
-
/** The one sealed session view repointed only while its native turn owns the queue. */
|
|
1559
|
-
secretProfileSessionView(sessionId) {
|
|
1560
|
-
const root = this.config.secretProfileRevisionRoot;
|
|
1561
|
-
const subprocessUid = this.config.subprocessUser?.uid;
|
|
1562
|
-
if (!root || subprocessUid === void 0) throw new Error("Secret profile session view is unavailable");
|
|
1563
|
-
const launchRoot = join(root, "launch");
|
|
1564
|
-
return {
|
|
1565
|
-
launchRoot,
|
|
1566
|
-
view: secretSessionViewWorkspace({
|
|
1567
|
-
launchRoot,
|
|
1568
|
-
sharedWorkspaceDir: this.storagePath,
|
|
1569
|
-
sessionId,
|
|
1570
|
-
subprocessUid
|
|
1571
|
-
}),
|
|
1572
|
-
subprocessUid
|
|
1573
|
-
};
|
|
1574
|
-
}
|
|
1575
|
-
/** Bind the private revision into the session's stable native cwd. */
|
|
1576
|
-
bindSecretProfileSessionView(revisionWorkspaceDir, sessionId) {
|
|
1577
|
-
const { launchRoot, view, subprocessUid } = this.secretProfileSessionView(sessionId);
|
|
1578
|
-
bindSecretSessionView({
|
|
1579
|
-
launchRoot,
|
|
1580
|
-
viewWorkspaceDir: view,
|
|
1581
|
-
revisionWorkspaceDir,
|
|
1582
|
-
subprocessUid
|
|
1583
|
-
});
|
|
1584
|
-
return view;
|
|
1585
|
-
}
|
|
1586
|
-
async materializeProfileResources(inputProfile, workspaceRoot, ownerId, sessionId, scope = "turn") {
|
|
1587
|
-
if (!inputProfile && !this.config.runtimeAttachments) return void 0;
|
|
1588
|
-
const started = performance.now();
|
|
1589
|
-
try {
|
|
1590
|
-
return await this.applyProfileResources(inputProfile, workspaceRoot, ownerId, sessionId);
|
|
1591
|
-
} finally {
|
|
1592
|
-
this.logger.observePhase?.("profile_materialize", performance.now() - started, scope);
|
|
1593
|
-
}
|
|
1594
|
-
}
|
|
1595
|
-
/** This spawn's plan: a strict refusal throws, a best-effort one is logged. */
|
|
1596
|
-
planProfile(profile, harness, agentInstructions = false) {
|
|
1597
|
-
const plan = materializeProfile(profile, harness, {
|
|
1598
|
-
runtimeAttachments: this.config.runtimeAttachments,
|
|
1599
|
-
binds: this.profileMaterializationBinds(),
|
|
1600
|
-
...agentInstructions ? { instructionsRoot: "agent" } : {}
|
|
1601
|
-
});
|
|
1602
|
-
const unsupported = enforceWorkspacePlanSupport(plan, profile.resources?.failOnError);
|
|
1603
|
-
if (unsupported.length > 0) this.logger.warn("profile contains unsupported best-effort resources", {
|
|
1604
|
-
harness,
|
|
1605
|
-
unsupported
|
|
1606
|
-
});
|
|
1607
|
-
return plan;
|
|
1608
|
-
}
|
|
1609
|
-
async applyProfileResources(inputProfile, workspaceRoot, ownerId, sessionId) {
|
|
1610
|
-
const profile = inputProfile ?? {};
|
|
1611
|
-
const harness = this.profileHarness;
|
|
1612
|
-
if (!harness) {
|
|
1613
|
-
if (hasPortableProfileControls(profile) || this.config.runtimeAttachments !== void 0) throw new Error(`${this.provider} does not support portable AgentProfile controls`);
|
|
1614
|
-
return;
|
|
1615
|
-
}
|
|
1616
|
-
const agentDir = this.profileAgentDirectory(workspaceRoot);
|
|
1617
|
-
const plan = this.planProfile(profile, harness, agentDir !== void 0);
|
|
1618
|
-
if (this.config.secretProfileRevisionRoot) return await this.applySecretProfileRevision(plan, this.config.secretProfileRevisionRoot, workspaceRoot, ownerId, sessionId);
|
|
1619
|
-
if (this.config.publicProfileRevisionRoot) {
|
|
1620
|
-
if (!ownerId) throw new Error("Concurrent public profile launch requires a session scope; idle prewarm is unsupported");
|
|
1621
|
-
const revision = await preparePublicConcurrentProfileRevision({
|
|
1622
|
-
plan,
|
|
1623
|
-
...ensurePublicConcurrentProfileRoots(this.config.publicProfileRevisionRoot),
|
|
1624
|
-
sharedWorkspaceDir: workspaceRoot,
|
|
1625
|
-
ownerId,
|
|
1626
|
-
secretProvider: runtimeSecretsOverlay(this.config.runtimeSecrets, this.profileSecretProvider)
|
|
1627
|
-
});
|
|
1628
|
-
this.logger.debug("materialized public concurrent profile revision", {
|
|
1629
|
-
harness,
|
|
1630
|
-
directory: revision.directory,
|
|
1631
|
-
digest: revision.manifest.materializedDigest
|
|
1632
|
-
});
|
|
1633
|
-
const launchDigest = revision.manifest.launchDigest;
|
|
1634
|
-
if (!launchDigest) throw new Error("Concurrent profile launch digest was not recorded");
|
|
1635
|
-
profileRevisionReceipts(this).public.set(revision.application, {
|
|
1636
|
-
workspacePlanDigest: revision.manifest.workspacePlanDigest,
|
|
1637
|
-
materializedDigest: revision.manifest.materializedDigest,
|
|
1638
|
-
launchDigest,
|
|
1639
|
-
conflicts: revision.manifest.conflicts
|
|
1640
|
-
});
|
|
1641
|
-
return revision.application;
|
|
1642
|
-
}
|
|
1643
|
-
const application = await applyWorkspacePlanWithSecretProvider(plan, workspaceRoot, runtimeSecretsOverlay(this.config.runtimeSecrets, this.profileSecretProvider), {
|
|
1644
|
-
ownership: this.subprocessIdentity(),
|
|
1645
|
-
...agentDir === void 0 ? {} : { agentDir }
|
|
1646
|
-
});
|
|
1647
|
-
this.logger.debug("materialized profile resources", {
|
|
1648
|
-
harness,
|
|
1649
|
-
files: plan.files.length,
|
|
1650
|
-
flags: plan.flags.length,
|
|
1651
|
-
environment: Object.keys(plan.env).length
|
|
1652
|
-
});
|
|
1653
|
-
return application;
|
|
1654
|
-
}
|
|
1655
|
-
/** Publish the profile as an executor-sealed revision for this session's subprocess identity, and record the receipt that binds it to the turn. */
|
|
1656
|
-
async applySecretProfileRevision(plan, secretProfileRevisionRoot, workspaceRoot, ownerId, sessionId) {
|
|
1657
|
-
if (!this.supportsSecretProfileRevisions || !ownerId || !sessionId || !this.config.subprocessUser) throw new Error("Secret concurrent profile needs an opted-in adapter, a session, and per-turn runtime secrets only");
|
|
1658
|
-
const namingKey = readSecretProfileNamingKey(this.config.secretProfileNamingKeyPath);
|
|
1659
|
-
const roots = ensurePublicConcurrentProfileRoots(secretProfileRevisionRoot);
|
|
1660
|
-
const revision = await prepareSecretConcurrentProfileRevision({
|
|
1661
|
-
plan,
|
|
1662
|
-
providerConfig: this.secretProfileProviderConfig(),
|
|
1663
|
-
...roots,
|
|
1664
|
-
sharedWorkspaceDir: workspaceRoot,
|
|
1665
|
-
ownerId,
|
|
1666
|
-
subprocessUid: this.config.subprocessUser.uid,
|
|
1667
|
-
namingKey,
|
|
1668
|
-
secretProvider: runtimeSecretsOverlay(this.config.runtimeSecrets, this.secretProfileRuntimeKeyProvider)
|
|
1669
|
-
});
|
|
1670
|
-
const launchDigest = revision.manifest.launchDigest;
|
|
1671
|
-
if (!launchDigest) throw new Error("Secret concurrent profile launch digest is absent");
|
|
1672
|
-
const executorReceipt = `hmac-sha256:${createHmac("sha256", namingKey).update(ownerId).update("\0").update(sessionId).update("\0").update(String(this.config.subprocessUser.uid)).update("\0").update(revision.manifest.workspacePlanDigest).update("\0").update(revision.manifest.materializedDigest).update("\0").update(launchDigest).digest("hex")}`;
|
|
1673
|
-
this.secretProfileRevisions.set(revision.application, {
|
|
1674
|
-
materializedDigest: revision.manifest.materializedDigest,
|
|
1675
|
-
references: revision.secretReferences
|
|
1676
|
-
});
|
|
1677
|
-
profileRevisionReceipts(this).secret.set(revision.application, {
|
|
1678
|
-
workspacePlanDigest: revision.manifest.workspacePlanDigest,
|
|
1679
|
-
executorReceipt,
|
|
1680
|
-
conflicts: revision.manifest.conflicts.map((conflict) => ({ ...conflict }))
|
|
1681
|
-
});
|
|
1682
|
-
return revision.application;
|
|
1683
|
-
}
|
|
1684
|
-
/** Prepare the same provider-composed native plan that execute() will check. */
|
|
1685
|
-
async preparePublicProfileRevision(input) {
|
|
1686
|
-
if (!this.config.publicProfileRevisionRoot) throw new Error("Concurrent public profile materialization is disabled");
|
|
1687
|
-
const sessionScope = input.userId ?? input.sessionId;
|
|
1688
|
-
if (!sessionScope) throw new Error("Concurrent public profile preparation needs a session scope");
|
|
1689
|
-
const application = await this.materializeProfileResources(this.resolveExecutionProfileForInput(input), input.workspaceRoot ?? this.storagePath, sessionScope, input.sessionId);
|
|
1690
|
-
const receipt = application ? profileRevisionReceipts(this).public.get(application) : void 0;
|
|
1691
|
-
if (!receipt) throw new Error("Concurrent public profile preparation produced no native plan");
|
|
1692
|
-
return receipt;
|
|
1693
|
-
}
|
|
1694
|
-
/** Prepare the private native plan and return only an opaque executor receipt. */
|
|
1695
|
-
async prepareSecretProfileRevision(input) {
|
|
1696
|
-
if (!this.config.secretProfileRevisionRoot) throw new Error("Secret concurrent profile materialization is disabled");
|
|
1697
|
-
const sessionScope = input.userId ?? input.sessionId;
|
|
1698
|
-
if (!sessionScope) throw new Error("Secret profile preparation needs a session scope");
|
|
1699
|
-
const startedAt = performance.now();
|
|
1700
|
-
const application = await this.materializeProfileResources(this.resolveExecutionProfileForInput(input), input.workspaceRoot ?? this.storagePath, sessionScope, input.sessionId);
|
|
1701
|
-
const materializedAt = performance.now();
|
|
1702
|
-
const receipt = application ? profileRevisionReceipts(this).secret.get(application) : void 0;
|
|
1703
|
-
if (!application || !receipt) throw new Error("Secret profile preparation produced no native plan");
|
|
1704
|
-
const materializedDigest = this.secretProfileRevisions.get(application)?.materializedDigest;
|
|
1705
|
-
if (!materializedDigest) throw new Error("Secret profile preparation produced no revision digest");
|
|
1706
|
-
const verification = `${this.secretProfileVerificationScope()}\0${materializedDigest}`;
|
|
1707
|
-
const verifyCached = verifiedSecretProfileRevisions.has(verification);
|
|
1708
|
-
if (!verifyCached) await this.verifyPreparedSecretProfileRevision(input, application);
|
|
1709
|
-
rememberVerifiedSecretProfileRevision(verification);
|
|
1710
|
-
this.logger.info("prepared secret profile revision", {
|
|
1711
|
-
materializeMs: Math.round(materializedAt - startedAt),
|
|
1712
|
-
verifyMs: Math.round(performance.now() - materializedAt),
|
|
1713
|
-
verifyCached
|
|
1714
|
-
});
|
|
1715
|
-
return receipt;
|
|
1716
|
-
}
|
|
1717
|
-
/** What the native health check's verdict depends on besides a revision's bytes: the executable that loads them. */
|
|
1718
|
-
secretProfileVerificationScope() {
|
|
1719
|
-
return this.provider;
|
|
1720
|
-
}
|
|
1721
|
-
/** A secret receipt is issued only after the provider can load this revision. */
|
|
1722
|
-
async verifyPreparedSecretProfileRevision(_input, _application) {
|
|
1723
|
-
throw new Error(`${this.provider} has no native secret profile health check`);
|
|
1724
|
-
}
|
|
1725
|
-
/** Resolve the canonical profile used for this execution. */
|
|
1726
|
-
resolveExecutionProfile(requestPrompt) {
|
|
1727
|
-
return profileForExecution(this.config.profile, requestPrompt);
|
|
1728
|
-
}
|
|
1729
|
-
/** Select the profile materialized for this exact turn. */
|
|
1730
|
-
resolveExecutionProfileForInput(input) {
|
|
1731
|
-
return this.resolveExecutionProfile(input.systemPrompt);
|
|
1732
|
-
}
|
|
1733
|
-
/** Compose the provider's user-message channel for this exact turn. */
|
|
1734
|
-
composePromptText(promptText, input) {
|
|
1735
|
-
return withEphemeralContext(promptText, input);
|
|
1736
|
-
}
|
|
1737
|
-
withProfilePlan(config, profilePlan) {
|
|
1738
|
-
if (!profilePlan || profilePlan.spawn.flags.length === 0) return config;
|
|
1739
|
-
const contract = this.sessionContract();
|
|
1740
|
-
if (contract) assertFlagsHonourSessionContract(this.provider, contract, profilePlan.spawn.flags, "profile flags");
|
|
1741
|
-
return {
|
|
1742
|
-
...config,
|
|
1743
|
-
extraFlags: [...config.extraFlags ?? [], ...profilePlan.spawn.flags]
|
|
1744
|
-
};
|
|
1745
|
-
}
|
|
1746
|
-
/** A profile's addition precedes host context while preserving its harness prompt. */
|
|
1747
|
-
resolveProfilePrompts(input, plan, profile = this.resolveExecutionProfileForInput(input)) {
|
|
1748
|
-
const addition = plan?.spawn.appendSystemPrompt;
|
|
1749
|
-
const keep = keepsHarnessSystemPrompt(profile, this.profileHarness);
|
|
1750
|
-
return {
|
|
1751
|
-
systemPrompt: keep ? void 0 : this.resolveProfileSystemPrompt(input.systemPrompt, plan),
|
|
1752
|
-
appendSystemPrompt: keep ? joinSystemPromptAdditions(addition, input.systemPrompt, input.appendSystemPrompt) : input.appendSystemPrompt ? joinSystemPromptAdditions(addition, input.appendSystemPrompt) : addition
|
|
1753
|
-
};
|
|
1754
|
-
}
|
|
1755
|
-
resolveProfileSystemPrompt(requestPrompt, profilePlan) {
|
|
1756
|
-
return resolveSystemPrompt(requestPrompt, profilePlan?.spawn.systemPrompt);
|
|
1757
|
-
}
|
|
1758
|
-
resolveProfileModel(requestModel) {
|
|
1759
|
-
return resolveModel(requestModel, this.config.profile?.model?.default, this.config.model, this.config.profile?.model?.provider);
|
|
1760
|
-
}
|
|
1761
|
-
mergeProfilePlanEnvironment(environment, profilePlan, preserveClearedVariables = false) {
|
|
1762
|
-
if (!profilePlan || Object.keys(profilePlan.spawn.env).length === 0) return environment;
|
|
1763
|
-
const merged = { ...environment };
|
|
1764
|
-
for (const [name, value] of Object.entries(profilePlan.spawn.env)) {
|
|
1765
|
-
const existing = merged[name];
|
|
1766
|
-
if (existing !== void 0 && existing !== value || preserveClearedVariables && existing === void 0 && Object.hasOwn(environment, name) && !this.profilePlanMayFillClearedEnvironmentVariable(name, value)) throw new Error(`profile environment conflicts with provider environment: ${name}`);
|
|
1767
|
-
merged[name] = value;
|
|
1768
|
-
}
|
|
1769
|
-
return merged;
|
|
1770
|
-
}
|
|
1771
|
-
/** Provider-owned allowlist for a generated revision control after env scrub. */
|
|
1772
|
-
profilePlanMayFillClearedEnvironmentVariable(_name, _value) {
|
|
1773
|
-
return false;
|
|
1774
|
-
}
|
|
1775
|
-
async preparePromptInput(parts, workspaceRoot, sessionId, options) {
|
|
1776
|
-
return preparePromptInputParts(parts, workspaceRoot, sessionId, {
|
|
1777
|
-
...options,
|
|
1778
|
-
nativeAttachmentKinds: options?.nativeAttachmentKinds ?? this.nativeAttachmentKinds()
|
|
1779
|
-
});
|
|
1780
|
-
}
|
|
1781
|
-
/** Attachment kinds this adapter forwards natively via buildExecArgs (argv flag / stdin content block) instead of the `[Image: ...]` / `[File: ...]` text marker. */
|
|
1782
|
-
nativeAttachmentKinds() {
|
|
1783
|
-
return [];
|
|
1784
|
-
}
|
|
1785
|
-
};
|
|
1786
|
-
//#endregion
|
|
1787
|
-
//#region src/native-session-enforcement.ts
|
|
1788
|
-
/** Every shipped harness is enforced. A missing receipt is a failed proof, not a skip. */
|
|
1789
|
-
function sessionContractFullyEnforced(_provider) {
|
|
1790
|
-
return true;
|
|
1791
|
-
}
|
|
1792
|
-
//#endregion
|
|
1793
|
-
//#region src/adapter-sessions.ts
|
|
1794
|
-
/** Providers without a repair tool share one empty set. */
|
|
1795
|
-
const NO_REPAIR_TOOL_NAMES$1 = /* @__PURE__ */ new Set();
|
|
1796
|
-
/** Owns durable transcripts, provider-native session bindings and the turn hooks providers override. */
|
|
1797
|
-
var CliAdapterSessions = class extends CliAdapterProfile {
|
|
1798
|
-
async createSession(options) {
|
|
1799
|
-
this.ensureInitialized();
|
|
1800
|
-
const session = await this.getSessionStore().create({ workspaceRoot: options?.workspaceRoot ?? this.storagePath });
|
|
1801
|
-
this.logger.debug("Created session", { sessionId: session.id });
|
|
1802
|
-
return session.id;
|
|
1803
|
-
}
|
|
1804
|
-
async deleteSession(sessionId) {
|
|
1805
|
-
this.ensureInitialized();
|
|
1806
|
-
await this.getMessageStore().clear(sessionId);
|
|
1807
|
-
const deleted = await this.getSessionStore().delete(sessionId);
|
|
1808
|
-
this.logger.debug("Deleted session", {
|
|
1809
|
-
sessionId,
|
|
1810
|
-
deleted
|
|
1811
|
-
});
|
|
1812
|
-
return deleted;
|
|
1813
|
-
}
|
|
1814
|
-
/**
|
|
1815
|
-
* Choose the native ID before its argv check, durable binding, and
|
|
1816
|
-
* reservation. `input` and `timeoutMs` are the turn's, for a harness that
|
|
1817
|
-
* decides here whether a process started earlier can serve it.
|
|
1818
|
-
*/
|
|
1819
|
-
mintNativeSessionId(_context) {
|
|
1820
|
-
return randomUUID();
|
|
1821
|
-
}
|
|
1822
|
-
/**
|
|
1823
|
-
* Make a pre-minted native session durable before the CLI starts, for a
|
|
1824
|
-
* harness that would otherwise write nothing until its first model reply
|
|
1825
|
-
* (Pi). Called after the argv check and before spawn.
|
|
1826
|
-
*/
|
|
1827
|
-
reserveNativeSession(_nativeSessionId, _context) {}
|
|
1828
|
-
/**
|
|
1829
|
-
* Verify that the provider can resume the native session binding. By
|
|
1830
|
-
* default a session in a per-session store is resumable only when one of
|
|
1831
|
-
* its declared files exists under the runtime HOME; a pre-minted id whose
|
|
1832
|
-
* first turn died before the CLI wrote anything is not.
|
|
1833
|
-
*/
|
|
1834
|
-
providerSessionIsResumable(runtimeHome, providerSessionId) {
|
|
1835
|
-
const contract = this.sessionContract();
|
|
1836
|
-
if (!contract) return true;
|
|
1837
|
-
if (!sessionContractFullyEnforced(this.provider) && contract.identity.kind !== "pre-minted") return true;
|
|
1838
|
-
return declaredSessionExists(contract, runtimeHome, providerSessionId);
|
|
1839
|
-
}
|
|
1840
|
-
/**
|
|
1841
|
-
* Resolve the provider-native session id the next turn can resume, or
|
|
1842
|
-
* undefined when no usable binding exists. Provider session data lives
|
|
1843
|
-
* under the runtime home that minted it. A different home or missing
|
|
1844
|
-
* provider storage cannot resume that session. A record without a home
|
|
1845
|
-
* stamp predates the field, so treat it as same-home.
|
|
1846
|
-
*/
|
|
1847
|
-
async resumableProviderSessionId(session) {
|
|
1848
|
-
if (!session.providerSessionId) return void 0;
|
|
1849
|
-
const currentHome = this.config.runtimeHome?.trim() || void 0;
|
|
1850
|
-
const mintedHome = session.providerSessionHome?.trim() || void 0;
|
|
1851
|
-
if (currentHome !== void 0 && mintedHome !== void 0 && mintedHome !== currentHome) return;
|
|
1852
|
-
if (currentHome !== void 0) {
|
|
1853
|
-
if (!await this.providerSessionIsResumable(currentHome, session.providerSessionId)) return;
|
|
1854
|
-
}
|
|
1855
|
-
return session.providerSessionId;
|
|
1856
|
-
}
|
|
1857
|
-
/** Bind a checkpoint-proven native session to this provider identity before execution. */
|
|
1858
|
-
async seedNativeSessionForResume(input, nativeSessionId) {
|
|
1859
|
-
if (!input.sessionId) throw new Error("Native resume requires a named session");
|
|
1860
|
-
const runtimeHome = this.config.runtimeHome?.trim();
|
|
1861
|
-
if (!runtimeHome || !await this.providerSessionIsResumable(runtimeHome, nativeSessionId)) throw new Error("Native resume session is absent from this provider home");
|
|
1862
|
-
const store = this.getSessionStore();
|
|
1863
|
-
const existing = await store.get(input.sessionId);
|
|
1864
|
-
if (!existing) await store.create({
|
|
1865
|
-
id: input.sessionId,
|
|
1866
|
-
workspaceRoot: input.workspaceRoot ?? this.storagePath,
|
|
1867
|
-
providerSessionId: nativeSessionId,
|
|
1868
|
-
providerSessionHome: runtimeHome
|
|
1869
|
-
});
|
|
1870
|
-
else if (existing.providerSessionId !== nativeSessionId || existing.providerSessionHome !== runtimeHome) await store.update(input.sessionId, {
|
|
1871
|
-
providerSessionId: nativeSessionId,
|
|
1872
|
-
providerSessionHome: runtimeHome
|
|
1873
|
-
});
|
|
1874
|
-
}
|
|
1875
|
-
/**
|
|
1876
|
-
* Forks the persisted session and copies its messages up to `messageId`.
|
|
1877
|
-
* The forked record carries no provider session id, so its first turn
|
|
1878
|
-
* starts a fresh CLI session and receives the copied transcript as
|
|
1879
|
-
* execution-only context. Callers that address turns by their own session
|
|
1880
|
-
* id (the sidecar) pass that id as `targetSessionId` so later turns find
|
|
1881
|
-
* the forked record.
|
|
1882
|
-
*/
|
|
1883
|
-
async forkSession(options) {
|
|
1884
|
-
this.ensureInitialized();
|
|
1885
|
-
const result = await forkSessionWithMessages(this.getSessionStore(), this.getMessageStore(), options.sessionId, {
|
|
1886
|
-
id: options.targetSessionId,
|
|
1887
|
-
messageId: options.messageId,
|
|
1888
|
-
workspaceRoot: options.workspaceRoot
|
|
1889
|
-
});
|
|
1890
|
-
this.logger.debug("Forked session", {
|
|
1891
|
-
parentId: options.sessionId,
|
|
1892
|
-
newId: result.session.id,
|
|
1893
|
-
messagesCopied: result.messagesCopied
|
|
1894
|
-
});
|
|
1895
|
-
return result.session.id;
|
|
1896
|
-
}
|
|
1897
|
-
async getMessages(options) {
|
|
1898
|
-
this.ensureInitialized();
|
|
1899
|
-
return (await this.getMessageStore().list(options.sessionId, {
|
|
1900
|
-
limit: options.limit,
|
|
1901
|
-
offset: options.offset
|
|
1902
|
-
})).map((msg) => this.toBackendMessage(msg));
|
|
1903
|
-
}
|
|
1904
|
-
toBackendMessage(msg) {
|
|
1905
|
-
return {
|
|
1906
|
-
id: msg.id,
|
|
1907
|
-
role: msg.role,
|
|
1908
|
-
parts: msg.parts,
|
|
1909
|
-
timestamp: msg.timestamp.toISOString(),
|
|
1910
|
-
metadata: msg.metadata
|
|
1911
|
-
};
|
|
1912
|
-
}
|
|
1913
|
-
buildAssistantParts(state) {
|
|
1914
|
-
const parts = [];
|
|
1915
|
-
for (const reasoning of state.reasoningBuffer) parts.push({
|
|
1916
|
-
type: "reasoning",
|
|
1917
|
-
text: reasoning
|
|
1918
|
-
});
|
|
1919
|
-
if (state.textBuffer) parts.push({
|
|
1920
|
-
type: "text",
|
|
1921
|
-
text: state.textBuffer
|
|
1922
|
-
});
|
|
1923
|
-
for (const toolCall of state.toolCalls.values()) {
|
|
1924
|
-
const inputRecord = toolCall.input ?? {};
|
|
1925
|
-
let toolState;
|
|
1926
|
-
if (toolCall.status === "completed") toolState = {
|
|
1927
|
-
status: "completed",
|
|
1928
|
-
input: inputRecord,
|
|
1929
|
-
output: toolCall.output
|
|
1930
|
-
};
|
|
1931
|
-
else if (toolCall.status === "failed") toolState = {
|
|
1932
|
-
status: "failed",
|
|
1933
|
-
input: inputRecord,
|
|
1934
|
-
error: toolCall.output ? String(toolCall.output) : void 0
|
|
1935
|
-
};
|
|
1936
|
-
else if (toolCall.status === "running") toolState = {
|
|
1937
|
-
status: "running",
|
|
1938
|
-
input: inputRecord
|
|
1939
|
-
};
|
|
1940
|
-
else toolState = {
|
|
1941
|
-
status: "pending",
|
|
1942
|
-
input: inputRecord
|
|
1943
|
-
};
|
|
1944
|
-
parts.push({
|
|
1945
|
-
type: "tool",
|
|
1946
|
-
tool: toolCall.name,
|
|
1947
|
-
callID: toolCall.id,
|
|
1948
|
-
state: toolState
|
|
1949
|
-
});
|
|
1950
|
-
}
|
|
1951
|
-
for (const file of state.fileParts ?? []) parts.push({
|
|
1952
|
-
type: "file",
|
|
1953
|
-
id: file.id,
|
|
1954
|
-
filename: file.filename,
|
|
1955
|
-
mediaType: file.mediaType,
|
|
1956
|
-
url: file.url
|
|
1957
|
-
});
|
|
1958
|
-
return parts;
|
|
1959
|
-
}
|
|
1960
|
-
extractToolInvocations(state) {
|
|
1961
|
-
const invocations = [];
|
|
1962
|
-
for (const toolCall of state.toolCalls.values()) invocations.push({
|
|
1963
|
-
toolName: toolCall.name,
|
|
1964
|
-
input: toolCall.input,
|
|
1965
|
-
result: toolCall.output,
|
|
1966
|
-
...toolCall.status === "failed" ? { isError: true } : {}
|
|
1967
|
-
});
|
|
1968
|
-
return invocations;
|
|
1969
|
-
}
|
|
1970
|
-
/**
|
|
1971
|
-
* Names of tools this provider substitutes for a model tool call it could
|
|
1972
|
-
* not parse into any real tool.
|
|
1973
|
-
*
|
|
1974
|
-
* Default: none. A provider that has one must say so, because these calls
|
|
1975
|
-
* do not present as failures. OpenCode's `invalid` reports the parse error
|
|
1976
|
-
* as its output and completes, so the repeated-failure guard reads a run of
|
|
1977
|
-
* them as progress and clears its count on every one.
|
|
1978
|
-
*/
|
|
1979
|
-
toolCallRepairToolNames() {
|
|
1980
|
-
return NO_REPAIR_TOOL_NAMES$1;
|
|
1981
|
-
}
|
|
1982
|
-
/**
|
|
1983
|
-
* Decide whether a non-zero exit should be treated as success.
|
|
1984
|
-
*
|
|
1985
|
-
* Default: no — any non-zero exit is a failure.
|
|
1986
|
-
*
|
|
1987
|
-
* Providers override this when their CLI has a known "exits non-zero
|
|
1988
|
-
* on a successful run" quirk (e.g. kimi writes its resume trailer to
|
|
1989
|
-
* stderr and exits non-zero even when the task completed cleanly).
|
|
1990
|
-
*
|
|
1991
|
-
* The decision is made AFTER the stream has drained and the exit
|
|
1992
|
-
* code is known, so `state` reflects the full set of events the
|
|
1993
|
-
* parser emitted (including any terminal `result` / `done` event).
|
|
1994
|
-
* A safe heuristic is: "non-zero exit + stderr matches known trailer
|
|
1995
|
-
* pattern + at least one assistant part was recorded".
|
|
1996
|
-
*/
|
|
1997
|
-
isBenignNonZeroExit(_params) {
|
|
1998
|
-
return false;
|
|
1999
|
-
}
|
|
2000
|
-
/** Observe settled process output before success/failure classification. */
|
|
2001
|
-
async afterCliProcessExit(_params) {}
|
|
2002
|
-
executionOutcome(_state, _input) {
|
|
2003
|
-
return { type: "completed" };
|
|
2004
|
-
}
|
|
2005
|
-
/**
|
|
2006
|
-
* Format a message for stdin input.
|
|
2007
|
-
*/
|
|
2008
|
-
formatStdinMessage(message) {
|
|
2009
|
-
return message;
|
|
2010
|
-
}
|
|
2011
|
-
};
|
|
2012
|
-
/**
|
|
2013
|
-
* Whether any file below `root` (relative path) matches one of `matchers`.
|
|
2014
|
-
* Bounded: native stores nest a few levels and hold one file per session.
|
|
2015
|
-
*/
|
|
2016
|
-
async function anyFileMatches(root, matchers) {
|
|
2017
|
-
const pending = [""];
|
|
2018
|
-
let visited = 0;
|
|
2019
|
-
while (pending.length > 0 && visited < 2e4) {
|
|
2020
|
-
const relative = pending.pop();
|
|
2021
|
-
if (relative === void 0) break;
|
|
2022
|
-
let entries;
|
|
2023
|
-
try {
|
|
2024
|
-
entries = await readdir(path.join(root, relative), { withFileTypes: true });
|
|
2025
|
-
} catch {
|
|
2026
|
-
continue;
|
|
2027
|
-
}
|
|
2028
|
-
for (const entry of entries) {
|
|
2029
|
-
visited++;
|
|
2030
|
-
const child = relative ? `${relative}/${entry.name}` : entry.name;
|
|
2031
|
-
if (entry.isDirectory()) pending.push(child);
|
|
2032
|
-
else if (entry.isFile() && matchers.some((matcher) => matcher.test(child))) return true;
|
|
2033
|
-
}
|
|
2034
|
-
}
|
|
2035
|
-
return false;
|
|
2036
|
-
}
|
|
2037
|
-
/** Whether a per-session store holds a declared file for this id. */
|
|
2038
|
-
async function declaredSessionExists(contract, runtimeHome, nativeSessionId) {
|
|
2039
|
-
for (const store of contract.stores) {
|
|
2040
|
-
if (store.shared) return true;
|
|
2041
|
-
const matchers = store.files.map((glob) => compileSessionStoreGlob(glob, nativeSessionId));
|
|
2042
|
-
if (await anyFileMatches(path.join(runtimeHome, store.root), matchers)) return true;
|
|
2043
|
-
}
|
|
2044
|
-
return false;
|
|
2045
|
-
}
|
|
2046
|
-
//#endregion
|
|
2047
|
-
//#region src/cli-exit-diagnostics.ts
|
|
2048
|
-
/** Characters of stderr a failure message carries, taken from the tail. */
|
|
2049
|
-
const FAILURE_STDERR_TAIL_LIMIT = 600;
|
|
2050
|
-
/**
|
|
2051
|
-
* Attach the process-level diagnostics this run already collected to a failure
|
|
2052
|
-
* message.
|
|
2053
|
-
*
|
|
2054
|
-
* A provider that reports its OWN terminal failure — an empty event stream, an
|
|
2055
|
-
* unfinished model step — explains what it saw but not how the process ended:
|
|
2056
|
-
* a CLI that exited 0 having emitted nothing is misconfigured, one killed on the
|
|
2057
|
-
* timeout is stuck, one that exited 9 with stderr crashed. Each part is skipped
|
|
2058
|
-
* when the message already states it, so a message derived FROM stderr or the
|
|
2059
|
-
* exit code is never doubled.
|
|
2060
|
-
*/
|
|
2061
|
-
function withCliExitDiagnostics(message, exit, stderr) {
|
|
2062
|
-
const parts = [];
|
|
2063
|
-
if (exit.stdinError && !message.includes(exit.stdinError)) parts.push(`stdin failed: ${exit.stdinError}`);
|
|
2064
|
-
const timeout = exit.timedOut ? exit.timeoutDiagnostic ?? "timed out" : "";
|
|
2065
|
-
if (timeout && !message.toLowerCase().includes(timeout.toLowerCase())) parts.push(timeout);
|
|
2066
|
-
if (exit.code !== null) {
|
|
2067
|
-
if (!message.includes(`code ${exit.code}`)) parts.push(`exit code ${exit.code}`);
|
|
2068
|
-
} else if (exit.signal) parts.push(`killed by ${exit.signal}`);
|
|
2069
|
-
const trimmedStderr = stderr.trim();
|
|
2070
|
-
if (trimmedStderr && !message.includes(trimmedStderr)) {
|
|
2071
|
-
const tail = trimmedStderr.slice(-FAILURE_STDERR_TAIL_LIMIT);
|
|
2072
|
-
parts.push(`stderr: ${tail === trimmedStderr ? "" : "…"}${tail}`);
|
|
2073
|
-
}
|
|
2074
|
-
return parts.length === 0 ? message : `${message} (${parts.join("; ")})`;
|
|
2075
|
-
}
|
|
2076
|
-
//#endregion
|
|
2077
|
-
//#region src/mcp-commands.ts
|
|
2078
|
-
/** Execute one prepared MCP registration command. Serialization belongs to the adapter. */
|
|
2079
|
-
async function addCliMcp(options) {
|
|
2080
|
-
const { name, config, run, logger } = options;
|
|
2081
|
-
logger.debug("Adding MCP server", {
|
|
2082
|
-
name,
|
|
2083
|
-
type: config.type
|
|
2084
|
-
});
|
|
2085
|
-
const result = await run();
|
|
2086
|
-
if (!result.exit.success) throw new Error(`Failed to add MCP server "${name}": ${result.stderr || `exit code ${result.exit.code}`}`);
|
|
2087
|
-
logger.info("Added MCP server", { name });
|
|
2088
|
-
}
|
|
2089
|
-
/** List native MCP servers with the provider's parser and command budget. */
|
|
2090
|
-
async function listCliMcps(options) {
|
|
2091
|
-
const { provider, args, run, logger, parse } = options;
|
|
2092
|
-
if (!args) {
|
|
2093
|
-
logger.warn(`getMcpStatus: ${provider} CLI does not support listing MCP servers`);
|
|
2094
|
-
return { servers: {} };
|
|
2095
|
-
}
|
|
2096
|
-
const result = await run(args);
|
|
2097
|
-
if (!result.exit.success) throw new Error(withCliExitDiagnostics(`${provider} failed to list MCP servers`, result.exit, result.stderr));
|
|
2098
|
-
return parse(result.stdout ?? "");
|
|
2099
|
-
}
|
|
2100
|
-
const MCP_STATUSES = [
|
|
2101
|
-
"connected",
|
|
2102
|
-
"disconnected",
|
|
2103
|
-
"error",
|
|
2104
|
-
"unknown"
|
|
2105
|
-
];
|
|
2106
|
-
const MCP_TYPES = [
|
|
2107
|
-
"local",
|
|
2108
|
-
"remote",
|
|
2109
|
-
"stdio",
|
|
2110
|
-
"http"
|
|
2111
|
-
];
|
|
2112
|
-
function pick(known, value, fallback) {
|
|
2113
|
-
return known.includes(value) ? value : fallback;
|
|
2114
|
-
}
|
|
2115
|
-
/**
|
|
2116
|
-
* Read an `mcp list` report: a JSON array of `{name, status, type}`, a JSON
|
|
2117
|
-
* object keyed by name, or a whitespace table whose first column is the name
|
|
2118
|
-
* and whose next columns may hold the type and the status.
|
|
2119
|
-
*/
|
|
2120
|
-
function parseMcpListReport(output) {
|
|
2121
|
-
const servers = {};
|
|
2122
|
-
const add = (name, status, type) => {
|
|
2123
|
-
servers[name] = {
|
|
2124
|
-
name,
|
|
2125
|
-
status: pick(MCP_STATUSES, status, "unknown"),
|
|
2126
|
-
type: pick(MCP_TYPES, type, "stdio")
|
|
2127
|
-
};
|
|
2128
|
-
};
|
|
2129
|
-
let data;
|
|
2130
|
-
try {
|
|
2131
|
-
data = JSON.parse(output);
|
|
2132
|
-
} catch {
|
|
2133
|
-
for (const line of output.trim().split("\n")) {
|
|
2134
|
-
if (!line.trim() || line.includes("NAME") || line.startsWith("-")) continue;
|
|
2135
|
-
const [name = "", type, status] = line.trim().split(/\s{2,}/);
|
|
2136
|
-
add(name, status, type);
|
|
2137
|
-
}
|
|
2138
|
-
return { servers };
|
|
2139
|
-
}
|
|
2140
|
-
if (Array.isArray(data)) for (const server of data) add(server.name, server.status ?? "connected", server.type);
|
|
2141
|
-
else if (typeof data === "object" && data !== null) for (const [name, info] of Object.entries(data)) add(name, info.status, void 0);
|
|
2142
|
-
return { servers };
|
|
2143
|
-
}
|
|
2144
|
-
/** A local MCP server's argv: the `command` array, or `command` then `args`. */
|
|
2145
|
-
function localMcpArgv(config) {
|
|
2146
|
-
const local = config;
|
|
2147
|
-
return Array.isArray(local.command) ? local.command : [local.command, ...local.args ?? []];
|
|
2148
|
-
}
|
|
2149
|
-
/**
|
|
2150
|
-
* A local MCP server's environment, from `environment` (agent-backend-types)
|
|
2151
|
-
* or `env` (agent-interface): reading one drops the other family's variables.
|
|
2152
|
-
*/
|
|
2153
|
-
function localMcpEnv(config) {
|
|
2154
|
-
return config.environment ?? config.env;
|
|
2155
|
-
}
|
|
2156
|
-
const isLocalMcpConfig = (c) => c.type === "local" || c.type === "stdio";
|
|
2157
|
-
/** Build MCP add command arguments. Override for CLI-specific syntax. */
|
|
2158
|
-
function defaultMcpAddArgs(name, config) {
|
|
2159
|
-
if (isLocalMcpConfig(config)) {
|
|
2160
|
-
const env = Object.entries(localMcpEnv(config) ?? {});
|
|
2161
|
-
return [
|
|
2162
|
-
"mcp",
|
|
2163
|
-
"add",
|
|
2164
|
-
name,
|
|
2165
|
-
"--",
|
|
2166
|
-
...localMcpArgv(config),
|
|
2167
|
-
...env.flatMap(([key, value]) => ["--env", `${key}=${value}`])
|
|
2168
|
-
];
|
|
2169
|
-
}
|
|
2170
|
-
const headers = Object.entries(config.headers ?? {});
|
|
2171
|
-
return [
|
|
2172
|
-
"mcp",
|
|
2173
|
-
"add",
|
|
2174
|
-
name,
|
|
2175
|
-
config.url,
|
|
2176
|
-
...headers.flatMap(([key, value]) => ["--header", `${key}: ${value}`])
|
|
2177
|
-
];
|
|
2178
|
-
}
|
|
2179
|
-
//#endregion
|
|
2180
|
-
//#region src/adapter-mcp.ts
|
|
2181
|
-
/** Owns serialized MCP mutations and provider-native MCP status commands. */
|
|
2182
|
-
var CliAdapterMcp = class extends CliAdapterSessions {
|
|
2183
|
-
/** Budget for one `mcp list` run, CLI startup and every server's handshake. */
|
|
2184
|
-
mcpStatusTimeoutMs = 1e4;
|
|
2185
|
-
mcpMutationChain = Promise.resolve();
|
|
2186
|
-
async addMcp(name, config) {
|
|
2187
|
-
const run = this.mcpMutationChain.then(() => this.addMcpSerialized(name, config));
|
|
2188
|
-
this.mcpMutationChain = run.catch(() => void 0);
|
|
2189
|
-
return run;
|
|
2190
|
-
}
|
|
2191
|
-
async addMcpSerialized(name, config) {
|
|
2192
|
-
this.ensureInitialized();
|
|
2193
|
-
const args = this.buildMcpAddArgs(name, config);
|
|
2194
|
-
return addCliMcp({
|
|
2195
|
-
name,
|
|
2196
|
-
config,
|
|
2197
|
-
logger: this.logger,
|
|
2198
|
-
run: () => this.runAuxiliaryCli(args, 3e4)
|
|
2199
|
-
});
|
|
2200
|
-
}
|
|
2201
|
-
async getMcpStatus() {
|
|
2202
|
-
this.ensureInitialized();
|
|
2203
|
-
return listCliMcps({
|
|
2204
|
-
provider: this.provider,
|
|
2205
|
-
args: this.buildMcpListArgs(),
|
|
2206
|
-
logger: this.logger,
|
|
2207
|
-
run: (args) => this.runAuxiliaryCli(args, this.mcpStatusTimeoutMs),
|
|
2208
|
-
parse: (output) => this.parseMcpListOutput(output)
|
|
2209
|
-
});
|
|
2210
|
-
}
|
|
2211
|
-
async ensureMcps(required) {
|
|
2212
|
-
const currentStatus = await this.getMcpStatus();
|
|
2213
|
-
const existingServers = new Set(Object.keys(currentStatus.servers));
|
|
2214
|
-
for (const [name, config] of Object.entries(required)) {
|
|
2215
|
-
if (existingServers.has(name)) {
|
|
2216
|
-
this.logger.debug("MCP server already exists, skipping", { name });
|
|
2217
|
-
continue;
|
|
2218
|
-
}
|
|
2219
|
-
await this.addMcp(name, config);
|
|
2220
|
-
}
|
|
2221
|
-
}
|
|
2222
|
-
/** Build MCP add command arguments. Override for CLI-specific syntax. */
|
|
2223
|
-
buildMcpAddArgs(name, config) {
|
|
2224
|
-
return defaultMcpAddArgs(name, config);
|
|
2225
|
-
}
|
|
2226
|
-
/**
|
|
2227
|
-
* Build MCP list command arguments. Return null if not supported.
|
|
2228
|
-
*/
|
|
2229
|
-
buildMcpListArgs() {
|
|
2230
|
-
return null;
|
|
2231
|
-
}
|
|
2232
|
-
/** Parse MCP list output. Override for CLI-specific output format. */
|
|
2233
|
-
parseMcpListOutput(output) {
|
|
2234
|
-
return parseMcpListReport(output);
|
|
2235
|
-
}
|
|
2236
|
-
};
|
|
2237
|
-
//#endregion
|
|
2238
|
-
//#region src/execution-state.ts
|
|
2239
|
-
/** A file of the sandbox's cgroup (#7571); undefined off Linux or outside one. */
|
|
2240
|
-
function readCgroupFile(name) {
|
|
2241
|
-
try {
|
|
2242
|
-
return readFileSync(`/sys/fs/cgroup/${name}`, "utf8").trim();
|
|
2243
|
-
} catch {
|
|
2244
|
-
return;
|
|
2245
|
-
}
|
|
2246
|
-
}
|
|
2247
|
-
/** The sandbox's OOM-kill count: a running total for the whole box. */
|
|
2248
|
-
function oomKills() {
|
|
2249
|
-
const events = readCgroupFile("memory.events") ?? "";
|
|
2250
|
-
return Number(/^oom_kill (\d+)$/m.exec(events)?.[1] ?? 0);
|
|
2251
|
-
}
|
|
2252
|
-
/** Creates a new execution state. */
|
|
2253
|
-
function createExecutionState(sessionId, messageId, workspaceRoot) {
|
|
2254
|
-
return {
|
|
2255
|
-
sessionId,
|
|
2256
|
-
messageId: messageId ?? randomUUID(),
|
|
2257
|
-
workspaceRoot,
|
|
2258
|
-
toolCalls: /* @__PURE__ */ new Map(),
|
|
2259
|
-
textBuffer: "",
|
|
2260
|
-
reasoningBuffer: [],
|
|
2261
|
-
oomKillsAtStart: oomKills()
|
|
2262
|
-
};
|
|
2263
|
-
}
|
|
2264
|
-
//#endregion
|
|
2265
|
-
//#region src/session-preparation.ts
|
|
2266
|
-
async function prepareExecutionSession(context, input) {
|
|
2267
|
-
let sessionId = input.sessionId;
|
|
2268
|
-
let providerSessionId;
|
|
2269
|
-
let transcriptReplay;
|
|
2270
|
-
if (sessionId) {
|
|
2271
|
-
const session = await context.getSessionStore().get(sessionId);
|
|
2272
|
-
if (session) {
|
|
2273
|
-
providerSessionId = await context.resumableProviderSessionId(session);
|
|
2274
|
-
if (session.parentId !== void 0 && session.providerSessionId === void 0) transcriptReplay = "fork";
|
|
2275
|
-
else if (context.config.secretProfileRevisionRoot && session.providerSessionId === void 0 && (await context.getMessageStore().list(sessionId)).length > 0) transcriptReplay = "restored";
|
|
2276
|
-
else if (session.providerSessionId && providerSessionId === void 0) {
|
|
2277
|
-
context.logger.info("Provider session data is not resumable; replaying durable history into a fresh provider session", {
|
|
2278
|
-
sessionId,
|
|
2279
|
-
providerSessionId: session.providerSessionId
|
|
2280
|
-
});
|
|
2281
|
-
transcriptReplay = "restored";
|
|
2282
|
-
}
|
|
2283
|
-
await context.getSessionStore().update(sessionId, {});
|
|
2284
|
-
} else {
|
|
2285
|
-
if (context.config.secretProfileRevisionRoot) try {
|
|
2286
|
-
transcriptReplay = (await context.getMessageStore().list(sessionId, {
|
|
2287
|
-
requireComplete: true,
|
|
2288
|
-
allowAbsent: true
|
|
2289
|
-
})).length > 0 ? "restored" : void 0;
|
|
2290
|
-
} catch (error) {
|
|
2291
|
-
throw new SessionRehydrationError(`Session ${sessionId} has incomplete persisted history`, error);
|
|
2292
|
-
}
|
|
2293
|
-
else transcriptReplay = "restored";
|
|
2294
|
-
await context.getSessionStore().create({
|
|
2295
|
-
id: sessionId,
|
|
2296
|
-
workspaceRoot: input.workspaceRoot ?? context.storagePath
|
|
2297
|
-
});
|
|
2298
|
-
}
|
|
2299
|
-
} else sessionId = await context.createSession({ workspaceRoot: input.workspaceRoot ?? context.storagePath });
|
|
2300
|
-
return {
|
|
2301
|
-
sessionId,
|
|
2302
|
-
providerSessionId,
|
|
2303
|
-
transcriptReplay
|
|
2304
|
-
};
|
|
2305
|
-
}
|
|
2306
|
-
//#endregion
|
|
2307
|
-
//#region src/turn-execution.ts
|
|
2308
|
-
/**
|
|
2309
|
-
* Turn idempotency: if the caller supplied a turnId and a previously-
|
|
2310
|
-
* completed turn with that id exists, return its cached result without
|
|
2311
|
-
* re-issuing the upstream LLM call. Both `findCompletedTurn` and
|
|
2312
|
-
* `setTurnId` are optional on the recorder interface so providers using
|
|
2313
|
-
* a no-op recorder (sdk-backend) still typecheck.
|
|
2314
|
-
*/
|
|
2315
|
-
async function findCachedTurn(recorder, input, logger, sessionId) {
|
|
2316
|
-
if (!(input.turnId && recorder.findCompletedTurn)) return void 0;
|
|
2317
|
-
const cached = await recorder.findCompletedTurn(input.turnId);
|
|
2318
|
-
if (!cached) return void 0;
|
|
2319
|
-
logger.info("Returning cached turn result (idempotent retry)", {
|
|
2320
|
-
turnId: input.turnId,
|
|
2321
|
-
sessionId
|
|
2322
|
-
});
|
|
2323
|
-
return {
|
|
2324
|
-
...cached,
|
|
2325
|
-
outcome: AgentExecutionOutcomeSchema.parse(cached.outcome)
|
|
2326
|
-
};
|
|
2327
|
-
}
|
|
2328
|
-
/**
|
|
2329
|
-
* The persisted transcript, read before this turn's user message is appended
|
|
2330
|
-
* so the block never contains the current message. Execution-only: it reaches
|
|
2331
|
-
* the model prompt and stays out of both stores.
|
|
2332
|
-
*/
|
|
2333
|
-
async function buildTranscriptContext(params) {
|
|
2334
|
-
const { provider, sessionId, transcriptReplay, secretProfileRevisionRoot } = params;
|
|
2335
|
-
if (!transcriptReplay) return void 0;
|
|
2336
|
-
try {
|
|
2337
|
-
return (secretProfileRevisionRoot ? buildCompleteForkContextBlock : buildForkContextBlock)(await params.getMessageStore().list(sessionId, { requireComplete: Boolean(secretProfileRevisionRoot) }), transcriptReplay === "restored" ? {
|
|
2338
|
-
preamble: RESTORED_SESSION_CONTEXT_PREAMBLE,
|
|
2339
|
-
tag: RESTORED_SESSION_CONTEXT_TAG,
|
|
2340
|
-
...secretProfileRevisionRoot ? { maxChars: MAX_SECRET_PROFILE_REPLAY_CHARS } : {}
|
|
2341
|
-
} : secretProfileRevisionRoot ? { maxChars: MAX_SECRET_PROFILE_REPLAY_CHARS } : void 0);
|
|
2342
|
-
} catch (error) {
|
|
2343
|
-
throw new SessionRehydrationError(`Session ${sessionId} has persisted history that could not be read for replay into a fresh ${provider} session: ${error instanceof Error ? error.message : String(error)}`, error);
|
|
2344
|
-
}
|
|
2345
|
-
}
|
|
2346
|
-
/**
|
|
2347
|
-
* A harness that can only report its id in an event must do so promptly; a
|
|
2348
|
-
* turn whose native session cannot be named is not traceable. Returns the
|
|
2349
|
-
* deadline timer, or undefined when the contract sets none.
|
|
2350
|
-
*/
|
|
2351
|
-
function armNativeSessionDeadline(params) {
|
|
2352
|
-
const { provider, sessionId, sessionContract, state, observer, logger } = params;
|
|
2353
|
-
const identity = sessionContract?.identity;
|
|
2354
|
-
if (identity?.kind !== "first-event" || !sessionContractFullyEnforced(provider)) return;
|
|
2355
|
-
const deadline = setTimeout(() => {
|
|
2356
|
-
if (observer.nativeSessionObserved || state.terminalStatus !== void 0) return;
|
|
2357
|
-
const detail = new HarnessSessionContractError("native_session_id_unobserved", `${provider} reported no native session id within ${identity.deadlineMs} ms`).message;
|
|
2358
|
-
state.terminalStatus = "failed";
|
|
2359
|
-
state.lastDiagnostic = detail;
|
|
2360
|
-
logger.error("Native session id unobserved; stopping turn", { sessionId });
|
|
2361
|
-
params.kill();
|
|
2362
|
-
}, identity.deadlineMs);
|
|
2363
|
-
deadline.unref();
|
|
2364
|
-
return deadline;
|
|
2365
|
-
}
|
|
2366
|
-
function idIn(glob, rel) {
|
|
2367
|
-
const g = glob.split("/");
|
|
2368
|
-
const r = rel.split("/");
|
|
2369
|
-
let gi = 0;
|
|
2370
|
-
let ri = 0;
|
|
2371
|
-
let id = null;
|
|
2372
|
-
while (gi < g.length && ri < r.length) {
|
|
2373
|
-
const part = g[gi];
|
|
2374
|
-
if (part === "**") {
|
|
2375
|
-
const rest = g.slice(gi + 1).join("/");
|
|
2376
|
-
if (!rest) return id;
|
|
2377
|
-
for (let k = ri; k < r.length; k++) {
|
|
2378
|
-
const found = idIn(rest, r.slice(k).join("/"));
|
|
2379
|
-
if (found) return found;
|
|
2380
|
-
}
|
|
2381
|
-
return null;
|
|
2382
|
-
}
|
|
2383
|
-
if (part?.includes("{id}")) {
|
|
2384
|
-
const [pre, post] = part.split("{id}");
|
|
2385
|
-
const name = r[ri] ?? "";
|
|
2386
|
-
if (!name.startsWith(pre ?? "") || !name.endsWith(post ?? "")) return null;
|
|
2387
|
-
id = name.slice((pre ?? "").length, name.length - (post ?? "").length);
|
|
2388
|
-
} else if (part !== "*" && part !== r[ri]) return null;
|
|
2389
|
-
gi += 1;
|
|
2390
|
-
ri += 1;
|
|
2391
|
-
}
|
|
2392
|
-
return gi === g.length && ri === r.length ? id : null;
|
|
2393
|
-
}
|
|
2394
|
-
function filesUnder(dir, prefix, out) {
|
|
2395
|
-
let entries;
|
|
2396
|
-
try {
|
|
2397
|
-
entries = readdirSync(dir, { withFileTypes: true });
|
|
2398
|
-
} catch {
|
|
2399
|
-
return;
|
|
2400
|
-
}
|
|
2401
|
-
for (const entry of entries) {
|
|
2402
|
-
const name = String(entry.name);
|
|
2403
|
-
const rel = prefix ? `${prefix}/${name}` : name;
|
|
2404
|
-
if (entry.isDirectory()) filesUnder(join(dir, name), rel, out);
|
|
2405
|
-
else out.push(rel);
|
|
2406
|
-
}
|
|
2407
|
-
}
|
|
2408
|
-
function idsNow(home, stores) {
|
|
2409
|
-
const ids = /* @__PURE__ */ new Set();
|
|
2410
|
-
for (const store of stores) {
|
|
2411
|
-
const root = join(home, store.root);
|
|
2412
|
-
if (store.shared && store.write === "sqlite-wal") {
|
|
2413
|
-
try {
|
|
2414
|
-
const db = new DatabaseSync(join(root, store.files[0] ?? "state.db"), { readOnly: true });
|
|
2415
|
-
try {
|
|
2416
|
-
let rows;
|
|
2417
|
-
try {
|
|
2418
|
-
rows = db.prepare("SELECT id FROM sessions").all();
|
|
2419
|
-
} catch {
|
|
2420
|
-
rows = db.prepare("SELECT session_id AS id FROM session_windows").all();
|
|
2421
|
-
}
|
|
2422
|
-
for (const row of rows) if (row.id) ids.add(row.id);
|
|
2423
|
-
} finally {
|
|
2424
|
-
db.close();
|
|
2425
|
-
}
|
|
2426
|
-
} catch {}
|
|
2427
|
-
continue;
|
|
2428
|
-
}
|
|
2429
|
-
const found = [];
|
|
2430
|
-
filesUnder(root, "", found);
|
|
2431
|
-
for (const rel of found) for (const glob of store.files) {
|
|
2432
|
-
const id = idIn(glob, rel);
|
|
2433
|
-
if (id) ids.add(id);
|
|
2434
|
-
}
|
|
2435
|
-
}
|
|
2436
|
-
return ids;
|
|
2437
|
-
}
|
|
2438
|
-
function watchNewSessionIds(params) {
|
|
2439
|
-
const home = params.runtimeHome?.trim().replace(/\/+$/u, "");
|
|
2440
|
-
const contract = params.sessionContract;
|
|
2441
|
-
if (!home || contract?.identity.kind !== "first-event") return () => {};
|
|
2442
|
-
const seen = idsNow(home, contract.stores);
|
|
2443
|
-
const timer = setInterval(() => {
|
|
2444
|
-
if (params.observer.nativeSessionObserved) return;
|
|
2445
|
-
for (const id of idsNow(home, contract.stores)) {
|
|
2446
|
-
if (seen.has(id)) continue;
|
|
2447
|
-
seen.add(id);
|
|
2448
|
-
params.observer.onEvent({
|
|
2449
|
-
type: "native.session.observed",
|
|
2450
|
-
provider: params.provider,
|
|
2451
|
-
nativeSessionId: id
|
|
2452
|
-
});
|
|
2453
|
-
}
|
|
2454
|
-
}, 200);
|
|
2455
|
-
timer.unref();
|
|
2456
|
-
return () => clearInterval(timer);
|
|
2457
|
-
}
|
|
2458
|
-
function collectStderr(cliProcess) {
|
|
2459
|
-
let text = "";
|
|
2460
|
-
const stderrDecoder = new StringDecoder("utf8");
|
|
2461
|
-
cliProcess.stderr?.on("data", (chunk) => {
|
|
2462
|
-
text += stderrDecoder.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
2463
|
-
});
|
|
2464
|
-
cliProcess.stderr?.on("end", () => {
|
|
2465
|
-
text += stderrDecoder.end();
|
|
2466
|
-
});
|
|
2467
|
-
return { get text() {
|
|
2468
|
-
return text;
|
|
2469
|
-
} };
|
|
2470
|
-
}
|
|
2471
|
-
/** A turn's stderr or ACP error when its link to its harness's server dropped (#8819). */
|
|
2472
|
-
const HARNESS_LINK_LOST = /\b(?:ECONNRESET|ECONNREFUSED|EPIPE|UND_ERR_SOCKET)\b|ended before (?:it|the turn) completed|Gateway disconnected/u;
|
|
2473
|
-
/**
|
|
2474
|
-
* Names a SIGKILL from the memory limit (#7571): the CLI runs in the sandbox's
|
|
2475
|
-
* cgroup, whose `memory.events` counts OOM kills under the `memory.max` limit.
|
|
2476
|
-
* The count is the box's running total, so only a kill since the turn started
|
|
2477
|
-
* is this turn's.
|
|
2478
|
-
*/
|
|
2479
|
-
function memoryLimitKill(killsAtStart = 0) {
|
|
2480
|
-
const kills = oomKills() - killsAtStart;
|
|
2481
|
-
if (kills < 1) return void 0;
|
|
2482
|
-
const max = readCgroupFile("memory.max") ?? "max";
|
|
2483
|
-
return `The sandbox's ${max === "max" ? "host" : `${Number(max) / 2 ** 20} MiB`} memory limit OOM-killed ${kills} process(es) during this turn: give the sandbox more memory or make the tool use less`;
|
|
2484
|
-
}
|
|
2485
|
-
/**
|
|
2486
|
-
* A failed CLI turn's end and error (#16404): a guard's end is its own, the
|
|
2487
|
-
* rest is `turnEndReason`'s; a session death and a memory-limit kill carry
|
|
2488
|
-
* their wire code.
|
|
2489
|
-
*/
|
|
2490
|
-
function endFailedTurn(params) {
|
|
2491
|
-
const { provider, state, exitResult, stderr, startTime, endTime } = params;
|
|
2492
|
-
const oom = exitResult.signal === "SIGKILL" && memoryLimitKill(state.oomKillsAtStart);
|
|
2493
|
-
state.endReason ??= turnEndReason({
|
|
2494
|
-
aborted: params.aborted,
|
|
2495
|
-
timedOut: exitResult.timedOut,
|
|
2496
|
-
harnessFailed: Boolean(state.terminalStatus),
|
|
2497
|
-
linkLost: HARNESS_LINK_LOST.test(stderr),
|
|
2498
|
-
memoryKilled: Boolean(oom),
|
|
2499
|
-
signal: exitResult.signal
|
|
2500
|
-
});
|
|
2501
|
-
if (oom && state.endReason === "memory_limit") state.failure ??= {
|
|
2502
|
-
code: "MEMORY_LIMIT_KILLED",
|
|
2503
|
-
message: oom
|
|
2504
|
-
};
|
|
2505
|
-
if (["gateway_disconnect", "harness_exit"].includes(state.endReason)) state.failure ??= {
|
|
2506
|
-
code: "SESSION_DIED",
|
|
2507
|
-
message: `Harness session died (${state.endReason}): start a new session, or send the next turn to this session to resume it`
|
|
2508
|
-
};
|
|
2509
|
-
const nix = params.nixBinPath ? ` (NIX_BIN_PATH=${params.nixBinPath})` : " (NIX_BIN_PATH unset — is the Nix profile mounted?)";
|
|
2510
|
-
const errorMessage = exitResult.spawnError ? `Failed to spawn "${params.command}": ${exitResult.spawnError}${exitResult.spawnError.includes("ENOENT") ? ` — binary not found in PATH${nix}` : ""}` : withCliExitDiagnostics(state.failure?.message ?? state.lastDiagnostic ?? (exitResult.timedOut ? exitResult.timeoutDiagnostic ?? "Execution timed out" : void 0) ?? (stderr.trim() || (state.terminalStatus === "failed" ? `${provider} reported a failed terminal status` : `Process exited with code ${exitResult.code}`)), exitResult, stderr);
|
|
2511
|
-
const failure = new AgentExecutionError(`${provider} execution failed: ${errorMessage}`, {
|
|
2512
|
-
...state.tokenUsage ? { tokenUsage: state.tokenUsage } : {},
|
|
2513
|
-
timing: {
|
|
2514
|
-
startedAt: startTime,
|
|
2515
|
-
completedAt: endTime,
|
|
2516
|
-
durationMs: endTime - startTime
|
|
2517
|
-
}
|
|
2518
|
-
});
|
|
2519
|
-
Object.assign(failure, {
|
|
2520
|
-
reason: state.endReason,
|
|
2521
|
-
...state.failure?.code ? { code: state.failure.code } : {},
|
|
2522
|
-
...state.failure?.statusCode === void 0 ? {} : {
|
|
2523
|
-
statusCode: state.failure.statusCode,
|
|
2524
|
-
status: state.failure.statusCode
|
|
2525
|
-
},
|
|
2526
|
-
...state.failure?.resetAt ? { resetAt: state.failure.resetAt } : {}
|
|
2527
|
-
});
|
|
2528
|
-
return {
|
|
2529
|
-
errorMessage,
|
|
2530
|
-
failure
|
|
2531
|
-
};
|
|
2532
|
-
}
|
|
2533
|
-
/**
|
|
2534
|
-
* Best-effort: stamp the assistant message interrupted so reattach can
|
|
2535
|
-
* surface "this turn died" without inferring from absence. Swallow any
|
|
2536
|
-
* metadata-write error — the upstream failure is the one the caller
|
|
2537
|
-
* needs to see. The coalesced part updates drain first so no stale
|
|
2538
|
-
* snapshot lands after the terminal stamp.
|
|
2539
|
-
*/
|
|
2540
|
-
async function recordTurnInterruption(params) {
|
|
2541
|
-
const { recorder, observer, logger, errorMessage, exitResult } = params;
|
|
2542
|
-
await observer.flushPartUpdates();
|
|
2543
|
-
if (recorder.markTurnInterrupted) try {
|
|
2544
|
-
await recorder.markTurnInterrupted({
|
|
2545
|
-
reason: errorMessage,
|
|
2546
|
-
exitCode: exitResult.code,
|
|
2547
|
-
timedOut: exitResult.timedOut,
|
|
2548
|
-
failureReceipt: params.failure.receipt
|
|
2549
|
-
});
|
|
2550
|
-
} catch (markError) {
|
|
2551
|
-
logger.warn("markTurnInterrupted failed (suppressed to preserve original error)", { error: markError instanceof Error ? markError.message : String(markError) });
|
|
2552
|
-
}
|
|
2553
|
-
}
|
|
2554
|
-
/** The result of a turn that completed. */
|
|
2555
|
-
function buildExecutionResult(params) {
|
|
2556
|
-
const { state, publicProfileRevision, startTime, endTime } = params;
|
|
2557
|
-
return {
|
|
2558
|
-
outcome: params.outcome,
|
|
2559
|
-
text: state.textBuffer,
|
|
2560
|
-
toolInvocations: params.toolInvocations,
|
|
2561
|
-
reasoning: state.reasoningBuffer.length > 0 ? state.reasoningBuffer : void 0,
|
|
2562
|
-
sessionId: params.sessionId,
|
|
2563
|
-
...publicProfileRevision ? { metadata: { publicProfileRevision } } : {},
|
|
2564
|
-
tokenUsage: state.tokenUsage ? {
|
|
2565
|
-
inputTokens: state.tokenUsage.inputTokens ?? 0,
|
|
2566
|
-
outputTokens: state.tokenUsage.outputTokens ?? 0,
|
|
2567
|
-
totalTokens: state.tokenUsage.totalTokens,
|
|
2568
|
-
reasoningTokens: state.tokenUsage.reasoningTokens,
|
|
2569
|
-
cacheReadInputTokens: state.tokenUsage.cacheReadInputTokens,
|
|
2570
|
-
cacheCreationInputTokens: state.tokenUsage.cacheCreationInputTokens,
|
|
2571
|
-
cost: state.tokenUsage.cost
|
|
2572
|
-
} : void 0,
|
|
2573
|
-
timing: {
|
|
2574
|
-
startedAt: startTime,
|
|
2575
|
-
completedAt: endTime,
|
|
2576
|
-
durationMs: endTime - startTime
|
|
2577
|
-
}
|
|
2578
|
-
};
|
|
2579
|
-
}
|
|
2580
|
-
function failOnProviderSessionPersistence(observer, state) {
|
|
2581
|
-
if (!observer.providerSessionPersistenceError) return;
|
|
2582
|
-
const message = observer.providerSessionPersistenceError instanceof Error ? observer.providerSessionPersistenceError.message : String(observer.providerSessionPersistenceError);
|
|
2583
|
-
state.terminalStatus = "failed";
|
|
2584
|
-
state.lastDiagnostic = `Provider session persistence failed: ${message}`;
|
|
2585
|
-
}
|
|
2586
|
-
function logAcceptedCliExit(logger, exitResult, stderr) {
|
|
2587
|
-
if (!exitResult.success) logger.info("CLI process exited non-zero but was treated as success by provider", {
|
|
2588
|
-
exitCode: exitResult.code,
|
|
2589
|
-
stderr: stderr.slice(0, 500)
|
|
2590
|
-
});
|
|
2591
|
-
if (stderr.trim()) logger.debug("CLI process stderr (success)", { stderr: stderr.slice(0, 1e3) });
|
|
2592
|
-
}
|
|
2593
|
-
//#endregion
|
|
2594
|
-
//#region src/part-update-queue.ts
|
|
2595
|
-
const MAX_PENDING_PART_UPDATES = 256;
|
|
2596
|
-
/**
|
|
2597
|
-
* Per-chunk persistence backpressure. Accumulated-text providers re-emit
|
|
2598
|
-
* the whole part on every token, and each update joins the recorder's
|
|
2599
|
-
* serialized write chain — if disk latency exceeds the token rate, the
|
|
2600
|
-
* chain grows for the turn's lifetime. Coalescing keeps only the newest
|
|
2601
|
-
* pending snapshot per partId (a stale one is a strict prefix of it), so
|
|
2602
|
-
* the queue stays O(distinct parts); a single drain loop preserves write
|
|
2603
|
-
* order. The terminal paths flush the drain before their final writes so
|
|
2604
|
-
* no stale snapshot can land after the consolidated parts.
|
|
2605
|
-
*/
|
|
2606
|
-
var PartUpdateQueue = class {
|
|
2607
|
-
pending = /* @__PURE__ */ new Map();
|
|
2608
|
-
drain = null;
|
|
2609
|
-
constructor(recorder, logger) {
|
|
2610
|
-
this.recorder = recorder;
|
|
2611
|
-
this.logger = logger;
|
|
2612
|
-
}
|
|
2613
|
-
schedule(part, delta) {
|
|
2614
|
-
const partId = part.id;
|
|
2615
|
-
if (!partId) {
|
|
2616
|
-
this.recorder.recordAssistantPartUpdate?.(part, delta)?.catch((cause) => {
|
|
2617
|
-
this.logger.warn("Per-chunk persistence failed (continuing stream)", { error: cause instanceof Error ? cause.message : String(cause) });
|
|
2618
|
-
});
|
|
2619
|
-
return;
|
|
2620
|
-
}
|
|
2621
|
-
this.pending.set(partId, {
|
|
2622
|
-
part,
|
|
2623
|
-
delta
|
|
2624
|
-
});
|
|
2625
|
-
if (this.pending.size > MAX_PENDING_PART_UPDATES) {
|
|
2626
|
-
const oldestPending = this.pending.keys().next();
|
|
2627
|
-
if (!oldestPending.done) this.pending.delete(oldestPending.value);
|
|
2628
|
-
this.logger.warn("Part-update persist backlog overflow, dropped oldest pending snapshot", { pendingLimit: MAX_PENDING_PART_UPDATES });
|
|
2629
|
-
}
|
|
2630
|
-
if (!this.drain) this.drain = this.drainPending().finally(() => {
|
|
2631
|
-
this.drain = null;
|
|
2632
|
-
});
|
|
2633
|
-
}
|
|
2634
|
-
async flush() {
|
|
2635
|
-
while (this.drain) await this.drain;
|
|
2636
|
-
}
|
|
2637
|
-
async drainPending() {
|
|
2638
|
-
for (;;) {
|
|
2639
|
-
const oldest = this.pending.keys().next();
|
|
2640
|
-
if (oldest.done) break;
|
|
2641
|
-
const update = this.pending.get(oldest.value);
|
|
2642
|
-
this.pending.delete(oldest.value);
|
|
2643
|
-
if (!update) continue;
|
|
2644
|
-
try {
|
|
2645
|
-
await this.recorder.recordAssistantPartUpdate?.(update.part, update.delta);
|
|
2646
|
-
} catch (err) {
|
|
2647
|
-
this.logger.warn("Per-chunk persistence failed (continuing stream)", { error: err instanceof Error ? err.message : String(err) });
|
|
2648
|
-
}
|
|
2649
|
-
}
|
|
2650
|
-
}
|
|
2651
|
-
};
|
|
2652
|
-
//#endregion
|
|
2653
|
-
//#region src/repeated-tool-failure.ts
|
|
2654
|
-
const NO_REPAIR_TOOL_NAMES = /* @__PURE__ */ new Set();
|
|
2655
|
-
/** A part in one of these states has not resolved, so it decides nothing. */
|
|
2656
|
-
function isUnresolved(state) {
|
|
2657
|
-
return state.status === "pending" || state.status === "running";
|
|
2658
|
-
}
|
|
2659
|
-
function readString(input, key) {
|
|
2660
|
-
const value = input[key];
|
|
2661
|
-
return typeof value === "string" && value.trim() !== "" ? value : void 0;
|
|
2662
|
-
}
|
|
2663
|
-
/** The tool's own error text, for the states that carry one. */
|
|
2664
|
-
function readStateError(state) {
|
|
2665
|
-
if (state.status !== "error" && state.status !== "failed") return void 0;
|
|
2666
|
-
return state.error?.trim() ? state.error : void 0;
|
|
2667
|
-
}
|
|
2668
|
-
/**
|
|
2669
|
-
* Describe a resolved tool part as a failure, or return `undefined` when it
|
|
2670
|
-
* produced something.
|
|
2671
|
-
*
|
|
2672
|
-
* A repair tool reports the call it stood in for through its input, so the
|
|
2673
|
-
* trip names `write` rather than the substitute. That is the tool the reader
|
|
2674
|
-
* has to go and look at.
|
|
2675
|
-
*/
|
|
2676
|
-
function describeFailure(part, repairToolNames) {
|
|
2677
|
-
const { state } = part;
|
|
2678
|
-
if (isUnresolved(state)) return void 0;
|
|
2679
|
-
if (repairToolNames.has(part.tool)) return {
|
|
2680
|
-
tool: readString(state.input, "tool") ?? part.tool,
|
|
2681
|
-
detail: readString(state.input, "error") ?? readStateError(state) ?? `${part.tool} reported a tool call it could not parse`
|
|
2682
|
-
};
|
|
2683
|
-
if (state.status !== "error" && state.status !== "failed") return void 0;
|
|
2684
|
-
return {
|
|
2685
|
-
tool: part.tool,
|
|
2686
|
-
detail: readStateError(state) ?? `${part.tool} failed without reporting a reason`
|
|
2687
|
-
};
|
|
2688
|
-
}
|
|
2689
|
-
function createRepeatedToolFailureGuard(options) {
|
|
2690
|
-
const { limit } = options;
|
|
2691
|
-
const repairToolNames = options.repairToolNames ?? NO_REPAIR_TOOL_NAMES;
|
|
2692
|
-
let signature;
|
|
2693
|
-
let previousInput;
|
|
2694
|
-
let count = 0;
|
|
2695
|
-
let lastScoredPartId;
|
|
2696
|
-
let tripped = false;
|
|
2697
|
-
return {
|
|
2698
|
-
/**
|
|
2699
|
-
* Feed one tool part. Returns the trip on the call that reaches the limit,
|
|
2700
|
-
* and `undefined` every other time. A tripped guard stays tripped and
|
|
2701
|
-
* reports once, so a caller can kill the turn without racing the events
|
|
2702
|
-
* still in flight behind it.
|
|
2703
|
-
*/
|
|
2704
|
-
observe(part) {
|
|
2705
|
-
if (tripped) return void 0;
|
|
2706
|
-
if (isUnresolved(part.state)) return void 0;
|
|
2707
|
-
if (part.id === lastScoredPartId) return void 0;
|
|
2708
|
-
lastScoredPartId = part.id;
|
|
2709
|
-
const failure = describeFailure(part, repairToolNames);
|
|
2710
|
-
if (!failure) {
|
|
2711
|
-
signature = void 0;
|
|
2712
|
-
previousInput = void 0;
|
|
2713
|
-
count = 0;
|
|
2714
|
-
return;
|
|
2715
|
-
}
|
|
2716
|
-
const next = JSON.stringify([failure.tool, failure.detail]);
|
|
2717
|
-
count = next === signature && isDeepStrictEqual(part.state.input, previousInput) ? count + 1 : 1;
|
|
2718
|
-
previousInput = structuredClone(part.state.input);
|
|
2719
|
-
signature = next;
|
|
2720
|
-
if (count < limit) return void 0;
|
|
2721
|
-
tripped = true;
|
|
2722
|
-
return {
|
|
2723
|
-
...failure,
|
|
2724
|
-
count
|
|
2725
|
-
};
|
|
2726
|
-
} };
|
|
2727
|
-
}
|
|
2728
|
-
//#endregion
|
|
2729
|
-
//#region src/turn-liveness.ts
|
|
2730
|
-
const STREAM_LIVENESS_QUIET_MS = 5e3;
|
|
2731
|
-
const STREAM_LIVENESS_CHECK_MS = 1e3;
|
|
2732
|
-
/**
|
|
2733
|
-
* How long the adapter vouches for an unbroken silence.
|
|
2734
|
-
*
|
|
2735
|
-
* A ping says the CLI process runs. It cannot say the model makes progress.
|
|
2736
|
-
* An unbounded ping stream would hide a CLI that starts and then hangs.
|
|
2737
|
-
*
|
|
2738
|
-
* No model in use here emits nothing for ten minutes. Past that the ticker
|
|
2739
|
-
* stops so the consumer can observe the silence. A later provider event
|
|
2740
|
-
* restarts the window.
|
|
2741
|
-
*/
|
|
2742
|
-
const STREAM_LIVENESS_MAX_QUIET_MS = 6e5;
|
|
2743
|
-
/**
|
|
2744
|
-
* The clocks and running-tool bookkeeping behind the stream liveness ping.
|
|
2745
|
-
*
|
|
2746
|
-
* `lastProviderEventAt` moves only on a real provider event, and it bounds
|
|
2747
|
-
* how long one silence can run. `lastLivenessAt` also moves on each ping, and
|
|
2748
|
-
* it paces the pings.
|
|
2749
|
-
*/
|
|
2750
|
-
var TurnLiveness = class {
|
|
2751
|
-
lastProviderEventAt = Date.now();
|
|
2752
|
-
lastLivenessAt = this.lastProviderEventAt;
|
|
2753
|
-
runningToolParts = /* @__PURE__ */ new Map();
|
|
2754
|
-
constructor(state, logger, onEvent) {
|
|
2755
|
-
this.state = state;
|
|
2756
|
-
this.logger = logger;
|
|
2757
|
-
this.onEvent = onEvent;
|
|
2758
|
-
}
|
|
2759
|
-
/** A real provider event arrived: reset both clocks. */
|
|
2760
|
-
noteProviderEvent() {
|
|
2761
|
-
this.lastProviderEventAt = Date.now();
|
|
2762
|
-
this.lastLivenessAt = this.lastProviderEventAt;
|
|
2763
|
-
}
|
|
2764
|
-
/** Track a tool part by its reported state; call after `noteProviderEvent`. */
|
|
2765
|
-
noteToolPart(id, tool, status) {
|
|
2766
|
-
if (status === "running") {
|
|
2767
|
-
if (!this.runningToolParts.has(id)) this.runningToolParts.set(id, {
|
|
2768
|
-
toolName: tool,
|
|
2769
|
-
startedAt: this.lastProviderEventAt
|
|
2770
|
-
});
|
|
2771
|
-
} else this.runningToolParts.delete(id);
|
|
2772
|
-
}
|
|
2773
|
-
start(executionSignal, startTime) {
|
|
2774
|
-
const livenessTicker = setInterval(() => {
|
|
2775
|
-
const now = Date.now();
|
|
2776
|
-
if (executionSignal.aborted || this.state.terminalStatus !== void 0 || now - this.lastLivenessAt < STREAM_LIVENESS_QUIET_MS || now - this.lastProviderEventAt > STREAM_LIVENESS_MAX_QUIET_MS) return;
|
|
2777
|
-
this.lastLivenessAt = now;
|
|
2778
|
-
const oldestRunningTool = this.runningToolParts.entries().next();
|
|
2779
|
-
const ping = oldestRunningTool.done ? {
|
|
2780
|
-
type: "model-processing",
|
|
2781
|
-
phase: "thinking",
|
|
2782
|
-
elapsedMs: now - startTime
|
|
2783
|
-
} : {
|
|
2784
|
-
type: "tool-heartbeat",
|
|
2785
|
-
toolName: oldestRunningTool.value[1].toolName,
|
|
2786
|
-
partId: oldestRunningTool.value[0],
|
|
2787
|
-
elapsedMs: now - oldestRunningTool.value[1].startedAt
|
|
2788
|
-
};
|
|
2789
|
-
try {
|
|
2790
|
-
this.onEvent?.(ping);
|
|
2791
|
-
} catch (error) {
|
|
2792
|
-
this.logger.warn("Stream liveness ping failed (continuing turn)", { error: error instanceof Error ? error.message : String(error) });
|
|
2793
|
-
}
|
|
2794
|
-
}, STREAM_LIVENESS_CHECK_MS);
|
|
2795
|
-
livenessTicker.unref();
|
|
2796
|
-
return livenessTicker;
|
|
2797
|
-
}
|
|
2798
|
-
};
|
|
2799
|
-
//#endregion
|
|
2800
|
-
//#region src/turn-observer.ts
|
|
2801
|
-
/**
|
|
2802
|
-
* How many times a tool call may be rejected identically in a row before the
|
|
2803
|
-
* adapter kills the turn.
|
|
2804
|
-
*
|
|
2805
|
-
* Three separates a model that recovers from one that cannot. The first
|
|
2806
|
-
* attempt is the mistake. The second is the model reading the error. A third
|
|
2807
|
-
* identical attempt says the error does not reach it. Against a staging run
|
|
2808
|
-
* that made 76 attempts, this cuts the turn on the third.
|
|
2809
|
-
*/
|
|
2810
|
-
const REPEATED_TOOL_FAILURE_LIMIT = 3;
|
|
2811
|
-
/**
|
|
2812
|
-
* Observes one turn's provider stream: keeps the execution state current,
|
|
2813
|
-
* persists parts and the provider session id as they arrive, stops a turn
|
|
2814
|
-
* that repeats a rejected tool call, and pings while the stream is quiet.
|
|
2815
|
-
*/
|
|
2816
|
-
var TurnObserver = class {
|
|
2817
|
-
providerSessionPersistence = Promise.resolve();
|
|
2818
|
-
providerSessionPersistenceError;
|
|
2819
|
-
nativeSessionObserved = false;
|
|
2820
|
-
partUpdates;
|
|
2821
|
-
liveness;
|
|
2822
|
-
repeatedToolFailureGuard;
|
|
2823
|
-
killForRepeatedToolFailure;
|
|
2824
|
-
constructor(options) {
|
|
2825
|
-
this.options = options;
|
|
2826
|
-
const { state, logger, recorder, repairToolNames, onEvent } = options;
|
|
2827
|
-
this.partUpdates = new PartUpdateQueue(recorder, logger);
|
|
2828
|
-
this.liveness = new TurnLiveness(state, logger, onEvent);
|
|
2829
|
-
this.repeatedToolFailureGuard = createRepeatedToolFailureGuard({
|
|
2830
|
-
limit: REPEATED_TOOL_FAILURE_LIMIT,
|
|
2831
|
-
repairToolNames
|
|
2832
|
-
});
|
|
2833
|
-
}
|
|
2834
|
-
flushPartUpdates() {
|
|
2835
|
-
return this.partUpdates.flush();
|
|
2836
|
-
}
|
|
2837
|
-
attachProcess(process) {
|
|
2838
|
-
this.killForRepeatedToolFailure = () => process.kill();
|
|
2839
|
-
}
|
|
2840
|
-
startLiveness(executionSignal, startTime) {
|
|
2841
|
-
return this.liveness.start(executionSignal, startTime);
|
|
2842
|
-
}
|
|
2843
|
-
onEvent = (event) => {
|
|
2844
|
-
const { provider, state, recorder } = this.options;
|
|
2845
|
-
this.liveness.noteProviderEvent();
|
|
2846
|
-
if (event.type === "message.part.updated" && event.part.type === "tool") this.observeToolPart(event.part);
|
|
2847
|
-
const providerSessionId = event.type === "native.session.observed" && event.provider === provider ? event.nativeSessionId : event.type === "session.updated" ? event.sessionId : void 0;
|
|
2848
|
-
if (event.type === "native.session.observed" && event.provider === provider) this.nativeSessionObserved = true;
|
|
2849
|
-
if (providerSessionId) {
|
|
2850
|
-
state.sessionId = providerSessionId;
|
|
2851
|
-
this.persistProviderSession(providerSessionId);
|
|
2852
|
-
}
|
|
2853
|
-
if (event.type === "warning") {
|
|
2854
|
-
state.lastDiagnostic = event.message;
|
|
2855
|
-
if (event.code === "EXECUTION_INTERRUPTED") state.failure ??= {
|
|
2856
|
-
code: event.code,
|
|
2857
|
-
message: event.message
|
|
2858
|
-
};
|
|
2859
|
-
}
|
|
2860
|
-
if (event.type === "status" && (event.status === "completed" || event.status === "failed")) {
|
|
2861
|
-
state.terminalStatus = event.status;
|
|
2862
|
-
if (event.status === "failed" && event.detail) state.lastDiagnostic = event.detail;
|
|
2863
|
-
}
|
|
2864
|
-
if (event.type === "message.part.updated" && recorder.recordAssistantPartUpdate) this.partUpdates.schedule(event.part, event.delta);
|
|
2865
|
-
this.options.onEvent?.(event);
|
|
2866
|
-
};
|
|
2867
|
-
observeToolPart(part) {
|
|
2868
|
-
const { provider, state, logger, sessionId } = this.options;
|
|
2869
|
-
const { id, tool, state: toolState } = part;
|
|
2870
|
-
this.liveness.noteToolPart(id, tool, toolState.status);
|
|
2871
|
-
const repeated = this.repeatedToolFailureGuard.observe({
|
|
2872
|
-
id,
|
|
2873
|
-
tool,
|
|
2874
|
-
state: toolState
|
|
2875
|
-
});
|
|
2876
|
-
if (repeated) {
|
|
2877
|
-
const detail = `${provider} sent the same ${repeated.tool} call ${repeated.count} times and every one was rejected. The turn stopped before it ran the model again: ${repeated.detail}`;
|
|
2878
|
-
state.endReason = "repeated_tool_failure";
|
|
2879
|
-
state.terminalStatus = "failed";
|
|
2880
|
-
state.lastDiagnostic = detail;
|
|
2881
|
-
logger.error("Repeated tool-call rejection stopped the turn", {
|
|
2882
|
-
tool: repeated.tool,
|
|
2883
|
-
attempts: repeated.count,
|
|
2884
|
-
reason: repeated.detail,
|
|
2885
|
-
sessionId
|
|
2886
|
-
});
|
|
2887
|
-
this.killForRepeatedToolFailure?.();
|
|
2888
|
-
}
|
|
2889
|
-
}
|
|
2890
|
-
persistProviderSession(providerSessionId) {
|
|
2891
|
-
const { getRuntimeHome, getSessionStore, sessionId } = this.options;
|
|
2892
|
-
this.providerSessionPersistence = this.providerSessionPersistence.then(async () => {
|
|
2893
|
-
const runtimeHome = getRuntimeHome();
|
|
2894
|
-
if (!await getSessionStore().update(sessionId, {
|
|
2895
|
-
providerSessionId,
|
|
2896
|
-
...runtimeHome?.trim() ? { providerSessionHome: runtimeHome.trim() } : {}
|
|
2897
|
-
})) throw new Error(`Session ${sessionId} disappeared before its provider session could be persisted`);
|
|
2898
|
-
}).catch((error) => {
|
|
2899
|
-
this.providerSessionPersistenceError ??= error;
|
|
2900
|
-
});
|
|
2901
|
-
}
|
|
2902
|
-
};
|
|
2903
|
-
//#endregion
|
|
2904
|
-
//#region src/turn-process.ts
|
|
2905
|
-
/** How one turn's argv runs: cwd, environment, identity or launcher, stdio. */
|
|
2906
|
-
function cliSpawnConfig(params) {
|
|
2907
|
-
const { execArgs, launcher, subprocessUser } = params;
|
|
2908
|
-
const stdio = !execArgs.useStdin ? { stdio: [
|
|
2909
|
-
"ignore",
|
|
2910
|
-
"pipe",
|
|
2911
|
-
"pipe"
|
|
2912
|
-
] } : {};
|
|
2913
|
-
return {
|
|
2914
|
-
command: execArgs.command,
|
|
2915
|
-
args: execArgs.args,
|
|
2916
|
-
cwd: execArgs.cwd ?? params.workspaceRoot,
|
|
2917
|
-
env: params.env,
|
|
2918
|
-
inheritEnv: params.inheritEnv,
|
|
2919
|
-
...params.ownProcessGroup ? { ownProcessGroup: true } : {},
|
|
2920
|
-
...launcher ? {
|
|
2921
|
-
launcher,
|
|
2922
|
-
spawnOptions: stdio
|
|
2923
|
-
} : {
|
|
2924
|
-
...subprocessUser ? { isolatedIdentity: subprocessUser } : {},
|
|
2925
|
-
spawnOptions: {
|
|
2926
|
-
...params.subprocessSpawnOptions,
|
|
2927
|
-
...stdio
|
|
2928
|
-
}
|
|
2929
|
-
}
|
|
2930
|
-
};
|
|
2931
|
-
}
|
|
2932
|
-
/** A started CLI as the turn drives it, timing its spawn and first output. */
|
|
2933
|
-
function cliTurnProcess(child, logger, spawnStarted) {
|
|
2934
|
-
if (logger.observePhase) {
|
|
2935
|
-
child.process.once("spawn", () => {
|
|
2936
|
-
logger.observePhase?.("cli_spawn", performance.now() - spawnStarted);
|
|
2937
|
-
});
|
|
2938
|
-
child.process.stdout?.once("data", () => {
|
|
2939
|
-
logger.observePhase?.("cli_boot", performance.now() - spawnStarted);
|
|
2940
|
-
});
|
|
2941
|
-
}
|
|
2942
|
-
return {
|
|
2943
|
-
pid: child.pid,
|
|
2944
|
-
stderr: child.process.stderr ?? void 0,
|
|
2945
|
-
write: child.write,
|
|
2946
|
-
closeInput: child.closeInput,
|
|
2947
|
-
kill: child.kill,
|
|
2948
|
-
exitPromise: child.exitPromise,
|
|
2949
|
-
beginRawCapture: child.beginRawCapture
|
|
2950
|
-
};
|
|
2951
|
-
}
|
|
2952
|
-
//#endregion
|
|
2953
|
-
//#region src/base-adapter.ts
|
|
2954
|
-
/** Base adapter for CLI-based AI agent providers. */
|
|
2955
|
-
var CliProviderAdapterBase = class extends CliAdapterMcp {
|
|
2956
|
-
supportsManagedQuote = false;
|
|
2957
|
-
async execute(input, services, onEvent) {
|
|
2958
|
-
this.ensureInitialized();
|
|
2959
|
-
const quoteVersion = input.managedQuoteVersion;
|
|
2960
|
-
if (quoteVersion !== void 0 && !this.supportsManagedQuote) throw new Error(`${this.provider} does not support the managed-quote native model transport`);
|
|
2961
|
-
const correlationHeaders = executionCorrelationHeaders(input.interactionBinding, input.traceId, quoteVersion);
|
|
2962
|
-
return this.withExecution(input.abortSignal, (signal) => this.executeInternal(input, services, onEvent, signal, correlationHeaders));
|
|
2963
|
-
}
|
|
2964
|
-
async executeInternal(input, services, onEvent, executionSignal, correlationHeaders) {
|
|
2965
|
-
const startTime = Date.now();
|
|
2966
|
-
const recorder = services.recorder;
|
|
2967
|
-
const opened = await this.openTurnSession(input, recorder, onEvent);
|
|
2968
|
-
if (opened.cached) return opened.cached;
|
|
2969
|
-
const { sessionId, providerSessionId, providerMessageIds } = opened;
|
|
2970
|
-
const { executionProfile, profilePlan } = await this.materializeTurnProfile(input, sessionId).catch(rejectNotLaunched);
|
|
2971
|
-
const { preparedInput, promptText } = await this.recordTurnPrompt(input, opened, recorder).catch(rejectNotLaunched);
|
|
2972
|
-
onEvent?.({
|
|
2973
|
-
type: "status",
|
|
2974
|
-
status: "started"
|
|
2975
|
-
});
|
|
2976
|
-
const launch = await this.planLaunch({
|
|
2977
|
-
input,
|
|
2978
|
-
sessionId,
|
|
2979
|
-
providerSessionId,
|
|
2980
|
-
executionSignal,
|
|
2981
|
-
correlationHeaders,
|
|
2982
|
-
executionProfile,
|
|
2983
|
-
profilePlan,
|
|
2984
|
-
attachments: preparedInput.attachments,
|
|
2985
|
-
promptText
|
|
2986
|
-
});
|
|
2987
|
-
const { workspaceRoot } = launch;
|
|
2988
|
-
const state = createExecutionState(sessionId, providerMessageIds?.assistantMessageId, workspaceRoot);
|
|
2989
|
-
const observer = new TurnObserver({
|
|
2990
|
-
provider: this.provider,
|
|
2991
|
-
state,
|
|
2992
|
-
sessionId,
|
|
2993
|
-
getRuntimeHome: () => this.config.runtimeHome,
|
|
2994
|
-
recorder,
|
|
2995
|
-
getSessionStore: () => this.getSessionStore(),
|
|
2996
|
-
logger: this.logger,
|
|
2997
|
-
repairToolNames: this.toolCallRepairToolNames(),
|
|
2998
|
-
onEvent
|
|
2999
|
-
});
|
|
3000
|
-
await this.bindNativeSession({
|
|
3001
|
-
launch,
|
|
3002
|
-
providerSessionId,
|
|
3003
|
-
observer
|
|
3004
|
-
});
|
|
3005
|
-
const { exitResult, stderr } = await this.runCliTurn({
|
|
3006
|
-
input,
|
|
3007
|
-
sessionId,
|
|
3008
|
-
providerSessionId,
|
|
3009
|
-
executionSignal,
|
|
3010
|
-
launch,
|
|
3011
|
-
promptText,
|
|
3012
|
-
state,
|
|
3013
|
-
observer,
|
|
3014
|
-
startTime
|
|
3015
|
-
});
|
|
3016
|
-
await observer.providerSessionPersistence;
|
|
3017
|
-
failOnProviderSessionPersistence(observer, state);
|
|
3018
|
-
const endTime = Date.now();
|
|
3019
|
-
const benignNonZeroExit = !exitResult.success && !exitResult.timedOut && exitResult.stdinError === void 0 && this.isBenignNonZeroExit({
|
|
3020
|
-
exitCode: exitResult.code,
|
|
3021
|
-
stderr,
|
|
3022
|
-
state
|
|
3023
|
-
});
|
|
3024
|
-
if (!exitResult.success && !benignNonZeroExit || state.terminalStatus === "failed" || state.endReason) return this.failCliTurn({
|
|
3025
|
-
aborted: executionSignal.aborted,
|
|
3026
|
-
onEvent,
|
|
3027
|
-
recorder,
|
|
3028
|
-
observer,
|
|
3029
|
-
state,
|
|
3030
|
-
exitResult,
|
|
3031
|
-
stderr,
|
|
3032
|
-
launch,
|
|
3033
|
-
startTime,
|
|
3034
|
-
endTime
|
|
3035
|
-
});
|
|
3036
|
-
logAcceptedCliExit(this.logger, exitResult, stderr);
|
|
3037
|
-
return this.completeTurn({
|
|
3038
|
-
input,
|
|
3039
|
-
sessionId,
|
|
3040
|
-
state,
|
|
3041
|
-
observer,
|
|
3042
|
-
recorder,
|
|
3043
|
-
providerMessageIds,
|
|
3044
|
-
profilePlan,
|
|
3045
|
-
startTime,
|
|
3046
|
-
endTime
|
|
3047
|
-
});
|
|
3048
|
-
}
|
|
3049
|
-
async completeTurn(params) {
|
|
3050
|
-
const { input, sessionId, state, recorder, profilePlan } = params;
|
|
3051
|
-
if (state.sessionId && sessionId && state.sessionId !== sessionId) await this.getSessionStore().update(sessionId, {
|
|
3052
|
-
providerSessionId: state.sessionId,
|
|
3053
|
-
...this.config.runtimeHome?.trim() ? { providerSessionHome: this.config.runtimeHome.trim() } : {}
|
|
3054
|
-
});
|
|
3055
|
-
const assistantParts = this.buildAssistantParts(state);
|
|
3056
|
-
await params.observer?.flushPartUpdates();
|
|
3057
|
-
await recorder.appendAssistantParts(assistantParts);
|
|
3058
|
-
await this.getMessageStore().append({
|
|
3059
|
-
sessionId,
|
|
3060
|
-
...params.providerMessageIds ? { id: params.providerMessageIds.assistantMessageId } : {},
|
|
3061
|
-
role: "assistant",
|
|
3062
|
-
parts: assistantParts,
|
|
3063
|
-
tokenUsage: state.tokenUsage ? {
|
|
3064
|
-
inputTokens: state.tokenUsage.inputTokens,
|
|
3065
|
-
outputTokens: state.tokenUsage.outputTokens
|
|
3066
|
-
} : void 0
|
|
3067
|
-
});
|
|
3068
|
-
const publicProfileRevision = profilePlan ? profileRevisionReceipts(this).public.get(profilePlan) : void 0;
|
|
3069
|
-
const result = buildExecutionResult({
|
|
3070
|
-
outcome: await this.executionOutcome(state, input),
|
|
3071
|
-
state,
|
|
3072
|
-
toolInvocations: this.extractToolInvocations(state),
|
|
3073
|
-
sessionId,
|
|
3074
|
-
publicProfileRevision,
|
|
3075
|
-
startTime: params.startTime,
|
|
3076
|
-
endTime: params.endTime
|
|
3077
|
-
});
|
|
3078
|
-
if (recorder.markTurnCompleted) await recorder.markTurnCompleted({ result });
|
|
3079
|
-
return result;
|
|
3080
|
-
}
|
|
3081
|
-
/** Report and record a failed turn, then throw its error. */
|
|
3082
|
-
async failCliTurn(params) {
|
|
3083
|
-
const { onEvent, recorder, observer, state, exitResult, stderr } = params;
|
|
3084
|
-
const { launch, startTime, endTime } = params;
|
|
3085
|
-
const { errorMessage, failure } = endFailedTurn({
|
|
3086
|
-
provider: this.provider,
|
|
3087
|
-
state,
|
|
3088
|
-
exitResult,
|
|
3089
|
-
stderr,
|
|
3090
|
-
aborted: params.aborted,
|
|
3091
|
-
command: launch.execArgs.command,
|
|
3092
|
-
nixBinPath: launch.env.NIX_BIN_PATH,
|
|
3093
|
-
startTime,
|
|
3094
|
-
endTime
|
|
3095
|
-
});
|
|
3096
|
-
this.logger.error("CLI process failed", {
|
|
3097
|
-
exitCode: exitResult.code,
|
|
3098
|
-
timedOut: exitResult.timedOut,
|
|
3099
|
-
timeoutReason: exitResult.timeoutReason,
|
|
3100
|
-
timeoutDiagnostic: exitResult.timeoutDiagnostic,
|
|
3101
|
-
spawnError: exitResult.spawnError,
|
|
3102
|
-
stdinError: exitResult.stdinError,
|
|
3103
|
-
stderr: stderr.slice(0, 2e3),
|
|
3104
|
-
diagnostic: state.lastDiagnostic,
|
|
3105
|
-
durationMs: endTime - startTime,
|
|
3106
|
-
command: launch.execArgs.command
|
|
3107
|
-
});
|
|
3108
|
-
if (exitResult.timedOut) onEvent?.({
|
|
3109
|
-
type: "warning",
|
|
3110
|
-
code: "TIMEOUT",
|
|
3111
|
-
message: exitResult.timeoutDiagnostic ?? "Execution timed out"
|
|
3112
|
-
});
|
|
3113
|
-
onEvent?.({
|
|
3114
|
-
type: "status",
|
|
3115
|
-
status: "failed",
|
|
3116
|
-
detail: errorMessage
|
|
3117
|
-
});
|
|
3118
|
-
await recordTurnInterruption({
|
|
3119
|
-
recorder,
|
|
3120
|
-
observer,
|
|
3121
|
-
logger: this.logger,
|
|
3122
|
-
errorMessage,
|
|
3123
|
-
exitResult,
|
|
3124
|
-
failure
|
|
3125
|
-
});
|
|
3126
|
-
throw failure;
|
|
3127
|
-
}
|
|
3128
|
-
async openTurnSession(input, recorder, onEvent) {
|
|
3129
|
-
const sessionPrepareStarted = performance.now();
|
|
3130
|
-
const adapter = this;
|
|
3131
|
-
const prepared = await prepareExecutionSession({
|
|
3132
|
-
get config() {
|
|
3133
|
-
return adapter.config;
|
|
3134
|
-
},
|
|
3135
|
-
get logger() {
|
|
3136
|
-
return adapter.logger;
|
|
3137
|
-
},
|
|
3138
|
-
get storagePath() {
|
|
3139
|
-
return adapter.storagePath;
|
|
3140
|
-
},
|
|
3141
|
-
get getSessionStore() {
|
|
3142
|
-
return adapter.getSessionStore.bind(adapter);
|
|
3143
|
-
},
|
|
3144
|
-
get getMessageStore() {
|
|
3145
|
-
return adapter.getMessageStore.bind(adapter);
|
|
3146
|
-
},
|
|
3147
|
-
get resumableProviderSessionId() {
|
|
3148
|
-
return adapter.resumableProviderSessionId.bind(adapter);
|
|
3149
|
-
},
|
|
3150
|
-
get createSession() {
|
|
3151
|
-
return adapter.createSession.bind(adapter);
|
|
3152
|
-
}
|
|
3153
|
-
}, input).catch(rejectNotLaunched);
|
|
3154
|
-
this.logger.observePhase?.("session_prepare", performance.now() - sessionPrepareStarted);
|
|
3155
|
-
this.logger.observePhase?.(prepared.transcriptReplay ? "history_replay" : prepared.providerSessionId ? "native_resume" : "native_session_new", 0);
|
|
3156
|
-
const providerMessageIds = readProviderMessageIds(input);
|
|
3157
|
-
const cached = await findCachedTurn(recorder, input, this.logger, prepared.sessionId);
|
|
3158
|
-
if (cached) {
|
|
3159
|
-
onEvent?.({
|
|
3160
|
-
type: "status",
|
|
3161
|
-
status: "completed"
|
|
3162
|
-
});
|
|
3163
|
-
return {
|
|
3164
|
-
...prepared,
|
|
3165
|
-
providerMessageIds,
|
|
3166
|
-
cached
|
|
3167
|
-
};
|
|
3168
|
-
}
|
|
3169
|
-
if (input.turnId) recorder.setTurnId?.(input.turnId);
|
|
3170
|
-
return {
|
|
3171
|
-
...prepared,
|
|
3172
|
-
providerMessageIds,
|
|
3173
|
-
cached: void 0
|
|
3174
|
-
};
|
|
3175
|
-
}
|
|
3176
|
-
async materializeTurnProfile(input, sessionId) {
|
|
3177
|
-
const executionProfile = this.resolveExecutionProfileForInput(input);
|
|
3178
|
-
const profilePlan = await this.materializeProfileResources(executionProfile, input.workspaceRoot ?? this.storagePath, input.userId ?? sessionId, sessionId);
|
|
3179
|
-
assertExecutionProfileRevision(this, () => this.config, input, profilePlan);
|
|
3180
|
-
return {
|
|
3181
|
-
executionProfile,
|
|
3182
|
-
profilePlan
|
|
3183
|
-
};
|
|
3184
|
-
}
|
|
3185
|
-
async recordTurnPrompt(input, session, recorder, attachmentOptions) {
|
|
3186
|
-
const { sessionId, providerMessageIds } = session;
|
|
3187
|
-
const preparedInput = await this.preparePromptInput(normalizeInputParts(input), input.workspaceRoot ?? this.storagePath, sessionId, attachmentOptions);
|
|
3188
|
-
const transcriptContext = await buildTranscriptContext({
|
|
3189
|
-
provider: this.provider,
|
|
3190
|
-
sessionId,
|
|
3191
|
-
transcriptReplay: session.transcriptReplay,
|
|
3192
|
-
secretProfileRevisionRoot: this.config.secretProfileRevisionRoot,
|
|
3193
|
-
getMessageStore: () => this.getMessageStore()
|
|
3194
|
-
});
|
|
3195
|
-
await recorder.recordUserMessage(preparedInput.persistedParts);
|
|
3196
|
-
await this.getMessageStore().append({
|
|
3197
|
-
sessionId,
|
|
3198
|
-
...providerMessageIds ? { id: providerMessageIds.userMessageId } : {},
|
|
3199
|
-
role: "user",
|
|
3200
|
-
parts: preparedInput.persistedParts
|
|
3201
|
-
});
|
|
3202
|
-
return {
|
|
3203
|
-
preparedInput,
|
|
3204
|
-
promptText: this.composePromptText(prependPromptSection(transcriptContext, preparedInput.promptText), input)
|
|
3205
|
-
};
|
|
3206
|
-
}
|
|
3207
|
-
/** Resolve this turn's config, environment and argv, and check them against the session contract. */
|
|
3208
|
-
async planLaunch(params) {
|
|
3209
|
-
const { input, sessionId, providerSessionId, executionSignal, correlationHeaders, executionProfile, profilePlan, attachments, promptText } = params;
|
|
3210
|
-
const workspaceRoot = input.workspaceRoot ?? this.storagePath;
|
|
3211
|
-
const turnConfig = withTurnInteractions(this.config, input);
|
|
3212
|
-
const executionConfig = this.withProfilePlan(turnConfig, profilePlan);
|
|
3213
|
-
const sessionContract = this.sessionContract();
|
|
3214
|
-
const executionModel = this.resolveProfileModel(input.model?.model);
|
|
3215
|
-
const env = this.buildTurnEnvironment(executionConfig, profilePlan, workspaceRoot, correlationHeaders);
|
|
3216
|
-
const buildExecArgs = (mintedSessionId) => this.buildExecArgs({
|
|
3217
|
-
message: promptText,
|
|
3218
|
-
config: executionConfig,
|
|
3219
|
-
correlationHeaders,
|
|
3220
|
-
model: executionModel,
|
|
3221
|
-
sessionId: providerSessionId,
|
|
3222
|
-
...mintedSessionId ? { mintedSessionId } : {},
|
|
3223
|
-
workspaceRoot,
|
|
3224
|
-
...this.resolveProfilePrompts(input, profilePlan, executionProfile),
|
|
3225
|
-
attachments,
|
|
3226
|
-
profilePlan
|
|
3227
|
-
});
|
|
3228
|
-
const timeoutMs = input.timeoutMs;
|
|
3229
|
-
const mintedSessionId = sessionContract?.identity.kind === "pre-minted" && !providerSessionId ? params.reserveHeldProcess === false ? randomUUID() : await this.mintNativeSessionId({
|
|
3230
|
-
sessionId,
|
|
3231
|
-
workspaceRoot,
|
|
3232
|
-
config: executionConfig,
|
|
3233
|
-
model: executionModel,
|
|
3234
|
-
env,
|
|
3235
|
-
signal: executionSignal,
|
|
3236
|
-
buildExecArgs,
|
|
3237
|
-
input,
|
|
3238
|
-
...timeoutMs === void 0 ? {} : { timeoutMs }
|
|
3239
|
-
}) : void 0;
|
|
3240
|
-
const execArgs = buildExecArgs(mintedSessionId);
|
|
3241
|
-
if (sessionContract) {
|
|
3242
|
-
assertArgvHonoursSessionContract(this.provider, sessionContract, execArgs.args, { ...mintedSessionId ? { mintedId: mintedSessionId } : {} });
|
|
3243
|
-
const runtimeHome = this.config.runtimeHome?.trim();
|
|
3244
|
-
if (runtimeHome && sessionContractFullyEnforced(this.provider)) assertStoreLocationHonoursSessionContract(this.provider, sessionContract, {
|
|
3245
|
-
runtimeHome,
|
|
3246
|
-
cwd: workspaceRoot,
|
|
3247
|
-
env,
|
|
3248
|
-
argv: execArgs.args
|
|
3249
|
-
});
|
|
3250
|
-
}
|
|
3251
|
-
this.logger.debug("CLI execution", {
|
|
3252
|
-
command: execArgs.command,
|
|
3253
|
-
cwd: workspaceRoot,
|
|
3254
|
-
hasSessionId: !!providerSessionId
|
|
3255
|
-
});
|
|
3256
|
-
return {
|
|
3257
|
-
workspaceRoot,
|
|
3258
|
-
executionConfig,
|
|
3259
|
-
executionModel,
|
|
3260
|
-
env,
|
|
3261
|
-
sessionContract,
|
|
3262
|
-
timeoutMs,
|
|
3263
|
-
mintedSessionId,
|
|
3264
|
-
execArgs
|
|
3265
|
-
};
|
|
3266
|
-
}
|
|
3267
|
-
/** Reserve and announce the native session id a pre-minted harness already has. */
|
|
3268
|
-
async bindNativeSession(params) {
|
|
3269
|
-
const { launch, providerSessionId, observer } = params;
|
|
3270
|
-
const { workspaceRoot, sessionContract, mintedSessionId } = launch;
|
|
3271
|
-
const nativeBindStarted = performance.now();
|
|
3272
|
-
if (mintedSessionId) await this.reserveNativeSession(mintedSessionId, { workspaceRoot });
|
|
3273
|
-
const knownNativeSessionId = sessionContract?.identity.kind === "pre-minted" ? mintedSessionId ?? providerSessionId : void 0;
|
|
3274
|
-
if (knownNativeSessionId) {
|
|
3275
|
-
observer.onEvent({
|
|
3276
|
-
type: "native.session.observed",
|
|
3277
|
-
provider: this.provider,
|
|
3278
|
-
nativeSessionId: knownNativeSessionId
|
|
3279
|
-
});
|
|
3280
|
-
await observer.providerSessionPersistence;
|
|
3281
|
-
if (observer.providerSessionPersistenceError) throw observer.providerSessionPersistenceError;
|
|
3282
|
-
this.logger.observePhase?.("native_session_bind", performance.now() - nativeBindStarted);
|
|
3283
|
-
}
|
|
3284
|
-
}
|
|
3285
|
-
async runCliTurn(params) {
|
|
3286
|
-
const { input, sessionId, providerSessionId, executionSignal } = params;
|
|
3287
|
-
const { launch, promptText, state, observer, startTime } = params;
|
|
3288
|
-
const { execArgs, workspaceRoot, env } = launch;
|
|
3289
|
-
const parserConfig = this.createEventParser(state, observer.onEvent);
|
|
3290
|
-
const spawnedProcess = this.spawnExecutionProcess({
|
|
3291
|
-
input,
|
|
3292
|
-
sessionId,
|
|
3293
|
-
providerSessionId,
|
|
3294
|
-
config: launch.executionConfig,
|
|
3295
|
-
execArgs,
|
|
3296
|
-
workspaceRoot,
|
|
3297
|
-
env,
|
|
3298
|
-
signal: executionSignal,
|
|
3299
|
-
timeoutMs: launch.timeoutMs,
|
|
3300
|
-
parser: parserConfig
|
|
3301
|
-
});
|
|
3302
|
-
const cliProcess = spawnedProcess instanceof Promise ? await spawnedProcess : spawnedProcess;
|
|
3303
|
-
this.activeProcesses.add(cliProcess);
|
|
3304
|
-
observer.attachProcess(cliProcess);
|
|
3305
|
-
const stopSessionWatch = watchNewSessionIds({
|
|
3306
|
-
provider: this.provider,
|
|
3307
|
-
runtimeHome: this.config.runtimeHome,
|
|
3308
|
-
sessionContract: launch.sessionContract,
|
|
3309
|
-
observer
|
|
3310
|
-
});
|
|
3311
|
-
if (providerSessionId && launch.sessionContract?.identity.kind === "first-event") observer.onEvent({
|
|
3312
|
-
type: "native.session.observed",
|
|
3313
|
-
provider: this.provider,
|
|
3314
|
-
nativeSessionId: providerSessionId
|
|
3315
|
-
});
|
|
3316
|
-
const identityDeadline = armNativeSessionDeadline({
|
|
3317
|
-
provider: this.provider,
|
|
3318
|
-
sessionId,
|
|
3319
|
-
sessionContract: launch.sessionContract,
|
|
3320
|
-
state,
|
|
3321
|
-
observer,
|
|
3322
|
-
logger: this.logger,
|
|
3323
|
-
kill: () => cliProcess.kill()
|
|
3324
|
-
});
|
|
3325
|
-
const stderr = collectStderr(cliProcess);
|
|
3326
|
-
const livenessTicker = observer.startLiveness(executionSignal, startTime);
|
|
3327
|
-
let exitResult;
|
|
3328
|
-
try {
|
|
3329
|
-
if (execArgs.useStdin) {
|
|
3330
|
-
cliProcess.write(this.formatStdinMessage(execArgs.stdin || promptText));
|
|
3331
|
-
cliProcess.closeInput();
|
|
3332
|
-
}
|
|
3333
|
-
exitResult = await cliProcess.exitPromise;
|
|
3334
|
-
} catch (error) {
|
|
3335
|
-
cliProcess.kill();
|
|
3336
|
-
await cliProcess.exitPromise;
|
|
3337
|
-
throw error;
|
|
3338
|
-
} finally {
|
|
3339
|
-
stopSessionWatch();
|
|
3340
|
-
clearInterval(livenessTicker);
|
|
3341
|
-
if (identityDeadline) clearTimeout(identityDeadline);
|
|
3342
|
-
this.activeProcesses.delete(cliProcess);
|
|
3343
|
-
}
|
|
3344
|
-
await this.afterCliProcessExit({
|
|
3345
|
-
state,
|
|
3346
|
-
stderr: stderr.text,
|
|
3347
|
-
exit: exitResult,
|
|
3348
|
-
workspaceRoot,
|
|
3349
|
-
env,
|
|
3350
|
-
onEvent: observer.onEvent
|
|
3351
|
-
});
|
|
3352
|
-
return {
|
|
3353
|
-
exitResult,
|
|
3354
|
-
stderr: stderr.text
|
|
3355
|
-
};
|
|
3356
|
-
}
|
|
3357
|
-
/** How one turn's argv runs: cwd, environment, identity or launcher, stdio. */
|
|
3358
|
-
turnSpawnConfig(execArgs, workspaceRoot, env) {
|
|
3359
|
-
return cliSpawnConfig({
|
|
3360
|
-
execArgs,
|
|
3361
|
-
workspaceRoot,
|
|
3362
|
-
env,
|
|
3363
|
-
inheritEnv: this.inheritProcessEnv,
|
|
3364
|
-
launcher: this.launcher,
|
|
3365
|
-
ownProcessGroup: this.ownProcessGroup,
|
|
3366
|
-
subprocessUser: this.config.subprocessUser,
|
|
3367
|
-
subprocessSpawnOptions: this.subprocessSpawnOptions()
|
|
3368
|
-
});
|
|
3369
|
-
}
|
|
3370
|
-
/** The launch one turn would make, without starting it: the spawn config (argv, cwd, environment, identity or launcher). */
|
|
3371
|
-
async prepareLaunch(input, options = {}) {
|
|
3372
|
-
this.ensureInitialized();
|
|
3373
|
-
const sessionId = input.sessionId ?? randomUUID();
|
|
3374
|
-
const { executionProfile, profilePlan } = await this.materializeTurnProfile(input, sessionId);
|
|
3375
|
-
const workspaceRoot = input.workspaceRoot ?? this.storagePath;
|
|
3376
|
-
const preparedInput = await this.preparePromptInput(normalizeInputParts(input), workspaceRoot, sessionId);
|
|
3377
|
-
const promptText = this.composePromptText(preparedInput.promptText, input);
|
|
3378
|
-
const launch = await this.planLaunch({
|
|
3379
|
-
input,
|
|
3380
|
-
sessionId,
|
|
3381
|
-
providerSessionId: options.providerSessionId,
|
|
3382
|
-
executionSignal: input.abortSignal ?? new AbortController().signal,
|
|
3383
|
-
correlationHeaders: executionCorrelationHeaders(input.interactionBinding, input.traceId),
|
|
3384
|
-
executionProfile,
|
|
3385
|
-
profilePlan,
|
|
3386
|
-
attachments: preparedInput.attachments,
|
|
3387
|
-
promptText,
|
|
3388
|
-
reserveHeldProcess: false
|
|
3389
|
-
});
|
|
3390
|
-
const { execArgs } = launch;
|
|
3391
|
-
return {
|
|
3392
|
-
spawn: this.turnSpawnConfig(execArgs, launch.workspaceRoot, launch.env),
|
|
3393
|
-
...execArgs.useStdin ? { stdin: this.formatStdinMessage(execArgs.stdin || promptText) } : {},
|
|
3394
|
-
...launch.mintedSessionId ? { nativeSessionId: launch.mintedSessionId } : {},
|
|
3395
|
-
...profilePlan ? { workspacePlan: profilePlan.receipt } : {}
|
|
3396
|
-
};
|
|
3397
|
-
}
|
|
3398
|
-
spawnExecutionProcess(request) {
|
|
3399
|
-
const { execArgs, workspaceRoot, env, signal, timeoutMs, parser } = request;
|
|
3400
|
-
const spawnStarted = performance.now();
|
|
3401
|
-
const config = {
|
|
3402
|
-
...this.turnSpawnConfig(execArgs, workspaceRoot, env),
|
|
3403
|
-
signal,
|
|
3404
|
-
...globalThis.process.getuid?.() === 0 && this.config.runtimeHome?.trim() ? {
|
|
3405
|
-
rawCapturePath: resolveRawCapturePath(this.config.runtimeHome.trim()),
|
|
3406
|
-
...request.deferRawCapture ? { rawCaptureDeferred: true } : {}
|
|
3407
|
-
} : {},
|
|
3408
|
-
...timeoutMs === void 0 ? {} : { timeoutMs }
|
|
3409
|
-
};
|
|
3410
|
-
const turnProcess = (process) => cliTurnProcess(process, this.logger, spawnStarted);
|
|
3411
|
-
return this.launcher ? launchCliProcess(config, parser).then(turnProcess) : turnProcess(spawnCliProcess(config, parser));
|
|
3412
|
-
}
|
|
3413
|
-
};
|
|
3414
|
-
//#endregion
|
|
3415
|
-
//#region src/metering.ts
|
|
3416
|
-
const UsageRecordSchema = z.record(z.string(), z.unknown());
|
|
3417
|
-
const UsageNumberSchema = z.union([z.number(), z.string().trim().min(1)]).transform(Number).pipe(z.number().finite().nonnegative());
|
|
3418
|
-
const TOKEN_USAGE_INPUT_KEYS = Object.freeze([
|
|
3419
|
-
"inputTokens",
|
|
3420
|
-
"input",
|
|
3421
|
-
"input_tokens",
|
|
3422
|
-
"promptTokens",
|
|
3423
|
-
"prompt_tokens"
|
|
3424
|
-
]);
|
|
3425
|
-
const TOKEN_USAGE_OUTPUT_KEYS = Object.freeze([
|
|
3426
|
-
"outputTokens",
|
|
3427
|
-
"output",
|
|
3428
|
-
"output_tokens",
|
|
3429
|
-
"completionTokens",
|
|
3430
|
-
"completion_tokens"
|
|
3431
|
-
]);
|
|
3432
|
-
const TOKEN_USAGE_TOTAL_KEYS = Object.freeze([
|
|
3433
|
-
"totalTokens",
|
|
3434
|
-
"total_tokens",
|
|
3435
|
-
"total"
|
|
3436
|
-
]);
|
|
3437
|
-
const TOKEN_USAGE_REASONING_KEYS = Object.freeze([
|
|
3438
|
-
"reasoningTokens",
|
|
3439
|
-
"reasoning_tokens",
|
|
3440
|
-
"reasoning",
|
|
3441
|
-
"reasoning_output_tokens"
|
|
3442
|
-
]);
|
|
3443
|
-
const TOKEN_USAGE_COST_KEYS = Object.freeze([
|
|
3444
|
-
"totalCostUsd",
|
|
3445
|
-
"costUsd",
|
|
3446
|
-
"total_cost_usd",
|
|
3447
|
-
"cost_usd",
|
|
3448
|
-
"cost"
|
|
3449
|
-
]);
|
|
3450
|
-
const CACHE_READ_INPUT_KEYS = Object.freeze([
|
|
3451
|
-
"cacheReadInputTokens",
|
|
3452
|
-
"cache_read_input_tokens",
|
|
3453
|
-
"cacheRead",
|
|
3454
|
-
"cache_read",
|
|
3455
|
-
"cached_input_tokens"
|
|
3456
|
-
]);
|
|
3457
|
-
const CACHE_CREATION_INPUT_KEYS = Object.freeze([
|
|
3458
|
-
"cacheCreationInputTokens",
|
|
3459
|
-
"cache_creation_input_tokens",
|
|
3460
|
-
"cacheWrite",
|
|
3461
|
-
"cache_write"
|
|
3462
|
-
]);
|
|
3463
|
-
const NESTED_COST_KEYS = Object.freeze([
|
|
3464
|
-
"total",
|
|
3465
|
-
"totalUsd",
|
|
3466
|
-
"total_usd",
|
|
3467
|
-
"usd"
|
|
3468
|
-
]);
|
|
3469
|
-
function asRecord(raw) {
|
|
3470
|
-
const parsed = UsageRecordSchema.safeParse(raw);
|
|
3471
|
-
return parsed.success ? parsed.data : void 0;
|
|
3472
|
-
}
|
|
3473
|
-
function tokenCount(raw) {
|
|
3474
|
-
const parsed = UsageNumberSchema.safeParse(raw);
|
|
3475
|
-
return parsed.success ? Math.trunc(parsed.data) : void 0;
|
|
3476
|
-
}
|
|
3477
|
-
function firstTokenCount(source, keys) {
|
|
3478
|
-
for (const key of keys) {
|
|
3479
|
-
const count = tokenCount(source[key]);
|
|
3480
|
-
if (count !== void 0) return count;
|
|
3481
|
-
}
|
|
3482
|
-
}
|
|
3483
|
-
function firstUsageCostUsd(source, keys = TOKEN_USAGE_COST_KEYS) {
|
|
3484
|
-
for (const key of keys) {
|
|
3485
|
-
const parsed = UsageNumberSchema.safeParse(source[key]);
|
|
3486
|
-
if (parsed.success) return parsed.data;
|
|
3487
|
-
}
|
|
3488
|
-
}
|
|
3489
|
-
function usageRecord(source) {
|
|
3490
|
-
return asRecord(source.tokenUsage) ?? asRecord(source.usage) ?? source;
|
|
3491
|
-
}
|
|
3492
|
-
function readTokenUsage(source) {
|
|
3493
|
-
const usageSource = usageRecord(source);
|
|
3494
|
-
const inputTokens = firstTokenCount(usageSource, TOKEN_USAGE_INPUT_KEYS);
|
|
3495
|
-
const outputTokens = firstTokenCount(usageSource, TOKEN_USAGE_OUTPUT_KEYS);
|
|
3496
|
-
return inputTokens !== void 0 || outputTokens !== void 0 ? {
|
|
3497
|
-
inputTokens,
|
|
3498
|
-
outputTokens
|
|
3499
|
-
} : void 0;
|
|
3500
|
-
}
|
|
3501
|
-
function readTokenCostUsd(source) {
|
|
3502
|
-
const usageSource = usageRecord(source);
|
|
3503
|
-
return firstUsageCostUsd(source) ?? firstUsageCostUsd(usageSource) ?? readNestedCostUsd(source) ?? readNestedCostUsd(usageSource);
|
|
3504
|
-
}
|
|
3505
|
-
function readNestedCostUsd(source) {
|
|
3506
|
-
const cost = asRecord(source.cost);
|
|
3507
|
-
return cost ? firstUsageCostUsd(cost, NESTED_COST_KEYS) : void 0;
|
|
3508
|
-
}
|
|
3509
|
-
function readCliTokenUsage(raw) {
|
|
3510
|
-
const parsed = UsageRecordSchema.safeParse(raw);
|
|
3511
|
-
if (!parsed.success) return void 0;
|
|
3512
|
-
const source = parsed.data;
|
|
3513
|
-
const usageSource = usageRecord(source);
|
|
3514
|
-
const usage = readTokenUsage(source) ?? readTokenUsage(usageSource);
|
|
3515
|
-
const cost = readTokenCostUsd(source);
|
|
3516
|
-
const totalTokens = firstTokenCount(usageSource, TOKEN_USAGE_TOTAL_KEYS);
|
|
3517
|
-
const reasoningTokens = firstTokenCount(usageSource, TOKEN_USAGE_REASONING_KEYS);
|
|
3518
|
-
const cacheReadInputTokens = firstTokenCount(usageSource, CACHE_READ_INPUT_KEYS);
|
|
3519
|
-
const cacheCreationInputTokens = firstTokenCount(usageSource, CACHE_CREATION_INPUT_KEYS);
|
|
3520
|
-
if (!usage && totalTokens === void 0 && reasoningTokens === void 0 && cost === void 0 && cacheReadInputTokens === void 0 && cacheCreationInputTokens === void 0) return;
|
|
3521
|
-
return {
|
|
3522
|
-
inputTokens: usage?.inputTokens,
|
|
3523
|
-
outputTokens: usage?.outputTokens,
|
|
3524
|
-
totalTokens,
|
|
3525
|
-
reasoningTokens,
|
|
3526
|
-
cacheReadInputTokens,
|
|
3527
|
-
cacheCreationInputTokens,
|
|
3528
|
-
cost
|
|
3529
|
-
};
|
|
3530
|
-
}
|
|
3531
|
-
function recordCliTokenUsage(state, ...sources) {
|
|
3532
|
-
for (const source of sources) {
|
|
3533
|
-
const tokenUsage = readCliTokenUsage(source);
|
|
3534
|
-
if (tokenUsage) {
|
|
3535
|
-
state.tokenUsage = tokenUsage;
|
|
3536
|
-
return true;
|
|
3537
|
-
}
|
|
3538
|
-
}
|
|
3539
|
-
return false;
|
|
3540
|
-
}
|
|
3541
|
-
function addUsage(left, right) {
|
|
3542
|
-
if (left === void 0) return right;
|
|
3543
|
-
if (right === void 0) return left;
|
|
3544
|
-
const sum = left + right;
|
|
3545
|
-
return Number.isFinite(sum) ? sum : Math.max(left, right);
|
|
3546
|
-
}
|
|
3547
|
-
/**
|
|
3548
|
-
* Adds one usage report to the running total instead of replacing it.
|
|
3549
|
-
*
|
|
3550
|
-
* For a CLI that reports usage PER TURN (pi) rather than once per run,
|
|
3551
|
-
* `recordCliTokenUsage` would leave the execution billed as its final turn
|
|
3552
|
-
* alone. Use this wherever a single execution can produce more than one usage
|
|
3553
|
-
* report; use `recordCliTokenUsage` when the CLI emits one cumulative total.
|
|
3554
|
-
*
|
|
3555
|
-
* Returns true when a usage report was found in `sources` and folded in.
|
|
3556
|
-
*/
|
|
3557
|
-
function accumulateCliTokenUsage(state, ...sources) {
|
|
3558
|
-
for (const source of sources) {
|
|
3559
|
-
const tokenUsage = readCliTokenUsage(source);
|
|
3560
|
-
if (!tokenUsage) continue;
|
|
3561
|
-
const total = state.tokenUsage;
|
|
3562
|
-
state.tokenUsage = total ? {
|
|
3563
|
-
inputTokens: addUsage(total.inputTokens, tokenUsage.inputTokens),
|
|
3564
|
-
outputTokens: addUsage(total.outputTokens, tokenUsage.outputTokens),
|
|
3565
|
-
totalTokens: addUsage(total.totalTokens, tokenUsage.totalTokens),
|
|
3566
|
-
reasoningTokens: addUsage(total.reasoningTokens, tokenUsage.reasoningTokens),
|
|
3567
|
-
cacheReadInputTokens: addUsage(total.cacheReadInputTokens, tokenUsage.cacheReadInputTokens),
|
|
3568
|
-
cacheCreationInputTokens: addUsage(total.cacheCreationInputTokens, tokenUsage.cacheCreationInputTokens),
|
|
3569
|
-
cost: addUsage(total.cost, tokenUsage.cost)
|
|
3570
|
-
} : tokenUsage;
|
|
3571
|
-
return true;
|
|
3572
|
-
}
|
|
3573
|
-
return false;
|
|
3574
|
-
}
|
|
3575
|
-
function parseCliUsageLine(line) {
|
|
3576
|
-
if (!line.startsWith("usage:")) return void 0;
|
|
3577
|
-
const raw = line.slice(6).trim();
|
|
3578
|
-
if (!raw) return void 0;
|
|
3579
|
-
if (raw.startsWith("{")) return asRecord(JSON.parse(raw));
|
|
3580
|
-
const fields = {};
|
|
3581
|
-
for (const part of raw.split(/[,\s]+/)) {
|
|
3582
|
-
if (!part) continue;
|
|
3583
|
-
const separator = part.includes("=") ? "=" : part.includes(":") ? ":" : "";
|
|
3584
|
-
if (!separator) continue;
|
|
3585
|
-
const [key, value] = part.split(separator, 2);
|
|
3586
|
-
if (!key || value === void 0) continue;
|
|
3587
|
-
const numeric = Number(value);
|
|
3588
|
-
fields[key] = Number.isFinite(numeric) ? numeric : value;
|
|
3589
|
-
}
|
|
3590
|
-
return Object.keys(fields).length > 0 ? fields : void 0;
|
|
3591
|
-
}
|
|
3592
|
-
//#endregion
|
|
3593
|
-
//#region src/acp-session-adapter.ts
|
|
3594
|
-
const DEFAULT_PROMPT_TIMEOUT_MS = 6e5;
|
|
3595
|
-
const DEFAULT_BOOTSTRAP_TIMEOUT_MS = 12e4;
|
|
3596
|
-
const DEFAULT_BRIDGE_IDLE_MS = 300 * 1e3;
|
|
3597
|
-
const ABORT_GRACE_MS = 250;
|
|
3598
|
-
var AcpProviderAdapterBase = class extends CliProviderAdapterBase {
|
|
3599
|
-
acpSessions = /* @__PURE__ */ new Map();
|
|
3600
|
-
spawnBridge(request) {
|
|
3601
|
-
return spawnAcpBridge(request);
|
|
3602
|
-
}
|
|
3603
|
-
checkAgentProtocolVersion(_advertised, _turn) {}
|
|
3604
|
-
resumeSessionId() {}
|
|
3605
|
-
async onSessionOpened(_bridge, _acpSessionId, _turn, _options) {}
|
|
3606
|
-
onBridgeSpawned(_bridge, _sessionId, _turn) {}
|
|
3607
|
-
beginTurn(_sessionId, _input, _onEvent) {
|
|
3608
|
-
return () => {};
|
|
3609
|
-
}
|
|
3610
|
-
onToolFailed(_sessionId, _failureText) {
|
|
3611
|
-
return [];
|
|
3612
|
-
}
|
|
3613
|
-
takeTurnFailure(_sessionId) {}
|
|
3614
|
-
onSessionEvicted(_sessionId) {}
|
|
3615
|
-
turnLogContext(_turn) {
|
|
3616
|
-
return {};
|
|
3617
|
-
}
|
|
3618
|
-
async execute(input, services, onEvent) {
|
|
3619
|
-
this.ensureInitialized();
|
|
3620
|
-
const turn = this.resolveTurn(input);
|
|
3621
|
-
const { config } = turn;
|
|
3622
|
-
const { recorder } = services;
|
|
3623
|
-
const startTime = Date.now();
|
|
3624
|
-
const workspaceRoot = input.workspaceRoot ?? this.storagePath;
|
|
3625
|
-
const turnInput = input.sessionId ? input : {
|
|
3626
|
-
...input,
|
|
3627
|
-
sessionId: input.interactionBinding?.sessionId
|
|
3628
|
-
};
|
|
3629
|
-
const opened = await this.openTurnSession(turnInput, recorder, onEvent);
|
|
3630
|
-
if (opened.cached) return opened.cached;
|
|
3631
|
-
const { sessionId, providerSessionId, providerMessageIds } = opened;
|
|
3632
|
-
const endTurn = this.beginTurn(sessionId, input, onEvent);
|
|
3633
|
-
try {
|
|
3634
|
-
const { preparedInput, promptText } = await this.recordTurnPrompt(turnInput, opened, recorder);
|
|
3635
|
-
onEvent?.({
|
|
3636
|
-
type: "status",
|
|
3637
|
-
status: "started"
|
|
3638
|
-
});
|
|
3639
|
-
const state = createExecutionState(sessionId, providerMessageIds?.assistantMessageId);
|
|
3640
|
-
let entry;
|
|
3641
|
-
try {
|
|
3642
|
-
entry = await this.acquireAcpSession({
|
|
3643
|
-
sessionId,
|
|
3644
|
-
...this.loadsNativeSessions && providerSessionId ? { providerSessionId } : {},
|
|
3645
|
-
turn,
|
|
3646
|
-
workspaceRoot,
|
|
3647
|
-
onEvent
|
|
3648
|
-
});
|
|
3649
|
-
} catch (err) {
|
|
3650
|
-
onEvent?.({
|
|
3651
|
-
type: "status",
|
|
3652
|
-
status: "failed",
|
|
3653
|
-
detail: err instanceof Error ? err.message : String(err)
|
|
3654
|
-
});
|
|
3655
|
-
throw err instanceof Error ? err : new Error(String(err));
|
|
3656
|
-
}
|
|
3657
|
-
const { bridge, acpSessionId } = entry;
|
|
3658
|
-
onEvent?.({
|
|
3659
|
-
type: "session.updated",
|
|
3660
|
-
sessionId
|
|
3661
|
-
});
|
|
3662
|
-
onEvent?.({
|
|
3663
|
-
type: "native.session.observed",
|
|
3664
|
-
provider: this.provider,
|
|
3665
|
-
nativeSessionId: acpSessionId
|
|
3666
|
-
});
|
|
3667
|
-
const timeoutMs = config.promptTimeoutMs ?? DEFAULT_PROMPT_TIMEOUT_MS;
|
|
3668
|
-
const prompt = this.promptBlocks(input, {
|
|
3669
|
-
...preparedInput,
|
|
3670
|
-
promptText
|
|
3671
|
-
}, turn);
|
|
3672
|
-
let promptResult;
|
|
3673
|
-
try {
|
|
3674
|
-
for (let attempt = 0; attempt < 3; attempt++) try {
|
|
3675
|
-
promptResult = await this.promptAcpSession(entry, {
|
|
3676
|
-
prompt,
|
|
3677
|
-
timeoutMs: Math.min(timeoutMs, 12e4),
|
|
3678
|
-
abortSignal: input.abortSignal,
|
|
3679
|
-
onUpdate: (params) => {
|
|
3680
|
-
for (const event of this.mapAcpUpdate(params, state, sessionId)) onEvent?.(event);
|
|
3681
|
-
}
|
|
3682
|
-
});
|
|
3683
|
-
break;
|
|
3684
|
-
} catch (err) {
|
|
3685
|
-
const detail = this.formatBridgeError(err);
|
|
3686
|
-
if (detail.includes("timed out")) this.interruptStuckTurn();
|
|
3687
|
-
if (attempt === 0 && detail.includes("timed out")) continue;
|
|
3688
|
-
if (attempt < 2 && await this.acknowledgeLostTurn(detail)) continue;
|
|
3689
|
-
throw err;
|
|
3690
|
-
}
|
|
3691
|
-
const recorded = this.takeTurnFailure(sessionId);
|
|
3692
|
-
if (recorded) throw recorded;
|
|
3693
|
-
recordCliTokenUsage(state, promptResult);
|
|
3694
|
-
const stop = promptResult?.stopReason;
|
|
3695
|
-
if (stop !== "end_turn" && stop !== "max_tokens") throw new Error(`${this.provider} stopped the turn unfinished (stop reason ${stop ?? "missing"}): send the next turn to continue it`);
|
|
3696
|
-
onEvent?.({
|
|
3697
|
-
type: "status",
|
|
3698
|
-
status: "completed"
|
|
3699
|
-
});
|
|
3700
|
-
await this.getSessionStore().update(sessionId, { providerSessionId: acpSessionId });
|
|
3701
|
-
return await this.completeTurn({
|
|
3702
|
-
input: turnInput,
|
|
3703
|
-
sessionId,
|
|
3704
|
-
state,
|
|
3705
|
-
recorder,
|
|
3706
|
-
providerMessageIds,
|
|
3707
|
-
startTime,
|
|
3708
|
-
endTime: Date.now()
|
|
3709
|
-
});
|
|
3710
|
-
} catch (err) {
|
|
3711
|
-
const finalError = this.takeTurnFailure(sessionId) ?? err;
|
|
3712
|
-
const detail = this.formatBridgeError(finalError);
|
|
3713
|
-
this.logger.error(`${this.provider} execute failed`, {
|
|
3714
|
-
...this.turnLogContext(turn),
|
|
3715
|
-
error: detail,
|
|
3716
|
-
stderrTail: bridge.getStderrTail().slice(-500)
|
|
3717
|
-
});
|
|
3718
|
-
onEvent?.({
|
|
3719
|
-
type: "status",
|
|
3720
|
-
status: "failed",
|
|
3721
|
-
detail
|
|
3722
|
-
});
|
|
3723
|
-
this.evictAcpSession(sessionId, "execute-error");
|
|
3724
|
-
const error = finalError instanceof JsonRpcError && detail !== finalError.message ? new Error(detail, { cause: finalError }) : finalError instanceof Error ? finalError : new Error(detail);
|
|
3725
|
-
throw Object.assign(error, { reason: error.reason ?? turnEndReason({
|
|
3726
|
-
aborted: input.abortSignal?.aborted,
|
|
3727
|
-
timedOut: finalError instanceof RpcRequestTimeoutError,
|
|
3728
|
-
linkLost: finalError instanceof BridgeClosedError || HARNESS_LINK_LOST.test(detail)
|
|
3729
|
-
}) });
|
|
3730
|
-
} finally {
|
|
3731
|
-
this.scheduleIdleReap(sessionId, config.bridgeIdleMs);
|
|
3732
|
-
}
|
|
3733
|
-
} finally {
|
|
3734
|
-
endTurn();
|
|
3735
|
-
}
|
|
3736
|
-
}
|
|
3737
|
-
interruptStuckTurn() {}
|
|
3738
|
-
async acknowledgeLostTurn(_detail) {
|
|
3739
|
-
return false;
|
|
3740
|
-
}
|
|
3741
|
-
async acquireAcpSession(opts) {
|
|
3742
|
-
const { sessionId, turn, workspaceRoot, onEvent } = opts;
|
|
3743
|
-
const existing = this.acpSessions.get(sessionId);
|
|
3744
|
-
if (existing && !existing.bridge.closed) {
|
|
3745
|
-
if (existing.processKey === turn.processKey) {
|
|
3746
|
-
this.cancelIdleReap(existing);
|
|
3747
|
-
return existing;
|
|
3748
|
-
}
|
|
3749
|
-
this.logger.warn(`${this.provider}: cached session was started for another process configuration — respawning`, { sessionId });
|
|
3750
|
-
this.evictAcpSession(sessionId, "process-change");
|
|
3751
|
-
}
|
|
3752
|
-
const storedSessionId = opts.providerSessionId ? this.resumeSessionId() : void 0;
|
|
3753
|
-
const spec = await this.processSpec(turn, {
|
|
3754
|
-
sessionId,
|
|
3755
|
-
workspaceRoot
|
|
3756
|
-
});
|
|
3757
|
-
onEvent?.({
|
|
3758
|
-
type: "status",
|
|
3759
|
-
status: "processing",
|
|
3760
|
-
detail: spec.statusDetail
|
|
3761
|
-
});
|
|
3762
|
-
let bridge;
|
|
3763
|
-
const spawnStarted = performance.now();
|
|
3764
|
-
try {
|
|
3765
|
-
const runtimeHome = this.config.runtimeHome?.trim();
|
|
3766
|
-
bridge = this.spawnBridge({
|
|
3767
|
-
command: spec.command,
|
|
3768
|
-
args: spec.args,
|
|
3769
|
-
cwd: workspaceRoot,
|
|
3770
|
-
env: withAgentEnvironment(spec.env, this.agentEnvironmentValues()),
|
|
3771
|
-
...this.subprocessSpawnOptions(),
|
|
3772
|
-
...globalThis.process.getuid?.() === 0 && runtimeHome ? { rawCapturePath: resolveRawCapturePath(runtimeHome) } : {}
|
|
3773
|
-
});
|
|
3774
|
-
} catch (err) {
|
|
3775
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
3776
|
-
throw new Error(this.spawnFailureMessage({
|
|
3777
|
-
message,
|
|
3778
|
-
missingBinary: message.includes("ENOENT") || message.includes("not found")
|
|
3779
|
-
}, turn));
|
|
3780
|
-
}
|
|
3781
|
-
bridge.process?.once("spawn", () => {
|
|
3782
|
-
this.logger.observePhase?.("cli_spawn", performance.now() - spawnStarted);
|
|
3783
|
-
});
|
|
3784
|
-
this.onBridgeSpawned(bridge, sessionId, turn);
|
|
3785
|
-
const timeoutMs = turn.config.bootstrapTimeoutMs ?? DEFAULT_BOOTSTRAP_TIMEOUT_MS;
|
|
3786
|
-
let acpSessionId;
|
|
3787
|
-
const initializeStarted = performance.now();
|
|
3788
|
-
try {
|
|
3789
|
-
const requested = turn.config.protocolVersion ?? 1;
|
|
3790
|
-
const init = await bridge.request("initialize", {
|
|
3791
|
-
protocolVersion: requested,
|
|
3792
|
-
clientCapabilities: {}
|
|
3793
|
-
}, { timeoutMs });
|
|
3794
|
-
this.checkProtocolVersion(init, requested, turn);
|
|
3795
|
-
acpSessionId = await this.openNativeSession(bridge, {
|
|
3796
|
-
providerSessionId: opts.providerSessionId,
|
|
3797
|
-
storedSessionId,
|
|
3798
|
-
cwd: workspaceRoot,
|
|
3799
|
-
timeoutMs
|
|
3800
|
-
});
|
|
3801
|
-
await this.getSessionStore().update(sessionId, { providerSessionId: acpSessionId });
|
|
3802
|
-
await this.onSessionOpened(bridge, acpSessionId, turn, { timeoutMs });
|
|
3803
|
-
this.logger.observePhase?.("cli_initialize", performance.now() - initializeStarted);
|
|
3804
|
-
} catch (err) {
|
|
3805
|
-
try {
|
|
3806
|
-
await bridge.close();
|
|
3807
|
-
} catch {}
|
|
3808
|
-
throw this.sessionSetupError(err, bridge);
|
|
3809
|
-
}
|
|
3810
|
-
const unsubscribeClose = bridge.onClose((reason) => {
|
|
3811
|
-
this.logger.warn(`${this.provider}: agent process closed unexpectedly — evicting`, {
|
|
3812
|
-
sessionId,
|
|
3813
|
-
reason: reason.reason,
|
|
3814
|
-
code: reason.code,
|
|
3815
|
-
signal: reason.signal
|
|
3816
|
-
});
|
|
3817
|
-
this.evictAcpSession(sessionId, "process-exit");
|
|
3818
|
-
});
|
|
3819
|
-
const entry = {
|
|
3820
|
-
bridge,
|
|
3821
|
-
acpSessionId,
|
|
3822
|
-
processKey: turn.processKey,
|
|
3823
|
-
inflight: Promise.resolve(),
|
|
3824
|
-
unsubscribeClose
|
|
3825
|
-
};
|
|
3826
|
-
this.acpSessions.set(sessionId, entry);
|
|
3827
|
-
return entry;
|
|
3828
|
-
}
|
|
3829
|
-
checkProtocolVersion(init, requested, turn) {
|
|
3830
|
-
const advertised = init.protocolVersion;
|
|
3831
|
-
if (typeof advertised !== "number") throw new Error(`${this.provider}: initialize response missing protocolVersion (got ${JSON.stringify(init)})`);
|
|
3832
|
-
if (advertised > requested) throw new Error(`${this.provider}: agent advertises protocolVersion ${advertised} but client speaks at most ${requested}. Upgrade the provider package or pin an older agent binary.`);
|
|
3833
|
-
if (advertised < 1) throw new Error(`${this.provider}: agent advertises protocolVersion ${advertised} but client requires at least 1. The backing agent binary is too old; upgrade it.`);
|
|
3834
|
-
this.checkAgentProtocolVersion(advertised, turn);
|
|
3835
|
-
}
|
|
3836
|
-
async openNativeSession(bridge, opts) {
|
|
3837
|
-
const load = async (sessionId) => {
|
|
3838
|
-
try {
|
|
3839
|
-
const loaded = await bridge.request("session/load", {
|
|
3840
|
-
sessionId,
|
|
3841
|
-
cwd: opts.cwd,
|
|
3842
|
-
mcpServers: []
|
|
3843
|
-
}, { timeoutMs: opts.timeoutMs });
|
|
3844
|
-
if (!loaded || typeof loaded !== "object") return void 0;
|
|
3845
|
-
return loaded.sessionId ?? sessionId;
|
|
3846
|
-
} catch (err) {
|
|
3847
|
-
this.logger.debug("session/load failed", { error: err instanceof Error ? err.message : String(err) });
|
|
3848
|
-
return;
|
|
3849
|
-
}
|
|
3850
|
-
};
|
|
3851
|
-
const stored = opts.storedSessionId;
|
|
3852
|
-
if (stored) {
|
|
3853
|
-
const loaded = await load(stored);
|
|
3854
|
-
if (loaded) return loaded;
|
|
3855
|
-
}
|
|
3856
|
-
if (opts.providerSessionId && opts.providerSessionId !== stored) {
|
|
3857
|
-
const loaded = await load(opts.providerSessionId);
|
|
3858
|
-
if (loaded) return loaded;
|
|
3859
|
-
}
|
|
3860
|
-
return (await bridge.request("session/new", {
|
|
3861
|
-
cwd: opts.cwd,
|
|
3862
|
-
mcpServers: []
|
|
3863
|
-
}, { timeoutMs: opts.timeoutMs })).sessionId;
|
|
3864
|
-
}
|
|
3865
|
-
async promptAcpSession(entry, turn) {
|
|
3866
|
-
const { bridge, acpSessionId } = entry;
|
|
3867
|
-
const unsubscribe = bridge.onNotification("session/update", (params) => {
|
|
3868
|
-
if (!params || params.sessionId !== acpSessionId) return;
|
|
3869
|
-
turn.onUpdate(params);
|
|
3870
|
-
});
|
|
3871
|
-
const onAbort = () => {
|
|
3872
|
-
try {
|
|
3873
|
-
bridge.notify("session/cancel", { sessionId: acpSessionId });
|
|
3874
|
-
} catch (err) {
|
|
3875
|
-
this.logger.warn(`${this.provider}: session/cancel notify failed on abort`, { error: err instanceof Error ? err.message : String(err) });
|
|
3876
|
-
}
|
|
3877
|
-
setTimeout(() => {
|
|
3878
|
-
bridge.close().catch(() => {});
|
|
3879
|
-
}, ABORT_GRACE_MS).unref();
|
|
3880
|
-
};
|
|
3881
|
-
const { abortSignal } = turn;
|
|
3882
|
-
if (abortSignal) if (abortSignal.aborted) onAbort();
|
|
3883
|
-
else abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
3884
|
-
const previous = entry.inflight;
|
|
3885
|
-
let releaseLock;
|
|
3886
|
-
const lock = new Promise((resolve) => {
|
|
3887
|
-
releaseLock = resolve;
|
|
3888
|
-
});
|
|
3889
|
-
entry.inflight = previous.then(() => lock).catch(() => lock);
|
|
3890
|
-
const readyWaitStarted = performance.now();
|
|
3891
|
-
await previous.catch(() => {});
|
|
3892
|
-
this.logger.observePhase?.("cli_ready_wait", performance.now() - readyWaitStarted);
|
|
3893
|
-
try {
|
|
3894
|
-
return await bridge.request("session/prompt", {
|
|
3895
|
-
sessionId: acpSessionId,
|
|
3896
|
-
prompt: turn.prompt
|
|
3897
|
-
}, { timeoutMs: turn.timeoutMs });
|
|
3898
|
-
} finally {
|
|
3899
|
-
unsubscribe();
|
|
3900
|
-
abortSignal?.removeEventListener("abort", onAbort);
|
|
3901
|
-
releaseLock();
|
|
3902
|
-
}
|
|
3903
|
-
}
|
|
3904
|
-
scheduleIdleReap(sessionId, bridgeIdleMs) {
|
|
3905
|
-
const entry = this.acpSessions.get(sessionId);
|
|
3906
|
-
if (!entry) return;
|
|
3907
|
-
const idleMs = bridgeIdleMs ?? DEFAULT_BRIDGE_IDLE_MS;
|
|
3908
|
-
if (idleMs <= 0) {
|
|
3909
|
-
this.evictAcpSession(sessionId, "idle-disabled");
|
|
3910
|
-
return;
|
|
3911
|
-
}
|
|
3912
|
-
if (entry.idleTimer) clearTimeout(entry.idleTimer);
|
|
3913
|
-
entry.idleTimer = setTimeout(() => {
|
|
3914
|
-
this.evictAcpSession(sessionId, "idle-reap");
|
|
3915
|
-
}, idleMs);
|
|
3916
|
-
entry.idleTimer.unref();
|
|
3917
|
-
}
|
|
3918
|
-
cancelIdleReap(entry) {
|
|
3919
|
-
if (entry.idleTimer) {
|
|
3920
|
-
clearTimeout(entry.idleTimer);
|
|
3921
|
-
entry.idleTimer = void 0;
|
|
3922
|
-
}
|
|
3923
|
-
}
|
|
3924
|
-
evictAcpSession(sessionId, reason) {
|
|
3925
|
-
const entry = this.acpSessions.get(sessionId);
|
|
3926
|
-
if (!entry) return;
|
|
3927
|
-
this.acpSessions.delete(sessionId);
|
|
3928
|
-
this.onSessionEvicted(sessionId);
|
|
3929
|
-
this.cancelIdleReap(entry);
|
|
3930
|
-
try {
|
|
3931
|
-
entry.unsubscribeClose();
|
|
3932
|
-
} catch {}
|
|
3933
|
-
if (!entry.bridge.closed) entry.bridge.close().catch((err) => {
|
|
3934
|
-
this.logger.warn(`${this.provider}: bridge.close threw during evict`, {
|
|
3935
|
-
sessionId,
|
|
3936
|
-
reason,
|
|
3937
|
-
error: err instanceof Error ? err.message : String(err)
|
|
3938
|
-
});
|
|
3939
|
-
});
|
|
3940
|
-
}
|
|
3941
|
-
evictAcpSessionsWhere(matches, reason) {
|
|
3942
|
-
for (const [sessionId, entry] of [...this.acpSessions]) if (matches(entry.processKey)) this.evictAcpSession(sessionId, reason);
|
|
3943
|
-
}
|
|
3944
|
-
mapAcpUpdate(params, state, sessionId) {
|
|
3945
|
-
const update = params.update;
|
|
3946
|
-
if (!update || typeof update !== "object") return [];
|
|
3947
|
-
const kind = update.sessionUpdate;
|
|
3948
|
-
if (kind === "agent_message_chunk" || kind === "agent_thought_chunk") return acpMessageChunkEvents(kind, update, state);
|
|
3949
|
-
if (kind === "tool_call") return acpToolCallEvents(update, state);
|
|
3950
|
-
if (kind === "tool_call_update") {
|
|
3951
|
-
const change = update;
|
|
3952
|
-
const existing = state.toolCalls.get(change.toolCallId);
|
|
3953
|
-
if (!existing) return [];
|
|
3954
|
-
const mapped = this.mapToolStatus(change.status);
|
|
3955
|
-
const outputText = applyAcpToolCallUpdate(existing, mapped, change);
|
|
3956
|
-
return [...mapped === "failed" ? this.onToolFailed(sessionId, typeof existing.output === "string" ? existing.output : outputText || void 0) : [], ...acpToolCallUpdateEvents(existing, state)];
|
|
3957
|
-
}
|
|
3958
|
-
if (kind === "plan") return acpPlanEvents(update, state, this.provider);
|
|
3959
|
-
if (kind === "available_commands_update") return [{
|
|
3960
|
-
type: "raw",
|
|
3961
|
-
backend: this.provider,
|
|
3962
|
-
event: {
|
|
3963
|
-
sessionUpdate: "available_commands_update",
|
|
3964
|
-
availableCommands: update.availableCommands ?? []
|
|
3965
|
-
}
|
|
3966
|
-
}];
|
|
3967
|
-
return [{
|
|
3968
|
-
type: "raw",
|
|
3969
|
-
backend: this.provider,
|
|
3970
|
-
event: update
|
|
3971
|
-
}];
|
|
3972
|
-
}
|
|
3973
|
-
mapToolStatus(status) {
|
|
3974
|
-
switch (status) {
|
|
3975
|
-
case "completed": return "completed";
|
|
3976
|
-
case "failed": return "failed";
|
|
3977
|
-
case "in_progress": return "running";
|
|
3978
|
-
case "pending":
|
|
3979
|
-
case void 0: return "pending";
|
|
3980
|
-
default:
|
|
3981
|
-
this.logger.warn(`${this.provider}: unknown tool_call_update status`, {
|
|
3982
|
-
status,
|
|
3983
|
-
mappedTo: "running"
|
|
3984
|
-
});
|
|
3985
|
-
return "running";
|
|
3986
|
-
}
|
|
3987
|
-
}
|
|
3988
|
-
formatBridgeError(raw) {
|
|
3989
|
-
if (raw instanceof BridgeClosedError) return `${this.provider} bridge closed unexpectedly: ${raw.message}`;
|
|
3990
|
-
if (raw instanceof RpcRequestTimeoutError) return `${this.provider} request "${raw.method}" timed out after ${raw.timeoutMs}ms`;
|
|
3991
|
-
if (raw instanceof JsonRpcError) return jsonRpcErrorText(raw);
|
|
3992
|
-
if (raw instanceof Error) return raw.message;
|
|
3993
|
-
return String(raw);
|
|
3994
|
-
}
|
|
3995
|
-
sessionSetupError(err, bridge) {
|
|
3996
|
-
if (err instanceof BridgeClosedError) {
|
|
3997
|
-
const stderr = bridge.getStderrTail().trim().slice(-500);
|
|
3998
|
-
return new Error(`${this.provider}: agent exited before its session opened (${err.message})${stderr ? `; stderr: ${stderr}` : ""}`, { cause: err });
|
|
3999
|
-
}
|
|
4000
|
-
if (err instanceof JsonRpcError) return new Error(`${this.provider}: ${jsonRpcErrorText(err)}`, { cause: err });
|
|
4001
|
-
return err instanceof Error ? err : new Error(String(err));
|
|
4002
|
-
}
|
|
4003
|
-
async healthCheck() {
|
|
4004
|
-
return true;
|
|
4005
|
-
}
|
|
4006
|
-
async shutdown() {
|
|
4007
|
-
this.evictAcpSessionsWhere(() => true, "adapter-shutdown");
|
|
4008
|
-
await super.shutdown();
|
|
4009
|
-
}
|
|
4010
|
-
async addMcp(_name, _config) {
|
|
4011
|
-
throw new Error(`${this.provider}: addMcp is not supported — an ACP agent takes MCP servers through session/new`);
|
|
4012
|
-
}
|
|
4013
|
-
async getMcpStatus() {
|
|
4014
|
-
return { servers: {} };
|
|
4015
|
-
}
|
|
4016
|
-
buildExecArgs() {
|
|
4017
|
-
throw new Error(`${this.provider}: buildExecArgs is not used — ACP turns run over the RPC bridge`);
|
|
4018
|
-
}
|
|
4019
|
-
buildExecEnv(_config) {
|
|
4020
|
-
return {};
|
|
4021
|
-
}
|
|
4022
|
-
createEventParser() {
|
|
4023
|
-
throw new Error(`${this.provider}: createEventParser is not used — session/update notifications arrive over the RPC bridge`);
|
|
4024
|
-
}
|
|
4025
|
-
buildMcpAddArgs(_name, _config) {
|
|
4026
|
-
throw new Error(`${this.provider}: buildMcpAddArgs is not used`);
|
|
4027
|
-
}
|
|
4028
|
-
};
|
|
4029
|
-
//#endregion
|
|
4030
|
-
//#region src/event-parsers.ts
|
|
4031
|
-
/**
|
|
4032
|
-
* Parser for harnesses that print one JSON event per line (claude-code,
|
|
4033
|
-
* codex, amp, factory-droids). Values that fail `accept` are logged and
|
|
4034
|
-
* dropped; unparseable lines become a `PARSE_ERROR` warning. The first event
|
|
4035
|
-
* `isReady` accepts reports the CLI's own start time as `cli_initialize`,
|
|
4036
|
-
* measured from parser creation, which immediately precedes the spawn.
|
|
4037
|
-
*/
|
|
4038
|
-
function jsonEventParser(logger, onEvent, spec) {
|
|
4039
|
-
const startedAt = performance.now();
|
|
4040
|
-
let readyReported = false;
|
|
4041
|
-
return {
|
|
4042
|
-
transform: (raw) => {
|
|
4043
|
-
if (spec.accept(raw)) return raw;
|
|
4044
|
-
logger.warn(`Received non-${spec.label} event`, { raw });
|
|
4045
|
-
return null;
|
|
4046
|
-
},
|
|
4047
|
-
onEvent: (event) => {
|
|
4048
|
-
if (!readyReported && spec.isReady?.(event)) {
|
|
4049
|
-
readyReported = true;
|
|
4050
|
-
logger.observePhase?.("cli_initialize", performance.now() - startedAt);
|
|
4051
|
-
}
|
|
4052
|
-
for (const stream of spec.map(event)) onEvent?.(stream);
|
|
4053
|
-
},
|
|
4054
|
-
onError: (error, line) => {
|
|
4055
|
-
logger.warn("Failed to parse event", {
|
|
4056
|
-
error: error.message,
|
|
4057
|
-
line
|
|
4058
|
-
});
|
|
4059
|
-
onEvent?.({
|
|
4060
|
-
type: "warning",
|
|
4061
|
-
code: "PARSE_ERROR",
|
|
4062
|
-
message: `Failed to parse event: ${error.message}`
|
|
4063
|
-
});
|
|
4064
|
-
}
|
|
4065
|
-
};
|
|
4066
|
-
}
|
|
4067
|
-
/**
|
|
4068
|
-
* Parser for harnesses that print their reply as plain stdout lines (hermes,
|
|
4069
|
-
* nanoclaw). Lines classify into provider events; the reply is
|
|
4070
|
-
* emitted as one text part when stdout closes. A turn that printed errors
|
|
4071
|
-
* but no reply ends `failed` with the last error instead of an empty
|
|
4072
|
-
* `completed`.
|
|
4073
|
-
*/
|
|
4074
|
-
function plaintextResponseParser(state, onEvent, spec) {
|
|
4075
|
-
const lines = [];
|
|
4076
|
-
let errorSeen = false;
|
|
4077
|
-
let lastError;
|
|
4078
|
-
const response = {
|
|
4079
|
-
text: (line) => {
|
|
4080
|
-
lines.push(line);
|
|
4081
|
-
},
|
|
4082
|
-
error: (message) => {
|
|
4083
|
-
errorSeen = true;
|
|
4084
|
-
lastError = message;
|
|
4085
|
-
}
|
|
4086
|
-
};
|
|
4087
|
-
return {
|
|
4088
|
-
parseAs: "lines",
|
|
4089
|
-
transform: (raw) => spec.classify(String(raw)),
|
|
4090
|
-
onEvent: (event) => {
|
|
4091
|
-
for (const stream of spec.map(event, response)) onEvent?.(stream);
|
|
4092
|
-
},
|
|
4093
|
-
onComplete: () => {
|
|
4094
|
-
const text = lines.join("\n").trimEnd();
|
|
4095
|
-
if (text.length > 0) {
|
|
4096
|
-
const reply = finalTextEvent(state, text);
|
|
4097
|
-
onEvent?.(reply);
|
|
4098
|
-
onEvent?.({
|
|
4099
|
-
type: "status",
|
|
4100
|
-
status: "completed"
|
|
4101
|
-
});
|
|
4102
|
-
} else if (errorSeen) onEvent?.({
|
|
4103
|
-
type: "status",
|
|
4104
|
-
status: "failed",
|
|
4105
|
-
detail: lastError ?? spec.emptyFailureDetail
|
|
4106
|
-
});
|
|
4107
|
-
}
|
|
4108
|
-
};
|
|
4109
|
-
}
|
|
4110
|
-
//#endregion
|
|
4111
|
-
//#region src/interaction-mcp-server.ts
|
|
4112
|
-
var InteractionMcpServer = class extends InteractionMcpServer$1 {
|
|
4113
|
-
constructor(config) {
|
|
4114
|
-
super({
|
|
4115
|
-
...config,
|
|
4116
|
-
onStop: config.onStop ?? (() => void 0)
|
|
4117
|
-
});
|
|
4118
|
-
}
|
|
4119
|
-
};
|
|
4120
|
-
//#endregion
|
|
4121
|
-
//#region src/terminal-plan-support.ts
|
|
4122
|
-
/** One-shot durable plan submission for a single provider turn. */
|
|
4123
|
-
var TerminalPlanSupport = class {
|
|
4124
|
-
committed;
|
|
4125
|
-
submittedBody;
|
|
4126
|
-
inFlight;
|
|
4127
|
-
confirmInFlight;
|
|
4128
|
-
confirmed = false;
|
|
4129
|
-
constructor(options) {
|
|
4130
|
-
this.options = options;
|
|
4131
|
-
}
|
|
4132
|
-
get plan() {
|
|
4133
|
-
return this.committed;
|
|
4134
|
-
}
|
|
4135
|
-
/** Wait for a submission already accepted by this turn before failure cleanup. */
|
|
4136
|
-
async settleSubmission() {
|
|
4137
|
-
await this.inFlight?.catch(() => void 0);
|
|
4138
|
-
}
|
|
4139
|
-
async submit(body, sourceToolCallId) {
|
|
4140
|
-
const normalized = body.trim();
|
|
4141
|
-
if (!normalized) throw new Error("Plan body must not be empty");
|
|
4142
|
-
if (this.submittedBody && this.submittedBody !== normalized) throw new Error(this.committed ? "This turn already submitted a different plan" : "This turn is already submitting a different plan");
|
|
4143
|
-
if (this.committed) return this.committed;
|
|
4144
|
-
if (this.inFlight) return this.inFlight;
|
|
4145
|
-
this.submittedBody = normalized;
|
|
4146
|
-
this.inFlight = this.options.planHost.submit({
|
|
4147
|
-
body: normalized,
|
|
4148
|
-
sourceToolCallId,
|
|
4149
|
-
providerState: this.options.providerState()
|
|
4150
|
-
}).then(async (plan) => {
|
|
4151
|
-
this.committed = plan;
|
|
4152
|
-
await this.options.onCommitted?.(plan);
|
|
4153
|
-
return plan;
|
|
4154
|
-
}).catch((error) => {
|
|
4155
|
-
this.inFlight = void 0;
|
|
4156
|
-
if (!this.committed) this.submittedBody = void 0;
|
|
4157
|
-
throw error;
|
|
4158
|
-
});
|
|
4159
|
-
return this.inFlight;
|
|
4160
|
-
}
|
|
4161
|
-
async confirm() {
|
|
4162
|
-
if (!this.committed) throw new Error("No durable plan has been committed by this turn");
|
|
4163
|
-
if (this.confirmed) return this.committed;
|
|
4164
|
-
if (this.confirmInFlight) return this.confirmInFlight;
|
|
4165
|
-
const committed = this.committed;
|
|
4166
|
-
this.confirmInFlight = this.options.planHost.confirm(committed.id).then(() => {
|
|
4167
|
-
this.confirmed = true;
|
|
4168
|
-
try {
|
|
4169
|
-
this.options.emit?.({
|
|
4170
|
-
type: "plan.submitted",
|
|
4171
|
-
plan: committed
|
|
4172
|
-
});
|
|
4173
|
-
} catch {}
|
|
4174
|
-
return committed;
|
|
4175
|
-
}).catch((error) => {
|
|
4176
|
-
this.confirmInFlight = void 0;
|
|
4177
|
-
throw error;
|
|
4178
|
-
});
|
|
4179
|
-
return this.confirmInFlight;
|
|
4180
|
-
}
|
|
4181
|
-
async withdraw(reason) {
|
|
4182
|
-
if (!this.committed) return;
|
|
4183
|
-
await this.options.planHost.withdraw(this.committed.id, reason);
|
|
4184
|
-
}
|
|
4185
|
-
};
|
|
4186
|
-
//#endregion
|
|
4187
|
-
//#region src/interaction-support.ts
|
|
4188
|
-
var McpInteractionSupport = class {
|
|
4189
|
-
/** The shared emit → block → respond → fail-closed round-trip. */
|
|
4190
|
-
broker = new InteractionBroker$1();
|
|
4191
|
-
activeServer;
|
|
4192
|
-
terminalPlan;
|
|
4193
|
-
constructor(serverName, kinds) {
|
|
4194
|
-
this.serverName = serverName;
|
|
4195
|
-
this.kinds = kinds;
|
|
4196
|
-
}
|
|
4197
|
-
/** The interaction kinds to advertise from `getCapabilities().interactions`. */
|
|
4198
|
-
capabilityKinds() {
|
|
4199
|
-
return [...this.kinds];
|
|
4200
|
-
}
|
|
4201
|
-
/** The active per-execution server (its `url`/`token`), once started. */
|
|
4202
|
-
get server() {
|
|
4203
|
-
return this.activeServer;
|
|
4204
|
-
}
|
|
4205
|
-
/**
|
|
4206
|
-
* Start a per-execution server bound to 127.0.0.1 with an ephemeral bearer
|
|
4207
|
-
* token, registering only the enabled tools. `emit` is the live event sink;
|
|
4208
|
-
* broker-backed tools block on an answer, while plans emit only after their
|
|
4209
|
-
* terminal confirmation. Returns the server for CLI injection.
|
|
4210
|
-
*/
|
|
4211
|
-
async start(sessionId, binding, emit, plan) {
|
|
4212
|
-
const tools = brokerInteractionTools$1(this.broker, {
|
|
4213
|
-
sessionId,
|
|
4214
|
-
binding,
|
|
4215
|
-
emit
|
|
4216
|
-
});
|
|
4217
|
-
const has = (kind) => this.kinds.includes(kind);
|
|
4218
|
-
if (has(InteractionKind.Plan) && !plan) throw new Error("Plan interaction requires a durable plan host and provider resume state");
|
|
4219
|
-
this.terminalPlan = plan ? new TerminalPlanSupport({
|
|
4220
|
-
...plan,
|
|
4221
|
-
emit
|
|
4222
|
-
}) : void 0;
|
|
4223
|
-
const server = new InteractionMcpServer$1({
|
|
4224
|
-
serverName: this.serverName,
|
|
4225
|
-
onStop: tools.onStop,
|
|
4226
|
-
...has(InteractionKind.Permission) ? { requestPermission: tools.requestPermission } : {},
|
|
4227
|
-
...has(InteractionKind.Question) ? { askUser: tools.askUser } : {},
|
|
4228
|
-
...has(InteractionKind.Plan) && this.terminalPlan ? { requestPlan: ({ plan: body, sourceToolCallId }) => this.terminalPlan.submit(body, sourceToolCallId) } : {}
|
|
4229
|
-
});
|
|
4230
|
-
await server.start();
|
|
4231
|
-
this.activeServer = server;
|
|
4232
|
-
return server;
|
|
4233
|
-
}
|
|
4234
|
-
async confirmSubmittedPlan() {
|
|
4235
|
-
if (!this.terminalPlan) throw new Error("No terminal plan support is active for this turn");
|
|
4236
|
-
return this.terminalPlan.confirm();
|
|
4237
|
-
}
|
|
4238
|
-
async withdrawSubmittedPlan(reason) {
|
|
4239
|
-
await this.terminalPlan?.withdraw(reason);
|
|
4240
|
-
}
|
|
4241
|
-
/** Close the request boundary before inspecting or withdrawing its plan. */
|
|
4242
|
-
async drainSubmittedPlan(sessionId) {
|
|
4243
|
-
this.broker.failSession(sessionId);
|
|
4244
|
-
let stopError;
|
|
4245
|
-
try {
|
|
4246
|
-
await this.activeServer?.stop();
|
|
4247
|
-
this.activeServer = void 0;
|
|
4248
|
-
} catch (error) {
|
|
4249
|
-
stopError = error;
|
|
4250
|
-
}
|
|
4251
|
-
await this.terminalPlan?.settleSubmission();
|
|
4252
|
-
if (stopError) throw stopError;
|
|
4253
|
-
}
|
|
4254
|
-
/**
|
|
4255
|
-
* Fail-closed every pending interaction for the session, then stop the server.
|
|
4256
|
-
* `failSession` runs FIRST so any request still blocked in the server resolves
|
|
4257
|
-
* (deny) before close, and the whole thing swallows errors — it runs in adapter
|
|
4258
|
-
* `finally` blocks and must never mask the turn's real error.
|
|
4259
|
-
*/
|
|
4260
|
-
async stop(sessionId) {
|
|
4261
|
-
try {
|
|
4262
|
-
this.broker.failSession(sessionId);
|
|
4263
|
-
const server = this.activeServer;
|
|
4264
|
-
this.activeServer = void 0;
|
|
4265
|
-
await server?.stop();
|
|
4266
|
-
} catch {}
|
|
4267
|
-
}
|
|
4268
|
-
/**
|
|
4269
|
-
* Route an inbound live `/interactions` response for permission or question.
|
|
4270
|
-
* Plans are durable records and never enter this response channel.
|
|
4271
|
-
*/
|
|
4272
|
-
respond(response) {
|
|
4273
|
-
if (this.broker.respond(response)) return true;
|
|
4274
|
-
return this.broker.respondQuestion(response);
|
|
4275
|
-
}
|
|
4276
|
-
};
|
|
4277
|
-
//#endregion
|
|
4278
|
-
//#region src/secret-native-proof.ts
|
|
4279
|
-
const proofKey = Symbol.for("tangle.secret-native-binary-proofs");
|
|
4280
|
-
const runtime$1 = globalThis;
|
|
4281
|
-
const proven = runtime$1[proofKey] ?? /* @__PURE__ */ new Set();
|
|
4282
|
-
runtime$1[proofKey] = proven;
|
|
4283
|
-
/** Prove an immutable executable, under the launch's own identity and environment. */
|
|
4284
|
-
async function provenStoreBinary(expected, config) {
|
|
4285
|
-
if (!config.isolatedIdentity || config.inheritEnv !== false) refuseSecretLaunch("Secret native binary proof requires an isolated identity and environment", { path: config.command });
|
|
4286
|
-
const command = resolveExecutableRealpath(config.command, config.env?.PATH);
|
|
4287
|
-
if (!command || !isTrustedStoreBinary(command)) refuseSecretLaunch("Secret native launch requires a root-owned Nix store binary", { path: command ?? config.command });
|
|
4288
|
-
const key = `${command}\0${expected}`;
|
|
4289
|
-
if (proven.has(key)) return command;
|
|
4290
|
-
const probe = await execCli({
|
|
4291
|
-
command,
|
|
4292
|
-
cwd: config.cwd,
|
|
4293
|
-
env: config.env,
|
|
4294
|
-
inheritEnv: config.inheritEnv,
|
|
4295
|
-
signal: config.signal,
|
|
4296
|
-
isolatedIdentity: config.isolatedIdentity,
|
|
4297
|
-
args: ["--version"],
|
|
4298
|
-
spawnOptions: config.isolatedIdentity,
|
|
4299
|
-
timeoutMs: 15e3
|
|
4300
|
-
});
|
|
4301
|
-
const version = probe.stdout?.trim();
|
|
4302
|
-
if (!probe.exit.success || version !== expected) refuseSecretLaunch(`Secret native launch expected ${expected} at ${command}, received no matching version`, { exitCode: probe.exit.code });
|
|
4303
|
-
proven.add(key);
|
|
4304
|
-
return command;
|
|
4305
|
-
}
|
|
4306
|
-
/** A reported version alone cannot establish an immutable executable. */
|
|
4307
|
-
function isTrustedStoreBinary(realpath) {
|
|
4308
|
-
if (!realpath.startsWith("/nix/store/")) return false;
|
|
4309
|
-
try {
|
|
4310
|
-
for (const path of [realpath, dirname(realpath)]) {
|
|
4311
|
-
const stat = fs$1.lstatSync(path);
|
|
4312
|
-
if (stat.uid !== 0 || (stat.mode & 18) !== 0) return false;
|
|
4313
|
-
}
|
|
4314
|
-
return true;
|
|
4315
|
-
} catch {
|
|
4316
|
-
return false;
|
|
4317
|
-
}
|
|
4318
|
-
}
|
|
4319
|
-
/** The real file a launch of `command` executes under `pathEnv`. */
|
|
4320
|
-
function resolveExecutableRealpath(command, pathEnv) {
|
|
4321
|
-
if (command.includes("/") && !isAbsolute(command)) return void 0;
|
|
4322
|
-
const candidates = command.includes("/") ? [command] : (pathEnv ?? "").split(":").filter(isAbsolute).map((dir) => join(dir, command));
|
|
4323
|
-
for (const candidate of candidates) try {
|
|
4324
|
-
fs$1.accessSync(candidate, fs$1.constants.X_OK);
|
|
4325
|
-
if (fs$1.statSync(candidate).isFile()) return fs$1.realpathSync(candidate);
|
|
4326
|
-
} catch {}
|
|
4327
|
-
}
|
|
4328
|
-
const SEALED_NATIVE_HOME_LAYOUT = {
|
|
4329
|
-
codex: [
|
|
4330
|
-
[
|
|
4331
|
-
".",
|
|
4332
|
-
1016,
|
|
4333
|
-
true
|
|
4334
|
-
],
|
|
4335
|
-
[
|
|
4336
|
-
".codex",
|
|
4337
|
-
1016,
|
|
4338
|
-
true
|
|
4339
|
-
],
|
|
4340
|
-
[
|
|
4341
|
-
".agents",
|
|
4342
|
-
360,
|
|
4343
|
-
true
|
|
4344
|
-
],
|
|
4345
|
-
[
|
|
4346
|
-
".codex/skills",
|
|
4347
|
-
360,
|
|
4348
|
-
true
|
|
4349
|
-
],
|
|
4350
|
-
[
|
|
4351
|
-
".codex/rules",
|
|
4352
|
-
360,
|
|
4353
|
-
true
|
|
4354
|
-
],
|
|
4355
|
-
[
|
|
4356
|
-
".codex/config.toml",
|
|
4357
|
-
288,
|
|
4358
|
-
false
|
|
4359
|
-
],
|
|
4360
|
-
[
|
|
4361
|
-
".codex/AGENTS.md",
|
|
4362
|
-
288,
|
|
4363
|
-
false
|
|
4364
|
-
],
|
|
4365
|
-
[
|
|
4366
|
-
".codex/AGENTS.override.md",
|
|
4367
|
-
288,
|
|
4368
|
-
false
|
|
4369
|
-
],
|
|
4370
|
-
[
|
|
4371
|
-
".codex/hooks.json",
|
|
4372
|
-
288,
|
|
4373
|
-
false
|
|
4374
|
-
]
|
|
4375
|
-
],
|
|
4376
|
-
opencode: [[
|
|
4377
|
-
".",
|
|
4378
|
-
456,
|
|
4379
|
-
true
|
|
4380
|
-
], [
|
|
4381
|
-
".opencode",
|
|
4382
|
-
360,
|
|
4383
|
-
true
|
|
4384
|
-
]]
|
|
4385
|
-
};
|
|
4386
|
-
function assertSealedNativeHome(home, gid, layout) {
|
|
4387
|
-
for (const [relative, mode, directory] of layout) {
|
|
4388
|
-
const path = join(home, relative);
|
|
4389
|
-
const stat = fs$1.lstatSync(path);
|
|
4390
|
-
if (fs$1.realpathSync(path) !== path || stat.uid !== 0 || stat.gid !== gid || (stat.mode & 4095) !== mode || stat.isDirectory() !== directory) refuseSecretLaunch(`Secret native HOME authority is not sealed at ${path}: expected uid0 gid${gid} mode${mode.toString(8)} directory=${directory}; actual uid${stat.uid} gid${stat.gid} mode${(stat.mode & 4095).toString(8)} directory=${stat.isDirectory()}`);
|
|
4391
|
-
}
|
|
4392
|
-
}
|
|
4393
|
-
/** Only selected structural facts enter a refusal; payloads and causes never do. */
|
|
4394
|
-
function refuseSecretLaunch(reason, { path, expected, actual, exitCode } = {}) {
|
|
4395
|
-
const detail = JSON.stringify({
|
|
4396
|
-
path,
|
|
4397
|
-
expected,
|
|
4398
|
-
actual
|
|
4399
|
-
}).slice(0, 768);
|
|
4400
|
-
throw new Error(detail === "{}" ? reason : `${reason} ${detail}`, { cause: typeof exitCode === "number" || exitCode === null ? { exitCode } : void 0 });
|
|
4401
|
-
}
|
|
4402
|
-
//#endregion
|
|
4403
|
-
//#region src/reasoning-effort.ts
|
|
4404
|
-
/** How each CLI takes agent-interface's native control token on its argv. */
|
|
4405
|
-
const EFFORT_FLAG = {
|
|
4406
|
-
"claude-code": (value) => ["--effort", value],
|
|
4407
|
-
codex: (value) => ["-c", `model_reasoning_effort=${value}`],
|
|
4408
|
-
openclaw: (value) => ["--thinking", value],
|
|
4409
|
-
pi: (value) => ["--thinking", value]
|
|
4410
|
-
};
|
|
4411
|
-
/** The argv carrying `nativeReasoningControl` for a profile effort, or none. */
|
|
4412
|
-
function nativeEffortArgs(harness, effort) {
|
|
4413
|
-
const value = nativeReasoningControl(harness, effort ?? null);
|
|
4414
|
-
const flag = EFFORT_FLAG[harness];
|
|
4415
|
-
return value === null || flag === void 0 ? [] : flag(value);
|
|
4416
|
-
}
|
|
4417
|
-
/**
|
|
4418
|
-
* Kimi stays local until #14839: the sandbox ships kimi-cli 1.49.0 as
|
|
4419
|
-
* `kimi-code` (binary `--thinking`/`--no-thinking`) while other paths run
|
|
4420
|
-
* kimi-code 2.x. A model whose name has "thinking" or "reason" always thinks
|
|
4421
|
-
* (kimi-cli `derive_model_capabilities`), so `off` throws there.
|
|
4422
|
-
*/
|
|
4423
|
-
function kimiThinkingArgs(level, modelId) {
|
|
4424
|
-
if (level === void 0) return [];
|
|
4425
|
-
const off = level === "none" || level === "minimal";
|
|
4426
|
-
if (!REASONING_EFFORTS.includes(level) || off && /thinking|reason/i.test(modelId ?? "")) throw new Error(`kimi-code cannot honor reasoning effort "${level}"`);
|
|
4427
|
-
return off ? ["--no-thinking"] : ["--thinking"];
|
|
4428
|
-
}
|
|
4429
|
-
//#endregion
|
|
4430
|
-
//#region src/secret-profile-mcp.ts
|
|
4431
|
-
/** Executor-held MCP credentials shared by native private-profile adapters. */
|
|
4432
|
-
const SECRET_REFERENCE_PREFIX = "TANGLE_PROFILE_SECRET_";
|
|
4433
|
-
const SECRET_REFERENCE = /\{env:(TANGLE_PROFILE_SECRET_[0-9A-F]{24})\}/g;
|
|
4434
|
-
function requireRecord(raw, message) {
|
|
4435
|
-
return isRecord(raw) ? raw : refuseSecretLaunch(message);
|
|
4436
|
-
}
|
|
4437
|
-
/** A config value with its secret references resolved, or undefined. */
|
|
4438
|
-
function resolveReferences(value, references) {
|
|
4439
|
-
let unresolved = false;
|
|
4440
|
-
const resolved = value.replace(SECRET_REFERENCE, (_match, reference) => {
|
|
4441
|
-
const secret = references[reference];
|
|
4442
|
-
if (secret === void 0) unresolved = true;
|
|
4443
|
-
return secret ?? "";
|
|
4444
|
-
});
|
|
4445
|
-
return unresolved || /\{(?:env|file):/.test(resolved) ? void 0 : resolved;
|
|
4446
|
-
}
|
|
4447
|
-
/** A stdio server the executor can run: its command and its environment with every secret reference resolved. */
|
|
4448
|
-
function servableStdioServer(name, entry, references) {
|
|
4449
|
-
const command = entry.command;
|
|
4450
|
-
if (!Array.isArray(command) || command.length === 0 || command.some((part) => typeof part !== "string" || /\{(?:env|file):/.test(part))) return "its command is not a fixed argument list";
|
|
4451
|
-
if (entry.cwd !== void 0) return "it names a working directory";
|
|
4452
|
-
const declared = entry.environment === void 0 ? {} : entry.environment;
|
|
4453
|
-
if (!isRecord(declared)) return "its environment is not a map";
|
|
4454
|
-
const environment = {};
|
|
4455
|
-
const secrets = /* @__PURE__ */ new Set();
|
|
4456
|
-
for (const [variable, value] of Object.entries(declared)) {
|
|
4457
|
-
if (typeof value !== "string") return `its ${variable} is not text`;
|
|
4458
|
-
const resolved = resolveReferences(value, references);
|
|
4459
|
-
if (resolved === void 0) return `its ${variable} names a value the executor cannot resolve`;
|
|
4460
|
-
environment[variable] = resolved;
|
|
4461
|
-
for (const [, reference] of value.matchAll(SECRET_REFERENCE)) {
|
|
4462
|
-
const secret = references[reference];
|
|
4463
|
-
if (secret) secrets.add(secret);
|
|
4464
|
-
}
|
|
4465
|
-
}
|
|
4466
|
-
return {
|
|
4467
|
-
name,
|
|
4468
|
-
command,
|
|
4469
|
-
environment,
|
|
4470
|
-
secrets: [...secrets]
|
|
4471
|
-
};
|
|
4472
|
-
}
|
|
4473
|
-
/** A remote server the executor can serve: its fixed upstream URL and every header with its secret references resolved. */
|
|
4474
|
-
function servableMcpServer(name, entry, references) {
|
|
4475
|
-
const url = entry.url;
|
|
4476
|
-
if (typeof url !== "string" || /\{(?:env|file):/.test(url)) return "its URL is not a fixed address";
|
|
4477
|
-
try {
|
|
4478
|
-
const parsed = new URL(url);
|
|
4479
|
-
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return "its URL is not http(s)";
|
|
4480
|
-
} catch {
|
|
4481
|
-
return "its URL is not a fixed address";
|
|
4482
|
-
}
|
|
4483
|
-
const headers = {};
|
|
4484
|
-
const declared = entry.headers === void 0 ? {} : entry.headers;
|
|
4485
|
-
if (!isRecord(declared)) return "its headers are not a map";
|
|
4486
|
-
for (const [header, value] of Object.entries(declared)) {
|
|
4487
|
-
if (typeof value !== "string") return `its ${header} header is not text`;
|
|
4488
|
-
const resolved = resolveReferences(value, references);
|
|
4489
|
-
if (resolved === void 0) return `its ${header} header names a value the executor cannot resolve`;
|
|
4490
|
-
headers[header] = resolved;
|
|
4491
|
-
}
|
|
4492
|
-
return {
|
|
4493
|
-
name,
|
|
4494
|
-
url,
|
|
4495
|
-
headers
|
|
4496
|
-
};
|
|
4497
|
-
}
|
|
4498
|
-
/** Join the queue at `key`; `previous` settles when earlier holders release. */
|
|
4499
|
-
function joinQueue(queues, key) {
|
|
4500
|
-
const previous = queues.get(key) ?? Promise.resolve();
|
|
4501
|
-
let release;
|
|
4502
|
-
const finished = new Promise((resolve) => {
|
|
4503
|
-
release = resolve;
|
|
4504
|
-
});
|
|
4505
|
-
const tail = previous.then(() => finished);
|
|
4506
|
-
queues.set(key, tail);
|
|
4507
|
-
tail.then(() => {
|
|
4508
|
-
if (queues.get(key) === tail) queues.delete(key);
|
|
4509
|
-
});
|
|
4510
|
-
return {
|
|
4511
|
-
previous,
|
|
4512
|
-
release
|
|
4513
|
-
};
|
|
4514
|
-
}
|
|
4515
|
-
function untilSettledOrAborted(previous, signal) {
|
|
4516
|
-
const aborted = () => signal.reason ?? /* @__PURE__ */ new Error("Secret native turn aborted before its session view was free");
|
|
4517
|
-
if (signal.aborted) return Promise.reject(aborted());
|
|
4518
|
-
return new Promise((resolve, reject) => {
|
|
4519
|
-
const onAbort = () => reject(aborted());
|
|
4520
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
4521
|
-
previous.then(() => {
|
|
4522
|
-
signal.removeEventListener("abort", onAbort);
|
|
4523
|
-
resolve();
|
|
4524
|
-
});
|
|
4525
|
-
});
|
|
4526
|
-
}
|
|
4527
|
-
/** Every native discovery ancestor is sealed and free of ambient configuration. */
|
|
4528
|
-
function assertSealedNativeSearchPath(cwd, configNames) {
|
|
4529
|
-
for (let child = cwd, parent = dirname(cwd);; child = parent, parent = dirname(parent)) {
|
|
4530
|
-
const stat = lstatSync(child);
|
|
4531
|
-
if (!stat.isDirectory() || stat.isSymbolicLink() || realpathSync(child) !== child || stat.uid !== 0 || (stat.mode & 18) !== 0 || child !== cwd && configNames.some((name) => lstatSync(join(child, name), { throwIfNoEntry: false }))) throw new Error("Secret native launch has mutable or ambient discovery authority");
|
|
4532
|
-
if (parent === child) break;
|
|
4533
|
-
}
|
|
4534
|
-
}
|
|
4535
|
-
/** One session view changes only after its previous physical native process exits. */
|
|
4536
|
-
const turnsKey = Symbol.for("tangle.secret-native-session-turns");
|
|
4537
|
-
const turnsRuntime = globalThis;
|
|
4538
|
-
const sessionTurns = turnsRuntime[turnsKey] ?? /* @__PURE__ */ new Map();
|
|
4539
|
-
turnsRuntime[turnsKey] = sessionTurns;
|
|
4540
|
-
function secretSessionTurnQueue(view) {
|
|
4541
|
-
return joinQueue(sessionTurns, view);
|
|
4542
|
-
}
|
|
4543
|
-
/** An HTTP URL on loopback with an explicit port and no URL credentials. */
|
|
4544
|
-
function isLoopbackEndpoint(value) {
|
|
4545
|
-
if (typeof value !== "string") return false;
|
|
4546
|
-
try {
|
|
4547
|
-
const url = new URL(value);
|
|
4548
|
-
return url.protocol === "http:" && [
|
|
4549
|
-
"127.0.0.1",
|
|
4550
|
-
"[::1]",
|
|
4551
|
-
"localhost"
|
|
4552
|
-
].includes(url.hostname) && url.port !== "" && !url.username && !url.password;
|
|
4553
|
-
} catch {
|
|
4554
|
-
return false;
|
|
4555
|
-
}
|
|
4556
|
-
}
|
|
4557
|
-
//#endregion
|
|
4558
|
-
//#region src/secret-profile-scope.ts
|
|
4559
|
-
const key = Symbol.for("tangle.secret-profile-mcp.scopes");
|
|
4560
|
-
const runtime = globalThis;
|
|
4561
|
-
const openScopes = runtime[key] ??= /* @__PURE__ */ new Map();
|
|
4562
|
-
function secretMcpScope(sessionId, uid, owner) {
|
|
4563
|
-
if (sessionId.includes("\0")) throw new Error("A secret MCP session id has no NUL");
|
|
4564
|
-
const scope = `${sessionId}\0${uid ?? ""}`;
|
|
4565
|
-
if (owner?.proxy) {
|
|
4566
|
-
const owners = openScopes.get(scope) ?? /* @__PURE__ */ new Set();
|
|
4567
|
-
owners.add(owner);
|
|
4568
|
-
openScopes.set(scope, owners);
|
|
4569
|
-
}
|
|
4570
|
-
return scope;
|
|
4571
|
-
}
|
|
4572
|
-
async function forgetSessionScopes(target) {
|
|
4573
|
-
for (const [scope, owners] of openScopes) {
|
|
4574
|
-
if ("sessionId" in target && !scope.startsWith(`${target.sessionId}\0`)) continue;
|
|
4575
|
-
for (const owner of [...owners]) {
|
|
4576
|
-
if ("owner" in target && owner !== target.owner) continue;
|
|
4577
|
-
if (![...owners].some((other) => other !== owner && other.proxy === owner.proxy)) await owner.proxy.forget(scope);
|
|
4578
|
-
owners.delete(owner);
|
|
4579
|
-
}
|
|
4580
|
-
if (owners.size === 0) openScopes.delete(scope);
|
|
4581
|
-
}
|
|
4582
|
-
}
|
|
4583
|
-
//#endregion
|
|
1
|
+
import { $ as withEphemeralContext, A as TerminalPlanSupport, At as toolStartEvents, B as readCliTokenUsage, C as SEALED_NATIVE_HOME_LAYOUT, Ct as finalTextEvent, D as refuseSecretLaunch, Dt as textDeltaEvents, E as provenStoreBinary, Et as streamPartBase, F as jsonEventParser, Ft as spawnAcpBridge, G as isLocalMcpConfig, H as CliProviderAdapterBase, I as plaintextResponseParser, J as enabledInteractionKinds, K as localMcpArgv, L as AcpProviderAdapterBase, M as InteractionMcpServer, Mt as ACP_MIN_PROTOCOL_VERSION, N as brokerInteractionTools, Nt as ACP_PROTOCOL_VERSION, O as resolveExecutableRealpath, Ot as textSnapshotEvents, P as interactionMcpToolName, Pt as acpContentText, Q as turnInteractionEnabled, R as accumulateCliTokenUsage, S as NATIVE_SESSION_CONTRACTS, St as homeXdgEnv, T as isTrustedStoreBinary, Tt as sessionObservedEvents, U as createRepeatedToolFailureGuard, V as recordCliTokenUsage, W as createExecutionState, X as readEphemeralContext, Y as interactionToggleEnabled, Z as turnEndReason, _ as servableMcpServer, _t as EXECUTION_CORRELATION_HEADERS, a as codexDeveloperInstructionArgs, at as preparePromptInputParts, b as kimiThinkingArgs, bt as MANAGED_QUOTE_VERSION_HEADER, c as SECRET_REFERENCE, ct as deriveFilePart, d as isLoopbackEndpoint, dt as trackFilePart, et as withTurnInteractions, f as isPlainRecord, ft as InvalidAttachmentError, g as secretSessionTurnQueue, gt as runtimeSecretsOverlay, h as resolveReferences, ht as wasNeverLaunched, i as PERMISSION_DECISION_TIMEOUT_MS, it as partitionImageAttachments, j as DEFAULT_INTERACTION_MCP_SERVER_NAME, jt as trackReasoningBlock, k as McpInteractionSupport, kt as toolResultEvents, l as SECRET_REFERENCE_PREFIX, lt as deterministicFilePartId, m as requireRecord, mt as rejectNotLaunched, n as InteractionBroker, nt as MAX_TOTAL_INLINE_IMAGE_BYTES, o as forgetSessionScopes, ot as prependPromptSection, p as joinQueue, pt as SessionRehydrationError, q as localMcpEnv, r as InteractionHttpBridge, rt as PROMPT_SECTION_SEPARATOR, s as secretMcpScope, st as buildFileUrl, t as CODEX_DISABLED_FEATURE_ARGS, tt as MAX_INLINE_IMAGE_BYTES, u as assertSealedNativeSearchPath, ut as inferMediaType, v as servableStdioServer, vt as EXECUTION_ID_HEADER, w as assertSealedNativeHome, wt as reasoningEvents, x as nativeEffortArgs, xt as executionCorrelationHeaders, y as untilSettledOrAborted, yt as EXECUTION_TRACE_HEADER, z as parseCliUsageLine } from "./src-Fn8Pu1xq.mjs";
|
|
4584
2
|
export { ACP_MIN_PROTOCOL_VERSION, ACP_PROTOCOL_VERSION, AcpProviderAdapterBase, CODEX_DISABLED_FEATURE_ARGS, CliProviderAdapterBase, DEFAULT_INTERACTION_MCP_SERVER_NAME, EXECUTION_CORRELATION_HEADERS, EXECUTION_ID_HEADER, EXECUTION_TRACE_HEADER, InteractionBroker, InteractionHttpBridge, InteractionMcpServer, InvalidAttachmentError, MANAGED_QUOTE_VERSION_HEADER, MAX_INLINE_IMAGE_BYTES, MAX_TOTAL_INLINE_IMAGE_BYTES, McpInteractionSupport, NATIVE_SESSION_CONTRACTS, PERMISSION_DECISION_TIMEOUT_MS, PROMPT_SECTION_SEPARATOR, SEALED_NATIVE_HOME_LAYOUT, SECRET_REFERENCE, SECRET_REFERENCE_PREFIX, SessionRehydrationError, TerminalPlanSupport, accumulateCliTokenUsage, acpContentText, assertSealedNativeHome, assertSealedNativeSearchPath, brokerInteractionTools, buildFileUrl, codexDeveloperInstructionArgs, createExecutionState, createRepeatedToolFailureGuard, deriveFilePart, deterministicFilePartId, enabledInteractionKinds, executionCorrelationHeaders, finalTextEvent, forgetSessionScopes, homeXdgEnv, inferMediaType, interactionMcpToolName, interactionToggleEnabled, isLocalMcpConfig, isLoopbackEndpoint, isPlainRecord, isTrustedStoreBinary, joinQueue, jsonEventParser, kimiThinkingArgs, localMcpArgv, localMcpEnv, nativeEffortArgs, parseCliUsageLine, partitionImageAttachments, plaintextResponseParser, preparePromptInputParts, prependPromptSection, provenStoreBinary, readCliTokenUsage, readEphemeralContext, reasoningEvents, recordCliTokenUsage, refuseSecretLaunch, rejectNotLaunched, requireRecord, resolveExecutableRealpath, resolveReferences, runtimeSecretsOverlay, secretMcpScope, secretSessionTurnQueue, servableMcpServer, servableStdioServer, sessionObservedEvents, spawnAcpBridge, streamPartBase, textDeltaEvents, textSnapshotEvents, toolResultEvents, toolStartEvents, trackFilePart, trackReasoningBlock, turnEndReason, turnInteractionEnabled, untilSettledOrAborted, wasNeverLaunched, withEphemeralContext, withTurnInteractions };
|
|
4585
|
-
|
|
4586
|
-
//# sourceMappingURL=index.mjs.map
|