@gajae-code/coding-agent 0.11.9 → 0.11.11
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 +56 -1
- package/dist/types/cli.d.ts +8 -1
- package/dist/types/config/model-profile-activation.d.ts +1 -0
- package/dist/types/config/model-profile-contract.d.ts +2 -0
- package/dist/types/config/model-registry.d.ts +2 -0
- package/dist/types/config/settings-schema.d.ts +57 -2
- package/dist/types/config/settings.d.ts +1 -0
- package/dist/types/extensibility/extensions/types.d.ts +21 -0
- package/dist/types/gjc-runtime/linux-proc.d.ts +22 -14
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +60 -0
- package/dist/types/gjc-runtime/state-runtime.d.ts +3 -1
- package/dist/types/gjc-runtime/state-writer.d.ts +1 -0
- package/dist/types/modes/acp/acp-agent.d.ts +14 -9
- package/dist/types/modes/components/queue-pane.d.ts +8 -0
- package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
- package/dist/types/modes/components/welcome.d.ts +1 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +16 -8
- package/dist/types/modes/interactive-mode.d.ts +5 -1
- package/dist/types/runtime/memory-domain.d.ts +9 -0
- package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
- package/dist/types/runtime/memory-guard.d.ts +39 -0
- package/dist/types/runtime/memory-limit.d.ts +11 -0
- package/dist/types/runtime-mcp/manager.d.ts +8 -0
- package/dist/types/sdk/acp/final-text.d.ts +19 -0
- package/dist/types/sdk/acp/mcp.d.ts +23 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +2 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
- package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +11 -2
- package/dist/types/sdk/bus/operator-runtime.d.ts +1 -0
- package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +14 -7
- package/dist/types/sdk/bus/telegram-daemon.d.ts +97 -8
- package/dist/types/sdk/host/control/operations.d.ts +1 -0
- package/dist/types/sdk/lifecycle-session.d.ts +10 -1
- package/dist/types/sdk/session.d.ts +7 -0
- package/dist/types/sdk/startup-capability.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +11 -11
- package/dist/types/session/fallback-chain-controller.d.ts +18 -2
- package/dist/types/session/internal/managed-session-scope.d.ts +28 -1
- package/dist/types/session/session-manager.d.ts +34 -0
- package/dist/types/task/executor.d.ts +6 -0
- package/dist/types/task/provider-retry-status.d.ts +13 -0
- package/dist/types/task/types.d.ts +39 -0
- package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
- package/dist/types/tools/index.d.ts +3 -1
- package/dist/types/tools/resource-gc.d.ts +19 -0
- package/package.json +8 -8
- package/scripts/generate-sdk-operation-inventory.ts +3 -0
- package/scripts/run-test-manifest.ts +6 -4
- package/src/cli/args.ts +14 -8
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/notify-cli.ts +1 -0
- package/src/cli.ts +48 -2
- package/src/commands/sdk.ts +91 -7
- package/src/config/model-profile-activation.ts +1 -0
- package/src/config/model-profile-contract.ts +13 -0
- package/src/config/model-profiles.ts +31 -7
- package/src/config/model-registry.ts +4 -2
- package/src/config/settings-schema.ts +61 -2
- package/src/config/settings.ts +8 -0
- package/src/defaults/gjc/skills/ralplan/SKILL.md +21 -2
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +1 -1
- package/src/eval/js/tool-bridge.ts +1 -1
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +14 -0
- package/src/gjc-runtime/deep-interview-ambiguity.ts +16 -2
- package/src/gjc-runtime/deep-interview-draft.ts +2 -1
- package/src/gjc-runtime/deep-interview-recorder.ts +16 -1
- package/src/gjc-runtime/deep-interview-state.ts +25 -3
- package/src/gjc-runtime/linux-proc.ts +70 -36
- package/src/gjc-runtime/ralplan-runtime.ts +252 -1
- package/src/gjc-runtime/state-runtime.ts +48 -11
- package/src/gjc-runtime/state-writer.ts +1 -1
- package/src/hooks/skill-state.ts +23 -1
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/internal-urls/local-protocol.ts +23 -4
- package/src/modes/acp/acp-agent.ts +554 -60
- package/src/modes/acp/acp-event-mapper.ts +93 -6
- package/src/modes/components/model-selector.ts +15 -1
- package/src/modes/components/notifications-settings-editor.ts +1 -1
- package/src/modes/components/queue-pane.ts +55 -9
- package/src/modes/components/queued-message-selector.ts +53 -10
- package/src/modes/components/tool-status-header.ts +1 -4
- package/src/modes/components/welcome.ts +18 -3
- package/src/modes/controllers/command-controller.ts +4 -0
- package/src/modes/controllers/extension-ui-controller.ts +4 -10
- package/src/modes/controllers/input-controller.ts +44 -3
- package/src/modes/controllers/selector-controller.ts +38 -8
- package/src/modes/interactive-mode.ts +65 -26
- package/src/modes/runtime-init.ts +1 -0
- package/src/prompts/agent-fragments/restricted-bash.md +1 -1
- package/src/prompts/agents/architect.md +8 -1
- package/src/prompts/agents/critic.md +8 -1
- package/src/prompts/agents/executor.md +1 -1
- package/src/prompts/agents/planner.md +8 -1
- package/src/runtime/memory-domain.ts +66 -0
- package/src/runtime/memory-guard-contract.ts +73 -0
- package/src/runtime/memory-guard.ts +292 -0
- package/src/runtime/memory-limit.ts +50 -0
- package/src/runtime-mcp/manager.ts +26 -6
- package/src/sdk/acp/adapter.ts +8 -1
- package/src/sdk/acp/final-text.ts +40 -0
- package/src/sdk/acp/mcp.ts +26 -0
- package/src/sdk/broker/identity.ts +22 -12
- package/src/sdk/broker/lifecycle.ts +82 -1
- package/src/sdk/bus/chat-daemon-cli.ts +2 -2
- package/src/sdk/bus/chat-daemon-control.ts +4 -3
- package/src/sdk/bus/config.ts +1 -1
- package/src/sdk/bus/daemon-paths.ts +2 -0
- package/src/sdk/bus/index.ts +222 -27
- package/src/sdk/bus/notification-service.ts +19 -5
- package/src/sdk/bus/operator-runtime.ts +31 -5
- package/src/sdk/bus/rate-limit-pool.ts +8 -0
- package/src/sdk/bus/telegram-cli.ts +6 -13
- package/src/sdk/bus/telegram-daemon-cli.ts +8 -3
- package/src/sdk/bus/telegram-daemon-contract.ts +15 -7
- package/src/sdk/bus/telegram-daemon-control.ts +14 -5
- package/src/sdk/bus/telegram-daemon.ts +1191 -164
- package/src/sdk/host/control/dispatch.ts +2 -0
- package/src/sdk/host/control/operations.ts +1 -0
- package/src/sdk/host/host.ts +2 -2
- package/src/sdk/lifecycle-session.ts +19 -3
- package/src/sdk/protocol/operation-inventory.generated.json +58 -0
- package/src/sdk/protocol/operation-registry.ts +1 -0
- package/src/sdk/session.ts +24 -1
- package/src/sdk/startup-capability.ts +6 -0
- package/src/session/agent-session.ts +290 -132
- package/src/session/fallback-chain-controller.ts +47 -8
- package/src/session/internal/managed-session-scope.ts +173 -127
- package/src/session/internal/native-publish-outcome.ts +8 -3
- package/src/session/session-manager.ts +608 -25
- package/src/skill-state/workflow-mutation-guard.ts +41 -7
- package/src/task/executor.ts +152 -42
- package/src/task/index.ts +2 -0
- package/src/task/provider-retry-status.ts +88 -0
- package/src/task/render.ts +45 -26
- package/src/task/types.ts +25 -11
- package/src/task/ultragoal-redteam-activation.ts +75 -0
- package/src/tools/bash-allowed-prefixes.ts +45 -1
- package/src/tools/index.ts +3 -1
- package/src/tools/resource-gc.ts +496 -120
- package/src/tools/subagent-render.ts +74 -16
- package/src/tools/subagent.ts +2 -0
- package/src/workflow/workflow-intent-diff.ts +4 -1
- package/vendor/insane-search/MANIFEST.json +4 -1
- package/vendor/insane-search/engine/bias_check.py +5 -0
- package/vendor/insane-search/engine/fetch_chain.py +67 -1
- package/vendor/insane-search/engine/transport.py +13 -2
- package/vendor/insane-search/engine/validators.py +4 -1
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
type AuthenticateRequest,
|
|
7
7
|
type AuthenticateResponse,
|
|
8
8
|
type AuthMethod,
|
|
9
|
+
type AvailableCommand,
|
|
9
10
|
type ClientCapabilities,
|
|
10
11
|
type CloseSessionRequest,
|
|
11
12
|
type CloseSessionResponse,
|
|
@@ -33,7 +34,7 @@ import {
|
|
|
33
34
|
type SetSessionModeRequest,
|
|
34
35
|
type SetSessionModeResponse,
|
|
35
36
|
} from "@agentclientprotocol/sdk";
|
|
36
|
-
import { getAgentDir } from "@gajae-code/utils";
|
|
37
|
+
import { getAgentDir, logger } from "@gajae-code/utils";
|
|
37
38
|
import packageJson from "../../../package.json" with { type: "json" };
|
|
38
39
|
import {
|
|
39
40
|
type AcpProviderRegistration,
|
|
@@ -41,9 +42,15 @@ import {
|
|
|
41
42
|
AcpSdkAdapter,
|
|
42
43
|
AcpSdkAdapterError,
|
|
43
44
|
} from "../../sdk/acp";
|
|
45
|
+
import { resolveAcpFinalText } from "../../sdk/acp/final-text";
|
|
46
|
+
import { ACP_MCP_LIFECYCLE_TIMEOUT_MS, type SessionLifecycleMcpServer } from "../../sdk/acp/mcp";
|
|
44
47
|
import { ensureBroker } from "../../sdk/broker/ensure";
|
|
45
48
|
import { readSdkBrokerDiscovery, SdkClient, SdkClientError } from "../../sdk/client";
|
|
46
|
-
import {
|
|
49
|
+
import {
|
|
50
|
+
buildToolCallStartUpdate,
|
|
51
|
+
mapAgentSessionEventToAcpSessionUpdates,
|
|
52
|
+
mapAgentWireEventPayloadToAcpSessionUpdates,
|
|
53
|
+
} from "./acp-event-mapper";
|
|
47
54
|
import { resolveAcpPermissionMode } from "./permission-mode";
|
|
48
55
|
import type { AcpStartupOptions } from "./startup-options";
|
|
49
56
|
import { ACP_TERMINAL_AUTH_FLAG } from "./terminal-auth";
|
|
@@ -52,6 +59,8 @@ const ACP_DEFAULT_MODE_ID = "default";
|
|
|
52
59
|
const ACP_PLAN_MODE_ID = "plan";
|
|
53
60
|
const MODE_CONFIG_ID = "mode";
|
|
54
61
|
const MODEL_CONFIG_ID = "model";
|
|
62
|
+
const MODEL_PRESET_CONFIG_KEY = "modelPreset";
|
|
63
|
+
const ACP_CUSTOM_MODEL_PRESET = "__custom__";
|
|
55
64
|
const THINKING_CONFIG_ID = "thinking";
|
|
56
65
|
const SESSION_PAGE_SIZE = 50;
|
|
57
66
|
export const ACP_BOOTSTRAP_RACE_GUARD_MS = 50;
|
|
@@ -59,11 +68,10 @@ const MAX_ACP_REPLAY_PAGES = 10_000;
|
|
|
59
68
|
|
|
60
69
|
type JsonObject = Record<string, unknown>;
|
|
61
70
|
/**
|
|
62
|
-
* ACP prompt completion is tied to
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* can never settle the waiter that acknowledgement creates.
|
|
71
|
+
* ACP prompt completion is tied to lifecycle activity observed after dispatch.
|
|
72
|
+
* A fast host may stream and finish a turn before acknowledging `turn.prompt`,
|
|
73
|
+
* so pre-ack activity is retained but cannot resolve the waiter until the
|
|
74
|
+
* acknowledgement supplies its command/turn correlation.
|
|
67
75
|
*/
|
|
68
76
|
interface PromptWaiter {
|
|
69
77
|
cancelRequested: boolean;
|
|
@@ -75,6 +83,7 @@ interface PromptWaiter {
|
|
|
75
83
|
boundary: number;
|
|
76
84
|
correlation: PromptCorrelation;
|
|
77
85
|
messageProgress?: { textEmitted: boolean; thoughtEmitted: boolean };
|
|
86
|
+
emittedAssistantText: string;
|
|
78
87
|
pendingTerminal?: PromptCorrelation;
|
|
79
88
|
resolve: (response: PromptResponse) => void;
|
|
80
89
|
reject: (error: Error) => void;
|
|
@@ -97,6 +106,10 @@ type SessionRecord = {
|
|
|
97
106
|
inboundSequence: number;
|
|
98
107
|
/** Updated at ingress so a prompt acknowledgement can distinguish a steer from a fresh turn. */
|
|
99
108
|
busy: boolean;
|
|
109
|
+
/** Start/update args retained because tool_execution_end does not carry them. */
|
|
110
|
+
toolArgs: Map<string, unknown>;
|
|
111
|
+
/** Actionable model-profile authentication failure detected before prompt dispatch. */
|
|
112
|
+
authFailure?: string;
|
|
100
113
|
activePrompt?: PromptWaiter;
|
|
101
114
|
};
|
|
102
115
|
type Endpoint = { url: string; token: string };
|
|
@@ -106,6 +119,9 @@ type BrokerSession = {
|
|
|
106
119
|
locator?: { repo?: string };
|
|
107
120
|
live?: boolean;
|
|
108
121
|
endpointGeneration?: number;
|
|
122
|
+
endpointMtimeMs?: number;
|
|
123
|
+
title?: string;
|
|
124
|
+
updatedAt?: string;
|
|
109
125
|
};
|
|
110
126
|
|
|
111
127
|
function parseAcpStartupOptions(value: unknown): AcpStartupOptions | undefined {
|
|
@@ -136,7 +152,12 @@ function aggregateAcpFailure(code: string, message: string, failures: unknown[])
|
|
|
136
152
|
}
|
|
137
153
|
|
|
138
154
|
/** Applies ACP's offset cursor after narrowing the broker listing to the requested cwd. */
|
|
139
|
-
export function paginateAcpSessions(
|
|
155
|
+
export function paginateAcpSessions(
|
|
156
|
+
listed: unknown[],
|
|
157
|
+
cwd: string | undefined,
|
|
158
|
+
offset: number,
|
|
159
|
+
sessionMetadata: ReadonlyMap<string, { title?: string; updatedAt?: string }> = new Map(),
|
|
160
|
+
): ListSessionsResponse {
|
|
140
161
|
const filtered = listed
|
|
141
162
|
.map(value => object(value) as BrokerSession | undefined)
|
|
142
163
|
.filter(
|
|
@@ -144,12 +165,28 @@ export function paginateAcpSessions(listed: unknown[], cwd: string | undefined,
|
|
|
144
165
|
typeof value?.sessionId === "string" && typeof value.locator?.repo === "string",
|
|
145
166
|
)
|
|
146
167
|
.filter(value => !cwd || value.locator.repo === cwd);
|
|
147
|
-
const sessions = filtered
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
168
|
+
const sessions = filtered.slice(offset, offset + SESSION_PAGE_SIZE).map(value => {
|
|
169
|
+
const metadata = sessionMetadata.get(value.sessionId);
|
|
170
|
+
const updatedAt =
|
|
171
|
+
typeof metadata?.updatedAt === "string"
|
|
172
|
+
? metadata.updatedAt
|
|
173
|
+
: typeof value.updatedAt === "string"
|
|
174
|
+
? value.updatedAt
|
|
175
|
+
: typeof value.endpointMtimeMs === "number" && Number.isFinite(value.endpointMtimeMs)
|
|
176
|
+
? new Date(value.endpointMtimeMs).toISOString()
|
|
177
|
+
: undefined;
|
|
178
|
+
return {
|
|
179
|
+
sessionId: value.sessionId,
|
|
180
|
+
cwd: value.locator.repo,
|
|
181
|
+
title:
|
|
182
|
+
typeof metadata?.title === "string" && metadata.title
|
|
183
|
+
? metadata.title
|
|
184
|
+
: typeof value.title === "string" && value.title
|
|
185
|
+
? value.title
|
|
186
|
+
: value.sessionId,
|
|
187
|
+
...(updatedAt ? { updatedAt } : {}),
|
|
188
|
+
} satisfies SessionInfo;
|
|
189
|
+
});
|
|
153
190
|
return {
|
|
154
191
|
sessions,
|
|
155
192
|
nextCursor: offset + sessions.length < filtered.length ? String(offset + sessions.length) : undefined,
|
|
@@ -180,6 +217,26 @@ function pageItems(value: unknown): unknown[] {
|
|
|
180
217
|
return Array.isArray(page?.items) ? page.items : [];
|
|
181
218
|
}
|
|
182
219
|
|
|
220
|
+
/** Build the ACP command palette from the shared builtins and live SDK skill state. */
|
|
221
|
+
export function acpAvailableCommandsFromSkills(query: unknown): AvailableCommand[] {
|
|
222
|
+
const commands = new Map<string, AvailableCommand>();
|
|
223
|
+
for (const item of pageItems(query)) {
|
|
224
|
+
const skill = object(item);
|
|
225
|
+
if (typeof skill?.name !== "string" || !skill.name) continue;
|
|
226
|
+
const name = `skill:${skill.name}`;
|
|
227
|
+
if (commands.has(name)) continue;
|
|
228
|
+
commands.set(name, {
|
|
229
|
+
name,
|
|
230
|
+
description:
|
|
231
|
+
typeof skill.description === "string" && skill.description
|
|
232
|
+
? skill.description
|
|
233
|
+
: `Run the ${skill.name} skill`,
|
|
234
|
+
input: { hint: "[request]" },
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
return [...commands.values()];
|
|
238
|
+
}
|
|
239
|
+
|
|
183
240
|
function correlationFrom(...values: unknown[]): PromptCorrelation {
|
|
184
241
|
const correlation: PromptCorrelation = {};
|
|
185
242
|
for (const value of values) {
|
|
@@ -268,6 +325,7 @@ function receivedSdkEvent(frame: JsonObject): ReceivedSdkEvent | undefined {
|
|
|
268
325
|
const type = frame.state === "busy" ? "agent_start" : frame.state === "idle" ? "agent_end" : undefined;
|
|
269
326
|
return type ? { event: { type, ...correlationFrom(frame) } } : undefined;
|
|
270
327
|
}
|
|
328
|
+
if (frame.type === "agent_start" || frame.type === "agent_end") return { event: frame };
|
|
271
329
|
if (frame.type !== "event") return undefined;
|
|
272
330
|
const payload = object(frame.payload);
|
|
273
331
|
if (!payload) return undefined;
|
|
@@ -331,6 +389,19 @@ function configValues(query: unknown): Map<string, string> {
|
|
|
331
389
|
return values;
|
|
332
390
|
}
|
|
333
391
|
|
|
392
|
+
function modelPresetConfigOptions(query: unknown, current: string): { value: string; name: string }[] {
|
|
393
|
+
const options = new Map<string, string>();
|
|
394
|
+
for (const item of pageItems(query)) {
|
|
395
|
+
const profile = object(item);
|
|
396
|
+
if (!profile || typeof profile.id !== "string") continue;
|
|
397
|
+
if (profile.available === false && profile.id !== current) continue;
|
|
398
|
+
options.set(profile.id, typeof profile.displayName === "string" ? profile.displayName : profile.id);
|
|
399
|
+
}
|
|
400
|
+
if (current === ACP_CUSTOM_MODEL_PRESET) options.set(ACP_CUSTOM_MODEL_PRESET, "Custom (current model)");
|
|
401
|
+
else if (!options.has(current)) options.set(current, current);
|
|
402
|
+
return [...options].map(([value, name]) => ({ value, name }));
|
|
403
|
+
}
|
|
404
|
+
|
|
334
405
|
function modelConfigOptions(query: unknown, current: string | undefined): { value: string; name: string }[] {
|
|
335
406
|
const options = new Map<string, string>();
|
|
336
407
|
for (const item of pageItems(query)) {
|
|
@@ -348,9 +419,10 @@ const THINKING_CONFIG_OPTIONS = ["off", "minimal", "low", "medium", "high", "xhi
|
|
|
348
419
|
name: value,
|
|
349
420
|
}));
|
|
350
421
|
|
|
351
|
-
/** Maps live canonical SDK config and model
|
|
352
|
-
export function acpSessionStateFromConfig(query: unknown,
|
|
422
|
+
/** Maps live canonical SDK config and the selected model catalog into the ACP 1.2.1 session state surface. */
|
|
423
|
+
export function acpSessionStateFromConfig(query: unknown, modelCatalogQuery?: unknown, modelPreset?: string) {
|
|
353
424
|
const values = configValues(query);
|
|
425
|
+
const useModelPresets = modelPreset !== undefined;
|
|
354
426
|
const currentModeId = values.get(MODE_CONFIG_ID) === ACP_PLAN_MODE_ID ? ACP_PLAN_MODE_ID : ACP_DEFAULT_MODE_ID;
|
|
355
427
|
return {
|
|
356
428
|
configOptions: [
|
|
@@ -365,15 +437,28 @@ export function acpSessionStateFromConfig(query: unknown, modelsQuery?: unknown)
|
|
|
365
437
|
],
|
|
366
438
|
},
|
|
367
439
|
...ACP_CONFIG_OPTIONS.flatMap(option => {
|
|
368
|
-
const value =
|
|
440
|
+
const value =
|
|
441
|
+
option.id === MODEL_CONFIG_ID && useModelPresets
|
|
442
|
+
? (values.get(MODEL_PRESET_CONFIG_KEY) ?? ACP_CUSTOM_MODEL_PRESET)
|
|
443
|
+
: values.get(option.id);
|
|
369
444
|
if (value === undefined) return [];
|
|
370
445
|
const options =
|
|
371
446
|
option.id === MODEL_CONFIG_ID
|
|
372
|
-
?
|
|
447
|
+
? useModelPresets
|
|
448
|
+
? modelPresetConfigOptions(modelCatalogQuery, value)
|
|
449
|
+
: modelConfigOptions(modelCatalogQuery, value)
|
|
373
450
|
: option.id === THINKING_CONFIG_ID
|
|
374
451
|
? THINKING_CONFIG_OPTIONS
|
|
375
452
|
: [...option.options];
|
|
376
|
-
return [
|
|
453
|
+
return [
|
|
454
|
+
{
|
|
455
|
+
...option,
|
|
456
|
+
...(option.id === MODEL_CONFIG_ID && useModelPresets ? { name: "Preset" } : {}),
|
|
457
|
+
type: "select" as const,
|
|
458
|
+
currentValue: value,
|
|
459
|
+
options,
|
|
460
|
+
},
|
|
461
|
+
];
|
|
377
462
|
}),
|
|
378
463
|
],
|
|
379
464
|
modes: {
|
|
@@ -572,6 +657,7 @@ export class AcpAgent implements Agent {
|
|
|
572
657
|
readonly #attaching = new Map<string, PendingAttachment>();
|
|
573
658
|
readonly #resolvingExisting = new Map<string, PendingAttachment>();
|
|
574
659
|
readonly #knownSessionCwds = new Map<string, string>();
|
|
660
|
+
readonly #knownSessionMetadata = new Map<string, { title?: string; updatedAt?: string }>();
|
|
575
661
|
readonly #pendingCloseIdempotencyKeys = new Map<string, string>();
|
|
576
662
|
readonly #sessionEpochs = new Map<string, number>();
|
|
577
663
|
readonly #tearingDown = new Map<string, number>();
|
|
@@ -624,7 +710,14 @@ export class AcpAgent implements Agent {
|
|
|
624
710
|
agentCapabilities: {
|
|
625
711
|
loadSession: true,
|
|
626
712
|
promptCapabilities: { embeddedContext: true, image: true },
|
|
627
|
-
|
|
713
|
+
mcpCapabilities: { http: true, sse: true },
|
|
714
|
+
sessionCapabilities: {
|
|
715
|
+
list: {},
|
|
716
|
+
fork: {},
|
|
717
|
+
resume: {},
|
|
718
|
+
close: {},
|
|
719
|
+
delete: {},
|
|
720
|
+
},
|
|
628
721
|
},
|
|
629
722
|
};
|
|
630
723
|
}
|
|
@@ -636,16 +729,19 @@ export class AcpAgent implements Agent {
|
|
|
636
729
|
}
|
|
637
730
|
|
|
638
731
|
async newSession(params: NewSessionRequest): Promise<NewSessionResponse> {
|
|
639
|
-
this.#
|
|
732
|
+
const mcpServers = this.#mcpServers(params);
|
|
640
733
|
this.#assertAbsoluteCwd(params.cwd);
|
|
641
|
-
|
|
734
|
+
this.#assertNoAdditionalDirectories(params.additionalDirectories);
|
|
735
|
+
const result = await this.#launchSessionWithMcp(
|
|
642
736
|
"session.create",
|
|
643
737
|
{
|
|
644
738
|
cwd: params.cwd,
|
|
645
739
|
target: { path: params.cwd },
|
|
646
740
|
...(this.#startupOptions?.modelPreset ? { modelPreset: this.#startupOptions.modelPreset } : {}),
|
|
741
|
+
...(mcpServers.length > 0 ? { mcpServers, readinessTimeoutMs: ACP_MCP_LIFECYCLE_TIMEOUT_MS } : {}),
|
|
647
742
|
},
|
|
648
743
|
randomUUID(),
|
|
744
|
+
mcpServers,
|
|
649
745
|
);
|
|
650
746
|
const id = sessionId(result);
|
|
651
747
|
this.#knownSessionCwds.set(id, params.cwd);
|
|
@@ -653,7 +749,7 @@ export class AcpAgent implements Agent {
|
|
|
653
749
|
await this.#attach(id, params.cwd, endpoint(result));
|
|
654
750
|
await applyAcpStartupOptions(this.#adapter(id), this.#startupOptions);
|
|
655
751
|
this.#scheduleBootstrap(id);
|
|
656
|
-
return { sessionId: id, ...(await this.#sessionState(id)) };
|
|
752
|
+
return { sessionId: id, ...(await this.#sessionState(id, true)) };
|
|
657
753
|
} catch (error) {
|
|
658
754
|
await this.#discardNewSession(id);
|
|
659
755
|
throw error;
|
|
@@ -661,35 +757,40 @@ export class AcpAgent implements Agent {
|
|
|
661
757
|
}
|
|
662
758
|
|
|
663
759
|
async loadSession(params: LoadSessionRequest): Promise<LoadSessionResponse> {
|
|
664
|
-
this.#
|
|
760
|
+
const mcpServers = this.#mcpServers(params);
|
|
665
761
|
this.#assertAbsoluteCwd(params.cwd);
|
|
666
|
-
|
|
762
|
+
this.#assertNoAdditionalDirectories(params.additionalDirectories);
|
|
763
|
+
await this.#attachExisting(params.sessionId, params.cwd, mcpServers);
|
|
667
764
|
await this.#replaySession(params.sessionId);
|
|
668
765
|
this.#scheduleBootstrap(params.sessionId);
|
|
669
766
|
return await this.#sessionState(params.sessionId);
|
|
670
767
|
}
|
|
671
768
|
|
|
672
769
|
async resumeSession(params: ResumeSessionRequest): Promise<ResumeSessionResponse> {
|
|
673
|
-
this.#
|
|
770
|
+
const mcpServers = this.#mcpServers(params);
|
|
674
771
|
this.#assertAbsoluteCwd(params.cwd);
|
|
675
|
-
|
|
772
|
+
this.#assertNoAdditionalDirectories(params.additionalDirectories);
|
|
773
|
+
await this.#attachExisting(params.sessionId, params.cwd, mcpServers);
|
|
676
774
|
this.#scheduleBootstrap(params.sessionId);
|
|
677
775
|
return await this.#sessionState(params.sessionId);
|
|
678
776
|
}
|
|
679
777
|
|
|
680
778
|
async unstable_forkSession(params: ForkSessionRequest): Promise<ForkSessionResponse> {
|
|
681
|
-
this.#
|
|
779
|
+
const mcpServers = this.#mcpServers(params);
|
|
682
780
|
this.#assertAbsoluteCwd(params.cwd);
|
|
781
|
+
this.#assertNoAdditionalDirectories(params.additionalDirectories);
|
|
683
782
|
const source = await this.#resolveSavedSession(params.sessionId, params.cwd);
|
|
684
|
-
const result = await
|
|
783
|
+
const result = await this.#launchSessionWithMcp(
|
|
685
784
|
"session.fork",
|
|
686
785
|
{
|
|
687
786
|
cwd: params.cwd,
|
|
688
787
|
sourceSessionId: params.sessionId,
|
|
689
788
|
sourceSessionPath: source,
|
|
690
789
|
target: { path: params.cwd },
|
|
790
|
+
...(mcpServers.length > 0 ? { mcpServers, readinessTimeoutMs: ACP_MCP_LIFECYCLE_TIMEOUT_MS } : {}),
|
|
691
791
|
},
|
|
692
792
|
randomUUID(),
|
|
793
|
+
mcpServers,
|
|
693
794
|
);
|
|
694
795
|
const id = sessionId(result);
|
|
695
796
|
this.#knownSessionCwds.set(id, params.cwd);
|
|
@@ -730,7 +831,12 @@ export class AcpAgent implements Agent {
|
|
|
730
831
|
this.#knownSessionCwds.set(candidate.sessionId, params.cwd);
|
|
731
832
|
}
|
|
732
833
|
}
|
|
733
|
-
return paginateAcpSessions(
|
|
834
|
+
return paginateAcpSessions(
|
|
835
|
+
listed,
|
|
836
|
+
params.cwd ?? undefined,
|
|
837
|
+
this.#cursor(params.cursor),
|
|
838
|
+
this.#knownSessionMetadata,
|
|
839
|
+
);
|
|
734
840
|
}
|
|
735
841
|
|
|
736
842
|
closeSession(params: CloseSessionRequest): Promise<CloseSessionResponse> {
|
|
@@ -765,6 +871,7 @@ export class AcpAgent implements Agent {
|
|
|
765
871
|
} catch (error) {
|
|
766
872
|
if (error instanceof AcpSdkAdapterError && error.code === "not_found") {
|
|
767
873
|
this.#knownSessionCwds.delete(params.sessionId);
|
|
874
|
+
this.#knownSessionMetadata.delete(params.sessionId);
|
|
768
875
|
return {};
|
|
769
876
|
}
|
|
770
877
|
throw error;
|
|
@@ -775,6 +882,7 @@ export class AcpAgent implements Agent {
|
|
|
775
882
|
this.#lifecycleIdempotencyKey(params.sessionId, "session.delete"),
|
|
776
883
|
);
|
|
777
884
|
this.#knownSessionCwds.delete(params.sessionId);
|
|
885
|
+
this.#knownSessionMetadata.delete(params.sessionId);
|
|
778
886
|
return {};
|
|
779
887
|
} finally {
|
|
780
888
|
this.#finishTeardown(params.sessionId);
|
|
@@ -800,7 +908,11 @@ export class AcpAgent implements Agent {
|
|
|
800
908
|
await this.setSessionMode({ sessionId: params.sessionId, modeId: params.value });
|
|
801
909
|
break;
|
|
802
910
|
case MODEL_CONFIG_ID:
|
|
803
|
-
|
|
911
|
+
if (this.#startupOptions?.modelPreset === undefined) {
|
|
912
|
+
await this.#adapter(params.sessionId).setModel(params.value);
|
|
913
|
+
} else if (params.value !== ACP_CUSTOM_MODEL_PRESET) {
|
|
914
|
+
await this.#adapter(params.sessionId).control("model.profile.set", { id: params.value });
|
|
915
|
+
}
|
|
804
916
|
break;
|
|
805
917
|
case THINKING_CONFIG_ID:
|
|
806
918
|
await this.#adapter(params.sessionId).control("thinking.set", { level: params.value });
|
|
@@ -823,6 +935,7 @@ export class AcpAgent implements Agent {
|
|
|
823
935
|
const record = this.#sessions.get(params.sessionId);
|
|
824
936
|
if (!record) throw new AcpSdkAdapterError("not_found", `Unsupported ACP session: ${params.sessionId}`);
|
|
825
937
|
if (record.activePrompt) throw new AcpSdkAdapterError("conflict", "ACP session already has an active prompt.");
|
|
938
|
+
if (record.authFailure) throw new AcpSdkAdapterError("authentication_failed", record.authFailure);
|
|
826
939
|
const payload = acpPromptPayload(params.prompt);
|
|
827
940
|
let waiter!: PromptWaiter;
|
|
828
941
|
const response = new Promise<PromptResponse>((resolve, reject) => {
|
|
@@ -833,6 +946,7 @@ export class AcpAgent implements Agent {
|
|
|
833
946
|
steeringAtAcknowledgement: record.busy,
|
|
834
947
|
boundary: record.inboundSequence,
|
|
835
948
|
correlation: {},
|
|
949
|
+
emittedAssistantText: "",
|
|
836
950
|
resolve,
|
|
837
951
|
reject,
|
|
838
952
|
};
|
|
@@ -875,6 +989,14 @@ export class AcpAgent implements Agent {
|
|
|
875
989
|
|
|
876
990
|
async extMethod(method: string, params: JsonObject): Promise<JsonObject> {
|
|
877
991
|
try {
|
|
992
|
+
if (method === "session/set_model") {
|
|
993
|
+
const sessionId = typeof params.sessionId === "string" ? params.sessionId : undefined;
|
|
994
|
+
const modelId = typeof params.modelId === "string" ? params.modelId : undefined;
|
|
995
|
+
if (!sessionId) throw new AcpSdkAdapterError("invalid_input", "sessionId is required.");
|
|
996
|
+
if (!modelId) throw new AcpSdkAdapterError("invalid_input", "modelId is required.");
|
|
997
|
+
await this.setSessionConfigOption({ sessionId, configId: MODEL_CONFIG_ID, value: modelId });
|
|
998
|
+
return {};
|
|
999
|
+
}
|
|
878
1000
|
if (method === "_gjc/sdk/global") {
|
|
879
1001
|
const result = await (await this.#brokerAdapter()).handle(method, params);
|
|
880
1002
|
return object(result) ?? {};
|
|
@@ -946,12 +1068,14 @@ export class AcpAgent implements Agent {
|
|
|
946
1068
|
return !["terminal_uncertain", "cleanup_pending", "broker_restarting", "unavailable"].includes(error.code);
|
|
947
1069
|
}
|
|
948
1070
|
|
|
949
|
-
async #attachExisting(id: string, cwd: string): Promise<void> {
|
|
1071
|
+
async #attachExisting(id: string, cwd: string, mcpServers: SessionLifecycleMcpServer[] = []): Promise<void> {
|
|
950
1072
|
const epoch = this.#sessionEpoch(id);
|
|
951
1073
|
const attached = this.#sessions.get(id);
|
|
952
1074
|
if (attached) {
|
|
953
1075
|
if (path.resolve(attached.cwd) !== path.resolve(cwd))
|
|
954
1076
|
throw new AcpSdkAdapterError("conflict", `ACP session ${id} has conflicting cwd authority.`);
|
|
1077
|
+
if (mcpServers.length > 0)
|
|
1078
|
+
throw new AcpSdkAdapterError("conflict", `ACP session ${id} already has immutable MCP configuration.`);
|
|
955
1079
|
return;
|
|
956
1080
|
}
|
|
957
1081
|
const knownCwd = this.#knownSessionCwds.get(id);
|
|
@@ -968,7 +1092,7 @@ export class AcpAgent implements Agent {
|
|
|
968
1092
|
return;
|
|
969
1093
|
}
|
|
970
1094
|
|
|
971
|
-
const task = this.#resolveExistingAttachment(id, cwd, epoch);
|
|
1095
|
+
const task = this.#resolveExistingAttachment(id, cwd, epoch, mcpServers);
|
|
972
1096
|
const pending = { epoch, task };
|
|
973
1097
|
this.#resolvingExisting.set(id, pending);
|
|
974
1098
|
try {
|
|
@@ -979,11 +1103,18 @@ export class AcpAgent implements Agent {
|
|
|
979
1103
|
}
|
|
980
1104
|
}
|
|
981
1105
|
|
|
982
|
-
async #resolveExistingAttachment(
|
|
1106
|
+
async #resolveExistingAttachment(
|
|
1107
|
+
id: string,
|
|
1108
|
+
cwd: string,
|
|
1109
|
+
epoch: number,
|
|
1110
|
+
mcpServers: SessionLifecycleMcpServer[],
|
|
1111
|
+
): Promise<void> {
|
|
983
1112
|
this.#assertSessionEpoch(id, epoch);
|
|
984
1113
|
const indexed = await this.#scopedBrokerSession(id, cwd);
|
|
985
1114
|
this.#assertSessionEpoch(id, epoch);
|
|
986
1115
|
if (indexed?.live) {
|
|
1116
|
+
if (mcpServers.length > 0)
|
|
1117
|
+
throw new AcpSdkAdapterError("conflict", `ACP session ${id} already has immutable MCP configuration.`);
|
|
987
1118
|
const result = await this.#brokerEndpoint(id, indexed.endpointGeneration);
|
|
988
1119
|
this.#assertSessionEpoch(id, epoch);
|
|
989
1120
|
await this.#attach(id, cwd, endpoint(result), epoch);
|
|
@@ -992,10 +1123,17 @@ export class AcpAgent implements Agent {
|
|
|
992
1123
|
|
|
993
1124
|
const saved = await this.#resolveSavedSession(id, cwd);
|
|
994
1125
|
this.#assertSessionEpoch(id, epoch);
|
|
995
|
-
const result = await
|
|
1126
|
+
const result = await this.#launchSessionWithMcp(
|
|
996
1127
|
"session.resume",
|
|
997
|
-
{
|
|
1128
|
+
{
|
|
1129
|
+
cwd,
|
|
1130
|
+
sessionId: id,
|
|
1131
|
+
sessionPath: saved,
|
|
1132
|
+
target: { path: cwd },
|
|
1133
|
+
...(mcpServers.length > 0 ? { mcpServers, readinessTimeoutMs: ACP_MCP_LIFECYCLE_TIMEOUT_MS } : {}),
|
|
1134
|
+
},
|
|
998
1135
|
randomUUID(),
|
|
1136
|
+
mcpServers,
|
|
999
1137
|
);
|
|
1000
1138
|
this.#assertSessionEpoch(id, epoch);
|
|
1001
1139
|
await this.#attach(id, cwd, endpoint(result), epoch);
|
|
@@ -1065,6 +1203,7 @@ export class AcpAgent implements Agent {
|
|
|
1065
1203
|
frameTail: Promise.resolve(),
|
|
1066
1204
|
inboundSequence: 0,
|
|
1067
1205
|
busy: false,
|
|
1206
|
+
toolArgs: new Map(),
|
|
1068
1207
|
};
|
|
1069
1208
|
record.unsubscribe = adapter.onFrame(frame => this.#enqueueSdkFrame(id, adapter!, frame));
|
|
1070
1209
|
record.reconnectUnsubscribe = adapter.onReconnectFailed(error =>
|
|
@@ -1117,6 +1256,7 @@ export class AcpAgent implements Agent {
|
|
|
1117
1256
|
async #discardNewSession(id: string): Promise<void> {
|
|
1118
1257
|
await this.#teardownSession(id, "discarded", true);
|
|
1119
1258
|
this.#knownSessionCwds.delete(id);
|
|
1259
|
+
this.#knownSessionMetadata.delete(id);
|
|
1120
1260
|
}
|
|
1121
1261
|
|
|
1122
1262
|
async #closeOwnedSession(id: string): Promise<CloseSessionResponse> {
|
|
@@ -1128,6 +1268,7 @@ export class AcpAgent implements Agent {
|
|
|
1128
1268
|
if (attaching) await Promise.allSettled([attaching.task]);
|
|
1129
1269
|
await this.#teardownSession(id, "closed", true);
|
|
1130
1270
|
this.#knownSessionCwds.delete(id);
|
|
1271
|
+
this.#knownSessionMetadata.delete(id);
|
|
1131
1272
|
return {};
|
|
1132
1273
|
} finally {
|
|
1133
1274
|
this.#finishTeardown(id);
|
|
@@ -1307,13 +1448,23 @@ export class AcpAgent implements Agent {
|
|
|
1307
1448
|
const received = receivedSdkEvent(frame);
|
|
1308
1449
|
if (!received) return;
|
|
1309
1450
|
const { event, wirePayload } = received;
|
|
1451
|
+
const toolCallId = typeof event.toolCallId === "string" ? event.toolCallId : undefined;
|
|
1452
|
+
if (
|
|
1453
|
+
toolCallId &&
|
|
1454
|
+
(event.type === "tool_execution_start" || event.type === "tool_execution_update") &&
|
|
1455
|
+
"args" in event
|
|
1456
|
+
) {
|
|
1457
|
+
record.toolArgs.set(toolCallId, event.args);
|
|
1458
|
+
}
|
|
1310
1459
|
const correlation = correlationFrom(frame, event);
|
|
1311
1460
|
const activePrompt = record.activePrompt;
|
|
1312
1461
|
// Prompt ownership is updated before publishing client notifications, but a
|
|
1313
|
-
// terminal waiter is resolved only after
|
|
1314
|
-
//
|
|
1462
|
+
// terminal waiter is resolved only after acknowledgement and publication
|
|
1463
|
+
// succeed. Fast hosts may emit the whole turn before acknowledging the
|
|
1464
|
+
// control request; retaining that activity prevents an idle ACP prompt from
|
|
1465
|
+
// remaining open forever.
|
|
1315
1466
|
if (
|
|
1316
|
-
activePrompt
|
|
1467
|
+
activePrompt &&
|
|
1317
1468
|
sequence > activePrompt.boundary &&
|
|
1318
1469
|
!correlationsConflict(activePrompt.correlation, correlation)
|
|
1319
1470
|
) {
|
|
@@ -1322,6 +1473,8 @@ export class AcpAgent implements Agent {
|
|
|
1322
1473
|
} else if (event.type === "agent_end") {
|
|
1323
1474
|
if (
|
|
1324
1475
|
correlationsMatch(activePrompt.correlation, correlation) ||
|
|
1476
|
+
correlation.commandId !== undefined ||
|
|
1477
|
+
correlation.turnId !== undefined ||
|
|
1325
1478
|
activePrompt.activityObserved ||
|
|
1326
1479
|
activePrompt.steeringAtAcknowledgement
|
|
1327
1480
|
)
|
|
@@ -1331,17 +1484,69 @@ export class AcpAgent implements Agent {
|
|
|
1331
1484
|
if (wirePayload) {
|
|
1332
1485
|
for (const notification of mapAgentWireEventPayloadToAcpSessionUpdates(wirePayload as never, id, {
|
|
1333
1486
|
cwd: record.cwd,
|
|
1487
|
+
getToolArgs: id => record.toolArgs.get(id),
|
|
1334
1488
|
getMessageProgress: message => {
|
|
1335
1489
|
if (!activePrompt || !object(message)) return undefined;
|
|
1336
1490
|
activePrompt.messageProgress ??= { textEmitted: false, thoughtEmitted: false };
|
|
1337
1491
|
return activePrompt.messageProgress;
|
|
1338
1492
|
},
|
|
1339
|
-
}))
|
|
1493
|
+
})) {
|
|
1494
|
+
if (
|
|
1495
|
+
activePrompt &&
|
|
1496
|
+
notification.update.sessionUpdate === "agent_message_chunk" &&
|
|
1497
|
+
notification.update.content.type === "text"
|
|
1498
|
+
)
|
|
1499
|
+
activePrompt.emittedAssistantText += notification.update.content.text;
|
|
1340
1500
|
await this.#publishSessionUpdate(id, notification, adapter);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
if (toolCallId && event.type === "tool_execution_end") record.toolArgs.delete(toolCallId);
|
|
1504
|
+
if (event.type === "agent_start") {
|
|
1505
|
+
await this.#publishSessionUpdate(
|
|
1506
|
+
id,
|
|
1507
|
+
{
|
|
1508
|
+
sessionId: id,
|
|
1509
|
+
update: {
|
|
1510
|
+
sessionUpdate: "session_info_update",
|
|
1511
|
+
updatedAt: new Date().toISOString(),
|
|
1512
|
+
_meta: { gjcPhase: "working", running: true, gjcRunning: true },
|
|
1513
|
+
},
|
|
1514
|
+
},
|
|
1515
|
+
adapter,
|
|
1516
|
+
);
|
|
1341
1517
|
}
|
|
1342
1518
|
if (event.type === "message_end" && object(event.message)?.role === "assistant" && activePrompt)
|
|
1343
1519
|
activePrompt.messageProgress = undefined;
|
|
1344
|
-
if (event.type === "agent_end")
|
|
1520
|
+
if (event.type === "agent_end") {
|
|
1521
|
+
const finalText = typeof event.finalText === "string" ? event.finalText : "";
|
|
1522
|
+
if (activePrompt && finalText) {
|
|
1523
|
+
const resolution = resolveAcpFinalText(activePrompt.emittedAssistantText, finalText);
|
|
1524
|
+
if (resolution.kind === "emit") {
|
|
1525
|
+
activePrompt.emittedAssistantText += resolution.text;
|
|
1526
|
+
await this.#publishSessionUpdate(
|
|
1527
|
+
id,
|
|
1528
|
+
{
|
|
1529
|
+
sessionId: id,
|
|
1530
|
+
update: {
|
|
1531
|
+
sessionUpdate: "agent_message_chunk",
|
|
1532
|
+
content: { type: "text", text: resolution.text },
|
|
1533
|
+
...(resolution.final.truncated ? { _meta: { gjcFinalTextTruncated: true } } : {}),
|
|
1534
|
+
},
|
|
1535
|
+
},
|
|
1536
|
+
adapter,
|
|
1537
|
+
);
|
|
1538
|
+
} else if (resolution.kind === "divergent") {
|
|
1539
|
+
logger.warn("acp_final_text_diverged", {
|
|
1540
|
+
sessionId: id,
|
|
1541
|
+
...(activePrompt.correlation.commandId ? { commandId: activePrompt.correlation.commandId } : {}),
|
|
1542
|
+
...(activePrompt.correlation.turnId ? { turnId: activePrompt.correlation.turnId } : {}),
|
|
1543
|
+
streamedLength: activePrompt.emittedAssistantText.length,
|
|
1544
|
+
finalLength: resolution.final.text.length,
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
await this.#emitEndOfTurnUpdates(id, adapter);
|
|
1549
|
+
}
|
|
1345
1550
|
if (activePrompt) this.#settlePrompt(record, activePrompt);
|
|
1346
1551
|
}
|
|
1347
1552
|
|
|
@@ -1360,6 +1565,7 @@ export class AcpAgent implements Agent {
|
|
|
1360
1565
|
|
|
1361
1566
|
async #emitEndOfTurnUpdates(id: string, adapter: AcpSdkAdapter): Promise<void> {
|
|
1362
1567
|
let usage: JsonObject | undefined;
|
|
1568
|
+
let title: string | undefined;
|
|
1363
1569
|
try {
|
|
1364
1570
|
const response = object(await adapter.query("context.get"));
|
|
1365
1571
|
const result = object(response?.result) ?? response;
|
|
@@ -1367,6 +1573,15 @@ export class AcpAgent implements Agent {
|
|
|
1367
1573
|
} catch {
|
|
1368
1574
|
// Context usage is advisory ACP metadata; prompt completion remains authoritative.
|
|
1369
1575
|
}
|
|
1576
|
+
try {
|
|
1577
|
+
const response = object(await adapter.query("session.metadata"));
|
|
1578
|
+
const result = object(response?.result) ?? response;
|
|
1579
|
+
const metadata = pageItems(result)[0];
|
|
1580
|
+
const item = object(metadata);
|
|
1581
|
+
if (typeof item?.name === "string" && item.name) title = item.name;
|
|
1582
|
+
} catch {
|
|
1583
|
+
// Session naming is advisory; prompt completion remains authoritative.
|
|
1584
|
+
}
|
|
1370
1585
|
if (typeof usage?.tokens === "number" && typeof usage.contextWindow === "number") {
|
|
1371
1586
|
await this.#publishSessionUpdate(
|
|
1372
1587
|
id,
|
|
@@ -1381,13 +1596,16 @@ export class AcpAgent implements Agent {
|
|
|
1381
1596
|
adapter,
|
|
1382
1597
|
);
|
|
1383
1598
|
}
|
|
1599
|
+
const updatedAt = new Date().toISOString();
|
|
1600
|
+
this.#knownSessionMetadata.set(id, { ...(title ? { title } : {}), updatedAt });
|
|
1384
1601
|
await this.#publishSessionUpdate(
|
|
1385
1602
|
id,
|
|
1386
1603
|
{
|
|
1387
1604
|
sessionId: id,
|
|
1388
1605
|
update: {
|
|
1389
1606
|
sessionUpdate: "session_info_update",
|
|
1390
|
-
|
|
1607
|
+
...(title ? { title } : {}),
|
|
1608
|
+
updatedAt,
|
|
1391
1609
|
_meta: { gjcPhase: "idle", running: false, gjcRunning: false },
|
|
1392
1610
|
},
|
|
1393
1611
|
},
|
|
@@ -1411,27 +1629,68 @@ export class AcpAgent implements Agent {
|
|
|
1411
1629
|
}
|
|
1412
1630
|
}
|
|
1413
1631
|
|
|
1414
|
-
async #sessionState(
|
|
1632
|
+
async #sessionState(
|
|
1633
|
+
id: string,
|
|
1634
|
+
rejectUnavailableStartupPreset = false,
|
|
1635
|
+
): Promise<Pick<NewSessionResponse, "configOptions" | "modes">> {
|
|
1415
1636
|
const record = this.#sessions.get(id);
|
|
1416
1637
|
if (!record) throw new AcpSdkAdapterError("not_found", `Unsupported ACP session: ${id}`);
|
|
1417
|
-
const
|
|
1638
|
+
const modelPreset = this.#startupOptions?.modelPreset;
|
|
1639
|
+
const [config, modelCatalog] = await Promise.all([
|
|
1418
1640
|
record.adapter.query("config.list/get"),
|
|
1419
|
-
record.adapter.query("models.list/current"),
|
|
1641
|
+
record.adapter.query(modelPreset === undefined ? "models.list/current" : "models.profiles.list"),
|
|
1420
1642
|
]);
|
|
1421
|
-
|
|
1643
|
+
record.authFailure = undefined;
|
|
1644
|
+
if (modelPreset !== undefined) {
|
|
1645
|
+
const activePreset = configValues(config).get(MODEL_PRESET_CONFIG_KEY);
|
|
1646
|
+
const activeProfile = pageItems(modelCatalog)
|
|
1647
|
+
.map(item => object(item))
|
|
1648
|
+
.find(item => item?.id === activePreset);
|
|
1649
|
+
if (activePreset && activeProfile?.available === false) {
|
|
1650
|
+
record.authFailure =
|
|
1651
|
+
`Model preset "${activePreset}" has no usable provider credentials. ` +
|
|
1652
|
+
"Authenticate the required provider in Gajae Code or select an available preset before prompting.";
|
|
1653
|
+
if (rejectUnavailableStartupPreset && activePreset === modelPreset)
|
|
1654
|
+
throw new AcpSdkAdapterError("authentication_failed", record.authFailure);
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
return acpSessionStateFromConfig(config, modelCatalog, modelPreset);
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
async #publishAvailableCommands(id: string, adapter: AcpSdkAdapter): Promise<void> {
|
|
1661
|
+
let skills: unknown;
|
|
1662
|
+
try {
|
|
1663
|
+
skills = await adapter.query("skill.list/state");
|
|
1664
|
+
} catch {
|
|
1665
|
+
// Builtins remain useful when an older SDK host cannot expose skill state.
|
|
1666
|
+
}
|
|
1667
|
+
await this.#publishSessionUpdate(
|
|
1668
|
+
id,
|
|
1669
|
+
{
|
|
1670
|
+
sessionId: id,
|
|
1671
|
+
update: {
|
|
1672
|
+
sessionUpdate: "available_commands_update",
|
|
1673
|
+
availableCommands: acpAvailableCommandsFromSkills(skills),
|
|
1674
|
+
},
|
|
1675
|
+
},
|
|
1676
|
+
adapter,
|
|
1677
|
+
);
|
|
1422
1678
|
}
|
|
1423
1679
|
|
|
1424
1680
|
async #replaySession(id: string): Promise<void> {
|
|
1425
1681
|
const adapter = this.#adapter(id);
|
|
1682
|
+
const record = this.#sessions.get(id);
|
|
1683
|
+
if (!record) return;
|
|
1426
1684
|
let cursor: string | undefined;
|
|
1427
1685
|
let imageLimitationReported = false;
|
|
1686
|
+
const replayTools = new Map<string, { name: string; args: unknown }>();
|
|
1428
1687
|
for (let pageCount = 0; pageCount < MAX_ACP_REPLAY_PAGES; pageCount++) {
|
|
1429
1688
|
const response = object(await adapter.query("transcript.list", {}, cursor));
|
|
1430
1689
|
const result = object(response?.result) ?? response;
|
|
1431
1690
|
const page = object(result?.page);
|
|
1432
1691
|
for (const item of Array.isArray(page?.items) ? page.items : []) {
|
|
1433
1692
|
const message = object(item);
|
|
1434
|
-
if (!message
|
|
1693
|
+
if (!message) continue;
|
|
1435
1694
|
const content = transcriptReplayContent(message);
|
|
1436
1695
|
if (!imageLimitationReported) {
|
|
1437
1696
|
imageLimitationReported = true;
|
|
@@ -1448,6 +1707,95 @@ export class AcpAgent implements Agent {
|
|
|
1448
1707
|
);
|
|
1449
1708
|
}
|
|
1450
1709
|
const messageId = typeof message.id === "string" ? message.id : undefined;
|
|
1710
|
+
const richContent = Array.isArray(message.content) ? message.content : undefined;
|
|
1711
|
+
if ((message.role === "user" || message.role === "assistant") && richContent) {
|
|
1712
|
+
for (const rawBlock of richContent) {
|
|
1713
|
+
const block = object(rawBlock);
|
|
1714
|
+
if (!block || typeof block.type !== "string") continue;
|
|
1715
|
+
if (block.type === "text" && typeof block.text === "string" && block.text.length > 0) {
|
|
1716
|
+
await this.#publishSessionUpdate(
|
|
1717
|
+
id,
|
|
1718
|
+
{
|
|
1719
|
+
sessionId: id,
|
|
1720
|
+
update: {
|
|
1721
|
+
sessionUpdate: message.role === "user" ? "user_message_chunk" : "agent_message_chunk",
|
|
1722
|
+
content: { type: "text", text: block.text },
|
|
1723
|
+
...(messageId ? { messageId } : {}),
|
|
1724
|
+
},
|
|
1725
|
+
},
|
|
1726
|
+
adapter,
|
|
1727
|
+
);
|
|
1728
|
+
} else if (
|
|
1729
|
+
message.role === "assistant" &&
|
|
1730
|
+
block.type === "thinking" &&
|
|
1731
|
+
typeof block.thinking === "string" &&
|
|
1732
|
+
block.thinking.length > 0
|
|
1733
|
+
) {
|
|
1734
|
+
await this.#publishSessionUpdate(
|
|
1735
|
+
id,
|
|
1736
|
+
{
|
|
1737
|
+
sessionId: id,
|
|
1738
|
+
update: {
|
|
1739
|
+
sessionUpdate: "agent_thought_chunk",
|
|
1740
|
+
content: { type: "text", text: block.thinking },
|
|
1741
|
+
...(messageId ? { messageId } : {}),
|
|
1742
|
+
},
|
|
1743
|
+
},
|
|
1744
|
+
adapter,
|
|
1745
|
+
);
|
|
1746
|
+
} else if (
|
|
1747
|
+
message.role === "assistant" &&
|
|
1748
|
+
block.type === "toolCall" &&
|
|
1749
|
+
typeof block.id === "string" &&
|
|
1750
|
+
typeof block.name === "string"
|
|
1751
|
+
) {
|
|
1752
|
+
const args = block.arguments ?? {};
|
|
1753
|
+
replayTools.set(block.id, { name: block.name, args });
|
|
1754
|
+
await this.#publishSessionUpdate(
|
|
1755
|
+
id,
|
|
1756
|
+
{
|
|
1757
|
+
sessionId: id,
|
|
1758
|
+
update: buildToolCallStartUpdate({
|
|
1759
|
+
toolCallId: block.id,
|
|
1760
|
+
toolName: block.name,
|
|
1761
|
+
args,
|
|
1762
|
+
cwd: record.cwd,
|
|
1763
|
+
}),
|
|
1764
|
+
},
|
|
1765
|
+
adapter,
|
|
1766
|
+
);
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
continue;
|
|
1770
|
+
}
|
|
1771
|
+
if (message.role === "toolResult" && typeof message.toolCallId === "string") {
|
|
1772
|
+
const replayTool = replayTools.get(message.toolCallId);
|
|
1773
|
+
const toolName = typeof message.toolName === "string" ? message.toolName : replayTool?.name;
|
|
1774
|
+
if (!toolName) continue;
|
|
1775
|
+
const resultContent = richContent
|
|
1776
|
+
?.map(object)
|
|
1777
|
+
.filter(
|
|
1778
|
+
(block): block is JsonObject =>
|
|
1779
|
+
block !== undefined && block.type === "text" && typeof block.text === "string",
|
|
1780
|
+
)
|
|
1781
|
+
.map(block => ({ type: "text" as const, text: String(block.text) }));
|
|
1782
|
+
for (const notification of mapAgentSessionEventToAcpSessionUpdates(
|
|
1783
|
+
{
|
|
1784
|
+
type: "tool_execution_end",
|
|
1785
|
+
toolCallId: message.toolCallId,
|
|
1786
|
+
toolName,
|
|
1787
|
+
result: { content: resultContent ?? content.blocks },
|
|
1788
|
+
isError: message.isError === true,
|
|
1789
|
+
} as never,
|
|
1790
|
+
id,
|
|
1791
|
+
{ cwd: record.cwd, getToolArgs: toolCallId => replayTools.get(toolCallId)?.args },
|
|
1792
|
+
)) {
|
|
1793
|
+
await this.#publishSessionUpdate(id, notification, adapter);
|
|
1794
|
+
}
|
|
1795
|
+
replayTools.delete(message.toolCallId);
|
|
1796
|
+
continue;
|
|
1797
|
+
}
|
|
1798
|
+
if (message.role !== "user" && message.role !== "assistant") continue;
|
|
1451
1799
|
for (const block of content.blocks) {
|
|
1452
1800
|
await this.#publishSessionUpdate(
|
|
1453
1801
|
id,
|
|
@@ -1473,17 +1821,33 @@ export class AcpAgent implements Agent {
|
|
|
1473
1821
|
setTimeout(() => {
|
|
1474
1822
|
const record = this.#sessions.get(id);
|
|
1475
1823
|
if (!record || this.#connection.signal.aborted) return;
|
|
1476
|
-
void
|
|
1477
|
-
id,
|
|
1478
|
-
{
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1824
|
+
void (async () => {
|
|
1825
|
+
await this.#publishAvailableCommands(id, record.adapter);
|
|
1826
|
+
if (record.authFailure) {
|
|
1827
|
+
await this.#publishSessionUpdate(
|
|
1828
|
+
id,
|
|
1829
|
+
{
|
|
1830
|
+
sessionId: id,
|
|
1831
|
+
update: {
|
|
1832
|
+
sessionUpdate: "agent_thought_chunk",
|
|
1833
|
+
content: { type: "text", text: `[error:auth] ${record.authFailure}\n` },
|
|
1834
|
+
},
|
|
1835
|
+
},
|
|
1836
|
+
record.adapter,
|
|
1837
|
+
);
|
|
1838
|
+
}
|
|
1839
|
+
await this.#publishSessionUpdate(
|
|
1840
|
+
id,
|
|
1841
|
+
{
|
|
1842
|
+
sessionId: id,
|
|
1843
|
+
update: {
|
|
1844
|
+
sessionUpdate: "session_info_update",
|
|
1845
|
+
_meta: { gjcPhase: "idle", running: false, gjcRunning: false },
|
|
1846
|
+
},
|
|
1483
1847
|
},
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
).catch(() => undefined);
|
|
1848
|
+
record.adapter,
|
|
1849
|
+
);
|
|
1850
|
+
})().catch(() => undefined);
|
|
1487
1851
|
}, ACP_BOOTSTRAP_RACE_GUARD_MS);
|
|
1488
1852
|
}
|
|
1489
1853
|
|
|
@@ -1498,9 +1862,138 @@ export class AcpAgent implements Agent {
|
|
|
1498
1862
|
if (!path.isAbsolute(cwd)) throw new Error(`ACP cwd must be an absolute path: ${cwd}`);
|
|
1499
1863
|
}
|
|
1500
1864
|
|
|
1501
|
-
#
|
|
1502
|
-
if (
|
|
1503
|
-
throw new AcpSdkAdapterError("unsupported", "
|
|
1865
|
+
#assertNoAdditionalDirectories(directories: string[] | null | undefined): void {
|
|
1866
|
+
if (directories && directories.length > 0)
|
|
1867
|
+
throw new AcpSdkAdapterError("unsupported", "ACP additional directories are not supported.");
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
#mcpServers(params: { mcpServers?: unknown[] }): SessionLifecycleMcpServer[] {
|
|
1871
|
+
const servers = params.mcpServers ?? [];
|
|
1872
|
+
if (servers.length > 64)
|
|
1873
|
+
throw new AcpSdkAdapterError("unsupported", "ACP supports at most 64 MCP servers per session.");
|
|
1874
|
+
const result: SessionLifecycleMcpServer[] = [];
|
|
1875
|
+
const names = new Set<string>();
|
|
1876
|
+
for (const value of servers) {
|
|
1877
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
1878
|
+
throw new AcpSdkAdapterError("invalid_input", "ACP MCP server definitions must be objects.");
|
|
1879
|
+
const server = value as Record<string, unknown>;
|
|
1880
|
+
if (typeof server.name !== "string" || !/^[A-Za-z0-9_.-]{1,100}$/.test(server.name) || names.has(server.name))
|
|
1881
|
+
throw new AcpSdkAdapterError("invalid_input", "ACP MCP servers must have unique safe names.");
|
|
1882
|
+
names.add(server.name);
|
|
1883
|
+
if (server.type === "http" || server.type === "sse") {
|
|
1884
|
+
if (
|
|
1885
|
+
typeof server.url !== "string" ||
|
|
1886
|
+
server.url.length > 8_192 ||
|
|
1887
|
+
!Array.isArray(server.headers) ||
|
|
1888
|
+
server.headers.length > 100
|
|
1889
|
+
)
|
|
1890
|
+
throw new AcpSdkAdapterError("invalid_input", "ACP remote MCP servers require a valid URL and headers.");
|
|
1891
|
+
let parsedUrl: URL;
|
|
1892
|
+
try {
|
|
1893
|
+
parsedUrl = new URL(server.url);
|
|
1894
|
+
} catch {
|
|
1895
|
+
throw new AcpSdkAdapterError("invalid_input", "ACP MCP URL is invalid.");
|
|
1896
|
+
}
|
|
1897
|
+
if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:")
|
|
1898
|
+
throw new AcpSdkAdapterError("invalid_input", "ACP MCP URLs must use HTTP or HTTPS.");
|
|
1899
|
+
const headers: Record<string, string> = {};
|
|
1900
|
+
for (const value of server.headers) {
|
|
1901
|
+
const header = object(value);
|
|
1902
|
+
if (
|
|
1903
|
+
typeof header?.name !== "string" ||
|
|
1904
|
+
header.name.length === 0 ||
|
|
1905
|
+
header.name.length > 256 ||
|
|
1906
|
+
header.name.includes("\r") ||
|
|
1907
|
+
header.name.includes("\n") ||
|
|
1908
|
+
typeof header.value !== "string" ||
|
|
1909
|
+
header.value.length > 8_192 ||
|
|
1910
|
+
header.value.includes("\r") ||
|
|
1911
|
+
header.value.includes("\n") ||
|
|
1912
|
+
Object.hasOwn(headers, header.name)
|
|
1913
|
+
)
|
|
1914
|
+
throw new AcpSdkAdapterError(
|
|
1915
|
+
"invalid_input",
|
|
1916
|
+
"ACP MCP headers must have unique valid names and values.",
|
|
1917
|
+
);
|
|
1918
|
+
headers[header.name] = header.value;
|
|
1919
|
+
}
|
|
1920
|
+
result.push({
|
|
1921
|
+
type: server.type,
|
|
1922
|
+
name: server.name,
|
|
1923
|
+
url: parsedUrl.toString(),
|
|
1924
|
+
...(Object.keys(headers).length > 0 ? { headers } : {}),
|
|
1925
|
+
});
|
|
1926
|
+
continue;
|
|
1927
|
+
}
|
|
1928
|
+
if (
|
|
1929
|
+
(server.type !== undefined && server.type !== "stdio") ||
|
|
1930
|
+
typeof server.command !== "string" ||
|
|
1931
|
+
server.command.length > 4_096 ||
|
|
1932
|
+
!path.isAbsolute(server.command) ||
|
|
1933
|
+
!Array.isArray(server.args) ||
|
|
1934
|
+
server.args.length > 100 ||
|
|
1935
|
+
!server.args.every(argument => typeof argument === "string" && argument.length <= 8_192) ||
|
|
1936
|
+
!Array.isArray(server.env) ||
|
|
1937
|
+
server.env.length > 100
|
|
1938
|
+
)
|
|
1939
|
+
throw new AcpSdkAdapterError(
|
|
1940
|
+
"invalid_input",
|
|
1941
|
+
"ACP stdio MCP servers require an absolute command and bounded arguments and environment variables.",
|
|
1942
|
+
);
|
|
1943
|
+
const env: Record<string, string> = {};
|
|
1944
|
+
for (const value of server.env) {
|
|
1945
|
+
const variable = object(value);
|
|
1946
|
+
if (
|
|
1947
|
+
typeof variable?.name !== "string" ||
|
|
1948
|
+
!/^[A-Za-z_][A-Za-z0-9_]*$/.test(variable.name) ||
|
|
1949
|
+
typeof variable.value !== "string" ||
|
|
1950
|
+
variable.value.length > 32_768 ||
|
|
1951
|
+
Object.hasOwn(env, variable.name)
|
|
1952
|
+
)
|
|
1953
|
+
throw new AcpSdkAdapterError(
|
|
1954
|
+
"invalid_input",
|
|
1955
|
+
"ACP MCP environment variables must have unique valid names and string values.",
|
|
1956
|
+
);
|
|
1957
|
+
env[variable.name] = variable.value;
|
|
1958
|
+
}
|
|
1959
|
+
result.push({
|
|
1960
|
+
name: server.name,
|
|
1961
|
+
command: server.command,
|
|
1962
|
+
args: server.args as string[],
|
|
1963
|
+
...(Object.keys(env).length > 0 ? { env } : {}),
|
|
1964
|
+
});
|
|
1965
|
+
}
|
|
1966
|
+
return result;
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
async #launchSessionWithMcp(
|
|
1970
|
+
operation: "session.create" | "session.fork" | "session.resume",
|
|
1971
|
+
input: JsonObject,
|
|
1972
|
+
idempotencyKey: string,
|
|
1973
|
+
mcpServers: SessionLifecycleMcpServer[],
|
|
1974
|
+
): Promise<unknown> {
|
|
1975
|
+
try {
|
|
1976
|
+
return await (await this.#brokerAdapter()).global(operation, input, idempotencyKey);
|
|
1977
|
+
} catch (error) {
|
|
1978
|
+
const code =
|
|
1979
|
+
typeof error === "object" && error !== null && "code" in error && typeof error.code === "string"
|
|
1980
|
+
? error.code
|
|
1981
|
+
: undefined;
|
|
1982
|
+
if (
|
|
1983
|
+
mcpServers.length === 0 ||
|
|
1984
|
+
code === "invalid_input" ||
|
|
1985
|
+
code === "authentication_failed" ||
|
|
1986
|
+
code === "unknown_model_profile" ||
|
|
1987
|
+
code === "model_profile_registry_error"
|
|
1988
|
+
)
|
|
1989
|
+
throw error;
|
|
1990
|
+
const names = mcpServers
|
|
1991
|
+
.slice(0, 8)
|
|
1992
|
+
.map(server => server.name)
|
|
1993
|
+
.join(", ");
|
|
1994
|
+
const suffix = mcpServers.length > 8 ? `, and ${mcpServers.length - 8} more` : "";
|
|
1995
|
+
throw new AcpSdkAdapterError("unavailable", `MCP server request failed to start (${names}${suffix}).`);
|
|
1996
|
+
}
|
|
1504
1997
|
}
|
|
1505
1998
|
|
|
1506
1999
|
#beginDispose(): void {
|
|
@@ -1526,6 +2019,7 @@ export class AcpAgent implements Agent {
|
|
|
1526
2019
|
this.#attaching.clear();
|
|
1527
2020
|
this.#resolvingExisting.clear();
|
|
1528
2021
|
this.#knownSessionCwds.clear();
|
|
2022
|
+
this.#knownSessionMetadata.clear();
|
|
1529
2023
|
this.#pendingCloseIdempotencyKeys.clear();
|
|
1530
2024
|
if (this.#closing.size === 0) this.#closing.clear();
|
|
1531
2025
|
this.#tearingDown.clear();
|