@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.
Files changed (150) hide show
  1. package/CHANGELOG.md +56 -1
  2. package/dist/types/cli.d.ts +8 -1
  3. package/dist/types/config/model-profile-activation.d.ts +1 -0
  4. package/dist/types/config/model-profile-contract.d.ts +2 -0
  5. package/dist/types/config/model-registry.d.ts +2 -0
  6. package/dist/types/config/settings-schema.d.ts +57 -2
  7. package/dist/types/config/settings.d.ts +1 -0
  8. package/dist/types/extensibility/extensions/types.d.ts +21 -0
  9. package/dist/types/gjc-runtime/linux-proc.d.ts +22 -14
  10. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +60 -0
  11. package/dist/types/gjc-runtime/state-runtime.d.ts +3 -1
  12. package/dist/types/gjc-runtime/state-writer.d.ts +1 -0
  13. package/dist/types/modes/acp/acp-agent.d.ts +14 -9
  14. package/dist/types/modes/components/queue-pane.d.ts +8 -0
  15. package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
  16. package/dist/types/modes/components/welcome.d.ts +1 -0
  17. package/dist/types/modes/controllers/selector-controller.d.ts +16 -8
  18. package/dist/types/modes/interactive-mode.d.ts +5 -1
  19. package/dist/types/runtime/memory-domain.d.ts +9 -0
  20. package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
  21. package/dist/types/runtime/memory-guard.d.ts +39 -0
  22. package/dist/types/runtime/memory-limit.d.ts +11 -0
  23. package/dist/types/runtime-mcp/manager.d.ts +8 -0
  24. package/dist/types/sdk/acp/final-text.d.ts +19 -0
  25. package/dist/types/sdk/acp/mcp.d.ts +23 -0
  26. package/dist/types/sdk/broker/lifecycle.d.ts +2 -0
  27. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
  28. package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
  29. package/dist/types/sdk/bus/index.d.ts +11 -2
  30. package/dist/types/sdk/bus/operator-runtime.d.ts +1 -0
  31. package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
  32. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +14 -7
  33. package/dist/types/sdk/bus/telegram-daemon.d.ts +97 -8
  34. package/dist/types/sdk/host/control/operations.d.ts +1 -0
  35. package/dist/types/sdk/lifecycle-session.d.ts +10 -1
  36. package/dist/types/sdk/session.d.ts +7 -0
  37. package/dist/types/sdk/startup-capability.d.ts +6 -0
  38. package/dist/types/session/agent-session.d.ts +11 -11
  39. package/dist/types/session/fallback-chain-controller.d.ts +18 -2
  40. package/dist/types/session/internal/managed-session-scope.d.ts +28 -1
  41. package/dist/types/session/session-manager.d.ts +34 -0
  42. package/dist/types/task/executor.d.ts +6 -0
  43. package/dist/types/task/provider-retry-status.d.ts +13 -0
  44. package/dist/types/task/types.d.ts +39 -0
  45. package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
  46. package/dist/types/tools/index.d.ts +3 -1
  47. package/dist/types/tools/resource-gc.d.ts +19 -0
  48. package/package.json +8 -8
  49. package/scripts/generate-sdk-operation-inventory.ts +3 -0
  50. package/scripts/run-test-manifest.ts +6 -4
  51. package/src/cli/args.ts +14 -8
  52. package/src/cli/fast-help.ts +1 -1
  53. package/src/cli/notify-cli.ts +1 -0
  54. package/src/cli.ts +48 -2
  55. package/src/commands/sdk.ts +91 -7
  56. package/src/config/model-profile-activation.ts +1 -0
  57. package/src/config/model-profile-contract.ts +13 -0
  58. package/src/config/model-profiles.ts +31 -7
  59. package/src/config/model-registry.ts +4 -2
  60. package/src/config/settings-schema.ts +61 -2
  61. package/src/config/settings.ts +8 -0
  62. package/src/defaults/gjc/skills/ralplan/SKILL.md +21 -2
  63. package/src/defaults/gjc/skills/ultragoal/SKILL.md +1 -1
  64. package/src/eval/js/tool-bridge.ts +1 -1
  65. package/src/extensibility/extensions/runner.ts +4 -0
  66. package/src/extensibility/extensions/types.ts +14 -0
  67. package/src/gjc-runtime/deep-interview-ambiguity.ts +16 -2
  68. package/src/gjc-runtime/deep-interview-draft.ts +2 -1
  69. package/src/gjc-runtime/deep-interview-recorder.ts +16 -1
  70. package/src/gjc-runtime/deep-interview-state.ts +25 -3
  71. package/src/gjc-runtime/linux-proc.ts +70 -36
  72. package/src/gjc-runtime/ralplan-runtime.ts +252 -1
  73. package/src/gjc-runtime/state-runtime.ts +48 -11
  74. package/src/gjc-runtime/state-writer.ts +1 -1
  75. package/src/hooks/skill-state.ts +23 -1
  76. package/src/internal-urls/docs-index.generated.ts +7 -7
  77. package/src/internal-urls/local-protocol.ts +23 -4
  78. package/src/modes/acp/acp-agent.ts +554 -60
  79. package/src/modes/acp/acp-event-mapper.ts +93 -6
  80. package/src/modes/components/model-selector.ts +15 -1
  81. package/src/modes/components/notifications-settings-editor.ts +1 -1
  82. package/src/modes/components/queue-pane.ts +55 -9
  83. package/src/modes/components/queued-message-selector.ts +53 -10
  84. package/src/modes/components/tool-status-header.ts +1 -4
  85. package/src/modes/components/welcome.ts +18 -3
  86. package/src/modes/controllers/command-controller.ts +4 -0
  87. package/src/modes/controllers/extension-ui-controller.ts +4 -10
  88. package/src/modes/controllers/input-controller.ts +44 -3
  89. package/src/modes/controllers/selector-controller.ts +38 -8
  90. package/src/modes/interactive-mode.ts +65 -26
  91. package/src/modes/runtime-init.ts +1 -0
  92. package/src/prompts/agent-fragments/restricted-bash.md +1 -1
  93. package/src/prompts/agents/architect.md +8 -1
  94. package/src/prompts/agents/critic.md +8 -1
  95. package/src/prompts/agents/executor.md +1 -1
  96. package/src/prompts/agents/planner.md +8 -1
  97. package/src/runtime/memory-domain.ts +66 -0
  98. package/src/runtime/memory-guard-contract.ts +73 -0
  99. package/src/runtime/memory-guard.ts +292 -0
  100. package/src/runtime/memory-limit.ts +50 -0
  101. package/src/runtime-mcp/manager.ts +26 -6
  102. package/src/sdk/acp/adapter.ts +8 -1
  103. package/src/sdk/acp/final-text.ts +40 -0
  104. package/src/sdk/acp/mcp.ts +26 -0
  105. package/src/sdk/broker/identity.ts +22 -12
  106. package/src/sdk/broker/lifecycle.ts +82 -1
  107. package/src/sdk/bus/chat-daemon-cli.ts +2 -2
  108. package/src/sdk/bus/chat-daemon-control.ts +4 -3
  109. package/src/sdk/bus/config.ts +1 -1
  110. package/src/sdk/bus/daemon-paths.ts +2 -0
  111. package/src/sdk/bus/index.ts +222 -27
  112. package/src/sdk/bus/notification-service.ts +19 -5
  113. package/src/sdk/bus/operator-runtime.ts +31 -5
  114. package/src/sdk/bus/rate-limit-pool.ts +8 -0
  115. package/src/sdk/bus/telegram-cli.ts +6 -13
  116. package/src/sdk/bus/telegram-daemon-cli.ts +8 -3
  117. package/src/sdk/bus/telegram-daemon-contract.ts +15 -7
  118. package/src/sdk/bus/telegram-daemon-control.ts +14 -5
  119. package/src/sdk/bus/telegram-daemon.ts +1191 -164
  120. package/src/sdk/host/control/dispatch.ts +2 -0
  121. package/src/sdk/host/control/operations.ts +1 -0
  122. package/src/sdk/host/host.ts +2 -2
  123. package/src/sdk/lifecycle-session.ts +19 -3
  124. package/src/sdk/protocol/operation-inventory.generated.json +58 -0
  125. package/src/sdk/protocol/operation-registry.ts +1 -0
  126. package/src/sdk/session.ts +24 -1
  127. package/src/sdk/startup-capability.ts +6 -0
  128. package/src/session/agent-session.ts +290 -132
  129. package/src/session/fallback-chain-controller.ts +47 -8
  130. package/src/session/internal/managed-session-scope.ts +173 -127
  131. package/src/session/internal/native-publish-outcome.ts +8 -3
  132. package/src/session/session-manager.ts +608 -25
  133. package/src/skill-state/workflow-mutation-guard.ts +41 -7
  134. package/src/task/executor.ts +152 -42
  135. package/src/task/index.ts +2 -0
  136. package/src/task/provider-retry-status.ts +88 -0
  137. package/src/task/render.ts +45 -26
  138. package/src/task/types.ts +25 -11
  139. package/src/task/ultragoal-redteam-activation.ts +75 -0
  140. package/src/tools/bash-allowed-prefixes.ts +45 -1
  141. package/src/tools/index.ts +3 -1
  142. package/src/tools/resource-gc.ts +496 -120
  143. package/src/tools/subagent-render.ts +74 -16
  144. package/src/tools/subagent.ts +2 -0
  145. package/src/workflow/workflow-intent-diff.ts +4 -1
  146. package/vendor/insane-search/MANIFEST.json +4 -1
  147. package/vendor/insane-search/engine/bias_check.py +5 -0
  148. package/vendor/insane-search/engine/fetch_chain.py +67 -1
  149. package/vendor/insane-search/engine/transport.py +13 -2
  150. 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 { mapAgentWireEventPayloadToAcpSessionUpdates } from "./acp-event-mapper";
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 a post-acknowledgement lifecycle boundary.
63
- * AgentSession events do not carry the command identity themselves, so the
64
- * host stamps command/turn identities into its replay ring and ACP also keeps
65
- * a per-endpoint ingress sequence. A frame observed before an acknowledgement
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(listed: unknown[], cwd: string | undefined, offset: number): ListSessionsResponse {
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
- .slice(offset, offset + SESSION_PAGE_SIZE)
149
- .map(
150
- value =>
151
- ({ sessionId: value.sessionId, cwd: value.locator.repo, title: value.sessionId }) satisfies SessionInfo,
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 queries into the ACP 1.2.1 session state surface. */
352
- export function acpSessionStateFromConfig(query: unknown, modelsQuery?: 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 = values.get(option.id);
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
- ? modelConfigOptions(modelsQuery, value)
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 [{ ...option, type: "select" as const, currentValue: value, options }];
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
- sessionCapabilities: { list: {}, fork: {}, resume: {}, close: {}, delete: {} },
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.#assertNoMcpServers(params);
732
+ const mcpServers = this.#mcpServers(params);
640
733
  this.#assertAbsoluteCwd(params.cwd);
641
- const result = await (await this.#brokerAdapter()).global(
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.#assertNoMcpServers(params);
760
+ const mcpServers = this.#mcpServers(params);
665
761
  this.#assertAbsoluteCwd(params.cwd);
666
- await this.#attachExisting(params.sessionId, params.cwd);
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.#assertNoMcpServers(params);
770
+ const mcpServers = this.#mcpServers(params);
674
771
  this.#assertAbsoluteCwd(params.cwd);
675
- await this.#attachExisting(params.sessionId, params.cwd);
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.#assertNoMcpServers(params);
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 (await this.#brokerAdapter()).global(
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(listed, params.cwd ?? undefined, this.#cursor(params.cursor));
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
- await this.#adapter(params.sessionId).setModel(params.value);
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(id: string, cwd: string, epoch: number): Promise<void> {
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 (await this.#brokerAdapter()).global(
1126
+ const result = await this.#launchSessionWithMcp(
996
1127
  "session.resume",
997
- { cwd, sessionId: id, sessionPath: saved, target: { path: cwd } },
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 publication succeeds so a failed
1314
- // sessionUpdate rejects the prompt instead of reporting false completion.
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?.acknowledged &&
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") await this.#emitEndOfTurnUpdates(id, adapter);
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
- updatedAt: new Date().toISOString(),
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(id: string): Promise<Pick<NewSessionResponse, "configOptions" | "modes">> {
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 [config, models] = await Promise.all([
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
- return acpSessionStateFromConfig(config, models);
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 || (message.role !== "user" && message.role !== "assistant")) continue;
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 this.#publishSessionUpdate(
1477
- id,
1478
- {
1479
- sessionId: id,
1480
- update: {
1481
- sessionUpdate: "session_info_update",
1482
- _meta: { gjcPhase: "idle", running: false, gjcRunning: false },
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
- record.adapter,
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
- #assertNoMcpServers(params: { mcpServers?: unknown[] }): void {
1502
- if (params.mcpServers && params.mcpServers.length > 0)
1503
- throw new AcpSdkAdapterError("unsupported", "MCP servers are unsupported under SDK-backed ACP.");
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();