@gajae-code/coding-agent 0.12.0 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +82 -1
- package/dist/types/async/job-manager.d.ts +17 -3
- package/dist/types/config/settings-schema.d.ts +54 -0
- package/dist/types/exa/mcp-client.d.ts +10 -1
- package/dist/types/exec/bash-executor.d.ts +45 -12
- package/dist/types/extensibility/extensions/types.d.ts +16 -2
- package/dist/types/extensibility/gjc-plugins/index.d.ts +12 -2
- package/dist/types/extensibility/gjc-plugins/installer.d.ts +63 -7
- package/dist/types/extensibility/gjc-plugins/lifecycle-reconciliation.d.ts +55 -0
- package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +64 -0
- package/dist/types/extensibility/gjc-plugins/runtime-quarantine.d.ts +67 -0
- package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +3 -1
- package/dist/types/extensibility/gjc-plugins/types.d.ts +124 -0
- package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +18 -0
- package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
- package/dist/types/gjc-runtime/team-runtime.d.ts +25 -0
- package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +43 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +10 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +31 -80
- package/dist/types/modes/acp/acp-agent.d.ts +2 -19
- package/dist/types/modes/components/gjc-bundle-settings.d.ts +34 -0
- package/dist/types/modes/components/notifications-settings-editor.d.ts +1 -0
- package/dist/types/modes/components/session-observer-overlay.d.ts +18 -0
- package/dist/types/modes/components/settings-selector.d.ts +9 -0
- package/dist/types/modes/components/status-line/gh.d.ts +5 -0
- package/dist/types/modes/components/transcript-viewer-overlay.d.ts +18 -1
- package/dist/types/reminders/star-reminder.d.ts +3 -13
- package/dist/types/runtime-mcp/manager.d.ts +1 -0
- package/dist/types/sdk/acp/adapter.d.ts +3 -1
- package/dist/types/sdk/broker/transport.d.ts +2 -0
- package/dist/types/sdk/bus/attachment-registry.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +4 -1
- package/dist/types/sdk/bus/config.d.ts +6 -2
- package/dist/types/sdk/bus/index.d.ts +29 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +8 -1
- package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
- package/dist/types/sdk/bus/reconciliation-store.d.ts +7 -3
- package/dist/types/sdk/bus/rich-draft.d.ts +1 -0
- package/dist/types/sdk/bus/rich-render.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +165 -0
- package/dist/types/sdk/bus/telegram-cli.d.ts +12 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +11 -4
- package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -1
- package/dist/types/sdk/bus/telegram-reference.d.ts +11 -1
- package/dist/types/sdk/bus/threaded-render.d.ts +3 -2
- package/dist/types/sdk/bus/topic-registry.d.ts +11 -1
- package/dist/types/sdk/host/query/revision-store.d.ts +2 -0
- package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
- package/dist/types/sdk/prompt-status.d.ts +23 -3
- package/dist/types/sdk/session.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +38 -2
- package/dist/types/session/artifacts.d.ts +8 -0
- package/dist/types/session/blob-store.d.ts +57 -4
- package/dist/types/session/internal/managed-session-storage.d.ts +8 -1
- package/dist/types/session/session-manager.d.ts +23 -6
- package/dist/types/session/session-storage.d.ts +4 -0
- package/dist/types/session/streaming-output.d.ts +36 -7
- package/dist/types/setup/credential-import.d.ts +12 -0
- package/dist/types/skill-state/active-state.d.ts +12 -0
- package/dist/types/skill-state/workflow-hud.d.ts +6 -0
- package/dist/types/task/executor.d.ts +3 -2
- package/dist/types/task/index.d.ts +3 -1
- package/dist/types/task/provider-retry-status.d.ts +8 -0
- package/dist/types/task/receipt.d.ts +7 -1
- package/dist/types/task/types.d.ts +23 -0
- package/dist/types/tools/ask-answer-registry.d.ts +2 -2
- package/dist/types/tools/bash-interactive.d.ts +4 -1
- package/dist/types/tools/bash.d.ts +3 -1
- package/dist/types/tools/image-gen.d.ts +4 -0
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/tools/output-meta.d.ts +23 -2
- package/dist/types/tools/path-utils.d.ts +1 -0
- package/dist/types/tools/read.d.ts +0 -6
- package/dist/types/tools/review.d.ts +5 -7
- package/dist/types/tools/subagent.d.ts +2 -0
- package/dist/types/utils/gh.d.ts +13 -0
- package/dist/types/utils/image-resize.d.ts +1 -0
- package/dist/types/web/search/providers/kimi.d.ts +2 -0
- package/dist/types/web/search/providers/searxng.d.ts +7 -0
- package/dist/types/web/search/providers/xai.d.ts +2 -0
- package/package.json +10 -8
- package/scripts/acp-conformance-agent.ts +218 -0
- package/scripts/capture-gjc-bundle-settings.ts +306 -0
- package/scripts/generate-sdk-operation-inventory.ts +3 -0
- package/scripts/resident-memory-bench.ts +1341 -0
- package/scripts/run-acp-conformance.ts +285 -0
- package/src/async/job-manager.ts +78 -20
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/plugin-cli.ts +176 -18
- package/src/commands/team.ts +19 -1
- package/src/config/file-lock.ts +11 -2
- package/src/config/settings-schema.ts +51 -0
- package/src/coordinator-mcp/question-gate-codec.ts +1 -1
- package/src/coordinator-mcp/server.ts +5 -12
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -0
- package/src/defaults/gjc/skills/ralplan/SKILL.md +19 -16
- package/src/defaults/gjc/skills/team/SKILL.md +2 -0
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +37 -24
- package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +12 -0
- package/src/defaults/gjc-defaults.ts +4 -4
- package/src/exa/mcp-client.ts +12 -3
- package/src/exec/bash-executor.ts +144 -27
- package/src/extensibility/extensions/runner.ts +15 -0
- package/src/extensibility/extensions/types.ts +18 -1
- package/src/extensibility/gjc-plugins/constrained-hooks.ts +8 -2
- package/src/extensibility/gjc-plugins/index.ts +19 -2
- package/src/extensibility/gjc-plugins/installer.ts +189 -50
- package/src/extensibility/gjc-plugins/lifecycle-reconciliation.ts +199 -0
- package/src/extensibility/gjc-plugins/lifecycle.ts +626 -0
- package/src/extensibility/gjc-plugins/observability.ts +3 -3
- package/src/extensibility/gjc-plugins/prompt-appendix.ts +5 -1
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +12 -2
- package/src/extensibility/gjc-plugins/runtime-quarantine.ts +121 -0
- package/src/extensibility/gjc-plugins/schema.ts +84 -12
- package/src/extensibility/gjc-plugins/session-validation.ts +9 -3
- package/src/extensibility/gjc-plugins/types.ts +141 -0
- package/src/extensibility/gjc-plugins/validation.ts +9 -0
- package/src/gjc-runtime/launch-tmux.ts +25 -6
- package/src/gjc-runtime/managed-owner-admission.ts +1 -1
- package/src/gjc-runtime/ralplan-runtime.ts +411 -17
- package/src/gjc-runtime/team-launch.ts +8 -0
- package/src/gjc-runtime/team-runtime.ts +140 -32
- package/src/gjc-runtime/team-worker-heartbeat.ts +149 -0
- package/src/gjc-runtime/tmux-owner-isolation.ts +10 -0
- package/src/gjc-runtime/tmux-sessions.ts +58 -13
- package/src/gjc-runtime/ultragoal-runtime.ts +401 -611
- package/src/gjc-runtime/workflow-manifest.generated.json +8 -99
- package/src/gjc-runtime/workflow-manifest.ts +4 -68
- package/src/internal-urls/docs-index.generated.ts +15 -15
- package/src/modes/DESIGN.md +51 -0
- package/src/modes/acp/acp-agent.ts +298 -186
- package/src/modes/components/gjc-bundle-settings.ts +508 -0
- package/src/modes/components/notifications-settings-editor.ts +12 -1
- package/src/modes/components/oauth-selector.ts +68 -30
- package/src/modes/components/session-observer-overlay.ts +305 -86
- package/src/modes/components/settings-selector.ts +56 -5
- package/src/modes/components/status-line/gh.ts +16 -0
- package/src/modes/components/tool-status-header.ts +3 -14
- package/src/modes/components/transcript-viewer-overlay.ts +99 -24
- package/src/modes/controllers/command-controller.ts +4 -0
- package/src/modes/controllers/extension-ui-controller.ts +7 -0
- package/src/modes/controllers/selector-controller.ts +45 -0
- package/src/modes/runtime-init.ts +3 -0
- package/src/prompts/agent-fragments/ralplan-persistence.md +4 -3
- package/src/prompts/system/eager-todo.md +8 -4
- package/src/prompts/system/system-prompt.md +11 -4
- package/src/prompts/tools/bash.md +5 -5
- package/src/prompts/tools/search-tool-bm25.md +5 -0
- package/src/prompts/tools/task.md +1 -1
- package/src/reminders/star-reminder.ts +4 -42
- package/src/runtime-mcp/manager.ts +19 -4
- package/src/sdk/acp/adapter.ts +22 -6
- package/src/sdk/broker/transport.ts +35 -3
- package/src/sdk/bus/attachment-registry.ts +5 -1
- package/src/sdk/bus/chat-daemon-cli.ts +4 -39
- package/src/sdk/bus/chat-daemon-control.ts +6 -3
- package/src/sdk/bus/config.ts +18 -2
- package/src/sdk/bus/index.ts +661 -126
- package/src/sdk/bus/kind-aware-reconciliation.ts +148 -73
- package/src/sdk/bus/lifecycle-commands.ts +4 -1
- package/src/sdk/bus/lifecycle-control-runtime.ts +403 -88
- package/src/sdk/bus/reconciliation-store.ts +95 -16
- package/src/sdk/bus/rich-draft.ts +1 -1
- package/src/sdk/bus/rich-render.ts +2 -2
- package/src/sdk/bus/telegram-adoption-intent.ts +574 -0
- package/src/sdk/bus/telegram-cli.ts +33 -8
- package/src/sdk/bus/telegram-daemon-cli.ts +1 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +11 -4
- package/src/sdk/bus/telegram-daemon.ts +916 -23
- package/src/sdk/bus/telegram-reference.ts +40 -3
- package/src/sdk/bus/threaded-render.ts +48 -3
- package/src/sdk/bus/topic-registry.ts +20 -0
- package/src/sdk/host/query/revision-store.ts +36 -2
- package/src/sdk/host/reverse-leases.ts +44 -7
- package/src/sdk/prompt-status.ts +16 -3
- package/src/sdk/protocol/operation-inventory.generated.json +22 -0
- package/src/sdk/session.ts +88 -9
- package/src/session/agent-session.ts +822 -172
- package/src/session/artifacts.ts +22 -0
- package/src/session/auth-broker-config.ts +10 -3
- package/src/session/blob-store.ts +915 -24
- package/src/session/internal/managed-session-scope.ts +44 -6
- package/src/session/internal/managed-session-storage.ts +48 -6
- package/src/session/session-manager.ts +1614 -435
- package/src/session/session-storage.ts +9 -1
- package/src/session/streaming-output.ts +238 -68
- package/src/setup/credential-import.ts +75 -13
- package/src/skill-state/active-state.ts +32 -0
- package/src/skill-state/workflow-hud.ts +17 -0
- package/src/skill-state/workflow-mutation-guard.ts +496 -2
- package/src/task/executor.ts +122 -39
- package/src/task/index.ts +23 -14
- package/src/task/provider-retry-status.ts +16 -0
- package/src/task/receipt.ts +23 -7
- package/src/task/render.ts +6 -2
- package/src/task/types.ts +108 -0
- package/src/tools/ask-answer-registry.ts +12 -6
- package/src/tools/ask.ts +86 -55
- package/src/tools/ast-edit.ts +1 -0
- package/src/tools/ast-grep.ts +1 -0
- package/src/tools/bash-interactive.ts +8 -3
- package/src/tools/bash.ts +503 -60
- package/src/tools/find.ts +1 -0
- package/src/tools/image-gen.ts +29 -5
- package/src/tools/index.ts +4 -0
- package/src/tools/output-meta.ts +108 -14
- package/src/tools/path-utils.ts +3 -2
- package/src/tools/read.ts +18 -0
- package/src/tools/review.ts +5 -7
- package/src/tools/search.ts +1 -0
- package/src/tools/subagent-render.ts +6 -2
- package/src/tools/subagent.ts +6 -0
- package/src/tools/telegram-send.ts +6 -3
- package/src/utils/gh.ts +39 -0
- package/src/utils/image-resize.ts +3 -0
- package/src/web/search/providers/anthropic.ts +4 -4
- package/src/web/search/providers/kimi.ts +15 -5
- package/src/web/search/providers/searxng.ts +47 -4
- package/src/web/search/providers/xai.ts +7 -2
- package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +0 -23
- package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +0 -30
- package/src/gjc-runtime/ultragoal-pipeline.ts +0 -306
|
@@ -47,6 +47,7 @@ import { resolveAcpFinalText } from "../../sdk/acp/final-text";
|
|
|
47
47
|
import { ACP_MCP_LIFECYCLE_TIMEOUT_MS, type SessionLifecycleMcpServer } from "../../sdk/acp/mcp";
|
|
48
48
|
import { ensureBroker } from "../../sdk/broker/ensure";
|
|
49
49
|
import { readSdkBrokerDiscovery, SdkClient, SdkClientError } from "../../sdk/client";
|
|
50
|
+
import type { SdkPromptTerminalOutcome } from "../../sdk/prompt-status";
|
|
50
51
|
import {
|
|
51
52
|
buildToolCallStartUpdate,
|
|
52
53
|
mapAgentSessionEventToAcpSessionUpdates,
|
|
@@ -66,26 +67,21 @@ const THINKING_CONFIG_ID = "thinking";
|
|
|
66
67
|
const SESSION_PAGE_SIZE = 50;
|
|
67
68
|
export const ACP_BOOTSTRAP_RACE_GUARD_MS = 50;
|
|
68
69
|
const MAX_ACP_REPLAY_PAGES = 10_000;
|
|
70
|
+
/** Bounded retention of settled prompt correlations so late duplicates stay closed. */
|
|
71
|
+
const SETTLED_PROMPT_CORRELATION_RETENTION = 16;
|
|
69
72
|
|
|
70
73
|
type JsonObject = Record<string, unknown>;
|
|
71
|
-
/**
|
|
72
|
-
* ACP prompt completion is tied to lifecycle activity observed after dispatch.
|
|
73
|
-
* A fast host may stream and finish a turn before acknowledging `turn.prompt`,
|
|
74
|
-
* so pre-ack activity is retained but cannot resolve the waiter until the
|
|
75
|
-
* acknowledgement supplies its command/turn correlation.
|
|
76
|
-
*/
|
|
77
74
|
interface PromptWaiter {
|
|
78
|
-
cancelRequested: boolean;
|
|
79
75
|
acknowledged: boolean;
|
|
80
|
-
activityObserved: boolean;
|
|
81
|
-
/** The prompt was accepted while the host was already busy, so its next valid idle ends the steer. */
|
|
82
|
-
steeringAtAcknowledgement: boolean;
|
|
83
76
|
/** Highest inbound frame sequence already observed when the prompt was acknowledged. */
|
|
84
77
|
boundary: number;
|
|
85
78
|
correlation: PromptCorrelation;
|
|
86
79
|
messageProgress?: { textEmitted: boolean; thoughtEmitted: boolean };
|
|
87
80
|
emittedAssistantText: string;
|
|
88
|
-
|
|
81
|
+
settled: boolean;
|
|
82
|
+
terminal?: { outcome: SdkPromptTerminalOutcome; correlation: PromptCorrelation };
|
|
83
|
+
/** Frames for an already-settled correlation held until acknowledgement resolves ownership. */
|
|
84
|
+
deferredFrames: JsonObject[];
|
|
89
85
|
resolve: (response: PromptResponse) => void;
|
|
90
86
|
reject: (error: Error) => void;
|
|
91
87
|
}
|
|
@@ -110,6 +106,9 @@ type SessionRecord = {
|
|
|
110
106
|
/** Start/update args retained because tool_execution_end does not carry them. */
|
|
111
107
|
toolArgs: Map<string, unknown>;
|
|
112
108
|
/** Actionable model-profile authentication failure detected before prompt dispatch. */
|
|
109
|
+
connectionId?: string;
|
|
110
|
+
/** Bounded set of correlations already settled; they stay closed for publication. */
|
|
111
|
+
settledPromptCorrelations: PromptCorrelation[];
|
|
113
112
|
authFailure?: string;
|
|
114
113
|
activePrompt?: PromptWaiter;
|
|
115
114
|
};
|
|
@@ -257,31 +256,97 @@ function correlationFrom(...values: unknown[]): PromptCorrelation {
|
|
|
257
256
|
return correlation;
|
|
258
257
|
}
|
|
259
258
|
|
|
260
|
-
function
|
|
259
|
+
function hasCorrelation(correlation: PromptCorrelation): boolean {
|
|
260
|
+
return correlation.commandId !== undefined || correlation.turnId !== undefined;
|
|
261
|
+
}
|
|
262
|
+
function correlationsMatch(expected: PromptCorrelation, actual: PromptCorrelation): boolean {
|
|
261
263
|
return (
|
|
262
|
-
(
|
|
263
|
-
(expected.
|
|
264
|
+
hasCorrelation(actual) &&
|
|
265
|
+
(expected.commandId === undefined || expected.commandId === actual.commandId) &&
|
|
266
|
+
(expected.turnId === undefined || expected.turnId === actual.turnId)
|
|
264
267
|
);
|
|
265
268
|
}
|
|
266
269
|
|
|
267
|
-
function
|
|
270
|
+
function hasCompleteCorrelation(correlation: PromptCorrelation): correlation is { commandId: string; turnId: string } {
|
|
268
271
|
return (
|
|
269
|
-
|
|
270
|
-
(
|
|
272
|
+
typeof correlation.commandId === "string" &&
|
|
273
|
+
correlation.commandId.trim().length > 0 &&
|
|
274
|
+
typeof correlation.turnId === "string" &&
|
|
275
|
+
correlation.turnId.trim().length > 0
|
|
271
276
|
);
|
|
272
277
|
}
|
|
273
278
|
|
|
274
|
-
function
|
|
275
|
-
return
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
279
|
+
function correlationsExactlyMatch(expected: PromptCorrelation, actual: PromptCorrelation): boolean {
|
|
280
|
+
return (
|
|
281
|
+
hasCompleteCorrelation(expected) &&
|
|
282
|
+
hasCompleteCorrelation(actual) &&
|
|
283
|
+
expected.commandId === actual.commandId &&
|
|
284
|
+
expected.turnId === actual.turnId
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function promptAcknowledgement(value: unknown): PromptCorrelation | undefined {
|
|
289
|
+
const candidate = object(value);
|
|
290
|
+
if (
|
|
291
|
+
!candidate ||
|
|
292
|
+
candidate.ok === false ||
|
|
293
|
+
candidate.error !== undefined ||
|
|
294
|
+
(candidate.accepted !== undefined && candidate.accepted !== true)
|
|
295
|
+
)
|
|
296
|
+
return undefined;
|
|
297
|
+
const payload = object(candidate.result) ?? candidate;
|
|
298
|
+
if (payload.accepted !== true) return undefined;
|
|
299
|
+
if (typeof payload.commandId !== "string" || payload.commandId.trim().length === 0) return undefined;
|
|
300
|
+
if (typeof payload.turnId !== "string" || payload.turnId.trim().length === 0) return undefined;
|
|
301
|
+
return { commandId: payload.commandId, turnId: payload.turnId };
|
|
302
|
+
}
|
|
303
|
+
function strictCorrelationFrom(...values: unknown[]): PromptCorrelation | undefined {
|
|
304
|
+
const correlation: PromptCorrelation = {};
|
|
305
|
+
let malformed = false;
|
|
306
|
+
for (const value of values) {
|
|
307
|
+
const candidate = object(value);
|
|
308
|
+
if (!candidate) continue;
|
|
309
|
+
for (const [field, aliases] of [
|
|
310
|
+
["commandId", ["commandId", "command_id"]],
|
|
311
|
+
["turnId", ["turnId", "turn_id"]],
|
|
312
|
+
] as const) {
|
|
313
|
+
for (const alias of aliases) {
|
|
314
|
+
if (!Object.hasOwn(candidate, alias)) continue;
|
|
315
|
+
const identity = candidate[alias];
|
|
316
|
+
if (typeof identity !== "string" || identity.trim().length === 0) {
|
|
317
|
+
malformed = true;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
const previous = correlation[field];
|
|
321
|
+
if (previous !== undefined && previous !== identity) malformed = true;
|
|
322
|
+
correlation[field] = identity;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return malformed ? undefined : correlation;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function terminalOutcome(event: JsonObject): SdkPromptTerminalOutcome | undefined {
|
|
330
|
+
const outcome = object(event.outcome);
|
|
331
|
+
if (!outcome) return undefined;
|
|
332
|
+
if (
|
|
333
|
+
outcome.kind === "stopped" &&
|
|
334
|
+
(outcome.reason === "end_turn" ||
|
|
335
|
+
outcome.reason === "max_tokens" ||
|
|
336
|
+
outcome.reason === "max_turn_requests" ||
|
|
337
|
+
outcome.reason === "refusal" ||
|
|
338
|
+
outcome.reason === "cancelled") &&
|
|
339
|
+
(outcome.provenance === "agent" || outcome.provenance === "client_cancel")
|
|
340
|
+
)
|
|
341
|
+
return outcome as SdkPromptTerminalOutcome;
|
|
342
|
+
if (
|
|
343
|
+
outcome.kind === "failed" &&
|
|
344
|
+
(outcome.code === "prompt_failed" || outcome.code === "prompt_deadline_exceeded") &&
|
|
345
|
+
typeof outcome.message === "string" &&
|
|
346
|
+
(outcome.provenance === "agent_failed" || outcome.provenance === "deadline")
|
|
347
|
+
)
|
|
348
|
+
return outcome as SdkPromptTerminalOutcome;
|
|
349
|
+
return undefined;
|
|
285
350
|
}
|
|
286
351
|
|
|
287
352
|
export type TranscriptReplayBlock = { type: "text"; text: string } | { type: "image"; data: string; mimeType: string };
|
|
@@ -322,11 +387,9 @@ type ReceivedSdkEvent = {
|
|
|
322
387
|
* without treating notification-specific frames as agent lifecycle truth.
|
|
323
388
|
*/
|
|
324
389
|
function receivedSdkEvent(frame: JsonObject): ReceivedSdkEvent | undefined {
|
|
325
|
-
if (frame.type === "activity")
|
|
326
|
-
|
|
327
|
-
return
|
|
328
|
-
}
|
|
329
|
-
if (frame.type === "agent_start" || frame.type === "agent_end") return { event: frame };
|
|
390
|
+
if (frame.type === "activity") return undefined;
|
|
391
|
+
if (frame.type === "agent_start" || frame.type === "agent_end" || frame.type === "agent_failed")
|
|
392
|
+
return { event: frame };
|
|
330
393
|
if (frame.type !== "event") return undefined;
|
|
331
394
|
const payload = object(frame.payload);
|
|
332
395
|
if (!payload) return undefined;
|
|
@@ -547,8 +610,10 @@ export function acpRequestFailure(error: unknown): unknown {
|
|
|
547
610
|
switch (code) {
|
|
548
611
|
case "authentication_failed":
|
|
549
612
|
return RequestError.authRequired({ code, details: message }, message);
|
|
550
|
-
|
|
551
|
-
|
|
613
|
+
// `not_found` stays -32603 with its discriminator in `data`: ACP's
|
|
614
|
+
// `resourceNotFound` (-32002) is a URI-addressed resource error, and an unknown
|
|
615
|
+
// session id is not a resource URI. Pinned ACP core-v1 conformance also requires
|
|
616
|
+
// -32603/-32000 for a prompt against an unknown session.
|
|
552
617
|
case "invalid_input":
|
|
553
618
|
case "unsupported":
|
|
554
619
|
case "unsupported_content":
|
|
@@ -567,17 +632,59 @@ export function acpProviderRegistrations(
|
|
|
567
632
|
env: NodeJS.ProcessEnv = process.env,
|
|
568
633
|
): AcpProviderRegistration[] {
|
|
569
634
|
return [
|
|
635
|
+
// `fs.readTextFile` and `fs.writeTextFile` are independently optional, so the
|
|
636
|
+
// advertised methods travel with the lease instead of being inferred as both.
|
|
570
637
|
...(capabilities?.fs?.readTextFile || capabilities?.fs?.writeTextFile
|
|
571
|
-
? [
|
|
638
|
+
? [
|
|
639
|
+
{
|
|
640
|
+
capability: "fs",
|
|
641
|
+
definitions: [
|
|
642
|
+
...(capabilities.fs.readTextFile ? [{ name: "fs.readTextFile" }] : []),
|
|
643
|
+
...(capabilities.fs.writeTextFile ? [{ name: "fs.writeTextFile" }] : []),
|
|
644
|
+
],
|
|
645
|
+
},
|
|
646
|
+
]
|
|
572
647
|
: []),
|
|
573
648
|
...(capabilities?.terminal ? [{ capability: "terminal", definitions: [] }] : []),
|
|
574
649
|
...(resolveAcpPermissionMode(capabilities, env) === "prompt"
|
|
575
650
|
? [{ capability: "permission", definitions: [] }]
|
|
576
651
|
: []),
|
|
577
|
-
{ capability: "ui", definitions: [] },
|
|
652
|
+
...(capabilities?.elicitation?.form ? [{ capability: "ui", definitions: [] }] : []),
|
|
578
653
|
];
|
|
579
654
|
}
|
|
580
655
|
|
|
656
|
+
export function createAcpReverseConnection(connection: AgentSideConnection, sessionId: string): AcpReverseConnection {
|
|
657
|
+
const methods: Record<string, string> = {
|
|
658
|
+
request: "session/request_permission",
|
|
659
|
+
"permission.request": "session/request_permission",
|
|
660
|
+
"fs.readTextFile": "fs/read_text_file",
|
|
661
|
+
"fs.writeTextFile": "fs/write_text_file",
|
|
662
|
+
"terminal.create": "terminal/create",
|
|
663
|
+
"ui.elicit": "elicitation/create",
|
|
664
|
+
};
|
|
665
|
+
return {
|
|
666
|
+
request: async (
|
|
667
|
+
method: string,
|
|
668
|
+
params: JsonObject,
|
|
669
|
+
options?: { cancellationSignal?: AbortSignal },
|
|
670
|
+
): Promise<unknown> => {
|
|
671
|
+
const name = methods[method];
|
|
672
|
+
if (!name)
|
|
673
|
+
throw new AcpSdkAdapterError("acp_reverse_unavailable", `ACP reverse method is unavailable: ${method}`);
|
|
674
|
+
const rawRequest = (connection as unknown as Record<string, unknown>).request;
|
|
675
|
+
if (typeof rawRequest !== "function")
|
|
676
|
+
throw new AcpSdkAdapterError("acp_reverse_unavailable", "ACP reverse request surface is unavailable.");
|
|
677
|
+
return await (
|
|
678
|
+
rawRequest as (
|
|
679
|
+
method: string,
|
|
680
|
+
input: JsonObject,
|
|
681
|
+
options?: { cancellationSignal?: AbortSignal },
|
|
682
|
+
) => Promise<unknown>
|
|
683
|
+
).call(connection, name, { ...params, sessionId }, options);
|
|
684
|
+
},
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
|
|
581
688
|
/** Maps ACP permission handling to the session's canonical SDK policy. */
|
|
582
689
|
export async function applyAcpPermissionMode(
|
|
583
690
|
adapter: Pick<AcpSdkAdapter, "control">,
|
|
@@ -597,82 +704,6 @@ export async function applyAcpStartupOptions(
|
|
|
597
704
|
if (options?.thinkingLevel) await adapter.control("thinking.set", { level: options.thinkingLevel });
|
|
598
705
|
}
|
|
599
706
|
|
|
600
|
-
/** ACP form elicitation uses the client-facing reverse surface without owning a session runtime. */
|
|
601
|
-
export function createAcpExtensionUiContext(
|
|
602
|
-
connection: AgentSideConnection,
|
|
603
|
-
getSessionId: () => string,
|
|
604
|
-
capabilities: ClientCapabilities | undefined,
|
|
605
|
-
): {
|
|
606
|
-
select: (
|
|
607
|
-
message: string,
|
|
608
|
-
options: string[],
|
|
609
|
-
dialog?: { signal?: AbortSignal; timeout?: number; onTimeout?: () => void },
|
|
610
|
-
) => Promise<string | undefined>;
|
|
611
|
-
confirm: (
|
|
612
|
-
message: string,
|
|
613
|
-
detail?: string,
|
|
614
|
-
dialog?: { signal?: AbortSignal; timeout?: number; onTimeout?: () => void },
|
|
615
|
-
) => Promise<boolean>;
|
|
616
|
-
input: (
|
|
617
|
-
message: string,
|
|
618
|
-
placeholder?: string,
|
|
619
|
-
dialog?: { signal?: AbortSignal; timeout?: number; onTimeout?: () => void },
|
|
620
|
-
) => Promise<string | undefined>;
|
|
621
|
-
} {
|
|
622
|
-
const elicit = async (
|
|
623
|
-
kind: "select" | "confirm" | "input",
|
|
624
|
-
message: string,
|
|
625
|
-
options: string[] | undefined,
|
|
626
|
-
dialog: { signal?: AbortSignal; timeout?: number; onTimeout?: () => void } | undefined,
|
|
627
|
-
): Promise<unknown> => {
|
|
628
|
-
if (!capabilities?.elicitation?.form || dialog?.signal?.aborted) return undefined;
|
|
629
|
-
const request = (
|
|
630
|
-
connection as unknown as { unstable_createElicitation(input: JsonObject): Promise<JsonObject> }
|
|
631
|
-
).unstable_createElicitation({
|
|
632
|
-
sessionId: getSessionId(),
|
|
633
|
-
message,
|
|
634
|
-
requestedSchema: {
|
|
635
|
-
type: "object",
|
|
636
|
-
properties: {
|
|
637
|
-
value:
|
|
638
|
-
kind === "confirm" ? { type: "boolean" } : { type: "string", ...(options ? { enum: options } : {}) },
|
|
639
|
-
},
|
|
640
|
-
required: ["value"],
|
|
641
|
-
},
|
|
642
|
-
});
|
|
643
|
-
let timer: NodeJS.Timeout | undefined;
|
|
644
|
-
const timeout =
|
|
645
|
-
dialog?.timeout === undefined
|
|
646
|
-
? undefined
|
|
647
|
-
: new Promise<undefined>(resolve => {
|
|
648
|
-
timer = setTimeout(() => {
|
|
649
|
-
dialog.onTimeout?.();
|
|
650
|
-
resolve(undefined);
|
|
651
|
-
}, dialog.timeout);
|
|
652
|
-
});
|
|
653
|
-
try {
|
|
654
|
-
const response = timeout ? await Promise.race([request, timeout]) : await request;
|
|
655
|
-
return object(object(response)?.content)?.value;
|
|
656
|
-
} catch {
|
|
657
|
-
return undefined;
|
|
658
|
-
} finally {
|
|
659
|
-
if (timer) clearTimeout(timer);
|
|
660
|
-
}
|
|
661
|
-
};
|
|
662
|
-
return {
|
|
663
|
-
select: async (message, options, dialog) => {
|
|
664
|
-
const value = await elicit("select", message, options, dialog);
|
|
665
|
-
return typeof value === "string" && options.includes(value) ? value : undefined;
|
|
666
|
-
},
|
|
667
|
-
confirm: async (message, detail, dialog) =>
|
|
668
|
-
(await elicit("confirm", detail ? `${message}\n\n${detail}` : message, undefined, dialog)) === true,
|
|
669
|
-
input: async (message, placeholder, dialog) => {
|
|
670
|
-
const value = await elicit("input", placeholder ? `${message}\n\n${placeholder}` : message, undefined, dialog);
|
|
671
|
-
return typeof value === "string" ? value : undefined;
|
|
672
|
-
},
|
|
673
|
-
};
|
|
674
|
-
}
|
|
675
|
-
|
|
676
707
|
/**
|
|
677
708
|
* ACP is a pure SDK client. Session processes are created and resumed by the
|
|
678
709
|
* broker, while all per-session operations use that session's authenticated SDK
|
|
@@ -962,20 +993,19 @@ export class AcpAgent implements Agent {
|
|
|
962
993
|
|
|
963
994
|
async prompt(params: PromptRequest): Promise<PromptResponse> {
|
|
964
995
|
const record = this.#sessions.get(params.sessionId);
|
|
965
|
-
if (!record) throw new AcpSdkAdapterError("not_found", `
|
|
996
|
+
if (!record) throw new AcpSdkAdapterError("not_found", `Unknown session, not found: ${params.sessionId}`);
|
|
966
997
|
if (record.activePrompt) throw new AcpSdkAdapterError("conflict", "ACP session already has an active prompt.");
|
|
967
998
|
if (record.authFailure) throw new AcpSdkAdapterError("authentication_failed", record.authFailure);
|
|
968
999
|
const payload = acpPromptPayload(params.prompt);
|
|
969
1000
|
let waiter!: PromptWaiter;
|
|
970
1001
|
const response = new Promise<PromptResponse>((resolve, reject) => {
|
|
971
1002
|
waiter = {
|
|
972
|
-
cancelRequested: false,
|
|
973
1003
|
acknowledged: false,
|
|
974
|
-
activityObserved: false,
|
|
975
|
-
steeringAtAcknowledgement: record.busy,
|
|
976
1004
|
boundary: record.inboundSequence,
|
|
977
1005
|
correlation: {},
|
|
978
1006
|
emittedAssistantText: "",
|
|
1007
|
+
settled: false,
|
|
1008
|
+
deferredFrames: [],
|
|
979
1009
|
resolve,
|
|
980
1010
|
reject,
|
|
981
1011
|
};
|
|
@@ -986,14 +1016,29 @@ export class AcpAgent implements Agent {
|
|
|
986
1016
|
text: payload.text,
|
|
987
1017
|
...(payload.images.length ? { images: payload.images } : {}),
|
|
988
1018
|
});
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
1019
|
+
const acknowledgementCorrelation = promptAcknowledgement(acknowledgement);
|
|
1020
|
+
if (!acknowledgementCorrelation)
|
|
1021
|
+
throw new AcpSdkAdapterError(
|
|
1022
|
+
"invalid_prompt_acknowledgement",
|
|
1023
|
+
"SDK prompt acknowledgement must accept the prompt and include commandId and turnId.",
|
|
1024
|
+
);
|
|
1025
|
+
// Retain the acknowledgement ingress boundary with its complete correlation.
|
|
992
1026
|
waiter.boundary = record.inboundSequence;
|
|
993
|
-
waiter.correlation =
|
|
1027
|
+
waiter.correlation = acknowledgementCorrelation;
|
|
994
1028
|
waiter.acknowledged = true;
|
|
1029
|
+
// Frames held while ownership was unknown belong to this prompt only when the
|
|
1030
|
+
// acknowledgement proves their complete correlation matches exactly.
|
|
1031
|
+
const deferred = waiter.deferredFrames.splice(0);
|
|
1032
|
+
for (const deferredFrame of deferred)
|
|
1033
|
+
if (correlationsExactlyMatch(waiter.correlation, correlationFrom(deferredFrame)))
|
|
1034
|
+
record.frameTail = record.frameTail.then(
|
|
1035
|
+
async () => await this.#handleSdkFrame(params.sessionId, record.adapter, deferredFrame),
|
|
1036
|
+
);
|
|
995
1037
|
this.#settlePrompt(record, waiter);
|
|
996
1038
|
} catch (error) {
|
|
1039
|
+
waiter.deferredFrames.length = 0;
|
|
1040
|
+
waiter.terminal = undefined;
|
|
1041
|
+
waiter.settled = true;
|
|
997
1042
|
if (record.activePrompt === waiter) record.activePrompt = undefined;
|
|
998
1043
|
throw error;
|
|
999
1044
|
}
|
|
@@ -1002,8 +1047,7 @@ export class AcpAgent implements Agent {
|
|
|
1002
1047
|
|
|
1003
1048
|
async cancel(params: { sessionId: string }): Promise<void> {
|
|
1004
1049
|
const record = this.#sessions.get(params.sessionId);
|
|
1005
|
-
if (!record) throw new AcpSdkAdapterError("not_found", `
|
|
1006
|
-
const waiter = record.activePrompt;
|
|
1050
|
+
if (!record) throw new AcpSdkAdapterError("not_found", `Unknown session, not found: ${params.sessionId}`);
|
|
1007
1051
|
const acknowledgement = await record.adapter.cancel();
|
|
1008
1052
|
const result = object(object(acknowledgement)?.result) ?? object(acknowledgement);
|
|
1009
1053
|
if (result?.aborted !== true)
|
|
@@ -1011,9 +1055,6 @@ export class AcpAgent implements Agent {
|
|
|
1011
1055
|
"abort_unacknowledged",
|
|
1012
1056
|
"SDK did not acknowledge cancellation of the active prompt.",
|
|
1013
1057
|
);
|
|
1014
|
-
// Do not retroactively mark a waiter that already settled while the abort
|
|
1015
|
-
// request was in flight. A cancelled response means the abort itself won.
|
|
1016
|
-
if (waiter && record.activePrompt === waiter) waiter.cancelRequested = true;
|
|
1017
1058
|
}
|
|
1018
1059
|
|
|
1019
1060
|
async extMethod(method: string, params: JsonObject): Promise<JsonObject> {
|
|
@@ -1110,8 +1151,9 @@ export class AcpAgent implements Agent {
|
|
|
1110
1151
|
if (attached) {
|
|
1111
1152
|
if (path.resolve(attached.cwd) !== path.resolve(cwd))
|
|
1112
1153
|
throw new AcpSdkAdapterError("conflict", `ACP session ${id} has conflicting cwd authority.`);
|
|
1113
|
-
|
|
1114
|
-
|
|
1154
|
+
// ACP clients replay their declared MCP servers when reconnecting. The live
|
|
1155
|
+
// session host remains authoritative for its immutable configuration, so
|
|
1156
|
+
// attachment must not reinterpret the replay as a mutation request.
|
|
1115
1157
|
return;
|
|
1116
1158
|
}
|
|
1117
1159
|
const knownCwd = this.#knownSessionCwds.get(id);
|
|
@@ -1149,8 +1191,8 @@ export class AcpAgent implements Agent {
|
|
|
1149
1191
|
const indexed = await this.#scopedBrokerSession(id, cwd);
|
|
1150
1192
|
this.#assertSessionEpoch(id, epoch);
|
|
1151
1193
|
if (indexed?.live) {
|
|
1152
|
-
|
|
1153
|
-
|
|
1194
|
+
// A reconnect may repeat the client's MCP declaration. Attaching to the
|
|
1195
|
+
// existing endpoint preserves the live host's immutable configuration.
|
|
1154
1196
|
const result = await this.#brokerEndpoint(id, indexed.endpointGeneration);
|
|
1155
1197
|
this.#assertSessionEpoch(id, epoch);
|
|
1156
1198
|
await this.#attach(id, cwd, endpoint(result), epoch);
|
|
@@ -1229,6 +1271,19 @@ export class AcpAgent implements Agent {
|
|
|
1229
1271
|
connection: this.#reverseConnection(id),
|
|
1230
1272
|
providers: this.#providers(),
|
|
1231
1273
|
});
|
|
1274
|
+
let capabilities: JsonObject | undefined;
|
|
1275
|
+
try {
|
|
1276
|
+
const response = object(await adapter.query("runtime.capabilities"));
|
|
1277
|
+
const result = object(response?.result) ?? response;
|
|
1278
|
+
// Q18 is a paged query surface: the capability object arrives as the single
|
|
1279
|
+
// page item, so fall back to the envelope only for direct-result hosts.
|
|
1280
|
+
capabilities = object(pageItems(result)[0]) ?? result;
|
|
1281
|
+
} catch {}
|
|
1282
|
+
if (capabilities?.promptTerminalOutcomeVersion !== 1)
|
|
1283
|
+
throw new AcpSdkAdapterError(
|
|
1284
|
+
"unavailable",
|
|
1285
|
+
"This ACP client requires a newer GJC SDK session; restart the session.",
|
|
1286
|
+
);
|
|
1232
1287
|
this.#assertSessionEpoch(id, epoch);
|
|
1233
1288
|
const record: SessionRecord = {
|
|
1234
1289
|
cwd,
|
|
@@ -1237,7 +1292,9 @@ export class AcpAgent implements Agent {
|
|
|
1237
1292
|
unsubscribe: () => {},
|
|
1238
1293
|
reconnectUnsubscribe: () => {},
|
|
1239
1294
|
frameTail: Promise.resolve(),
|
|
1295
|
+
settledPromptCorrelations: [],
|
|
1240
1296
|
inboundSequence: 0,
|
|
1297
|
+
connectionId: adapter.connectionId,
|
|
1241
1298
|
busy: false,
|
|
1242
1299
|
toolArgs: new Map(),
|
|
1243
1300
|
};
|
|
@@ -1414,7 +1471,7 @@ export class AcpAgent implements Agent {
|
|
|
1414
1471
|
|
|
1415
1472
|
#adapter(id: string): AcpSdkAdapter {
|
|
1416
1473
|
const record = this.#sessions.get(id);
|
|
1417
|
-
if (!record) throw new AcpSdkAdapterError("not_found", `
|
|
1474
|
+
if (!record) throw new AcpSdkAdapterError("not_found", `Unknown session, not found: ${id}`);
|
|
1418
1475
|
return record.adapter;
|
|
1419
1476
|
}
|
|
1420
1477
|
|
|
@@ -1434,29 +1491,18 @@ export class AcpAgent implements Agent {
|
|
|
1434
1491
|
}
|
|
1435
1492
|
|
|
1436
1493
|
#reverseConnection(sessionId: string): AcpReverseConnection {
|
|
1437
|
-
|
|
1438
|
-
"fs.readTextFile": "readTextFile",
|
|
1439
|
-
"fs.writeTextFile": "writeTextFile",
|
|
1440
|
-
"terminal.create": "createTerminal",
|
|
1441
|
-
"permission.request": "requestPermission",
|
|
1442
|
-
"ui.elicit": "unstable_createElicitation",
|
|
1443
|
-
};
|
|
1444
|
-
return {
|
|
1445
|
-
request: async (method: string, params: JsonObject): Promise<unknown> => {
|
|
1446
|
-
const name = methods[method] ?? method;
|
|
1447
|
-
const target = (this.#connection as unknown as Record<string, unknown>)[name];
|
|
1448
|
-
if (typeof target !== "function")
|
|
1449
|
-
throw new AcpSdkAdapterError("acp_reverse_unavailable", `ACP reverse method is unavailable: ${method}`);
|
|
1450
|
-
const request = method === "permission.request" ? { ...params, sessionId } : params;
|
|
1451
|
-
return await (target as (input: JsonObject) => Promise<unknown>)(request);
|
|
1452
|
-
},
|
|
1453
|
-
};
|
|
1494
|
+
return createAcpReverseConnection(this.#connection, sessionId);
|
|
1454
1495
|
}
|
|
1455
1496
|
|
|
1456
1497
|
#observeSessionActivity(record: SessionRecord, frame: JsonObject): void {
|
|
1498
|
+
if (frame.type === "activity") {
|
|
1499
|
+
if (frame.state === "busy") record.busy = true;
|
|
1500
|
+
else if (frame.state === "idle") record.busy = false;
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1457
1503
|
const event = receivedSdkEvent(frame)?.event;
|
|
1458
1504
|
if (event?.type === "agent_start") record.busy = true;
|
|
1459
|
-
else if (event?.type === "agent_end") record.busy = false;
|
|
1505
|
+
else if (event?.type === "agent_end" || event?.type === "agent_failed") record.busy = false;
|
|
1460
1506
|
}
|
|
1461
1507
|
|
|
1462
1508
|
#frameProcessingFailure(error: unknown): AcpSdkAdapterError {
|
|
@@ -1468,22 +1514,72 @@ export class AcpAgent implements Agent {
|
|
|
1468
1514
|
#enqueueSdkFrame(id: string, adapter: AcpSdkAdapter, frame: JsonObject): void {
|
|
1469
1515
|
const record = this.#sessions.get(id);
|
|
1470
1516
|
if (!record || record.adapter !== adapter) return;
|
|
1471
|
-
//
|
|
1472
|
-
// A frame received before acknowledgement stays before that prompt's boundary.
|
|
1517
|
+
// Ingress ordering is recorded before queued work begins.
|
|
1473
1518
|
this.#observeSessionActivity(record, frame);
|
|
1474
|
-
|
|
1475
|
-
const task = record.frameTail.then(async () => await this.#handleSdkFrame(id, adapter, frame
|
|
1519
|
+
++record.inboundSequence;
|
|
1520
|
+
const task = record.frameTail.then(async () => await this.#handleSdkFrame(id, adapter, frame));
|
|
1476
1521
|
record.frameTail = task.catch(
|
|
1477
1522
|
async error => await this.#failSession(id, adapter, this.#frameProcessingFailure(error)),
|
|
1478
1523
|
);
|
|
1479
1524
|
}
|
|
1480
1525
|
|
|
1481
|
-
async #handleSdkFrame(id: string, adapter: AcpSdkAdapter, frame: JsonObject
|
|
1526
|
+
async #handleSdkFrame(id: string, adapter: AcpSdkAdapter, frame: JsonObject): Promise<void> {
|
|
1482
1527
|
const record = this.#sessions.get(id);
|
|
1483
1528
|
if (!record || record.adapter !== adapter) return;
|
|
1529
|
+
if ((frame.type === "hello" || frame.type === "server_hello") && typeof frame.connectionId === "string") {
|
|
1530
|
+
const reconnected = record.connectionId !== undefined && record.connectionId !== frame.connectionId;
|
|
1531
|
+
record.connectionId = frame.connectionId;
|
|
1532
|
+
if (reconnected) {
|
|
1533
|
+
const waiter = record.activePrompt;
|
|
1534
|
+
if (waiter && !waiter.settled && !waiter.terminal) {
|
|
1535
|
+
record.activePrompt = undefined;
|
|
1536
|
+
waiter.settled = true;
|
|
1537
|
+
if (hasCorrelation(waiter.correlation)) {
|
|
1538
|
+
record.settledPromptCorrelations.push(waiter.correlation);
|
|
1539
|
+
while (record.settledPromptCorrelations.length > SETTLED_PROMPT_CORRELATION_RETENTION)
|
|
1540
|
+
record.settledPromptCorrelations.shift();
|
|
1541
|
+
}
|
|
1542
|
+
waiter.reject(
|
|
1543
|
+
new AcpSdkAdapterError(
|
|
1544
|
+
"connection_closed",
|
|
1545
|
+
"The prompt owner connection was lost before completion.",
|
|
1546
|
+
),
|
|
1547
|
+
);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1484
1552
|
const received = receivedSdkEvent(frame);
|
|
1485
1553
|
if (!received) return;
|
|
1486
1554
|
const { event, wirePayload } = received;
|
|
1555
|
+
const isTerminal = event.type === "agent_end" || event.type === "agent_failed";
|
|
1556
|
+
const correlation = (isTerminal ? strictCorrelationFrom(frame, event) : correlationFrom(frame, event)) ?? {};
|
|
1557
|
+
const activePrompt = record.activePrompt;
|
|
1558
|
+
const outcome = isTerminal ? terminalOutcome(event) : undefined;
|
|
1559
|
+
if (isTerminal) {
|
|
1560
|
+
// Terminal ownership requires a complete identity. Unowned, partial, and
|
|
1561
|
+
// duplicate terminals are never allowed to publish or query anything.
|
|
1562
|
+
if (!hasCompleteCorrelation(correlation) || !activePrompt || activePrompt.settled) return;
|
|
1563
|
+
if (!activePrompt.acknowledged) {
|
|
1564
|
+
// Hold the entire frame until the prompt acknowledgement proves ownership.
|
|
1565
|
+
activePrompt.deferredFrames.push(frame);
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1568
|
+
if (!correlationsExactlyMatch(activePrompt.correlation, correlation) || activePrompt.terminal) return;
|
|
1569
|
+
if (!outcome) {
|
|
1570
|
+
const detail =
|
|
1571
|
+
typeof (event as { error?: { message?: unknown } }).error?.message === "string"
|
|
1572
|
+
? (event as { error: { message: string } }).error.message
|
|
1573
|
+
: "the prompt terminal omitted a valid normalized outcome";
|
|
1574
|
+
this.#rejectPrompt(
|
|
1575
|
+
record,
|
|
1576
|
+
activePrompt,
|
|
1577
|
+
new AcpSdkAdapterError("connection_closed", `ACP prompt terminal was invalid: ${detail}`),
|
|
1578
|
+
);
|
|
1579
|
+
return;
|
|
1580
|
+
}
|
|
1581
|
+
activePrompt.terminal = { outcome, correlation };
|
|
1582
|
+
}
|
|
1487
1583
|
const toolCallId = typeof event.toolCallId === "string" ? event.toolCallId : undefined;
|
|
1488
1584
|
if (
|
|
1489
1585
|
toolCallId &&
|
|
@@ -1492,31 +1588,21 @@ export class AcpAgent implements Agent {
|
|
|
1492
1588
|
) {
|
|
1493
1589
|
record.toolArgs.set(toolCallId, event.args);
|
|
1494
1590
|
}
|
|
1495
|
-
const
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
sequence > activePrompt.boundary &&
|
|
1505
|
-
!correlationsConflict(activePrompt.correlation, correlation)
|
|
1506
|
-
) {
|
|
1507
|
-
if (isPromptActivity(String(event.type))) {
|
|
1508
|
-
activePrompt.activityObserved = true;
|
|
1509
|
-
} else if (event.type === "agent_end") {
|
|
1510
|
-
if (
|
|
1511
|
-
correlationsMatch(activePrompt.correlation, correlation) ||
|
|
1512
|
-
correlation.commandId !== undefined ||
|
|
1513
|
-
correlation.turnId !== undefined ||
|
|
1514
|
-
activePrompt.activityObserved ||
|
|
1515
|
-
activePrompt.steeringAtAcknowledgement
|
|
1516
|
-
)
|
|
1517
|
-
activePrompt.pendingTerminal = correlation;
|
|
1591
|
+
const settledCorrelation = record.settledPromptCorrelations.some(settled =>
|
|
1592
|
+
correlationsMatch(settled, correlation),
|
|
1593
|
+
);
|
|
1594
|
+
if (settledCorrelation) {
|
|
1595
|
+
// Frames for an already-settled correlation stay closed until an active prompt
|
|
1596
|
+
// acknowledges the exact same identity.
|
|
1597
|
+
if (activePrompt && !activePrompt.settled && !activePrompt.acknowledged) {
|
|
1598
|
+
activePrompt.deferredFrames.push(frame);
|
|
1599
|
+
return;
|
|
1518
1600
|
}
|
|
1601
|
+
if (!activePrompt || activePrompt.settled || !correlationsMatch(activePrompt.correlation, correlation)) return;
|
|
1519
1602
|
}
|
|
1603
|
+
// After a correlated settlement with no active prompt, correlationless wire frames
|
|
1604
|
+
// have no prompt to belong to and must not publish further updates.
|
|
1605
|
+
if (!record.activePrompt && record.settledPromptCorrelations.length > 0 && !hasCorrelation(correlation)) return;
|
|
1520
1606
|
if (wirePayload) {
|
|
1521
1607
|
for (const notification of mapAgentWireEventPayloadToAcpSessionUpdates(wirePayload as never, id, {
|
|
1522
1608
|
cwd: record.cwd,
|
|
@@ -1582,21 +1668,47 @@ export class AcpAgent implements Agent {
|
|
|
1582
1668
|
}
|
|
1583
1669
|
}
|
|
1584
1670
|
await this.#emitEndOfTurnUpdates(id, adapter);
|
|
1671
|
+
} else if (event.type === "agent_failed") {
|
|
1672
|
+
await this.#emitEndOfTurnUpdates(id, adapter);
|
|
1585
1673
|
}
|
|
1586
1674
|
if (activePrompt) this.#settlePrompt(record, activePrompt);
|
|
1587
1675
|
}
|
|
1588
1676
|
|
|
1677
|
+
#rejectPrompt(record: SessionRecord, waiter: PromptWaiter, error: AcpSdkAdapterError): void {
|
|
1678
|
+
if (record.activePrompt !== waiter || waiter.settled) return;
|
|
1679
|
+
record.activePrompt = undefined;
|
|
1680
|
+
waiter.settled = true;
|
|
1681
|
+
waiter.deferredFrames.length = 0;
|
|
1682
|
+
waiter.terminal = undefined;
|
|
1683
|
+
if (hasCompleteCorrelation(waiter.correlation)) {
|
|
1684
|
+
record.settledPromptCorrelations.push(waiter.correlation);
|
|
1685
|
+
while (record.settledPromptCorrelations.length > SETTLED_PROMPT_CORRELATION_RETENTION)
|
|
1686
|
+
record.settledPromptCorrelations.shift();
|
|
1687
|
+
}
|
|
1688
|
+
waiter.reject(error);
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1589
1691
|
#settlePrompt(record: SessionRecord, waiter: PromptWaiter): void {
|
|
1590
|
-
if (record.activePrompt !== waiter || !waiter.acknowledged || !waiter.
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
!waiter.steeringAtAcknowledgement
|
|
1596
|
-
)
|
|
1692
|
+
if (record.activePrompt !== waiter || waiter.settled || !waiter.acknowledged || !waiter.terminal) return;
|
|
1693
|
+
// A terminal captured before acknowledgement is only this prompt's terminal when the
|
|
1694
|
+
// eventual acknowledgement correlates with it; otherwise it belonged to an earlier prompt.
|
|
1695
|
+
if (!correlationsExactlyMatch(waiter.correlation, waiter.terminal.correlation)) {
|
|
1696
|
+
waiter.terminal = undefined;
|
|
1597
1697
|
return;
|
|
1698
|
+
}
|
|
1598
1699
|
record.activePrompt = undefined;
|
|
1599
|
-
waiter.
|
|
1700
|
+
waiter.settled = true;
|
|
1701
|
+
if (hasCorrelation(waiter.correlation)) {
|
|
1702
|
+
record.settledPromptCorrelations.push(waiter.correlation);
|
|
1703
|
+
while (record.settledPromptCorrelations.length > SETTLED_PROMPT_CORRELATION_RETENTION)
|
|
1704
|
+
record.settledPromptCorrelations.shift();
|
|
1705
|
+
}
|
|
1706
|
+
const { outcome } = waiter.terminal;
|
|
1707
|
+
if (outcome.kind === "stopped") {
|
|
1708
|
+
waiter.resolve({ stopReason: outcome.reason });
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
waiter.reject(new AcpSdkAdapterError(outcome.code, outcome.message));
|
|
1600
1712
|
}
|
|
1601
1713
|
|
|
1602
1714
|
async #emitEndOfTurnUpdates(id: string, adapter: AcpSdkAdapter): Promise<void> {
|
|
@@ -1670,7 +1782,7 @@ export class AcpAgent implements Agent {
|
|
|
1670
1782
|
rejectUnavailableStartupPreset = false,
|
|
1671
1783
|
): Promise<Pick<NewSessionResponse, "configOptions" | "modes">> {
|
|
1672
1784
|
const record = this.#sessions.get(id);
|
|
1673
|
-
if (!record) throw new AcpSdkAdapterError("not_found", `
|
|
1785
|
+
if (!record) throw new AcpSdkAdapterError("not_found", `Unknown session, not found: ${id}`);
|
|
1674
1786
|
const modelPreset = this.#startupOptions?.modelPreset;
|
|
1675
1787
|
const [config, modelCatalog] = await Promise.all([
|
|
1676
1788
|
record.adapter.query("config.list/get"),
|