@jini-ai/agent-runtime 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/agent-protocol/acp/session.d.ts +206 -1
- package/dist/agent-protocol/acp/session.d.ts.map +1 -1
- package/dist/agent-protocol/acp/session.js +505 -350
- package/dist/agent-protocol/acp/session.js.map +1 -1
- package/dist/agent-protocol/core/json-line-stream.d.ts +53 -0
- package/dist/agent-protocol/core/json-line-stream.d.ts.map +1 -1
- package/dist/agent-protocol/core/json-line-stream.js +307 -255
- package/dist/agent-protocol/core/json-line-stream.js.map +1 -1
- package/dist/agent-protocol/pi-rpc/events.d.ts +22 -1
- package/dist/agent-protocol/pi-rpc/events.d.ts.map +1 -1
- package/dist/agent-protocol/pi-rpc/events.js +189 -153
- package/dist/agent-protocol/pi-rpc/events.js.map +1 -1
- package/dist/agent-protocol/pi-rpc/session.d.ts +39 -0
- package/dist/agent-protocol/pi-rpc/session.d.ts.map +1 -1
- package/dist/agent-protocol/pi-rpc/session.js +143 -107
- package/dist/agent-protocol/pi-rpc/session.js.map +1 -1
- package/dist/claude-stream.d.ts +64 -1
- package/dist/claude-stream.d.ts.map +1 -1
- package/dist/claude-stream.js +568 -441
- package/dist/claude-stream.js.map +1 -1
- package/dist/defs/aider.d.ts.map +1 -1
- package/dist/defs/aider.js +13 -1
- package/dist/defs/aider.js.map +1 -1
- package/dist/defs/amp.d.ts.map +1 -1
- package/dist/defs/amp.js +16 -1
- package/dist/defs/amp.js.map +1 -1
- package/dist/defs/amr.d.ts +2 -1
- package/dist/defs/amr.d.ts.map +1 -1
- package/dist/defs/amr.js +12 -1
- package/dist/defs/amr.js.map +1 -1
- package/dist/defs/antigravity.d.ts +41 -2
- package/dist/defs/antigravity.d.ts.map +1 -1
- package/dist/defs/antigravity.js +201 -22
- package/dist/defs/antigravity.js.map +1 -1
- package/dist/defs/claude.d.ts +2 -0
- package/dist/defs/claude.d.ts.map +1 -1
- package/dist/defs/claude.js +58 -3
- package/dist/defs/claude.js.map +1 -1
- package/dist/defs/codebuddy.d.ts.map +1 -1
- package/dist/defs/codebuddy.js +9 -1
- package/dist/defs/codebuddy.js.map +1 -1
- package/dist/defs/codex.d.ts +1 -0
- package/dist/defs/codex.d.ts.map +1 -1
- package/dist/defs/codex.js +34 -0
- package/dist/defs/codex.js.map +1 -1
- package/dist/defs/copilot.d.ts.map +1 -1
- package/dist/defs/copilot.js +15 -1
- package/dist/defs/copilot.js.map +1 -1
- package/dist/defs/cursor-agent.d.ts.map +1 -1
- package/dist/defs/cursor-agent.js +24 -2
- package/dist/defs/cursor-agent.js.map +1 -1
- package/dist/defs/deepseek.d.ts.map +1 -1
- package/dist/defs/deepseek.js +12 -0
- package/dist/defs/deepseek.js.map +1 -1
- package/dist/defs/devin.d.ts +2 -1
- package/dist/defs/devin.d.ts.map +1 -1
- package/dist/defs/devin.js +12 -7
- package/dist/defs/devin.js.map +1 -1
- package/dist/defs/grok-build.d.ts.map +1 -1
- package/dist/defs/grok-build.js +10 -0
- package/dist/defs/grok-build.js.map +1 -1
- package/dist/defs/hermes.d.ts +1 -0
- package/dist/defs/hermes.d.ts.map +1 -1
- package/dist/defs/hermes.js +3 -0
- package/dist/defs/hermes.js.map +1 -1
- package/dist/defs/kilo.d.ts +1 -0
- package/dist/defs/kilo.d.ts.map +1 -1
- package/dist/defs/kilo.js +3 -0
- package/dist/defs/kilo.js.map +1 -1
- package/dist/defs/kimi.d.ts +1 -0
- package/dist/defs/kimi.d.ts.map +1 -1
- package/dist/defs/kimi.js +3 -0
- package/dist/defs/kimi.js.map +1 -1
- package/dist/defs/kiro.d.ts +1 -0
- package/dist/defs/kiro.d.ts.map +1 -1
- package/dist/defs/kiro.js +3 -0
- package/dist/defs/kiro.js.map +1 -1
- package/dist/defs/pi.d.ts +1 -1
- package/dist/defs/pi.d.ts.map +1 -1
- package/dist/defs/pi.js +6 -1
- package/dist/defs/pi.js.map +1 -1
- package/dist/defs/qoder.d.ts +1 -0
- package/dist/defs/qoder.d.ts.map +1 -1
- package/dist/defs/qoder.js +15 -0
- package/dist/defs/qoder.js.map +1 -1
- package/dist/defs/qwen.d.ts.map +1 -1
- package/dist/defs/qwen.js +9 -0
- package/dist/defs/qwen.js.map +1 -1
- package/dist/defs/reasonix.d.ts +1 -0
- package/dist/defs/reasonix.d.ts.map +1 -1
- package/dist/defs/reasonix.js +3 -0
- package/dist/defs/reasonix.js.map +1 -1
- package/dist/defs/shared.d.ts +31 -1
- package/dist/defs/shared.d.ts.map +1 -1
- package/dist/defs/shared.js +35 -0
- package/dist/defs/shared.js.map +1 -1
- package/dist/defs/trae-cli.d.ts +1 -0
- package/dist/defs/trae-cli.d.ts.map +1 -1
- package/dist/defs/trae-cli.js +3 -0
- package/dist/defs/trae-cli.js.map +1 -1
- package/dist/defs/vibe.d.ts +1 -0
- package/dist/defs/vibe.d.ts.map +1 -1
- package/dist/defs/vibe.js +3 -0
- package/dist/defs/vibe.js.map +1 -1
- package/dist/detection.d.ts +1 -1
- package/dist/detection.d.ts.map +1 -1
- package/dist/detection.js +7 -2
- package/dist/detection.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/log-file.d.ts +23 -0
- package/dist/log-file.d.ts.map +1 -0
- package/dist/log-file.js +56 -0
- package/dist/log-file.js.map +1 -0
- package/dist/model-registry.d.ts +7 -45
- package/dist/model-registry.d.ts.map +1 -1
- package/dist/model-registry.js.map +1 -1
- package/dist/providers/anthropic-messages.d.ts +198 -6
- package/dist/providers/anthropic-messages.d.ts.map +1 -1
- package/dist/providers/anthropic-messages.js +317 -140
- package/dist/providers/anthropic-messages.js.map +1 -1
- package/dist/providers/azure-chat.d.ts +113 -3
- package/dist/providers/azure-chat.d.ts.map +1 -1
- package/dist/providers/azure-chat.js +114 -21
- package/dist/providers/azure-chat.js.map +1 -1
- package/dist/providers/connection-guard.d.ts +121 -1
- package/dist/providers/connection-guard.d.ts.map +1 -1
- package/dist/providers/connection-guard.js +205 -3
- package/dist/providers/connection-guard.js.map +1 -1
- package/dist/providers/connection-test.d.ts +62 -0
- package/dist/providers/connection-test.d.ts.map +1 -0
- package/dist/providers/connection-test.js +404 -0
- package/dist/providers/connection-test.js.map +1 -0
- package/dist/providers/elevenlabs.d.ts.map +1 -1
- package/dist/providers/elevenlabs.js +3 -2
- package/dist/providers/elevenlabs.js.map +1 -1
- package/dist/providers/google-messages.d.ts +208 -7
- package/dist/providers/google-messages.d.ts.map +1 -1
- package/dist/providers/google-messages.js +331 -105
- package/dist/providers/google-messages.js.map +1 -1
- package/dist/providers/index.d.ts +1 -0
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +1 -0
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/model-catalog.d.ts.map +1 -1
- package/dist/providers/model-catalog.js +21 -0
- package/dist/providers/model-catalog.js.map +1 -1
- package/dist/providers/ollama-chat.d.ts +190 -1
- package/dist/providers/ollama-chat.d.ts.map +1 -1
- package/dist/providers/ollama-chat.js +319 -110
- package/dist/providers/ollama-chat.js.map +1 -1
- package/dist/providers/openai-chat.d.ts +222 -14
- package/dist/providers/openai-chat.d.ts.map +1 -1
- package/dist/providers/openai-chat.js +377 -128
- package/dist/providers/openai-chat.js.map +1 -1
- package/dist/providers/sse-decode.d.ts +2 -2
- package/dist/providers/sse-decode.js +2 -2
- package/dist/types.d.ts +169 -87
- package/dist/types.d.ts.map +1 -1
- package/package.json +8 -4
|
@@ -5,6 +5,14 @@
|
|
|
5
5
|
* replies, artifact-write mirroring, DSML text suppression, and clean abort.
|
|
6
6
|
* Depends on every other acp/ file and the core JSON-line stream. Consumed by
|
|
7
7
|
* connection-test and server entry points (via the acp/ barrel).
|
|
8
|
+
*
|
|
9
|
+
* The `session/update` and result-routing message-kind handlers below
|
|
10
|
+
* (`handleSessionUpdate`, `routeResultById`, and everything they call) are
|
|
11
|
+
* exported as free functions over an explicit `AcpSessionState` +
|
|
12
|
+
* `AcpSessionEffects` pair rather than closures, specifically so they are
|
|
13
|
+
* directly unit-testable without spinning up a whole session. They are not
|
|
14
|
+
* re-exported from the package barrel — only `attachAcpSession` and the
|
|
15
|
+
* public types are part of this package's supported surface.
|
|
8
16
|
*/
|
|
9
17
|
import path from 'node:path';
|
|
10
18
|
import { createDsmlArtifactTextSuppressor, createToolCallTextSuppressor, } from './text-suppression.js';
|
|
@@ -16,6 +24,419 @@ import { acpRawEventShape, isAcpCompletedStatus, isAcpTerminalFailureStatus, acp
|
|
|
16
24
|
import { findModelConfigOption, currentModelFromSessionResult, modelSelectionErrorIsRecoverable, } from './models.js';
|
|
17
25
|
import { buildAcpSessionNewParams, buildPromptBlocks } from './session-params.js';
|
|
18
26
|
import { noopAccountFailureClassifier } from './account-failure.js';
|
|
27
|
+
/** Builds a fresh {@link AcpSessionState} with every field at its initial value. */
|
|
28
|
+
export function createAcpSessionState() {
|
|
29
|
+
return {
|
|
30
|
+
expectedId: 1,
|
|
31
|
+
nextId: 2,
|
|
32
|
+
promptRequestId: null,
|
|
33
|
+
setModelRequestId: null,
|
|
34
|
+
sessionId: null,
|
|
35
|
+
durableSessionId: null,
|
|
36
|
+
activeModel: null,
|
|
37
|
+
modelConfigId: null,
|
|
38
|
+
emittedThinkingStart: false,
|
|
39
|
+
emittedTextChunk: false,
|
|
40
|
+
emittedVisibleTextChunk: false,
|
|
41
|
+
emittedTextBuffer: '',
|
|
42
|
+
emittedToolCall: false,
|
|
43
|
+
emittedConcreteToolEvent: false,
|
|
44
|
+
dsmlArtifactSuppressor: null,
|
|
45
|
+
dsmlArtifactSuppressorLastSuppressedChars: 0,
|
|
46
|
+
dsmlArtifactSuppressorToolCallId: null,
|
|
47
|
+
dsmlArtifactSuppressorArmedAfterText: false,
|
|
48
|
+
dsmlArtifactSuppressorSawIncrementalProse: false,
|
|
49
|
+
acpArtifactWriteToolCallIds: new Set(),
|
|
50
|
+
acpArtifactRunEventState: new Map(),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** `typeof value === 'string' ? value : null` as a named predicate — too trivial to export on its own. */
|
|
54
|
+
function asOptionalString(value) {
|
|
55
|
+
return typeof value === 'string' ? value : null;
|
|
56
|
+
}
|
|
57
|
+
/** Fires the optional `onSessionInit` callback; kept as a one-liner so the optional-chaining call is scored against this function, not its caller. */
|
|
58
|
+
function notifySessionInit(effects) {
|
|
59
|
+
effects.onSessionInit?.();
|
|
60
|
+
}
|
|
61
|
+
/** Resolves a `session/new` response's model config id, if any. */
|
|
62
|
+
function resolveModelConfigId(modelConfig) {
|
|
63
|
+
return modelConfig?.configId ?? null;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Handles a JSON-RPC error frame from the agent.
|
|
67
|
+
*
|
|
68
|
+
* -32603 unexpected-id errors are cleanup noise. Expected-id model selection
|
|
69
|
+
* failures are recoverable; all other RPC errors are real protocol failures
|
|
70
|
+
* for initialize/session/new/session/prompt.
|
|
71
|
+
*/
|
|
72
|
+
export function handleRpcError({ state, effects, obj, error, rpcErr, }) {
|
|
73
|
+
if (obj.id === state.setModelRequestId &&
|
|
74
|
+
modelSelectionErrorIsRecoverable(error?.code) &&
|
|
75
|
+
state.promptRequestId === null) {
|
|
76
|
+
effects.recoverFromModelSelectionError();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (error?.code === -32603 && obj.id !== state.expectedId) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const details = rpcErrorData(obj);
|
|
83
|
+
const promotedPayload = promotedOpenCodeSessionErrorPayload(details, rpcErr);
|
|
84
|
+
if (promotedPayload) {
|
|
85
|
+
effects.failWithPayload(promotedPayload);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const retryable = rpcErrorRetryable(details);
|
|
89
|
+
effects.fail(rpcErr, {
|
|
90
|
+
details,
|
|
91
|
+
...(retryable === undefined ? {} : { retryable }),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/** Promotes an AMR retry-status `session/update` into a structured account-failure error, if the classifier matches. Returns `true` when it did (and thus failed the session). */
|
|
95
|
+
export function tryPromoteAmrRetryStatus({ effects, update, }) {
|
|
96
|
+
const promotedPayload = promotedAmrRetryStatusPayload(update, effects.accountFailureClassifier);
|
|
97
|
+
if (!promotedPayload)
|
|
98
|
+
return false;
|
|
99
|
+
effects.failWithPayload(promotedPayload);
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
/** Translates an `agent_thought_chunk` update into `thinking_start` (once) / `thinking_delta` events. */
|
|
103
|
+
export function handleThoughtChunkUpdate({ state, effects, update, }) {
|
|
104
|
+
effects.emitAcpRawShapeDiagnostic(update);
|
|
105
|
+
const text = extractAcpUpdateText(update);
|
|
106
|
+
if (!text)
|
|
107
|
+
return;
|
|
108
|
+
if (!state.emittedThinkingStart) {
|
|
109
|
+
state.emittedThinkingStart = true;
|
|
110
|
+
effects.send('agent', { type: 'thinking_start' });
|
|
111
|
+
}
|
|
112
|
+
effects.send('agent', { type: 'thinking_delta', delta: text });
|
|
113
|
+
}
|
|
114
|
+
/** Strips tool-call XML from a raw message delta via the session's tool-call text suppressor, noting the suppression reason. */
|
|
115
|
+
export function stripToolCallText({ effects, delta }) {
|
|
116
|
+
const wasSuppressingToolCall = effects.toolCallTextSuppressor.isSuppressing();
|
|
117
|
+
const toolCallStrippedDelta = effects.toolCallTextSuppressor.strip(delta);
|
|
118
|
+
effects.noteToolCallTextSuppression(wasSuppressingToolCall || toolCallStrippedDelta !== delta ? 'tool_call_xml' : 'tool_call_candidate');
|
|
119
|
+
return toolCallStrippedDelta;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Whether a non-cumulative message delta should be emitted as-is (prose)
|
|
123
|
+
* rather than the DSML artifact suppressor's stripped output. True when this
|
|
124
|
+
* delta isn't itself part of an artifact echo/candidate, AND either nothing
|
|
125
|
+
* was actually stripped, or the suppressor was armed mid-stream (after
|
|
126
|
+
* visible text) and prior deltas already established this is ordinary prose
|
|
127
|
+
* rather than the start of another artifact echo.
|
|
128
|
+
*/
|
|
129
|
+
export function shouldPreserveIncrementalProse(args) {
|
|
130
|
+
const { isCumulativeSnapshot, wasSuppressingArtifact, hadPendingArtifactCandidate, hasOpenArtifactCandidate, strippedDelta, toolCallStrippedDelta, dsmlArtifactSuppressorArmedAfterText, dsmlArtifactSuppressorSawIncrementalProse, } = args;
|
|
131
|
+
if (isCumulativeSnapshot || wasSuppressingArtifact || hadPendingArtifactCandidate || hasOpenArtifactCandidate) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
if (strippedDelta === toolCallStrippedDelta)
|
|
135
|
+
return true;
|
|
136
|
+
return (!dsmlArtifactSuppressorArmedAfterText &&
|
|
137
|
+
dsmlArtifactSuppressorSawIncrementalProse &&
|
|
138
|
+
!ACP_ARTIFACT_ECHO_START_RE.test(toolCallStrippedDelta));
|
|
139
|
+
}
|
|
140
|
+
/** Whether a delta passed through the DSML suppressor completely unchanged and there is no artifact suppression in progress. */
|
|
141
|
+
export function isPlainIncrementalDelta(args) {
|
|
142
|
+
const { strippedDelta, toolCallStrippedDelta, wasSuppressingArtifact, hadPendingArtifactCandidate, hasOpenArtifactCandidate, } = args;
|
|
143
|
+
return (strippedDelta === toolCallStrippedDelta &&
|
|
144
|
+
!wasSuppressingArtifact &&
|
|
145
|
+
!hadPendingArtifactCandidate &&
|
|
146
|
+
!hasOpenArtifactCandidate);
|
|
147
|
+
}
|
|
148
|
+
/** Runs a tool-call-stripped delta through the DSML artifact suppressor, emits whatever should stay visible, and arms/disarms the "saw incremental prose" and suppressor-cleared state. */
|
|
149
|
+
export function applyDsmlArtifactSuppression({ state, effects, suppressor, toolCallStrippedDelta, delta, isCumulativeSnapshot, }) {
|
|
150
|
+
const wasSuppressingArtifact = suppressor.isSuppressing();
|
|
151
|
+
const hadPendingArtifactCandidate = suppressor.hasPendingCandidate();
|
|
152
|
+
const strippedDelta = suppressor.strip(toolCallStrippedDelta);
|
|
153
|
+
effects.noteArtifactTextSuppression(wasSuppressingArtifact || strippedDelta !== toolCallStrippedDelta ? 'artifact_echo' : 'artifact_candidate');
|
|
154
|
+
const hasOpenArtifactCandidate = suppressor.isSuppressing() || suppressor.hasPendingCandidate();
|
|
155
|
+
const consumedArtifactText = wasSuppressingArtifact || strippedDelta !== delta;
|
|
156
|
+
const preserveIncrementalProse = shouldPreserveIncrementalProse({
|
|
157
|
+
isCumulativeSnapshot,
|
|
158
|
+
wasSuppressingArtifact,
|
|
159
|
+
hadPendingArtifactCandidate,
|
|
160
|
+
hasOpenArtifactCandidate,
|
|
161
|
+
strippedDelta,
|
|
162
|
+
toolCallStrippedDelta,
|
|
163
|
+
dsmlArtifactSuppressorArmedAfterText: state.dsmlArtifactSuppressorArmedAfterText,
|
|
164
|
+
dsmlArtifactSuppressorSawIncrementalProse: state.dsmlArtifactSuppressorSawIncrementalProse,
|
|
165
|
+
});
|
|
166
|
+
const outputDelta = preserveIncrementalProse ? toolCallStrippedDelta : strippedDelta;
|
|
167
|
+
if (outputDelta) {
|
|
168
|
+
effects.emitVisibleTextDelta(outputDelta);
|
|
169
|
+
}
|
|
170
|
+
if (isPlainIncrementalDelta({
|
|
171
|
+
strippedDelta,
|
|
172
|
+
toolCallStrippedDelta,
|
|
173
|
+
wasSuppressingArtifact,
|
|
174
|
+
hadPendingArtifactCandidate,
|
|
175
|
+
hasOpenArtifactCandidate,
|
|
176
|
+
})) {
|
|
177
|
+
state.dsmlArtifactSuppressorSawIncrementalProse = true;
|
|
178
|
+
}
|
|
179
|
+
if (consumedArtifactText && !hasOpenArtifactCandidate) {
|
|
180
|
+
state.dsmlArtifactSuppressor = null;
|
|
181
|
+
state.dsmlArtifactSuppressorToolCallId = null;
|
|
182
|
+
state.dsmlArtifactSuppressorArmedAfterText = false;
|
|
183
|
+
state.dsmlArtifactSuppressorSawIncrementalProse = false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/** Translates an `agent_message_chunk` update into `text_delta` events, applying tool-call and DSML-artifact text suppression. */
|
|
187
|
+
export function handleMessageChunkUpdate({ state, effects, update, }) {
|
|
188
|
+
effects.emitAcpRawShapeDiagnostic(update);
|
|
189
|
+
const text = extractAcpUpdateText(update);
|
|
190
|
+
if (!text)
|
|
191
|
+
return;
|
|
192
|
+
const isCumulativeSnapshot = text.startsWith(state.emittedTextBuffer);
|
|
193
|
+
const delta = isCumulativeSnapshot ? text.slice(state.emittedTextBuffer.length) : text;
|
|
194
|
+
if (delta.length === 0)
|
|
195
|
+
return;
|
|
196
|
+
state.emittedTextChunk = true;
|
|
197
|
+
state.emittedTextBuffer += delta;
|
|
198
|
+
const toolCallStrippedDelta = stripToolCallText({ effects, delta });
|
|
199
|
+
if (!toolCallStrippedDelta)
|
|
200
|
+
return;
|
|
201
|
+
if (state.dsmlArtifactSuppressor) {
|
|
202
|
+
applyDsmlArtifactSuppression({
|
|
203
|
+
state,
|
|
204
|
+
effects,
|
|
205
|
+
suppressor: state.dsmlArtifactSuppressor,
|
|
206
|
+
toolCallStrippedDelta,
|
|
207
|
+
delta,
|
|
208
|
+
isCumulativeSnapshot,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
effects.emitVisibleTextDelta(toolCallStrippedDelta);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// Mirror artifact-write tool calls into the daemon's canonical
|
|
216
|
+
// tool_use/tool_result event shape so a run-artifacts scanner can see
|
|
217
|
+
// ACP file writes. Without this, an ACP agent that emits only
|
|
218
|
+
// text/status/thinking events (never a tool_use/tool_result pair)
|
|
219
|
+
// would report zero artifacts even when the run wrote artifacts.
|
|
220
|
+
//
|
|
221
|
+
// This path only feeds the NO-PROJECT fallback (project runs use the
|
|
222
|
+
// filesystem snapshot). Two correctness rules, both learned the hard
|
|
223
|
+
// way in review:
|
|
224
|
+
// 1. Defer emission to the TERMINAL frame and accumulate the best
|
|
225
|
+
// concrete path across frames — ACP often sends `locations` only
|
|
226
|
+
// on the completing update, and emitting on the first frame would
|
|
227
|
+
// lock in a wrong/empty guess that a later path can't correct.
|
|
228
|
+
// 2. Never fabricate an artifact extension. `isArtifactPath` is what
|
|
229
|
+
// decides whether a write counts; feeding it a real path lets it
|
|
230
|
+
// correctly EXCLUDE non-artifact edits (`config.json`, `README.md`)
|
|
231
|
+
// and INCLUDE real artifacts. A write that never carries a concrete
|
|
232
|
+
// path stays keyed on its (extension-less) toolCallId, so it is
|
|
233
|
+
// simply not counted rather than inflating the metric with a
|
|
234
|
+
// synthetic `.html` — under-counting a truly opaque write is
|
|
235
|
+
// acceptable; a false-positive artifact is not.
|
|
236
|
+
export function mirrorArtifactWriteToolEvent({ state, effects, update, toolCallId, }) {
|
|
237
|
+
if (!toolCallId)
|
|
238
|
+
return;
|
|
239
|
+
const isWriteCall = isAcpArtifactWriteLabel(update) || state.acpArtifactWriteToolCallIds.has(toolCallId);
|
|
240
|
+
if (!isWriteCall)
|
|
241
|
+
return;
|
|
242
|
+
let st = state.acpArtifactRunEventState.get(toolCallId);
|
|
243
|
+
if (!st) {
|
|
244
|
+
st = { path: null, emitted: false };
|
|
245
|
+
state.acpArtifactRunEventState.set(toolCallId, st);
|
|
246
|
+
}
|
|
247
|
+
if (!st.path)
|
|
248
|
+
st.path = acpArtifactWritePath(update);
|
|
249
|
+
const failed = isAcpTerminalFailureStatus(update);
|
|
250
|
+
const shouldEmit = !st.emitted && (failed || isAcpCompletedStatus(update));
|
|
251
|
+
if (!shouldEmit)
|
|
252
|
+
return;
|
|
253
|
+
st.emitted = true;
|
|
254
|
+
effects.send('agent', {
|
|
255
|
+
type: 'tool_use',
|
|
256
|
+
id: toolCallId,
|
|
257
|
+
name: 'Write',
|
|
258
|
+
input: { file_path: st.path ?? toolCallId },
|
|
259
|
+
});
|
|
260
|
+
effects.send('agent', { type: 'tool_result', toolUseId: toolCallId, isError: failed });
|
|
261
|
+
state.emittedConcreteToolEvent = true;
|
|
262
|
+
}
|
|
263
|
+
/** Arms the DSML artifact suppressor when a tool_call/tool_call_update completes an artifact write, or disarms it once its owning write call terminally fails. */
|
|
264
|
+
export function updateDsmlSuppressorForToolCall({ state, update, toolCallId, }) {
|
|
265
|
+
if (isAcpArtifactWriteUpdate(update, state.acpArtifactWriteToolCallIds)) {
|
|
266
|
+
state.dsmlArtifactSuppressor = createDsmlArtifactTextSuppressor();
|
|
267
|
+
state.dsmlArtifactSuppressorLastSuppressedChars = 0;
|
|
268
|
+
state.dsmlArtifactSuppressorToolCallId = toolCallId;
|
|
269
|
+
state.dsmlArtifactSuppressorArmedAfterText = state.emittedTextBuffer.length > 0;
|
|
270
|
+
state.dsmlArtifactSuppressorSawIncrementalProse = false;
|
|
271
|
+
if (toolCallId)
|
|
272
|
+
state.acpArtifactWriteToolCallIds.delete(toolCallId);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (!toolCallId || !isAcpTerminalFailureStatus(update))
|
|
276
|
+
return;
|
|
277
|
+
const ownsPendingWriteSuppression = toolCallId === state.dsmlArtifactSuppressorToolCallId;
|
|
278
|
+
const ownsPendingWriteCall = state.acpArtifactWriteToolCallIds.has(toolCallId);
|
|
279
|
+
state.acpArtifactWriteToolCallIds.delete(toolCallId);
|
|
280
|
+
if (!ownsPendingWriteSuppression && !ownsPendingWriteCall)
|
|
281
|
+
return;
|
|
282
|
+
state.dsmlArtifactSuppressor = null;
|
|
283
|
+
state.dsmlArtifactSuppressorToolCallId = null;
|
|
284
|
+
state.dsmlArtifactSuppressorArmedAfterText = false;
|
|
285
|
+
state.dsmlArtifactSuppressorSawIncrementalProse = false;
|
|
286
|
+
}
|
|
287
|
+
/** Translates a `tool_call` / `tool_call_update` update: tracks it as real turn output, mirrors artifact writes, and manages DSML suppressor arming. */
|
|
288
|
+
export function handleToolCallUpdate({ state, effects, update, }) {
|
|
289
|
+
// The turn did real work (a tool call / file edit), which is valid output even
|
|
290
|
+
// when the model emits no closing assistant text. Track it so the prompt-complete
|
|
291
|
+
// handler does not misreport such a turn as "no output / model unavailable".
|
|
292
|
+
state.emittedToolCall = true;
|
|
293
|
+
const toolCallId = acpToolCallId(update);
|
|
294
|
+
if (toolCallId && isAcpArtifactWriteLabel(update)) {
|
|
295
|
+
state.acpArtifactWriteToolCallIds.add(toolCallId);
|
|
296
|
+
}
|
|
297
|
+
mirrorArtifactWriteToolEvent({ state, effects, update, toolCallId });
|
|
298
|
+
updateDsmlSuppressorForToolCall({ state, update, toolCallId });
|
|
299
|
+
}
|
|
300
|
+
/** Top-level `session/update` dispatcher: emits a generic status for non-streaming update kinds, then routes to the matching per-kind handler. */
|
|
301
|
+
export function handleSessionUpdate({ state, effects, update, }) {
|
|
302
|
+
if (effects.modelUnavailableErrorCode && tryPromoteAmrRetryStatus({ effects, update }))
|
|
303
|
+
return;
|
|
304
|
+
if (update.sessionUpdate !== 'agent_message_chunk' && update.sessionUpdate !== 'agent_thought_chunk') {
|
|
305
|
+
effects.send('agent', {
|
|
306
|
+
type: 'status',
|
|
307
|
+
label: String(update.sessionUpdate || 'session_update'),
|
|
308
|
+
elapsedMs: Date.now() - effects.runStartedAt,
|
|
309
|
+
});
|
|
310
|
+
effects.emitAcpRawShapeDiagnostic(update);
|
|
311
|
+
}
|
|
312
|
+
if (update.sessionUpdate === 'agent_thought_chunk') {
|
|
313
|
+
handleThoughtChunkUpdate({ state, effects, update });
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (update.sessionUpdate === 'agent_message_chunk') {
|
|
317
|
+
handleMessageChunkUpdate({ state, effects, update });
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (update.sessionUpdate === 'tool_call' || update.sessionUpdate === 'tool_call_update') {
|
|
321
|
+
handleToolCallUpdate({ state, effects, update });
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/** Handles the `initialize` ack: sends `session/load` (resume) or `session/new` (fresh). */
|
|
325
|
+
export function handleInitializeAck({ state, effects }) {
|
|
326
|
+
state.expectedId = state.nextId;
|
|
327
|
+
if (effects.resumeSessionId) {
|
|
328
|
+
// Resume the prior upstream session instead of creating a fresh one.
|
|
329
|
+
effects.writeRpc(state.nextId, 'session/load', { sessionId: effects.resumeSessionId, cwd: effects.effectiveCwd }, 'session/load');
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
effects.writeRpc(state.nextId, 'session/new', buildAcpSessionNewParams(effects.effectiveCwd, effects.mcpServers ? { mcpServers: effects.mcpServers, envFormat: effects.envFormat } : { envFormat: effects.envFormat }), 'session/new');
|
|
333
|
+
}
|
|
334
|
+
state.nextId += 1;
|
|
335
|
+
}
|
|
336
|
+
/** Sends `session/set_model` (or `session/set_config_option`) when a non-default model was requested. Returns `true` when it did (caller should not send the prompt yet). */
|
|
337
|
+
export function triggerSetModelIfNeeded({ state, effects, }) {
|
|
338
|
+
if (!state.sessionId || !effects.model || effects.model === 'default')
|
|
339
|
+
return false;
|
|
340
|
+
state.setModelRequestId = state.nextId;
|
|
341
|
+
state.expectedId = state.nextId;
|
|
342
|
+
const setModelMethod = state.modelConfigId ? 'session/set_config_option' : 'session/set_model';
|
|
343
|
+
const setModelParams = state.modelConfigId
|
|
344
|
+
? { sessionId: state.sessionId, configId: state.modelConfigId, value: effects.model }
|
|
345
|
+
: { sessionId: state.sessionId, modelId: effects.model };
|
|
346
|
+
effects.writeRpc(state.nextId, setModelMethod, setModelParams, setModelMethod);
|
|
347
|
+
state.nextId += 1;
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
/** Handles the `session/new` (or `session/load`) ack: records the session id, model config, and either triggers a model switch or sends the prompt. */
|
|
351
|
+
export function handleSessionNewAck({ state, effects, result, rawLine, }) {
|
|
352
|
+
state.sessionId = asOptionalString(result.sessionId);
|
|
353
|
+
// The durable handle for resuming this session on the next turn.
|
|
354
|
+
state.durableSessionId = asOptionalString(result.openCodeSessionId);
|
|
355
|
+
// session/new acknowledged with a session id = handshake done.
|
|
356
|
+
if (state.sessionId)
|
|
357
|
+
notifySessionInit(effects);
|
|
358
|
+
state.modelConfigId = resolveModelConfigId(findModelConfigOption(result.configOptions));
|
|
359
|
+
state.activeModel = currentModelFromSessionResult(result);
|
|
360
|
+
if (state.sessionId && state.activeModel) {
|
|
361
|
+
effects.send('agent', { type: 'status', label: 'model', model: state.activeModel });
|
|
362
|
+
}
|
|
363
|
+
if (triggerSetModelIfNeeded({ state, effects }))
|
|
364
|
+
return;
|
|
365
|
+
if (!state.sessionId) {
|
|
366
|
+
effects.fail(`invalid session/new response: ${rawLine}`);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
effects.sendPrompt();
|
|
370
|
+
}
|
|
371
|
+
/** Handles the `session/prompt` ack: fails a turn that reported model activity but produced no visible output, otherwise flushes and finishes cleanly. */
|
|
372
|
+
export function handlePromptResult({ state, effects, result, }) {
|
|
373
|
+
const usage = formatUsage(result.usage);
|
|
374
|
+
if (!state.emittedVisibleTextChunk && !state.emittedConcreteToolEvent && effects.modelUnavailableErrorCode) {
|
|
375
|
+
const outputTokens = usage?.output_tokens;
|
|
376
|
+
const hadCompletionTokens = typeof outputTokens === 'number' && outputTokens > 0;
|
|
377
|
+
if (hadCompletionTokens || state.emittedToolCall || state.emittedTextChunk) {
|
|
378
|
+
effects.fail('ACP session completed after reporting model activity, but did not produce visible assistant text, concrete tool results, or artifacts.', {
|
|
379
|
+
retryable: true,
|
|
380
|
+
details: {
|
|
381
|
+
kind: 'acp_no_visible_output',
|
|
382
|
+
output_tokens: outputTokens,
|
|
383
|
+
raw_tool_update_seen: state.emittedToolCall,
|
|
384
|
+
text_chunk_seen: state.emittedTextChunk,
|
|
385
|
+
},
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
effects.fail('ACP session completed without producing any assistant text. Refresh the AMR model list, choose a supported model, and retry this run.', { forceModelUnavailable: true });
|
|
390
|
+
}
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
effects.finishCleanPrompt(result.usage);
|
|
394
|
+
}
|
|
395
|
+
/** Whether an incoming result frame is the ack for an in-flight `session/set_model` (or `set_config_option`) request. */
|
|
396
|
+
export function isModelSetAckExpected({ state, effects, obj, }) {
|
|
397
|
+
return Boolean(state.sessionId && effects.model && effects.model !== 'default' && obj.id === state.expectedId);
|
|
398
|
+
}
|
|
399
|
+
/** Handles the `session/set_model` (or `set_config_option`) ack: records the confirmed model and sends the prompt. */
|
|
400
|
+
export function handleModelSetAck({ state, effects, result, }) {
|
|
401
|
+
// `isModelSetAckExpected` already confirmed `effects.model` is a truthy
|
|
402
|
+
// string before this runs; the assertion is only to carry that narrowing
|
|
403
|
+
// across the function boundary for the type checker (it doesn't survive
|
|
404
|
+
// the call), not a behavior change.
|
|
405
|
+
state.activeModel = currentModelFromSessionResult(result) ?? effects.model;
|
|
406
|
+
effects.send('agent', { type: 'status', label: 'model', model: state.activeModel });
|
|
407
|
+
effects.sendPrompt();
|
|
408
|
+
}
|
|
409
|
+
/** Top-level JSON-RPC result dispatcher, keyed by which request `expectedId`/`promptRequestId` is currently outstanding. */
|
|
410
|
+
export function routeResultById({ state, effects, obj, result, rawLine, }) {
|
|
411
|
+
if (obj.id !== state.expectedId || !result)
|
|
412
|
+
return;
|
|
413
|
+
if (state.expectedId === 1) {
|
|
414
|
+
handleInitializeAck({ state, effects });
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
if (state.expectedId === 2) {
|
|
418
|
+
handleSessionNewAck({ state, effects, result, rawLine });
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (state.promptRequestId !== null && obj.id === state.promptRequestId) {
|
|
422
|
+
handlePromptResult({ state, effects, result });
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
if (isModelSetAckExpected({ state, effects, obj })) {
|
|
426
|
+
handleModelSetAck({ state, effects, result });
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
function resolveAcpSessionOptionDefaults(options) {
|
|
430
|
+
return {
|
|
431
|
+
imagePaths: options.imagePaths ?? [],
|
|
432
|
+
envFormat: options.envFormat ?? 'array',
|
|
433
|
+
clientName: options.clientName ?? 'agent-runtime',
|
|
434
|
+
clientVersion: options.clientVersion ?? 'runtime-adapter',
|
|
435
|
+
stageTimeoutMs: options.stageTimeoutMs ?? DEFAULT_STAGE_TIMEOUT_MS,
|
|
436
|
+
executionProfile: options.executionProfile ?? 'filesystem',
|
|
437
|
+
accountFailureClassifier: options.accountFailureClassifier ?? noopAccountFailureClassifier,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
19
440
|
/**
|
|
20
441
|
* Attaches an ACP protocol session to an already-spawned child process and
|
|
21
442
|
* drives the full JSON-RPC conversation from handshake to prompt completion.
|
|
@@ -41,7 +462,9 @@ import { noopAccountFailureClassifier } from './account-failure.js';
|
|
|
41
462
|
* @returns A controller with `hasFatalError`, `getDurableSessionId`,
|
|
42
463
|
* `completedSuccessfully`, and `abort` methods.
|
|
43
464
|
*/
|
|
44
|
-
export function attachAcpSession(
|
|
465
|
+
export function attachAcpSession(options) {
|
|
466
|
+
const { child, prompt, cwd, model, mcpServers, send, modelUnavailableErrorCode, resumeSessionId, onPermissionRequest, onCliReady, onSessionInit, } = options;
|
|
467
|
+
const { imagePaths, envFormat, clientName, clientVersion, stageTimeoutMs, executionProfile, accountFailureClassifier, } = resolveAcpSessionOptionDefaults(options);
|
|
45
468
|
const runStartedAt = Date.now();
|
|
46
469
|
const effectiveCwd = path.resolve(cwd || process.cwd());
|
|
47
470
|
if (!child.stdin || !child.stdout) {
|
|
@@ -49,25 +472,8 @@ export function attachAcpSession({ child, prompt, cwd, model, imagePaths = [], m
|
|
|
49
472
|
}
|
|
50
473
|
const stdin = child.stdin;
|
|
51
474
|
const stdout = child.stdout;
|
|
52
|
-
|
|
53
|
-
let nextId = 2;
|
|
54
|
-
let promptRequestId = null;
|
|
55
|
-
let setModelRequestId = null;
|
|
56
|
-
let sessionId = null;
|
|
57
|
-
// The durable upstream session handle reported by the agent on session/new or
|
|
58
|
-
// session/load (e.g. a vendor bridge's own session id). The caller stores it
|
|
59
|
-
// per conversation to resume next turn. Distinct from `sessionId`, which is
|
|
60
|
-
// the ACP wrapper id.
|
|
61
|
-
let durableSessionId = null;
|
|
62
|
-
let activeModel = null;
|
|
63
|
-
let modelConfigId = null;
|
|
64
|
-
let emittedThinkingStart = false;
|
|
475
|
+
const state = createAcpSessionState();
|
|
65
476
|
let emittedFirstTokenStatus = false;
|
|
66
|
-
let emittedTextChunk = false;
|
|
67
|
-
let emittedVisibleTextChunk = false;
|
|
68
|
-
let emittedToolCall = false;
|
|
69
|
-
let emittedConcreteToolEvent = false;
|
|
70
|
-
let emittedTextBuffer = '';
|
|
71
477
|
let rawAcpShapeDiagnosticCount = 0;
|
|
72
478
|
let artifactSuppressionDiagnosticCount = 0;
|
|
73
479
|
let amrStderrRetryTail = '';
|
|
@@ -75,11 +481,6 @@ export function attachAcpSession({ child, prompt, cwd, model, imagePaths = [], m
|
|
|
75
481
|
let fatal = false;
|
|
76
482
|
let aborted = false;
|
|
77
483
|
let stageTimer = null;
|
|
78
|
-
let dsmlArtifactSuppressor = null;
|
|
79
|
-
let dsmlArtifactSuppressorLastSuppressedChars = 0;
|
|
80
|
-
let dsmlArtifactSuppressorToolCallId = null;
|
|
81
|
-
let dsmlArtifactSuppressorArmedAfterText = false;
|
|
82
|
-
let dsmlArtifactSuppressorSawIncrementalProse = false;
|
|
83
484
|
const pendingPermissionRequestIds = new Set();
|
|
84
485
|
const toolCallTextSuppressor = createToolCallTextSuppressor();
|
|
85
486
|
let toolCallTextSuppressorLastSuppressedChars = 0;
|
|
@@ -95,13 +496,6 @@ export function attachAcpSession({ child, prompt, cwd, model, imagePaths = [], m
|
|
|
95
496
|
openedBlocks: 0,
|
|
96
497
|
closedBlocks: 0,
|
|
97
498
|
};
|
|
98
|
-
const acpArtifactWriteToolCallIds = new Set();
|
|
99
|
-
// Per artifact-write tool call, accumulate the best concrete file path seen
|
|
100
|
-
// across its frames and whether we have already mirrored it into canonical
|
|
101
|
-
// tool_use/tool_result events. Emission is deferred to the terminal frame so
|
|
102
|
-
// a `locations`/`rawInput` path that ACP only sends on a later update is used
|
|
103
|
-
// for classification, instead of locking in a first-frame guess.
|
|
104
|
-
const acpArtifactRunEventState = new Map();
|
|
105
499
|
const stageWatchdogDisabled = stageTimeoutMs <= 0;
|
|
106
500
|
const resetStageTimer = (label) => {
|
|
107
501
|
if (stageTimer)
|
|
@@ -221,7 +615,7 @@ export function attachAcpSession({ child, prompt, cwd, model, imagePaths = [], m
|
|
|
221
615
|
// discipline (verified unreachable by tracing every call site — see
|
|
222
616
|
// source-map.md), not suppressed.
|
|
223
617
|
const emitVisibleTextDelta = (delta) => {
|
|
224
|
-
emittedVisibleTextChunk = true;
|
|
618
|
+
state.emittedVisibleTextChunk = true;
|
|
225
619
|
if (!emittedFirstTokenStatus) {
|
|
226
620
|
emittedFirstTokenStatus = true;
|
|
227
621
|
send('agent', {
|
|
@@ -233,13 +627,13 @@ export function attachAcpSession({ child, prompt, cwd, model, imagePaths = [], m
|
|
|
233
627
|
send('agent', { type: 'text_delta', delta });
|
|
234
628
|
};
|
|
235
629
|
const noteArtifactTextSuppression = (reason) => {
|
|
236
|
-
if (!dsmlArtifactSuppressor)
|
|
630
|
+
if (!state.dsmlArtifactSuppressor)
|
|
237
631
|
return;
|
|
238
|
-
const stats = dsmlArtifactSuppressor.stats();
|
|
239
|
-
const suppressedDelta = stats.suppressedChars - dsmlArtifactSuppressorLastSuppressedChars;
|
|
632
|
+
const stats = state.dsmlArtifactSuppressor.stats();
|
|
633
|
+
const suppressedDelta = stats.suppressedChars - state.dsmlArtifactSuppressorLastSuppressedChars;
|
|
240
634
|
if (suppressedDelta <= 0)
|
|
241
635
|
return;
|
|
242
|
-
dsmlArtifactSuppressorLastSuppressedChars = stats.suppressedChars;
|
|
636
|
+
state.dsmlArtifactSuppressorLastSuppressedChars = stats.suppressedChars;
|
|
243
637
|
artifactTextSuppressionSummary.suppressedChars += suppressedDelta;
|
|
244
638
|
artifactTextSuppressionSummary.suppressedChunks = stats.suppressedChunks;
|
|
245
639
|
artifactTextSuppressionSummary.openedBlocks = stats.openedBlocks;
|
|
@@ -323,10 +717,10 @@ export function attachAcpSession({ child, prompt, cwd, model, imagePaths = [], m
|
|
|
323
717
|
});
|
|
324
718
|
};
|
|
325
719
|
const sendPrompt = () => {
|
|
326
|
-
promptRequestId = nextId;
|
|
327
|
-
expectedId = promptRequestId;
|
|
328
|
-
writeRpc(promptRequestId, 'session/prompt', {
|
|
329
|
-
sessionId,
|
|
720
|
+
state.promptRequestId = state.nextId;
|
|
721
|
+
state.expectedId = state.promptRequestId;
|
|
722
|
+
writeRpc(state.promptRequestId, 'session/prompt', {
|
|
723
|
+
sessionId: state.sessionId,
|
|
330
724
|
prompt: buildPromptBlocks(prompt, imagePaths),
|
|
331
725
|
}, 'session/prompt');
|
|
332
726
|
send('agent', {
|
|
@@ -334,19 +728,21 @@ export function attachAcpSession({ child, prompt, cwd, model, imagePaths = [], m
|
|
|
334
728
|
label: 'waiting_for_first_output',
|
|
335
729
|
elapsedMs: Date.now() - runStartedAt,
|
|
336
730
|
});
|
|
337
|
-
nextId += 1;
|
|
731
|
+
state.nextId += 1;
|
|
338
732
|
};
|
|
339
|
-
// `finishCleanPrompt` has exactly one call site
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
// removed per this package's
|
|
345
|
-
// source-map.md.
|
|
733
|
+
// `finishCleanPrompt` has exactly one call site (`handlePromptResult`,
|
|
734
|
+
// called only from `routeResultById`'s promptRequestId-match branch),
|
|
735
|
+
// which is itself only ever reached from the parser callback's own `if
|
|
736
|
+
// (aborted || finished) return;` guard at entry — so this function can
|
|
737
|
+
// never actually be invoked once `finished` is true. The origin file
|
|
738
|
+
// carried its own re-entry guard here anyway; removed per this package's
|
|
739
|
+
// coverage-driven dead-branch discipline. See source-map.md.
|
|
346
740
|
const finishCleanPrompt = (usageSource) => {
|
|
347
741
|
const flushedToolText = toolCallTextSuppressor.flush();
|
|
348
742
|
noteToolCallTextSuppression('tool_call_xml_flush');
|
|
349
|
-
const flushedText = flushedToolText
|
|
743
|
+
const flushedText = flushedToolText
|
|
744
|
+
? (state.dsmlArtifactSuppressor?.strip(flushedToolText) ?? flushedToolText)
|
|
745
|
+
: '';
|
|
350
746
|
if (flushedText) {
|
|
351
747
|
emitVisibleTextDelta(flushedText);
|
|
352
748
|
}
|
|
@@ -429,11 +825,34 @@ export function attachAcpSession({ child, prompt, cwd, model, imagePaths = [], m
|
|
|
429
825
|
}
|
|
430
826
|
};
|
|
431
827
|
const recoverFromModelSelectionError = () => {
|
|
432
|
-
setModelRequestId = null;
|
|
433
|
-
activeModel = activeModel || 'default';
|
|
434
|
-
send('agent', { type: 'status', label: 'model', model: activeModel });
|
|
828
|
+
state.setModelRequestId = null;
|
|
829
|
+
state.activeModel = state.activeModel || 'default';
|
|
830
|
+
send('agent', { type: 'status', label: 'model', model: state.activeModel });
|
|
435
831
|
sendPrompt();
|
|
436
832
|
};
|
|
833
|
+
const effects = {
|
|
834
|
+
send,
|
|
835
|
+
fail,
|
|
836
|
+
failWithPayload,
|
|
837
|
+
writeRpc,
|
|
838
|
+
sendPrompt,
|
|
839
|
+
recoverFromModelSelectionError,
|
|
840
|
+
finishCleanPrompt,
|
|
841
|
+
emitVisibleTextDelta,
|
|
842
|
+
noteArtifactTextSuppression,
|
|
843
|
+
noteToolCallTextSuppression,
|
|
844
|
+
emitAcpRawShapeDiagnostic,
|
|
845
|
+
toolCallTextSuppressor,
|
|
846
|
+
runStartedAt,
|
|
847
|
+
modelUnavailableErrorCode,
|
|
848
|
+
accountFailureClassifier,
|
|
849
|
+
model,
|
|
850
|
+
onSessionInit,
|
|
851
|
+
resumeSessionId,
|
|
852
|
+
effectiveCwd,
|
|
853
|
+
mcpServers,
|
|
854
|
+
envFormat,
|
|
855
|
+
};
|
|
437
856
|
const parser = createJsonLineStream((raw, rawLine) => {
|
|
438
857
|
if (aborted || finished)
|
|
439
858
|
return;
|
|
@@ -449,39 +868,7 @@ export function attachAcpSession({ child, prompt, cwd, model, imagePaths = [], m
|
|
|
449
868
|
const result = asObject(obj.result);
|
|
450
869
|
const rpcErr = rpcErrorMessage(obj);
|
|
451
870
|
if (rpcErr) {
|
|
452
|
-
|
|
453
|
-
// finished (pipe-broken, cleanup race conditions, etc.) would be safe
|
|
454
|
-
// to ignore here — but this whole callback is already gated by `if
|
|
455
|
-
// (aborted || finished) return;` at its own top (line 499), and
|
|
456
|
-
// nothing between there and here can flip `finished` to true
|
|
457
|
-
// mid-invocation, so a second, inner check of the same condition can
|
|
458
|
-
// never actually fire. Removed per this package's coverage-driven
|
|
459
|
-
// dead-branch discipline (verified unreachable by tracing the
|
|
460
|
-
// callback's own control flow — see source-map.md), not suppressed.
|
|
461
|
-
// JSON-RPC error handling:
|
|
462
|
-
// -32603 unexpected-id errors are cleanup noise. Expected-id model
|
|
463
|
-
// selection failures are recoverable; all other RPC errors are real
|
|
464
|
-
// protocol failures for initialize/session/new/session/prompt.
|
|
465
|
-
if (obj.id === setModelRequestId &&
|
|
466
|
-
modelSelectionErrorIsRecoverable(error?.code) &&
|
|
467
|
-
promptRequestId === null) {
|
|
468
|
-
recoverFromModelSelectionError();
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
|
-
if (error?.code === -32603 && obj.id !== expectedId) {
|
|
472
|
-
return;
|
|
473
|
-
}
|
|
474
|
-
const details = rpcErrorData(obj);
|
|
475
|
-
const promotedPayload = promotedOpenCodeSessionErrorPayload(details, rpcErr);
|
|
476
|
-
if (promotedPayload) {
|
|
477
|
-
failWithPayload(promotedPayload);
|
|
478
|
-
return;
|
|
479
|
-
}
|
|
480
|
-
const retryable = rpcErrorRetryable(details);
|
|
481
|
-
fail(rpcErr, {
|
|
482
|
-
details,
|
|
483
|
-
...(retryable === undefined ? {} : { retryable }),
|
|
484
|
-
});
|
|
871
|
+
handleRpcError({ state, effects, obj, error, rpcErr });
|
|
485
872
|
return;
|
|
486
873
|
}
|
|
487
874
|
if (obj.method === 'session/request_permission') {
|
|
@@ -490,248 +877,10 @@ export function attachAcpSession({ child, prompt, cwd, model, imagePaths = [], m
|
|
|
490
877
|
}
|
|
491
878
|
const update = asObject(params?.update);
|
|
492
879
|
if (obj.method === 'session/update' && update) {
|
|
493
|
-
|
|
494
|
-
const promotedPayload = promotedAmrRetryStatusPayload(update, accountFailureClassifier);
|
|
495
|
-
if (promotedPayload) {
|
|
496
|
-
failWithPayload(promotedPayload);
|
|
497
|
-
return;
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
if (update.sessionUpdate !== 'agent_message_chunk' && update.sessionUpdate !== 'agent_thought_chunk') {
|
|
501
|
-
send('agent', {
|
|
502
|
-
type: 'status',
|
|
503
|
-
label: String(update.sessionUpdate || 'session_update'),
|
|
504
|
-
elapsedMs: Date.now() - runStartedAt,
|
|
505
|
-
});
|
|
506
|
-
emitAcpRawShapeDiagnostic(update);
|
|
507
|
-
}
|
|
508
|
-
if (update.sessionUpdate === 'agent_thought_chunk') {
|
|
509
|
-
emitAcpRawShapeDiagnostic(update);
|
|
510
|
-
const text = extractAcpUpdateText(update);
|
|
511
|
-
if (text) {
|
|
512
|
-
if (!emittedThinkingStart) {
|
|
513
|
-
emittedThinkingStart = true;
|
|
514
|
-
send('agent', { type: 'thinking_start' });
|
|
515
|
-
}
|
|
516
|
-
send('agent', { type: 'thinking_delta', delta: text });
|
|
517
|
-
}
|
|
518
|
-
return;
|
|
519
|
-
}
|
|
520
|
-
if (update.sessionUpdate === 'agent_message_chunk') {
|
|
521
|
-
emitAcpRawShapeDiagnostic(update);
|
|
522
|
-
const text = extractAcpUpdateText(update);
|
|
523
|
-
if (text) {
|
|
524
|
-
const isCumulativeSnapshot = text.startsWith(emittedTextBuffer);
|
|
525
|
-
const delta = isCumulativeSnapshot
|
|
526
|
-
? text.slice(emittedTextBuffer.length)
|
|
527
|
-
: text;
|
|
528
|
-
if (delta.length > 0) {
|
|
529
|
-
emittedTextChunk = true;
|
|
530
|
-
emittedTextBuffer += delta;
|
|
531
|
-
const wasSuppressingToolCall = toolCallTextSuppressor.isSuppressing();
|
|
532
|
-
const toolCallStrippedDelta = toolCallTextSuppressor.strip(delta);
|
|
533
|
-
noteToolCallTextSuppression(wasSuppressingToolCall || toolCallStrippedDelta !== delta
|
|
534
|
-
? 'tool_call_xml'
|
|
535
|
-
: 'tool_call_candidate');
|
|
536
|
-
if (!toolCallStrippedDelta) {
|
|
537
|
-
return;
|
|
538
|
-
}
|
|
539
|
-
if (dsmlArtifactSuppressor) {
|
|
540
|
-
const wasSuppressingArtifact = dsmlArtifactSuppressor.isSuppressing();
|
|
541
|
-
const hadPendingArtifactCandidate = dsmlArtifactSuppressor.hasPendingCandidate();
|
|
542
|
-
const strippedDelta = dsmlArtifactSuppressor.strip(toolCallStrippedDelta);
|
|
543
|
-
noteArtifactTextSuppression(wasSuppressingArtifact || strippedDelta !== toolCallStrippedDelta
|
|
544
|
-
? 'artifact_echo'
|
|
545
|
-
: 'artifact_candidate');
|
|
546
|
-
const hasOpenArtifactCandidate = dsmlArtifactSuppressor.isSuppressing() || dsmlArtifactSuppressor.hasPendingCandidate();
|
|
547
|
-
const consumedArtifactText = wasSuppressingArtifact || strippedDelta !== delta;
|
|
548
|
-
const shouldPreserveIncrementalProse = !isCumulativeSnapshot &&
|
|
549
|
-
!wasSuppressingArtifact &&
|
|
550
|
-
!hadPendingArtifactCandidate &&
|
|
551
|
-
!hasOpenArtifactCandidate &&
|
|
552
|
-
(strippedDelta === toolCallStrippedDelta ||
|
|
553
|
-
(!dsmlArtifactSuppressorArmedAfterText &&
|
|
554
|
-
dsmlArtifactSuppressorSawIncrementalProse &&
|
|
555
|
-
!ACP_ARTIFACT_ECHO_START_RE.test(toolCallStrippedDelta)));
|
|
556
|
-
const outputDelta = shouldPreserveIncrementalProse ? toolCallStrippedDelta : strippedDelta;
|
|
557
|
-
if (outputDelta) {
|
|
558
|
-
emitVisibleTextDelta(outputDelta);
|
|
559
|
-
}
|
|
560
|
-
if (strippedDelta === toolCallStrippedDelta &&
|
|
561
|
-
!wasSuppressingArtifact &&
|
|
562
|
-
!hadPendingArtifactCandidate &&
|
|
563
|
-
!hasOpenArtifactCandidate) {
|
|
564
|
-
dsmlArtifactSuppressorSawIncrementalProse = true;
|
|
565
|
-
}
|
|
566
|
-
if (consumedArtifactText && !hasOpenArtifactCandidate) {
|
|
567
|
-
dsmlArtifactSuppressor = null;
|
|
568
|
-
dsmlArtifactSuppressorToolCallId = null;
|
|
569
|
-
dsmlArtifactSuppressorArmedAfterText = false;
|
|
570
|
-
dsmlArtifactSuppressorSawIncrementalProse = false;
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
else {
|
|
574
|
-
emitVisibleTextDelta(toolCallStrippedDelta);
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
return;
|
|
579
|
-
}
|
|
580
|
-
if (update.sessionUpdate === 'tool_call' ||
|
|
581
|
-
update.sessionUpdate === 'tool_call_update') {
|
|
582
|
-
// The turn did real work (a tool call / file edit), which is valid output even
|
|
583
|
-
// when the model emits no closing assistant text. Track it so the prompt-complete
|
|
584
|
-
// handler does not misreport such a turn as "no output / model unavailable".
|
|
585
|
-
emittedToolCall = true;
|
|
586
|
-
const toolCallId = acpToolCallId(update);
|
|
587
|
-
if (toolCallId && isAcpArtifactWriteLabel(update)) {
|
|
588
|
-
acpArtifactWriteToolCallIds.add(toolCallId);
|
|
589
|
-
}
|
|
590
|
-
// Mirror artifact-write tool calls into the daemon's canonical
|
|
591
|
-
// tool_use/tool_result event shape so a run-artifacts scanner can see
|
|
592
|
-
// ACP file writes. Without this, an ACP agent that emits only
|
|
593
|
-
// text/status/thinking events (never a tool_use/tool_result pair)
|
|
594
|
-
// would report zero artifacts even when the run wrote artifacts.
|
|
595
|
-
//
|
|
596
|
-
// This path only feeds the NO-PROJECT fallback (project runs use the
|
|
597
|
-
// filesystem snapshot). Two correctness rules, both learned the hard
|
|
598
|
-
// way in review:
|
|
599
|
-
// 1. Defer emission to the TERMINAL frame and accumulate the best
|
|
600
|
-
// concrete path across frames — ACP often sends `locations` only
|
|
601
|
-
// on the completing update, and emitting on the first frame would
|
|
602
|
-
// lock in a wrong/empty guess that a later path can't correct.
|
|
603
|
-
// 2. Never fabricate an artifact extension. `isArtifactPath` is what
|
|
604
|
-
// decides whether a write counts; feeding it a real path lets it
|
|
605
|
-
// correctly EXCLUDE non-artifact edits (`config.json`, `README.md`)
|
|
606
|
-
// and INCLUDE real artifacts. A write that never carries a concrete
|
|
607
|
-
// path stays keyed on its (extension-less) toolCallId, so it is
|
|
608
|
-
// simply not counted rather than inflating the metric with a
|
|
609
|
-
// synthetic `.html` — under-counting a truly opaque write is
|
|
610
|
-
// acceptable; a false-positive artifact is not.
|
|
611
|
-
if (toolCallId) {
|
|
612
|
-
const isWriteCall = isAcpArtifactWriteLabel(update) || acpArtifactWriteToolCallIds.has(toolCallId);
|
|
613
|
-
if (isWriteCall) {
|
|
614
|
-
let st = acpArtifactRunEventState.get(toolCallId);
|
|
615
|
-
if (!st) {
|
|
616
|
-
st = { path: null, emitted: false };
|
|
617
|
-
acpArtifactRunEventState.set(toolCallId, st);
|
|
618
|
-
}
|
|
619
|
-
if (!st.path)
|
|
620
|
-
st.path = acpArtifactWritePath(update);
|
|
621
|
-
const failed = isAcpTerminalFailureStatus(update);
|
|
622
|
-
if (!st.emitted && (failed || isAcpCompletedStatus(update))) {
|
|
623
|
-
st.emitted = true;
|
|
624
|
-
send('agent', {
|
|
625
|
-
type: 'tool_use',
|
|
626
|
-
id: toolCallId,
|
|
627
|
-
name: 'Write',
|
|
628
|
-
input: { file_path: st.path ?? toolCallId },
|
|
629
|
-
});
|
|
630
|
-
send('agent', { type: 'tool_result', toolUseId: toolCallId, isError: failed });
|
|
631
|
-
emittedConcreteToolEvent = true;
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
if (isAcpArtifactWriteUpdate(update, acpArtifactWriteToolCallIds)) {
|
|
636
|
-
dsmlArtifactSuppressor = createDsmlArtifactTextSuppressor();
|
|
637
|
-
dsmlArtifactSuppressorLastSuppressedChars = 0;
|
|
638
|
-
dsmlArtifactSuppressorToolCallId = toolCallId;
|
|
639
|
-
dsmlArtifactSuppressorArmedAfterText = emittedTextBuffer.length > 0;
|
|
640
|
-
dsmlArtifactSuppressorSawIncrementalProse = false;
|
|
641
|
-
if (toolCallId)
|
|
642
|
-
acpArtifactWriteToolCallIds.delete(toolCallId);
|
|
643
|
-
}
|
|
644
|
-
else if (toolCallId && isAcpTerminalFailureStatus(update)) {
|
|
645
|
-
const ownsPendingWriteSuppression = toolCallId === dsmlArtifactSuppressorToolCallId;
|
|
646
|
-
const ownsPendingWriteCall = acpArtifactWriteToolCallIds.has(toolCallId);
|
|
647
|
-
acpArtifactWriteToolCallIds.delete(toolCallId);
|
|
648
|
-
if (ownsPendingWriteSuppression || ownsPendingWriteCall) {
|
|
649
|
-
dsmlArtifactSuppressor = null;
|
|
650
|
-
dsmlArtifactSuppressorToolCallId = null;
|
|
651
|
-
dsmlArtifactSuppressorArmedAfterText = false;
|
|
652
|
-
dsmlArtifactSuppressorSawIncrementalProse = false;
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
return;
|
|
656
|
-
}
|
|
657
|
-
return;
|
|
658
|
-
}
|
|
659
|
-
if (obj.id !== expectedId || !result) {
|
|
880
|
+
handleSessionUpdate({ state, effects, update });
|
|
660
881
|
return;
|
|
661
882
|
}
|
|
662
|
-
|
|
663
|
-
expectedId = nextId;
|
|
664
|
-
if (resumeSessionId) {
|
|
665
|
-
// Resume the prior upstream session instead of creating a fresh one.
|
|
666
|
-
writeRpc(nextId, 'session/load', { sessionId: resumeSessionId, cwd: effectiveCwd }, 'session/load');
|
|
667
|
-
}
|
|
668
|
-
else {
|
|
669
|
-
writeRpc(nextId, 'session/new', buildAcpSessionNewParams(effectiveCwd, mcpServers ? { mcpServers, envFormat } : { envFormat }), 'session/new');
|
|
670
|
-
}
|
|
671
|
-
nextId += 1;
|
|
672
|
-
return;
|
|
673
|
-
}
|
|
674
|
-
if (expectedId === 2) {
|
|
675
|
-
sessionId = typeof result.sessionId === 'string' ? result.sessionId : null;
|
|
676
|
-
// The durable handle for resuming this session on the next turn.
|
|
677
|
-
durableSessionId =
|
|
678
|
-
typeof result.openCodeSessionId === 'string' ? result.openCodeSessionId : null;
|
|
679
|
-
// session/new acknowledged with a session id = handshake done.
|
|
680
|
-
if (sessionId)
|
|
681
|
-
onSessionInit?.();
|
|
682
|
-
const modelConfig = findModelConfigOption(result.configOptions);
|
|
683
|
-
modelConfigId = modelConfig?.configId ?? null;
|
|
684
|
-
activeModel = currentModelFromSessionResult(result);
|
|
685
|
-
if (sessionId && activeModel) {
|
|
686
|
-
send('agent', { type: 'status', label: 'model', model: activeModel });
|
|
687
|
-
}
|
|
688
|
-
if (sessionId && model && model !== 'default') {
|
|
689
|
-
setModelRequestId = nextId;
|
|
690
|
-
expectedId = nextId;
|
|
691
|
-
const setModelMethod = modelConfigId ? 'session/set_config_option' : 'session/set_model';
|
|
692
|
-
const setModelParams = modelConfigId
|
|
693
|
-
? { sessionId, configId: modelConfigId, value: model }
|
|
694
|
-
: { sessionId, modelId: model };
|
|
695
|
-
writeRpc(nextId, setModelMethod, setModelParams, setModelMethod);
|
|
696
|
-
nextId += 1;
|
|
697
|
-
return;
|
|
698
|
-
}
|
|
699
|
-
if (!sessionId) {
|
|
700
|
-
fail(`invalid session/new response: ${rawLine}`);
|
|
701
|
-
return;
|
|
702
|
-
}
|
|
703
|
-
sendPrompt();
|
|
704
|
-
return;
|
|
705
|
-
}
|
|
706
|
-
if (promptRequestId !== null && obj.id === promptRequestId) {
|
|
707
|
-
const usage = formatUsage(result.usage);
|
|
708
|
-
if (!emittedVisibleTextChunk && !emittedConcreteToolEvent && modelUnavailableErrorCode) {
|
|
709
|
-
const outputTokens = usage?.output_tokens;
|
|
710
|
-
const hadCompletionTokens = typeof outputTokens === 'number' && outputTokens > 0;
|
|
711
|
-
if (hadCompletionTokens || emittedToolCall || emittedTextChunk) {
|
|
712
|
-
fail('ACP session completed after reporting model activity, but did not produce visible assistant text, concrete tool results, or artifacts.', {
|
|
713
|
-
retryable: true,
|
|
714
|
-
details: {
|
|
715
|
-
kind: 'acp_no_visible_output',
|
|
716
|
-
output_tokens: outputTokens,
|
|
717
|
-
raw_tool_update_seen: emittedToolCall,
|
|
718
|
-
text_chunk_seen: emittedTextChunk,
|
|
719
|
-
},
|
|
720
|
-
});
|
|
721
|
-
}
|
|
722
|
-
else {
|
|
723
|
-
fail('ACP session completed without producing any assistant text. Refresh the AMR model list, choose a supported model, and retry this run.', { forceModelUnavailable: true });
|
|
724
|
-
}
|
|
725
|
-
return;
|
|
726
|
-
}
|
|
727
|
-
finishCleanPrompt(result.usage);
|
|
728
|
-
return;
|
|
729
|
-
}
|
|
730
|
-
if (sessionId && model && model !== 'default' && obj.id === expectedId) {
|
|
731
|
-
activeModel = currentModelFromSessionResult(result) ?? model;
|
|
732
|
-
send('agent', { type: 'status', label: 'model', model: activeModel });
|
|
733
|
-
sendPrompt();
|
|
734
|
-
}
|
|
883
|
+
routeResultById({ state, effects, obj, result, rawLine });
|
|
735
884
|
});
|
|
736
885
|
stdout.on('data', (chunk) => parser.feed(chunk));
|
|
737
886
|
child.stderr?.setEncoding('utf8');
|
|
@@ -757,6 +906,34 @@ export function attachAcpSession({ child, prompt, cwd, model, imagePaths = [], m
|
|
|
757
906
|
clientCapabilities: { terminal: false },
|
|
758
907
|
clientInfo: { name: clientName, version: clientVersion },
|
|
759
908
|
}, 'initialize');
|
|
909
|
+
// ACP requires pending permission requests to receive a terminal
|
|
910
|
+
// `cancelled` response when the prompt turn is cancelled. Called before
|
|
911
|
+
// closing stdin, which may otherwise make the response impossible to
|
|
912
|
+
// deliver.
|
|
913
|
+
const cancelPendingPermissionRequests = () => {
|
|
914
|
+
for (const requestId of pendingPermissionRequestIds) {
|
|
915
|
+
try {
|
|
916
|
+
sendRpcResult(stdin, requestId, { outcome: { outcome: 'cancelled' } });
|
|
917
|
+
}
|
|
918
|
+
catch {
|
|
919
|
+
// Closing stdin/process signalling below remains the fallback.
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
pendingPermissionRequestIds.clear();
|
|
923
|
+
};
|
|
924
|
+
// Only cancel an established session; before session/new resolves there is
|
|
925
|
+
// no sessionId to cancel, but the caller still closes stdin below.
|
|
926
|
+
const sendSessionCancelIfEstablished = (activeStdin) => {
|
|
927
|
+
if (!state.sessionId)
|
|
928
|
+
return;
|
|
929
|
+
try {
|
|
930
|
+
sendRpc(activeStdin, state.nextId, 'session/cancel', { sessionId: state.sessionId });
|
|
931
|
+
state.nextId += 1;
|
|
932
|
+
}
|
|
933
|
+
catch {
|
|
934
|
+
// The caller owns process-signal fallback if the ACP transport is gone.
|
|
935
|
+
}
|
|
936
|
+
};
|
|
760
937
|
return {
|
|
761
938
|
/** Returns `true` when the session ended with a fatal protocol or transport error, allowing the caller to surface the failure. */
|
|
762
939
|
hasFatalError() {
|
|
@@ -767,7 +944,7 @@ export function attachAcpSession({ child, prompt, cwd, model, imagePaths = [], m
|
|
|
767
944
|
// session). Mirrors pi-rpc's getLastSessionPath().
|
|
768
945
|
/** Returns the durable upstream session id (e.g. a vendor bridge's own session id) to persist for next-turn resume, or `null` when the agent did not report one. */
|
|
769
946
|
getDurableSessionId() {
|
|
770
|
-
return durableSessionId;
|
|
947
|
+
return state.durableSessionId;
|
|
771
948
|
},
|
|
772
949
|
/** Returns `true` when the prompt request resolved cleanly without a fatal error and without an abort, even if the child process later exited via SIGTERM. */
|
|
773
950
|
completedSuccessfully() {
|
|
@@ -789,32 +966,10 @@ export function attachAcpSession({ child, prompt, cwd, model, imagePaths = [], m
|
|
|
789
966
|
aborted = true;
|
|
790
967
|
finished = true;
|
|
791
968
|
clearStageTimer();
|
|
792
|
-
|
|
793
|
-
// `cancelled` response when the prompt turn is cancelled. Do this
|
|
794
|
-
// before closing stdin, which may otherwise make the response
|
|
795
|
-
// impossible to deliver.
|
|
796
|
-
for (const requestId of pendingPermissionRequestIds) {
|
|
797
|
-
try {
|
|
798
|
-
sendRpcResult(stdin, requestId, { outcome: { outcome: 'cancelled' } });
|
|
799
|
-
}
|
|
800
|
-
catch {
|
|
801
|
-
// Closing stdin/process signalling below remains the fallback.
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
pendingPermissionRequestIds.clear();
|
|
969
|
+
cancelPendingPermissionRequests();
|
|
805
970
|
if (!child.stdin || child.stdin.destroyed || child.stdin.writableEnded)
|
|
806
971
|
return;
|
|
807
|
-
|
|
808
|
-
// is no sessionId to cancel, but we must still close stdin below.
|
|
809
|
-
if (sessionId) {
|
|
810
|
-
try {
|
|
811
|
-
sendRpc(child.stdin, nextId, 'session/cancel', { sessionId });
|
|
812
|
-
nextId += 1;
|
|
813
|
-
}
|
|
814
|
-
catch {
|
|
815
|
-
// The caller owns process-signal fallback if the ACP transport is gone.
|
|
816
|
-
}
|
|
817
|
-
}
|
|
972
|
+
sendSessionCancelIfEstablished(child.stdin);
|
|
818
973
|
// Always close stdin so the agent receives EOF and shuts down its own
|
|
819
974
|
// runtime — some ACP bridges tear down a private backing server on EOF —
|
|
820
975
|
// instead of lingering (and leaking that server) until the caller's
|