@jini-ai/daemon 0.6.7 → 0.6.9
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/CHANGELOG.md +8 -0
- package/dist/agent-executor/antigravity-permissions.d.ts +11 -0
- package/dist/agent-executor/antigravity-permissions.js +64 -0
- package/dist/agent-executor/contracts.d.ts +31 -6
- package/dist/agent-executor/event-translation.d.ts +6 -1
- package/dist/agent-executor/event-translation.js +1 -1
- package/dist/agent-executor/events.d.ts +1 -0
- package/dist/agent-executor/events.js +85 -74
- package/dist/agent-executor/launch.d.ts +4 -2
- package/dist/agent-executor/launch.js +306 -217
- package/dist/agent-executor/mcp-bridge-probe.js +51 -35
- package/dist/agent-executor/mcp-bridge.d.ts +18 -8
- package/dist/agent-executor/mcp-bridge.js +105 -65
- package/dist/agent-executor/run-output.d.ts +20 -0
- package/dist/agent-executor/run-output.js +80 -0
- package/dist/attachment-content.js +18 -15
- package/dist/core/read-only-tools.d.ts +26 -3
- package/dist/core/read-only-tools.js +38 -5
- package/dist/core/tool-audit.d.ts +3 -1
- package/dist/core/tool-audit.js +35 -22
- package/dist/core/tool-candidates.d.ts +8 -0
- package/dist/core/tool-candidates.js +6 -0
- package/dist/core/tool-gaps.d.ts +60 -0
- package/dist/core/tool-gaps.js +199 -0
- package/dist/delegated-tool-bridge.d.ts +1 -1
- package/dist/delegated-tool-bridge.js +161 -122
- package/dist/event-log.d.ts +45 -2
- package/dist/event-log.js +83 -4
- package/dist/http/attachments.d.ts +26 -1
- package/dist/http/attachments.js +129 -65
- package/dist/http/connectors.d.ts +1 -32
- package/dist/http/connectors.js +13 -68
- package/dist/http/delegated-tools.d.ts +5 -1
- package/dist/http/delegated-tools.js +29 -29
- package/dist/http/host-tools.d.ts +1 -0
- package/dist/http/host-tools.js +64 -59
- package/dist/http/index.d.ts +2 -2
- package/dist/http/index.js +1 -1
- package/dist/http/media.js +48 -69
- package/dist/http/memory.js +45 -36
- package/dist/http/model-proxy-fields.d.ts +23 -0
- package/dist/http/model-proxy-fields.js +29 -0
- package/dist/http/model-proxy.js +193 -395
- package/dist/http/research-tavily.adapter.js +72 -49
- package/dist/http/research.js +33 -23
- package/dist/http/resource-request.d.ts +37 -0
- package/dist/http/resource-request.js +59 -0
- package/dist/http/routines.js +49 -90
- package/dist/http/runs.d.ts +7 -5
- package/dist/http/runs.js +21 -7
- package/dist/http/terminals.js +23 -48
- package/dist/http/tool-catalog.d.ts +5 -0
- package/dist/http/xai.d.ts +11 -1
- package/dist/http/xai.js +125 -124
- package/dist/legacy-data-migration.js +41 -24
- package/dist/run/core/retry.js +51 -35
- package/dist/run/diagnostics/diagnostics.js +5 -17
- package/dist/run-credentials.js +49 -27
- package/dist/run-lifecycle.d.ts +12 -2
- package/dist/run-lifecycle.js +47 -26
- package/dist/store/agent-sessions/sqlite.d.ts +6 -0
- package/dist/store/agent-sessions/sqlite.js +6 -4
- package/dist/surface-exchanges.js +2 -1
- package/dist/terminal-session.d.ts +1 -1
- package/dist/terminal-session.js +26 -33
- package/dist/tool-audit.d.ts +1 -0
- package/dist/tool-audit.js +1 -0
- package/dist/tool-executor.d.ts +24 -3
- package/dist/tool-executor.js +277 -139
- package/dist/tool-recovery/recovery.js +64 -35
- package/dist/tool-recovery/stack.d.ts +2 -1
- package/dist/tool-recovery/stack.js +1 -1
- package/package.json +10 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
## 0.6.9 — 2026-10-22
|
|
2
|
+
|
|
3
|
+
- 2026-10-22: Memory fixes (bounded event log and tool-executor audit), chromeIntegration option, provider-plugin registration, coverage.
|
|
4
|
+
|
|
5
|
+
## 0.6.8 — 2026-10-16
|
|
6
|
+
|
|
7
|
+
- An unknown tool on the read-only gateway points to discovery, never a mutation 409 (75d37c32).
|
|
8
|
+
|
|
1
9
|
## 0.6.7 — 2026-10-15
|
|
2
10
|
|
|
3
11
|
- An unknown tool id is a `400 BAD_REQUEST`, not a redacted `500 INTERNAL_ERROR` (b4a3abad): `ToolExecutor` throws `ToolInputError` naming up to eight registered ids the unknown id prefixes (e.g. `content_read` → `content_read.menu`), and the delegated-tools route returns that message.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persist one server's MCP grant for agy, which has no run-scoped settings override.
|
|
3
|
+
* Reuses the platform lock and atomic writer; keeps the first existing file as a backup.
|
|
4
|
+
* Invalid settings and I/O failures reject before spawn rather than widening permissions.
|
|
5
|
+
* @complexity O(settings bytes), including the allow-list scan; bounded by the settings file.
|
|
6
|
+
*/
|
|
7
|
+
export declare function ensureAntigravityMcpPermission({ settingsPath, serverName }: {
|
|
8
|
+
settingsPath: string;
|
|
9
|
+
serverName: string;
|
|
10
|
+
}, _options?: Record<string, never>): Promise<void>;
|
|
11
|
+
//# sourceMappingURL=antigravity-permissions.d.ts.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { constants, promises as fs } from 'node:fs';
|
|
2
|
+
import { withFileLock, writeFileAtomicAsync } from '@jini-ai/platform/fs';
|
|
3
|
+
import { isRecord } from './values.js';
|
|
4
|
+
/** Read the current settings text; a missing file is undefined, every other failure rejects. */
|
|
5
|
+
async function readSettingsText(settingsPath) {
|
|
6
|
+
try {
|
|
7
|
+
return await fs.readFile(settingsPath, 'utf8');
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
if (error.code !== 'ENOENT')
|
|
11
|
+
throw error;
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** Parse and validate the settings shape; anything unexpected rejects rather than widening permissions. */
|
|
16
|
+
function parseSettings(raw) {
|
|
17
|
+
const settings = raw === undefined ? {} : JSON.parse(raw);
|
|
18
|
+
if (!isRecord(settings))
|
|
19
|
+
throw new Error('Antigravity settings must be an object');
|
|
20
|
+
const permissions = settings.permissions === undefined ? {} : settings.permissions;
|
|
21
|
+
if (!isRecord(permissions) || (permissions.allow !== undefined && !Array.isArray(permissions.allow))) {
|
|
22
|
+
throw new Error('Antigravity permissions.allow must be an array');
|
|
23
|
+
}
|
|
24
|
+
return { settings, permissions, allow: permissions.allow ?? [] };
|
|
25
|
+
}
|
|
26
|
+
/** The allow list holding exactly one copy of rule (first occurrence kept, order otherwise
|
|
27
|
+
* unchanged), or null when it already holds exactly one and nothing needs writing. */
|
|
28
|
+
function grantedAllowList(allow, rule) {
|
|
29
|
+
const first = allow.indexOf(rule);
|
|
30
|
+
if (first < 0)
|
|
31
|
+
return [...allow, rule];
|
|
32
|
+
const deduped = allow.filter((entry, index) => entry !== rule || index === first);
|
|
33
|
+
return deduped.length === allow.length ? null : deduped;
|
|
34
|
+
}
|
|
35
|
+
async function backupOriginalOnce(settingsPath) {
|
|
36
|
+
// Never replace the first backup, including on subsequent launches after operator edits.
|
|
37
|
+
await fs.copyFile(settingsPath, `${settingsPath}.jini-backup`, constants.COPYFILE_EXCL)
|
|
38
|
+
.catch((error) => { if (error.code !== 'EEXIST')
|
|
39
|
+
throw error; });
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Persist one server's MCP grant for agy, which has no run-scoped settings override.
|
|
43
|
+
* Reuses the platform lock and atomic writer; keeps the first existing file as a backup.
|
|
44
|
+
* Invalid settings and I/O failures reject before spawn rather than widening permissions.
|
|
45
|
+
* @complexity O(settings bytes), including the allow-list scan; bounded by the settings file.
|
|
46
|
+
*/
|
|
47
|
+
export async function ensureAntigravityMcpPermission({ settingsPath, serverName }, _options = {}) {
|
|
48
|
+
if (!/^[\w.-]+$/.test(serverName))
|
|
49
|
+
throw new Error('Invalid MCP server name');
|
|
50
|
+
const rule = `mcp(${serverName}/*)`;
|
|
51
|
+
await withFileLock({ lockPath: `${settingsPath}.jini-lock`, run: async () => {
|
|
52
|
+
const raw = await readSettingsText(settingsPath);
|
|
53
|
+
const { settings, permissions, allow } = parseSettings(raw);
|
|
54
|
+
const nextAllow = grantedAllowList(allow, rule);
|
|
55
|
+
if (nextAllow === null)
|
|
56
|
+
return;
|
|
57
|
+
if (raw !== undefined)
|
|
58
|
+
await backupOriginalOnce(settingsPath);
|
|
59
|
+
await writeFileAtomicAsync({ filePath: settingsPath, content: `${JSON.stringify({
|
|
60
|
+
...settings, permissions: { ...permissions, allow: nextAllow },
|
|
61
|
+
}, null, 2)}\n` }, { refuseSymlink: true });
|
|
62
|
+
} }, { createParent: true, timeoutMs: 5000 });
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=antigravity-permissions.js.map
|
|
@@ -92,7 +92,7 @@ export type AgentRuntimeEventTranslation = {
|
|
|
92
92
|
readonly kind: 'ignored';
|
|
93
93
|
};
|
|
94
94
|
/** Machine-readable failure reasons `run()` can reject with — every one is preceded by a `lifecycle.finish({status:'failed'})` call (see index.ts module doc's Invariant section). */
|
|
95
|
-
export type AgentExecutorErrorCode = 'AGENT_MODEL_UNRESOLVED' | 'AGENT_PERMISSION_MODE_INVALID' | 'AGENT_NOT_FOUND' | 'AGENT_RUNTIME_UNSUPPORTED' | 'AGENT_BINARY_NOT_RESOLVED' | 'AGENT_SPAWN_FAILED' | 'AGENT_PROMPT_TOO_LARGE';
|
|
95
|
+
export type AgentExecutorErrorCode = 'NATIVE_TOOL_POLICY_UNSUPPORTED' | 'AGENT_MODEL_UNRESOLVED' | 'AGENT_PERMISSION_MODE_INVALID' | 'AGENT_NOT_FOUND' | 'AGENT_RUNTIME_UNSUPPORTED' | 'AGENT_BINARY_NOT_RESOLVED' | 'AGENT_SPAWN_FAILED' | 'AGENT_PROMPT_TOO_LARGE';
|
|
96
96
|
/** Thrown by `AgentExecutor.run()` on every failure path — never a bare `Error`, so callers can branch on `.code` instead of parsing `.message`. */
|
|
97
97
|
export declare class AgentExecutorError extends Error {
|
|
98
98
|
readonly code: AgentExecutorErrorCode;
|
|
@@ -119,6 +119,12 @@ export interface AgentExecutorRunInput {
|
|
|
119
119
|
* delegate auto-approval, and any unknown mode is rejected before launching a process.
|
|
120
120
|
*/
|
|
121
121
|
readonly permissionMode?: 'bypass' | 'restricted';
|
|
122
|
+
/** Host-authorized native shell/edit grant; unsupported restricted providers fail before spawn. */
|
|
123
|
+
readonly nativeToolAccess?: boolean;
|
|
124
|
+
/** Independent host-verified permissions, snapshotted for this run. */
|
|
125
|
+
readonly nativeCapabilities?: Partial<import('@jini-ai/agent-runtime').NativeCapabilities>;
|
|
126
|
+
/** Trusted-host CLI parity; never derived from prompt/context fields. */
|
|
127
|
+
readonly cliParity?: boolean;
|
|
122
128
|
/**
|
|
123
129
|
* Host-validated image files. How each one actually reaches the model
|
|
124
130
|
* depends on `def.imageDelivery` (see `@jini-ai/agent-runtime`'s `types.ts`):
|
|
@@ -184,6 +190,8 @@ export interface AgentExecutorRunInput {
|
|
|
184
190
|
readonly settingSources?: readonly string[];
|
|
185
191
|
/** Forwarded verbatim to `RuntimeBuildOptions.settings` (extra settings JSON or file path for this run). */
|
|
186
192
|
readonly settings?: string;
|
|
193
|
+
/** Forwarded verbatim to `RuntimeBuildOptions.chromeIntegration`; `undefined` = no browser-integration flag. */
|
|
194
|
+
readonly chromeIntegration?: boolean;
|
|
187
195
|
}
|
|
188
196
|
export interface AgentExecutor {
|
|
189
197
|
/**
|
|
@@ -195,16 +203,19 @@ export interface AgentExecutor {
|
|
|
195
203
|
* underlying run is always already transitioned to `'failed'` via
|
|
196
204
|
* `lifecycle.finish()` before this rejects (see index.ts module doc's Invariant).
|
|
197
205
|
*/
|
|
198
|
-
run(requiredArgs: Pick<AgentExecutorRunInput, "runId" | "agentId" | "prompt" | "cwd">, optionalArgs?: Pick<AgentExecutorRunInput, "model" | "reasoning" | "permissionMode" | "imagePaths" | "imageContents" | "extraAllowedDirs" | "uploadRoot" | "credentialEnv" | "env" | "resumeSessionId" | "newSessionId" | "disallowedTools" | "allowedTools" | "settingSources" | "settings">): Promise<void>;
|
|
206
|
+
run(requiredArgs: Pick<AgentExecutorRunInput, "runId" | "agentId" | "prompt" | "cwd">, optionalArgs?: Pick<AgentExecutorRunInput, "model" | "reasoning" | "nativeToolAccess" | "nativeCapabilities" | "cliParity" | "permissionMode" | "imagePaths" | "imageContents" | "extraAllowedDirs" | "uploadRoot" | "credentialEnv" | "env" | "resumeSessionId" | "newSessionId" | "disallowedTools" | "allowedTools" | "settingSources" | "settings" | "chromeIntegration">): Promise<void>;
|
|
199
207
|
/**
|
|
200
208
|
* Delivers a message the human sent while `runId` is going straight into its live agent process
|
|
201
209
|
* (see {@link UserMessageDelivery} for each outcome). A stream-json agent is interrupted first and
|
|
202
210
|
* gets the message once the CLI acknowledges the interrupt, so this settles in milliseconds (at
|
|
203
211
|
* most `INTERRUPT_ACK_TIMEOUT_MS`); it never waits on the agent's reply.
|
|
212
|
+
* Optional `imageContents` contains host-authorized pixels, encoded exactly as in an idle send.
|
|
204
213
|
*/
|
|
205
214
|
sendUserMessage(requiredArgs: {
|
|
206
215
|
readonly runId: string;
|
|
207
216
|
readonly text: string;
|
|
217
|
+
}, optionalArgs?: {
|
|
218
|
+
readonly imageContents?: readonly MessageAttachmentImage[];
|
|
208
219
|
}): Promise<UserMessageDelivery>;
|
|
209
220
|
}
|
|
210
221
|
/**
|
|
@@ -212,7 +223,8 @@ export interface AgentExecutor {
|
|
|
212
223
|
* - `'delivered'` — the running turn was interrupted and the message written into the live
|
|
213
224
|
* session; the agent answers it next, in the same process and session.
|
|
214
225
|
* - `'unsupported'` — the run is live, but its agent has no way to take input mid-run (its prompt
|
|
215
|
-
* is plain text or an RPC request).
|
|
226
|
+
* is plain text or an RPC request). Queue attachment turns intact after the reply; text-only
|
|
227
|
+
* turns may retain the interrupt-and-resume fallback.
|
|
216
228
|
* - `'not-running'` — no live process will accept it: an unknown run, a finished one, or one whose
|
|
217
229
|
* last turn already ended and closed stdin. The run is ending; the host should send it next.
|
|
218
230
|
*/
|
|
@@ -260,7 +272,7 @@ export interface StdinCloseHandle {
|
|
|
260
272
|
/** Journals a sent-to-stdin byte chunk, queued through the same FIFO {@link wireChildLifecycle} already uses for emitted events. No-op when no journal was configured (see `CreateAgentExecutorOptions.journal`). */
|
|
261
273
|
recordSentBytes(content: string): void;
|
|
262
274
|
/** Interrupts this child's turn, then writes a mid-run user message into its open stdin — see {@link AgentExecutor.sendUserMessage}. */
|
|
263
|
-
sendUserMessage(text: string): Promise<UserMessageDelivery>;
|
|
275
|
+
sendUserMessage(text: string, imageContents?: readonly MessageAttachmentImage[]): Promise<UserMessageDelivery>;
|
|
264
276
|
}
|
|
265
277
|
/**
|
|
266
278
|
* Gap 3 (capability-routed continuation transport) — host-owned config for the
|
|
@@ -313,6 +325,17 @@ export interface ContinuationOptions {
|
|
|
313
325
|
* interface that defaults to *nothing* rather than a real implementation).
|
|
314
326
|
*/
|
|
315
327
|
export interface McpJsonInjectionOptions {
|
|
328
|
+
/** Read current host-registered tool IDs at launch, outside model-controlled input. */
|
|
329
|
+
readonly diagnosticToolIds?: () => readonly string[];
|
|
330
|
+
/**
|
|
331
|
+
* Visible registry IDs to resolve before the bridge's first tools/list, including for clients
|
|
332
|
+
* whose mid-run refresh is unverified. Independent of diagnostic/redaction IDs; this grants
|
|
333
|
+
* no execution permission. The bridge fetches authoritative descriptors using the run credential.
|
|
334
|
+
* @example startupToolIds: ({ runId }) => visibleToolsForRun(runId).map(tool => tool.id)
|
|
335
|
+
*/
|
|
336
|
+
readonly startupToolIds?: (requiredArgs: {
|
|
337
|
+
readonly runId: string;
|
|
338
|
+
}, optionalArgs?: Record<string, never>) => readonly string[];
|
|
316
339
|
/** Probe a reported bridge failure before emitting its error. @default true when injection is configured. */
|
|
317
340
|
readonly probeOnFailure?: boolean;
|
|
318
341
|
/** Absolute path (or PATH-resolvable name) to the `jini-mcp` bin entry (`packages/mcp/src/bin/serve.ts`) this driver tells the spawned CLI to launch as its own MCP server subprocess. */
|
|
@@ -401,12 +424,14 @@ export interface McpJsonInjectionOptions {
|
|
|
401
424
|
readonly path: string;
|
|
402
425
|
}) => Promise<void>;
|
|
403
426
|
/** `'codex-toml'` only. Links the run home's sessions/archived_sessions to the operator's
|
|
404
|
-
* persistent rollout store.
|
|
427
|
+
* persistent rollout store and authentication. Parity also links user home entries; config alone stays isolated.
|
|
405
428
|
* @default Native mkdir (0700) and directory symlinks; errors fail staging before spawn. */
|
|
406
429
|
readonly linkSessionStore?: (required: {
|
|
407
430
|
runHome: string;
|
|
408
431
|
sourceHome: string;
|
|
409
|
-
}, optional?: {
|
|
432
|
+
}, optional?: {
|
|
433
|
+
inheritUserConfig?: boolean;
|
|
434
|
+
}) => Promise<void>;
|
|
410
435
|
}
|
|
411
436
|
/** One `.mcp.json` `mcpServers` entry — the shape Claude Code's own config schema expects. */
|
|
412
437
|
export interface McpJsonServerEntry {
|
|
@@ -63,7 +63,12 @@ interface AgentTranslationSideEffectSink {
|
|
|
63
63
|
readonly onSessionId: (args: {
|
|
64
64
|
readonly sessionId: string;
|
|
65
65
|
}) => void;
|
|
66
|
-
|
|
66
|
+
/** Receives the already-narrowed `tool_use` payload, so a caller never re-checks its variant. */
|
|
67
|
+
readonly onToolCall: (args: {
|
|
68
|
+
readonly toolUse: Extract<RunAgentPayload, {
|
|
69
|
+
type: 'tool_use';
|
|
70
|
+
}>;
|
|
71
|
+
}) => void;
|
|
67
72
|
readonly onUserVisibleOutput: () => void;
|
|
68
73
|
}
|
|
69
74
|
/**
|
|
@@ -224,7 +224,7 @@ export function applyAgentTranslationSideEffects({ payload, sessionId, sink }) {
|
|
|
224
224
|
if (sessionId !== undefined)
|
|
225
225
|
sink.onSessionId({ sessionId });
|
|
226
226
|
if (payload.type === 'tool_use') {
|
|
227
|
-
sink.onToolCall();
|
|
227
|
+
sink.onToolCall({ toolUse: payload });
|
|
228
228
|
}
|
|
229
229
|
else if ((payload.type === 'text_delta' || payload.type === 'thinking_delta') && payload.delta.length > 0) {
|
|
230
230
|
sink.onUserVisibleOutput();
|
|
@@ -162,6 +162,7 @@ interface RunAcpDispatchInput {
|
|
|
162
162
|
readonly prompt: string;
|
|
163
163
|
readonly cwd: string;
|
|
164
164
|
readonly permissionMode: 'bypass' | 'restricted' | undefined;
|
|
165
|
+
readonly cliParity?: boolean | undefined;
|
|
165
166
|
readonly model: string | undefined;
|
|
166
167
|
readonly modelCatalog?: ModelCatalogSnapshot;
|
|
167
168
|
readonly imagePaths: readonly string[];
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { appendSerialTask } from "@jini-ai/core/primitives";
|
|
1
2
|
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import { createModelReceiptTracker } from './model-receipts.js';
|
|
4
|
+
import { observeRunOutput } from './run-output.js';
|
|
3
5
|
import { messageContentWithImages } from '../attachment-content.js';
|
|
4
6
|
import { redactSecrets, } from '@jini-ai/core';
|
|
5
7
|
import { createClaudeStreamHandler, createCopilotStreamHandler, createJsonEventStreamHandler, createQoderStreamHandler, attachAcpSession, attachPiRpcSession, } from '@jini-ai/agent-runtime';
|
|
@@ -61,6 +63,11 @@ async function terminateChildTree(deps, child) {
|
|
|
61
63
|
const pids = deps.collectProcessTreePids({ processes, rootPids: [child.pid] });
|
|
62
64
|
await deps.stopProcesses({ pids });
|
|
63
65
|
}
|
|
66
|
+
// A late emit can race a terminal run. Keep later events/cleanup ordered even after failure;
|
|
67
|
+
// lifecycle exceptions can contain provider text, so report only this fixed diagnostic.
|
|
68
|
+
function reportDroppedEmit() {
|
|
69
|
+
console.warn('[daemon] Queued lifecycle event could not be delivered.');
|
|
70
|
+
}
|
|
64
71
|
/** Default sink when a host does not supply `onCleanupFailure`: still observable, never silent. Redacted per SEC-007 (a spawn/permission error can embed paths/host detail). */
|
|
65
72
|
export function defaultCleanupFailureSink(context) {
|
|
66
73
|
// eslint-disable-next-line no-console
|
|
@@ -257,6 +264,7 @@ function bufferedStdoutTruncationNotice(droppedBytes, maxBytes) {
|
|
|
257
264
|
*/
|
|
258
265
|
export function wireChildLifecycle(ctx) {
|
|
259
266
|
const { runId, def, streamFormat, child, lifecycle, journal, continuation, classifyFailure } = ctx;
|
|
267
|
+
const output = observeRunOutput({ lifecycle, runId, agentName: def.name }, { plainStdout: streamFormat === 'plain' });
|
|
260
268
|
const modelReceipt = createModelReceiptTracker(ctx.startingModel, ctx.modelCatalog?.models);
|
|
261
269
|
let stdinClosed = false;
|
|
262
270
|
let cancelRequested = false;
|
|
@@ -324,16 +332,7 @@ export function wireChildLifecycle(ctx) {
|
|
|
324
332
|
/** Bytes the ceiling refused, reported verbatim on flush so truncation is never silent. */
|
|
325
333
|
let droppedStdoutBytes = 0;
|
|
326
334
|
function enqueueEmit(task) {
|
|
327
|
-
emitQueue =
|
|
328
|
-
try {
|
|
329
|
-
await task();
|
|
330
|
-
}
|
|
331
|
-
catch {
|
|
332
|
-
// A single emit failing (e.g. a race against an already-terminal
|
|
333
|
-
// run) must not block delivery of subsequently queued events —
|
|
334
|
-
// see this function's own doc.
|
|
335
|
-
}
|
|
336
|
-
});
|
|
335
|
+
emitQueue = appendSerialTask({ previous: emitQueue, task, onError: reportDroppedEmit });
|
|
337
336
|
}
|
|
338
337
|
if (ctx.startingModel)
|
|
339
338
|
enqueueEmit(() => lifecycle.emit({ runId, input: { event: 'agent', data: { type: 'status', label: 'starting_model', model: ctx.startingModel } } }));
|
|
@@ -398,8 +397,10 @@ export function wireChildLifecycle(ctx) {
|
|
|
398
397
|
controlScanBuffer += text;
|
|
399
398
|
if (!text.includes('\n'))
|
|
400
399
|
return;
|
|
401
|
-
|
|
402
|
-
|
|
400
|
+
// `text` holds a newline, so the buffer does: split off the complete lines, keep the partial tail.
|
|
401
|
+
const lastNewline = controlScanBuffer.lastIndexOf('\n');
|
|
402
|
+
const lines = controlScanBuffer.slice(0, lastNewline).split('\n');
|
|
403
|
+
controlScanBuffer = controlScanBuffer.slice(lastNewline + 1);
|
|
403
404
|
for (const line of lines) {
|
|
404
405
|
// Both checked: either word can also appear inside the other kind of line's text.
|
|
405
406
|
if (line.includes('"control_response"'))
|
|
@@ -526,24 +527,18 @@ export function wireChildLifecycle(ctx) {
|
|
|
526
527
|
const receipt = modelReceipt(translation.payload);
|
|
527
528
|
if (receipt)
|
|
528
529
|
enqueueEmit(() => lifecycle.emit({ runId, input: { event: 'agent', data: receipt } }));
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
if (translation.payload
|
|
540
|
-
|
|
541
|
-
toolCallSeen = true;
|
|
542
|
-
}
|
|
543
|
-
else if ((translation.payload.type === 'text_delta' || translation.payload.type === 'thinking_delta') &&
|
|
544
|
-
translation.payload.delta.length > 0) {
|
|
545
|
-
userVisibleOutputSeen = true;
|
|
546
|
-
}
|
|
530
|
+
applyAgentTranslationSideEffects({ payload: translation.payload, sessionId: translation.sessionId, sink: {
|
|
531
|
+
onSessionId: ({ sessionId }) => { capturedSessionId = sessionId; },
|
|
532
|
+
onToolCall: ({ toolUse }) => {
|
|
533
|
+
// A tool signal always carries the tool_use variant (the sink hands it over narrowed);
|
|
534
|
+
// retain its full input for continuation.
|
|
535
|
+
pendingToolUse = { id: toolUse.id, name: toolUse.name, input: toolUse.input };
|
|
536
|
+
toolCallSeen = true;
|
|
537
|
+
},
|
|
538
|
+
onUserVisibleOutput: () => { userVisibleOutputSeen = true; },
|
|
539
|
+
} });
|
|
540
|
+
if (rejectUnavailableBridge({ payload: translation.payload, rawEvent }, {}))
|
|
541
|
+
return;
|
|
547
542
|
enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'agent', data: statusWithChildIdentity({ payload: translation.payload, childPid: ctx.child.pid }, {}) } }));
|
|
548
543
|
}
|
|
549
544
|
else if (translation.kind === 'error') {
|
|
@@ -553,6 +548,17 @@ export function wireChildLifecycle(ctx) {
|
|
|
553
548
|
handleTurnEnd(translation.stopReason);
|
|
554
549
|
}
|
|
555
550
|
});
|
|
551
|
+
function rejectUnavailableBridge({ payload, rawEvent }, _options = {}) {
|
|
552
|
+
if (!bridgeChecked && payload.type === 'status' && payload.label === 'initializing') {
|
|
553
|
+
bridgeChecked = true;
|
|
554
|
+
const bridgeStatus = unavailableJiniBridgeStatus({ rawEvent });
|
|
555
|
+
if (bridgeStatus !== undefined) {
|
|
556
|
+
stopForUnavailableBridge(payload, bridgeStatus);
|
|
557
|
+
return true;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return false;
|
|
561
|
+
}
|
|
556
562
|
/**
|
|
557
563
|
* Ends a run whose CLI started without the `jini` bridge: emits the init status and a
|
|
558
564
|
* {@link MCP_BRIDGE_UNAVAILABLE} error, then stops the child's process tree before the model can
|
|
@@ -693,20 +699,23 @@ export function wireChildLifecycle(ctx) {
|
|
|
693
699
|
// Both of the next two steps are guarded: neither a failed cleanup nor a rejecting host
|
|
694
700
|
// classifier may prevent the terminal transition below — see each helper's own doc.
|
|
695
701
|
await cleanupStagedFilesSafely(ctx);
|
|
696
|
-
const status = bridgeUnavailable ? 'failed' : classifyRunCloseStatus({ cancelRequested, code }, { signal });
|
|
697
|
-
|
|
698
|
-
const resumable = status === 'failed' && !bridgeUnavailable && classifyFailure !== undefined
|
|
699
|
-
? await classifyFailureSafely(ctx, classifyFailure, {
|
|
700
|
-
runId,
|
|
701
|
-
agentId: def.id,
|
|
702
|
-
code,
|
|
703
|
-
signal: signal ?? null,
|
|
704
|
-
sideEffects: { userVisibleOutputSeen, toolCallSeen },
|
|
705
|
-
})
|
|
706
|
-
: false;
|
|
702
|
+
const status = await output.settle({ status: bridgeUnavailable ? 'failed' : classifyRunCloseStatus({ cancelRequested, code }, { signal }) });
|
|
703
|
+
const resumable = await resolveChildResumable({ status, code, signal }, {});
|
|
707
704
|
await lifecycle.finish({ runId, status, code, signal: signal ?? null, resumable }, { ...(capturedSessionId !== undefined ? { sessionRef: capturedSessionId } : {}) });
|
|
708
705
|
})();
|
|
709
706
|
});
|
|
707
|
+
async function resolveChildResumable({ status, code, signal }, _options = {}) {
|
|
708
|
+
// A bridge failure is not resumable: resuming would start the same CLI with the same bridge.
|
|
709
|
+
return status === 'failed' && !bridgeUnavailable && classifyFailure !== undefined
|
|
710
|
+
? await classifyFailureSafely(ctx, classifyFailure, {
|
|
711
|
+
runId,
|
|
712
|
+
agentId: def.id,
|
|
713
|
+
code,
|
|
714
|
+
signal: signal ?? null,
|
|
715
|
+
sideEffects: { userVisibleOutputSeen, toolCallSeen },
|
|
716
|
+
})
|
|
717
|
+
: false;
|
|
718
|
+
}
|
|
710
719
|
/**
|
|
711
720
|
* A mid-run message: first an `interrupt` control request on the still-open stdin, so the agent
|
|
712
721
|
* stops generating (or abandons the tool it is running) at once instead of finishing its whole
|
|
@@ -718,8 +727,10 @@ export function wireChildLifecycle(ctx) {
|
|
|
718
727
|
* `turn_end` has closed stdin, or the process exits before the ack, there is no session left to
|
|
719
728
|
* reach — `'not-running'`. Between turns, while stdin is held only for background tasks, there is
|
|
720
729
|
* no generation to stop: the line is written at once and starts the next turn.
|
|
730
|
+
* Host-prepared images use the same content blocks as the initial stdin turn; no file tools
|
|
731
|
+
* or new directory grants are needed to see these pixels.
|
|
721
732
|
*/
|
|
722
|
-
async function sendUserMessage(text) {
|
|
733
|
+
async function sendUserMessage(text, images = []) {
|
|
723
734
|
if (!acceptsMidRunUserMessages({ def: def }))
|
|
724
735
|
return 'unsupported';
|
|
725
736
|
const stdin = child.stdin;
|
|
@@ -727,7 +738,7 @@ export function wireChildLifecycle(ctx) {
|
|
|
727
738
|
return 'not-running';
|
|
728
739
|
if (heldForBackgroundTasks) {
|
|
729
740
|
heldForBackgroundTasks = false;
|
|
730
|
-
writeUserMessage(stdin, text);
|
|
741
|
+
writeUserMessage(stdin, text, images);
|
|
731
742
|
return 'delivered';
|
|
732
743
|
}
|
|
733
744
|
const requestId = `jini-interrupt-${randomUUID()}`;
|
|
@@ -753,12 +764,12 @@ export function wireChildLifecycle(ctx) {
|
|
|
753
764
|
pendingInterruptAcks.delete(requestId);
|
|
754
765
|
if (!processAlive || stdinClosed)
|
|
755
766
|
return 'not-running';
|
|
756
|
-
writeUserMessage(stdin, text);
|
|
767
|
+
writeUserMessage(stdin, text, images);
|
|
757
768
|
return 'delivered';
|
|
758
769
|
}
|
|
759
|
-
/** Writes a mid-run
|
|
760
|
-
function writeUserMessage(stdin, text) {
|
|
761
|
-
stdin.write(`${JSON.stringify(streamJsonUserMessage({ content:
|
|
770
|
+
/** Writes a complete mid-run turn using the idle image encoder; journals text without base64. */
|
|
771
|
+
function writeUserMessage(stdin, text, images) {
|
|
772
|
+
stdin.write(`${JSON.stringify(streamJsonUserMessage({ content: messageContentWithImages({ prompt: text, images }, {}) }))}\n`, 'utf8');
|
|
762
773
|
if (journal)
|
|
763
774
|
enqueueEmit(() => journal.record({ runId: runId, entry: sentJournalEntry(text) }));
|
|
764
775
|
enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'agent', data: { type: 'user_message', id: randomUUID(), text: text } } }));
|
|
@@ -815,6 +826,22 @@ function backgroundTaskListLength({ line }) {
|
|
|
815
826
|
function streamJsonUserMessage({ content }) {
|
|
816
827
|
return { type: 'user', message: { role: 'user', content: content } };
|
|
817
828
|
}
|
|
829
|
+
/** Preserve ACP's distinction between absent fields and explicitly supplied empty values. */
|
|
830
|
+
function acpSessionOptions({ ctx }, _options = {}) {
|
|
831
|
+
return {
|
|
832
|
+
cwd: ctx.cwd,
|
|
833
|
+
...(ctx.cliParity ? { cliParity: true, stageTimeoutMs: 0 } : {}),
|
|
834
|
+
...(ctx.permissionMode !== undefined ? { permissionMode: ctx.permissionMode } : {}),
|
|
835
|
+
...(ctx.model !== undefined ? { model: ctx.model } : {}),
|
|
836
|
+
...(ctx.imagePaths.length > 0 ? { imagePaths: [...ctx.imagePaths] } : {}),
|
|
837
|
+
...(ctx.envFormat !== undefined ? { envFormat: ctx.envFormat } : {}),
|
|
838
|
+
// Spread-when-present rather than always: passing `mcpServers: []` is not the same as passing
|
|
839
|
+
// nothing for every downstream ACP agent, and "no bridge configured" must stay byte-identical
|
|
840
|
+
// to before this field existed.
|
|
841
|
+
...(ctx.mcpServers !== undefined && ctx.mcpServers.length > 0 ? { mcpServers: [...ctx.mcpServers] } : {}),
|
|
842
|
+
...(ctx.onPermissionRequest !== undefined ? { onPermissionRequest: ctx.onPermissionRequest } : {}),
|
|
843
|
+
};
|
|
844
|
+
}
|
|
818
845
|
/**
|
|
819
846
|
* Wires an ACP child to a run. Unlike the JSON-stream path, ACP owns the
|
|
820
847
|
* prompt protocol and reports its parsed events through `attachAcpSession`'s
|
|
@@ -827,6 +854,7 @@ function streamJsonUserMessage({ content }) {
|
|
|
827
854
|
function wireAcpLifecycle(ctx) {
|
|
828
855
|
const modelReceipt = createModelReceiptTracker(ctx.model, ctx.modelCatalog?.models);
|
|
829
856
|
const { runId, agentId, child, lifecycle, journal, classifyFailure } = ctx;
|
|
857
|
+
const output = observeRunOutput({ lifecycle, runId, agentName: agentId });
|
|
830
858
|
let cancelRequested = false;
|
|
831
859
|
let emitQueue = Promise.resolve();
|
|
832
860
|
// Gap 5 (session resume) — see wireChildLifecycle's identical local for the full rationale.
|
|
@@ -835,15 +863,7 @@ function wireAcpLifecycle(ctx) {
|
|
|
835
863
|
let userVisibleOutputSeen = false;
|
|
836
864
|
let toolCallSeen = false;
|
|
837
865
|
function enqueueEmit(task) {
|
|
838
|
-
emitQueue =
|
|
839
|
-
try {
|
|
840
|
-
await task();
|
|
841
|
-
}
|
|
842
|
-
catch {
|
|
843
|
-
// A late event racing a terminal lifecycle is intentionally dropped;
|
|
844
|
-
// it must not prevent subsequent queued cleanup from running.
|
|
845
|
-
}
|
|
846
|
-
});
|
|
866
|
+
emitQueue = appendSerialTask({ previous: emitQueue, task, onError: reportDroppedEmit });
|
|
847
867
|
}
|
|
848
868
|
if (ctx.model)
|
|
849
869
|
enqueueEmit(() => lifecycle.emit({ runId, input: { event: 'agent', data: { type: 'status', label: 'starting_model', model: ctx.model } } }));
|
|
@@ -873,7 +893,7 @@ function wireAcpLifecycle(ctx) {
|
|
|
873
893
|
unsubscribeCancel();
|
|
874
894
|
// Guarded for the same reasons as the child-driven handler above.
|
|
875
895
|
await cleanupStagedFilesSafely(ctx);
|
|
876
|
-
const status = cancelRequested ? 'cancelled' : controller?.completedSuccessfully() ? 'succeeded' : 'failed';
|
|
896
|
+
const status = await output.settle({ status: cancelRequested ? 'cancelled' : controller?.completedSuccessfully() ? 'succeeded' : 'failed' });
|
|
877
897
|
const resumable = status === 'failed' && classifyFailure !== undefined
|
|
878
898
|
? await classifyFailureSafely(ctx, classifyFailure, {
|
|
879
899
|
runId,
|
|
@@ -908,11 +928,7 @@ function wireAcpLifecycle(ctx) {
|
|
|
908
928
|
if (event === 'error') {
|
|
909
929
|
enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'error', data: translateAcpError(payload) } }));
|
|
910
930
|
}
|
|
911
|
-
} },
|
|
912
|
-
// Spread-when-present rather than always: passing `mcpServers: []` is not the same as passing
|
|
913
|
-
// nothing for every downstream ACP agent, and "no bridge configured" must stay byte-identical
|
|
914
|
-
// to before this field existed.
|
|
915
|
-
...(ctx.mcpServers !== undefined && ctx.mcpServers.length > 0 ? { mcpServers: [...ctx.mcpServers] } : {}), ...(ctx.onPermissionRequest !== undefined ? { onPermissionRequest: ctx.onPermissionRequest } : {}) });
|
|
931
|
+
} }, acpSessionOptions({ ctx }, {}));
|
|
916
932
|
return controller;
|
|
917
933
|
}
|
|
918
934
|
/**
|
|
@@ -937,6 +953,7 @@ function wireAcpLifecycle(ctx) {
|
|
|
937
953
|
function wirePiRpcLifecycle(ctx) {
|
|
938
954
|
const modelReceipt = createModelReceiptTracker(ctx.model, ctx.modelCatalog?.models);
|
|
939
955
|
const { runId, agentId, child, lifecycle, journal, classifyFailure } = ctx;
|
|
956
|
+
const output = observeRunOutput({ lifecycle, runId, agentName: agentId });
|
|
940
957
|
let cancelRequested = false;
|
|
941
958
|
let emitQueue = Promise.resolve();
|
|
942
959
|
// Gap 5 (session resume) — see wireChildLifecycle's identical local for the full rationale.
|
|
@@ -945,15 +962,7 @@ function wirePiRpcLifecycle(ctx) {
|
|
|
945
962
|
let userVisibleOutputSeen = false;
|
|
946
963
|
let toolCallSeen = false;
|
|
947
964
|
function enqueueEmit(task) {
|
|
948
|
-
emitQueue =
|
|
949
|
-
try {
|
|
950
|
-
await task();
|
|
951
|
-
}
|
|
952
|
-
catch {
|
|
953
|
-
// A late event racing a terminal lifecycle is intentionally dropped;
|
|
954
|
-
// it must not prevent subsequent queued cleanup from running.
|
|
955
|
-
}
|
|
956
|
-
});
|
|
965
|
+
emitQueue = appendSerialTask({ previous: emitQueue, task, onError: reportDroppedEmit });
|
|
957
966
|
}
|
|
958
967
|
if (ctx.model)
|
|
959
968
|
enqueueEmit(() => lifecycle.emit({ runId, input: { event: 'agent', data: { type: 'status', label: 'starting_model', model: ctx.model } } }));
|
|
@@ -983,7 +992,7 @@ function wirePiRpcLifecycle(ctx) {
|
|
|
983
992
|
unsubscribeCancel();
|
|
984
993
|
// Guarded for the same reasons as the child-driven handler above.
|
|
985
994
|
await cleanupStagedFilesSafely(ctx);
|
|
986
|
-
const status = cancelRequested ? 'cancelled' : session?.hasFatalError() ? 'failed' : 'succeeded';
|
|
995
|
+
const status = await output.settle({ status: cancelRequested ? 'cancelled' : session?.hasFatalError() ? 'failed' : 'succeeded' });
|
|
987
996
|
const resumable = status === 'failed' && classifyFailure !== undefined
|
|
988
997
|
? await classifyFailureSafely(ctx, classifyFailure, {
|
|
989
998
|
runId,
|
|
@@ -1062,6 +1071,7 @@ export async function runAcpDispatch({ input, deps }) {
|
|
|
1062
1071
|
prompt: input.prompt,
|
|
1063
1072
|
cwd: input.cwd,
|
|
1064
1073
|
permissionMode: input.permissionMode,
|
|
1074
|
+
cliParity: input.cliParity,
|
|
1065
1075
|
model: input.model,
|
|
1066
1076
|
...(input.modelCatalog ? { modelCatalog: input.modelCatalog } : {}),
|
|
1067
1077
|
imagePaths: input.imagePaths,
|
|
@@ -1086,7 +1096,8 @@ export async function runAcpDispatch({ input, deps }) {
|
|
|
1086
1096
|
// fired-and-forgotten (SEC-007: "await where lifecycle ordering allows it").
|
|
1087
1097
|
await terminateChildTreeBestEffort({ listProcessSnapshots: deps.listProcessSnapshots, collectProcessTreePids: deps.collectProcessTreePids, stopProcesses: deps.stopProcesses }, input.child, input.runId, 'acp-attach-failure', deps.onCleanupFailure);
|
|
1088
1098
|
await deps.releaseStagedResources();
|
|
1089
|
-
|
|
1099
|
+
// `failBeforeSpawn` always rejects (`Promise<never>`); returning it propagates that rejection.
|
|
1100
|
+
return deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: could not attach ACP session for agent "${input.agentId}": ${errorMessage(err)}` });
|
|
1090
1101
|
}
|
|
1091
1102
|
}
|
|
1092
1103
|
/** Phase 15 (pi-rpc branch): same discipline as {@link runAcpDispatch}, for the one `'pi-rpc'` def. */
|
|
@@ -1118,7 +1129,7 @@ export async function runPiRpcDispatch({ input, deps }) {
|
|
|
1118
1129
|
// fire-and-forget (SEC-007).
|
|
1119
1130
|
await terminateChildTreeBestEffort({ listProcessSnapshots: deps.listProcessSnapshots, collectProcessTreePids: deps.collectProcessTreePids, stopProcesses: deps.stopProcesses }, input.child, input.runId, 'pi-rpc-attach-failure', deps.onCleanupFailure);
|
|
1120
1131
|
await deps.releaseStagedResources();
|
|
1121
|
-
|
|
1132
|
+
return deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: could not attach pi-rpc session for agent "${input.agentId}": ${errorMessage(err)}` });
|
|
1122
1133
|
}
|
|
1123
1134
|
}
|
|
1124
1135
|
//# sourceMappingURL=events.js.map
|
|
@@ -189,9 +189,9 @@ export declare function computeRuntimeContext({ preparedPromptFile, preparedLogF
|
|
|
189
189
|
/** Phase 9a: the def's `buildArgs` 4th argument. Historically omitted when every override was absent;
|
|
190
190
|
* that let defs silently enable auto-approval. Always supply the validated restricted default now. Pure. */
|
|
191
191
|
export declare function buildAgentBuildArgsOptions({ input, systemPromptOverlay }: {
|
|
192
|
-
readonly input: Pick<AgentExecutorRunInput, 'model' | 'reasoning' | 'permissionMode' | 'disallowedTools' | 'allowedTools' | 'settingSources' | 'settings'>;
|
|
192
|
+
readonly input: Pick<AgentExecutorRunInput, 'model' | 'reasoning' | 'permissionMode' | 'nativeToolAccess' | 'nativeCapabilities' | 'cliParity' | 'disallowedTools' | 'allowedTools' | 'settingSources' | 'settings' | 'chromeIntegration'>;
|
|
193
193
|
readonly systemPromptOverlay: string | null | undefined;
|
|
194
|
-
}): RuntimeBuildOptions
|
|
194
|
+
}): RuntimeBuildOptions;
|
|
195
195
|
/**
|
|
196
196
|
* **The single dispatch point from a computed system-prompt overlay to its delivery mechanism** —
|
|
197
197
|
* see `RuntimeAgentDef.systemPromptDelivery`'s own doc for the declared shape. Pure and
|
|
@@ -290,6 +290,7 @@ export declare function buildRunArgs({ input, deps }: {
|
|
|
290
290
|
* a defensive extra.
|
|
291
291
|
* @param input.def - Only used for its `id`, in the failure message, and its `systemPromptDelivery` declaration.
|
|
292
292
|
* @param input.overlay - The computed `PromptAugmenter.systemOverlay()` result for this run.
|
|
293
|
+
* @param deps.mkdtemp - Creates the run-scoped directory from a prefix; default a fresh `mkdtemp` under `os.tmpdir()`.
|
|
293
294
|
* @complexity O(1) plus one directory creation and one file write.
|
|
294
295
|
* @overallScore 100/100
|
|
295
296
|
*/
|
|
@@ -302,6 +303,7 @@ export declare function prepareSystemPromptOverlayFileIfNeeded({ input, deps }:
|
|
|
302
303
|
readonly deps: {
|
|
303
304
|
readonly releaseStagedResources: () => Promise<void>;
|
|
304
305
|
readonly failBeforeSpawn: FailBeforeSpawn;
|
|
306
|
+
readonly mkdtemp?: (prefix: string) => Promise<string>;
|
|
305
307
|
};
|
|
306
308
|
}): Promise<PreparedSystemPromptOverlayFile | null>;
|
|
307
309
|
/** Phase 11: post-`buildArgs` guard for argv-bound defs whose resolved binary is a Windows shim/.exe — a no-op off-Windows and for non-argv-bound defs. */
|