@gajae-code/coding-agent 0.11.10 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/CHANGELOG.md +86 -2
  2. package/dist/types/cli/read-cli.d.ts +1 -0
  3. package/dist/types/cli.d.ts +8 -1
  4. package/dist/types/commands/deep-interview.d.ts +9 -0
  5. package/dist/types/commands/harness.d.ts +22 -0
  6. package/dist/types/commands/read.d.ts +6 -0
  7. package/dist/types/config/model-profile-activation.d.ts +1 -0
  8. package/dist/types/config/model-profile-contract.d.ts +2 -0
  9. package/dist/types/config/model-registry.d.ts +4 -0
  10. package/dist/types/config/provider-auth-health.d.ts +14 -0
  11. package/dist/types/config/provider-ranking.d.ts +59 -0
  12. package/dist/types/config/settings-schema.d.ts +86 -3
  13. package/dist/types/config/settings.d.ts +3 -0
  14. package/dist/types/extensibility/extensions/types.d.ts +43 -2
  15. package/dist/types/extensibility/shared-events.d.ts +6 -0
  16. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
  17. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
  18. package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
  19. package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
  20. package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
  21. package/dist/types/gjc-runtime/linux-proc.d.ts +22 -14
  22. package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
  23. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +112 -1
  24. package/dist/types/gjc-runtime/state-runtime.d.ts +3 -1
  25. package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
  26. package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
  27. package/dist/types/gjc-runtime/team-runtime.d.ts +2 -1
  28. package/dist/types/gjc-runtime/team-store.d.ts +12 -0
  29. package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
  30. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +7 -0
  31. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
  32. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
  33. package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
  34. package/dist/types/main.d.ts +1 -1
  35. package/dist/types/memories/index.d.ts +2 -0
  36. package/dist/types/modes/acp/acp-agent.d.ts +22 -9
  37. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  38. package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
  39. package/dist/types/modes/components/queue-pane.d.ts +8 -0
  40. package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
  41. package/dist/types/modes/components/read-tool-group.d.ts +4 -4
  42. package/dist/types/modes/components/session-observer-overlay.d.ts +4 -0
  43. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  44. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  45. package/dist/types/modes/components/welcome.d.ts +1 -0
  46. package/dist/types/modes/controllers/event-controller.d.ts +3 -0
  47. package/dist/types/modes/interactive-mode.d.ts +7 -1
  48. package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
  49. package/dist/types/modes/types.d.ts +2 -0
  50. package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
  51. package/dist/types/runtime/memory-domain.d.ts +9 -0
  52. package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
  53. package/dist/types/runtime/memory-guard.d.ts +39 -0
  54. package/dist/types/runtime/memory-limit.d.ts +11 -0
  55. package/dist/types/runtime-mcp/manager.d.ts +8 -0
  56. package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
  57. package/dist/types/sdk/acp/final-text.d.ts +19 -0
  58. package/dist/types/sdk/acp/mcp.d.ts +23 -0
  59. package/dist/types/sdk/broker/lifecycle.d.ts +7 -0
  60. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
  61. package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
  62. package/dist/types/sdk/bus/index.d.ts +18 -2
  63. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +37 -0
  64. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
  65. package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
  66. package/dist/types/sdk/bus/reconciliation-store.d.ts +65 -0
  67. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -4
  68. package/dist/types/sdk/bus/telegram-daemon.d.ts +23 -3
  69. package/dist/types/sdk/host/control/operations.d.ts +2 -1
  70. package/dist/types/sdk/host/host.d.ts +2 -0
  71. package/dist/types/sdk/host/query/handlers.d.ts +5 -0
  72. package/dist/types/sdk/lifecycle-session.d.ts +10 -1
  73. package/dist/types/sdk/startup-capability.d.ts +6 -0
  74. package/dist/types/session/agent-session.d.ts +51 -19
  75. package/dist/types/session/fallback-chain-controller.d.ts +18 -2
  76. package/dist/types/session/internal/managed-session-scope.d.ts +27 -0
  77. package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
  78. package/dist/types/session/session-manager.d.ts +59 -13
  79. package/dist/types/session/streaming-output.d.ts +60 -5
  80. package/dist/types/skill-state/active-state.d.ts +0 -4
  81. package/dist/types/skill-state/workflow-hud.d.ts +3 -0
  82. package/dist/types/task/executor.d.ts +6 -0
  83. package/dist/types/task/types.d.ts +36 -0
  84. package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
  85. package/dist/types/tools/browser/launch.d.ts +7 -0
  86. package/dist/types/tools/fetch.d.ts +17 -1
  87. package/dist/types/tools/output-meta.d.ts +21 -1
  88. package/dist/types/tools/read-internals.d.ts +12 -0
  89. package/dist/types/tools/read.d.ts +45 -1
  90. package/dist/types/tools/resource-gc.d.ts +23 -0
  91. package/dist/types/tools/tool-result.d.ts +7 -1
  92. package/package.json +9 -13
  93. package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
  94. package/scripts/capture-sticky-viewport-showcase.ts +292 -0
  95. package/scripts/generate-sdk-operation-inventory.ts +4 -0
  96. package/scripts/run-test-manifest.ts +6 -4
  97. package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
  98. package/scripts/verify-sticky-viewport-showcase.ts +611 -0
  99. package/src/capability/index.ts +8 -0
  100. package/src/cli/args.ts +14 -8
  101. package/src/cli/fast-help.ts +1 -1
  102. package/src/cli/read-cli.ts +2 -1
  103. package/src/cli.ts +49 -2
  104. package/src/commands/deep-interview.ts +29 -9
  105. package/src/commands/harness.ts +35 -12
  106. package/src/commands/read.ts +11 -2
  107. package/src/commands/sdk.ts +91 -7
  108. package/src/commands/team.ts +1 -1
  109. package/src/config/model-profile-activation.ts +1 -0
  110. package/src/config/model-profile-contract.ts +13 -0
  111. package/src/config/model-profiles.ts +24 -0
  112. package/src/config/model-registry.ts +21 -2
  113. package/src/config/provider-auth-health.ts +42 -0
  114. package/src/config/provider-ranking.ts +119 -0
  115. package/src/config/settings-schema.ts +84 -3
  116. package/src/config/settings.ts +254 -108
  117. package/src/defaults/gjc/skills/deep-interview/SKILL.md +229 -82
  118. package/src/defaults/gjc/skills/ralplan/SKILL.md +78 -26
  119. package/src/defaults/gjc/skills/team/SKILL.md +1 -1
  120. package/src/defaults/gjc/skills/ultragoal/SKILL.md +26 -16
  121. package/src/eval/py/executor.ts +7 -2
  122. package/src/extensibility/extensions/runner.ts +4 -0
  123. package/src/extensibility/extensions/types.ts +42 -4
  124. package/src/extensibility/shared-events.ts +5 -0
  125. package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -68
  126. package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
  127. package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
  128. package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
  129. package/src/gjc-runtime/deep-interview-state.ts +12 -921
  130. package/src/gjc-runtime/gc-render.ts +1 -0
  131. package/src/gjc-runtime/gc-runtime.ts +11 -1
  132. package/src/gjc-runtime/linux-proc.ts +70 -36
  133. package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
  134. package/src/gjc-runtime/ralplan-runtime.ts +979 -71
  135. package/src/gjc-runtime/state-runtime.ts +50 -12
  136. package/src/gjc-runtime/state-writer.ts +70 -222
  137. package/src/gjc-runtime/team-launch.ts +34 -5
  138. package/src/gjc-runtime/team-runtime.ts +970 -33
  139. package/src/gjc-runtime/team-store.ts +32 -0
  140. package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
  141. package/src/gjc-runtime/team-workers.ts +1 -0
  142. package/src/gjc-runtime/tmux-owner-isolation.ts +18 -0
  143. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
  144. package/src/gjc-runtime/ultragoal-runtime.ts +13 -7
  145. package/src/gjc-runtime/workflow-manifest.generated.json +97 -350
  146. package/src/gjc-runtime/workflow-manifest.ts +34 -179
  147. package/src/hooks/native-skill-hook.ts +17 -4
  148. package/src/hooks/skill-state.ts +1 -23
  149. package/src/internal-urls/docs-index.generated.ts +10 -11
  150. package/src/internal-urls/local-protocol.ts +23 -4
  151. package/src/internal-urls/local-root-gc.ts +127 -0
  152. package/src/main.ts +9 -5
  153. package/src/memories/index.ts +10 -0
  154. package/src/modes/DESIGN.md +47 -0
  155. package/src/modes/acp/acp-agent.ts +597 -67
  156. package/src/modes/acp/acp-event-mapper.ts +106 -12
  157. package/src/modes/acp/acp-mode.ts +39 -3
  158. package/src/modes/components/assistant-message.ts +123 -22
  159. package/src/modes/components/irc-sidebar.ts +176 -62
  160. package/src/modes/components/model-selector.ts +76 -5
  161. package/src/modes/components/notifications-settings-editor.ts +1 -1
  162. package/src/modes/components/oauth-selector.ts +42 -12
  163. package/src/modes/components/queue-pane.ts +55 -9
  164. package/src/modes/components/queued-message-selector.ts +53 -10
  165. package/src/modes/components/read-tool-group.ts +82 -14
  166. package/src/modes/components/session-observer-overlay.ts +187 -32
  167. package/src/modes/components/settings-selector.ts +60 -18
  168. package/src/modes/components/tool-execution.ts +138 -27
  169. package/src/modes/components/tool-status-header.ts +1 -4
  170. package/src/modes/components/welcome.ts +18 -3
  171. package/src/modes/controllers/command-controller.ts +4 -0
  172. package/src/modes/controllers/event-controller.ts +109 -27
  173. package/src/modes/controllers/extension-ui-controller.ts +23 -12
  174. package/src/modes/controllers/input-controller.ts +62 -6
  175. package/src/modes/controllers/selector-controller.ts +83 -37
  176. package/src/modes/interactive-mode.ts +127 -29
  177. package/src/modes/irc-observation-ledger.ts +71 -15
  178. package/src/modes/runtime-init.ts +20 -2
  179. package/src/modes/types.ts +2 -0
  180. package/src/modes/utils/ui-helpers.ts +94 -1
  181. package/src/prompts/agents/architect.md +9 -1
  182. package/src/prompts/agents/critic.md +9 -0
  183. package/src/prompts/agents/executor.md +1 -1
  184. package/src/prompts/system/system-prompt.md +0 -1
  185. package/src/prompts/tools/read.md +4 -2
  186. package/src/runtime/memory-domain.ts +66 -0
  187. package/src/runtime/memory-guard-contract.ts +73 -0
  188. package/src/runtime/memory-guard.ts +292 -0
  189. package/src/runtime/memory-limit.ts +50 -0
  190. package/src/runtime-mcp/manager.ts +26 -6
  191. package/src/runtime-mcp/smithery-auth.ts +17 -2
  192. package/src/runtime-mcp/smithery-connect.ts +11 -1
  193. package/src/sdk/acp/adapter.ts +8 -1
  194. package/src/sdk/acp/final-text.ts +40 -0
  195. package/src/sdk/acp/mcp.ts +26 -0
  196. package/src/sdk/broker/lifecycle.ts +107 -7
  197. package/src/sdk/bus/chat-daemon-cli.ts +2 -2
  198. package/src/sdk/bus/chat-daemon-control.ts +4 -3
  199. package/src/sdk/bus/config.ts +1 -1
  200. package/src/sdk/bus/control-drain-lease.ts +104 -0
  201. package/src/sdk/bus/index.ts +503 -60
  202. package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
  203. package/src/sdk/bus/prompt-reconciliation.ts +2 -1
  204. package/src/sdk/bus/rate-limit-pool.ts +8 -0
  205. package/src/sdk/bus/reconciliation-store.ts +224 -0
  206. package/src/sdk/bus/telegram-daemon-contract.ts +6 -4
  207. package/src/sdk/bus/telegram-daemon.ts +531 -53
  208. package/src/sdk/host/control/dispatch.ts +7 -1
  209. package/src/sdk/host/control/operations.ts +2 -1
  210. package/src/sdk/host/host.ts +25 -3
  211. package/src/sdk/host/query/handlers.ts +55 -0
  212. package/src/sdk/lifecycle-session.ts +19 -3
  213. package/src/sdk/mcp/server.ts +46 -18
  214. package/src/sdk/protocol/operation-inventory.generated.json +76 -0
  215. package/src/sdk/protocol/operation-registry.ts +8 -2
  216. package/src/sdk/session.ts +25 -2
  217. package/src/sdk/startup-capability.ts +6 -0
  218. package/src/session/agent-session.ts +720 -190
  219. package/src/session/fallback-chain-controller.ts +47 -8
  220. package/src/session/internal/managed-session-scope.ts +40 -10
  221. package/src/session/internal/native-publish-outcome.ts +8 -3
  222. package/src/session/memory-guard-checkpoint-participant.ts +131 -0
  223. package/src/session/session-manager.ts +1372 -42
  224. package/src/session/streaming-output.ts +262 -43
  225. package/src/setup/provider-onboarding.ts +19 -4
  226. package/src/skill-state/active-state.ts +0 -10
  227. package/src/skill-state/workflow-hud.ts +25 -2
  228. package/src/skill-state/workflow-mutation-guard.ts +65 -7
  229. package/src/task/executor.ts +11 -3
  230. package/src/task/index.ts +2 -0
  231. package/src/task/types.ts +22 -11
  232. package/src/task/ultragoal-redteam-activation.ts +75 -0
  233. package/src/tools/ask.ts +89 -69
  234. package/src/tools/browser/launch.ts +41 -6
  235. package/src/tools/fetch.ts +114 -23
  236. package/src/tools/output-meta.ts +176 -9
  237. package/src/tools/path-utils.ts +5 -1
  238. package/src/tools/read-internals.ts +44 -0
  239. package/src/tools/read.ts +1542 -310
  240. package/src/tools/resource-gc.ts +665 -125
  241. package/src/tools/tool-result.ts +14 -1
  242. package/src/web/search/providers/codex.ts +1 -1
  243. package/src/workflow/workflow-intent-diff.ts +4 -1
  244. package/dist/types/gjc-runtime/deep-interview-draft.d.ts +0 -8
  245. package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
  246. package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
  247. package/src/gjc-runtime/deep-interview-draft.ts +0 -843
  248. package/src/gjc-runtime/deep-interview-payload.ts +0 -262
  249. package/src/gjc-runtime/deep-interview-repair.ts +0 -1674
@@ -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,
@@ -24,6 +25,7 @@ import {
24
25
  PROTOCOL_VERSION,
25
26
  type PromptRequest,
26
27
  type PromptResponse,
28
+ RequestError,
27
29
  type ResumeSessionRequest,
28
30
  type ResumeSessionResponse,
29
31
  type SessionInfo,
@@ -33,7 +35,7 @@ import {
33
35
  type SetSessionModeRequest,
34
36
  type SetSessionModeResponse,
35
37
  } from "@agentclientprotocol/sdk";
36
- import { getAgentDir } from "@gajae-code/utils";
38
+ import { getAgentDir, logger } from "@gajae-code/utils";
37
39
  import packageJson from "../../../package.json" with { type: "json" };
38
40
  import {
39
41
  type AcpProviderRegistration,
@@ -41,9 +43,15 @@ import {
41
43
  AcpSdkAdapter,
42
44
  AcpSdkAdapterError,
43
45
  } from "../../sdk/acp";
46
+ import { resolveAcpFinalText } from "../../sdk/acp/final-text";
47
+ import { ACP_MCP_LIFECYCLE_TIMEOUT_MS, type SessionLifecycleMcpServer } from "../../sdk/acp/mcp";
44
48
  import { ensureBroker } from "../../sdk/broker/ensure";
45
49
  import { readSdkBrokerDiscovery, SdkClient, SdkClientError } from "../../sdk/client";
46
- import { mapAgentWireEventPayloadToAcpSessionUpdates } from "./acp-event-mapper";
50
+ import {
51
+ buildToolCallStartUpdate,
52
+ mapAgentSessionEventToAcpSessionUpdates,
53
+ mapAgentWireEventPayloadToAcpSessionUpdates,
54
+ } from "./acp-event-mapper";
47
55
  import { resolveAcpPermissionMode } from "./permission-mode";
48
56
  import type { AcpStartupOptions } from "./startup-options";
49
57
  import { ACP_TERMINAL_AUTH_FLAG } from "./terminal-auth";
@@ -52,6 +60,8 @@ const ACP_DEFAULT_MODE_ID = "default";
52
60
  const ACP_PLAN_MODE_ID = "plan";
53
61
  const MODE_CONFIG_ID = "mode";
54
62
  const MODEL_CONFIG_ID = "model";
63
+ const MODEL_PRESET_CONFIG_KEY = "modelPreset";
64
+ const ACP_CUSTOM_MODEL_PRESET = "__custom__";
55
65
  const THINKING_CONFIG_ID = "thinking";
56
66
  const SESSION_PAGE_SIZE = 50;
57
67
  export const ACP_BOOTSTRAP_RACE_GUARD_MS = 50;
@@ -59,11 +69,10 @@ const MAX_ACP_REPLAY_PAGES = 10_000;
59
69
 
60
70
  type JsonObject = Record<string, unknown>;
61
71
  /**
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.
72
+ * ACP prompt completion is tied to lifecycle activity observed after dispatch.
73
+ * A fast host may stream and finish a turn before acknowledging `turn.prompt`,
74
+ * so pre-ack activity is retained but cannot resolve the waiter until the
75
+ * acknowledgement supplies its command/turn correlation.
67
76
  */
68
77
  interface PromptWaiter {
69
78
  cancelRequested: boolean;
@@ -75,6 +84,7 @@ interface PromptWaiter {
75
84
  boundary: number;
76
85
  correlation: PromptCorrelation;
77
86
  messageProgress?: { textEmitted: boolean; thoughtEmitted: boolean };
87
+ emittedAssistantText: string;
78
88
  pendingTerminal?: PromptCorrelation;
79
89
  resolve: (response: PromptResponse) => void;
80
90
  reject: (error: Error) => void;
@@ -97,6 +107,10 @@ type SessionRecord = {
97
107
  inboundSequence: number;
98
108
  /** Updated at ingress so a prompt acknowledgement can distinguish a steer from a fresh turn. */
99
109
  busy: boolean;
110
+ /** Start/update args retained because tool_execution_end does not carry them. */
111
+ toolArgs: Map<string, unknown>;
112
+ /** Actionable model-profile authentication failure detected before prompt dispatch. */
113
+ authFailure?: string;
100
114
  activePrompt?: PromptWaiter;
101
115
  };
102
116
  type Endpoint = { url: string; token: string };
@@ -106,6 +120,9 @@ type BrokerSession = {
106
120
  locator?: { repo?: string };
107
121
  live?: boolean;
108
122
  endpointGeneration?: number;
123
+ endpointMtimeMs?: number;
124
+ title?: string;
125
+ updatedAt?: string;
109
126
  };
110
127
 
111
128
  function parseAcpStartupOptions(value: unknown): AcpStartupOptions | undefined {
@@ -136,7 +153,12 @@ function aggregateAcpFailure(code: string, message: string, failures: unknown[])
136
153
  }
137
154
 
138
155
  /** 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 {
156
+ export function paginateAcpSessions(
157
+ listed: unknown[],
158
+ cwd: string | undefined,
159
+ offset: number,
160
+ sessionMetadata: ReadonlyMap<string, { title?: string; updatedAt?: string }> = new Map(),
161
+ ): ListSessionsResponse {
140
162
  const filtered = listed
141
163
  .map(value => object(value) as BrokerSession | undefined)
142
164
  .filter(
@@ -144,12 +166,28 @@ export function paginateAcpSessions(listed: unknown[], cwd: string | undefined,
144
166
  typeof value?.sessionId === "string" && typeof value.locator?.repo === "string",
145
167
  )
146
168
  .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
- );
169
+ const sessions = filtered.slice(offset, offset + SESSION_PAGE_SIZE).map(value => {
170
+ const metadata = sessionMetadata.get(value.sessionId);
171
+ const updatedAt =
172
+ typeof metadata?.updatedAt === "string"
173
+ ? metadata.updatedAt
174
+ : typeof value.updatedAt === "string"
175
+ ? value.updatedAt
176
+ : typeof value.endpointMtimeMs === "number" && Number.isFinite(value.endpointMtimeMs)
177
+ ? new Date(value.endpointMtimeMs).toISOString()
178
+ : undefined;
179
+ return {
180
+ sessionId: value.sessionId,
181
+ cwd: value.locator.repo,
182
+ title:
183
+ typeof metadata?.title === "string" && metadata.title
184
+ ? metadata.title
185
+ : typeof value.title === "string" && value.title
186
+ ? value.title
187
+ : value.sessionId,
188
+ ...(updatedAt ? { updatedAt } : {}),
189
+ } satisfies SessionInfo;
190
+ });
153
191
  return {
154
192
  sessions,
155
193
  nextCursor: offset + sessions.length < filtered.length ? String(offset + sessions.length) : undefined,
@@ -180,6 +218,26 @@ function pageItems(value: unknown): unknown[] {
180
218
  return Array.isArray(page?.items) ? page.items : [];
181
219
  }
182
220
 
221
+ /** Build the ACP command palette from the shared builtins and live SDK skill state. */
222
+ export function acpAvailableCommandsFromSkills(query: unknown): AvailableCommand[] {
223
+ const commands = new Map<string, AvailableCommand>();
224
+ for (const item of pageItems(query)) {
225
+ const skill = object(item);
226
+ if (typeof skill?.name !== "string" || !skill.name) continue;
227
+ const name = `skill:${skill.name}`;
228
+ if (commands.has(name)) continue;
229
+ commands.set(name, {
230
+ name,
231
+ description:
232
+ typeof skill.description === "string" && skill.description
233
+ ? skill.description
234
+ : `Run the ${skill.name} skill`,
235
+ input: { hint: "[request]" },
236
+ });
237
+ }
238
+ return [...commands.values()];
239
+ }
240
+
183
241
  function correlationFrom(...values: unknown[]): PromptCorrelation {
184
242
  const correlation: PromptCorrelation = {};
185
243
  for (const value of values) {
@@ -268,6 +326,7 @@ function receivedSdkEvent(frame: JsonObject): ReceivedSdkEvent | undefined {
268
326
  const type = frame.state === "busy" ? "agent_start" : frame.state === "idle" ? "agent_end" : undefined;
269
327
  return type ? { event: { type, ...correlationFrom(frame) } } : undefined;
270
328
  }
329
+ if (frame.type === "agent_start" || frame.type === "agent_end") return { event: frame };
271
330
  if (frame.type !== "event") return undefined;
272
331
  const payload = object(frame.payload);
273
332
  if (!payload) return undefined;
@@ -331,6 +390,19 @@ function configValues(query: unknown): Map<string, string> {
331
390
  return values;
332
391
  }
333
392
 
393
+ function modelPresetConfigOptions(query: unknown, current: string): { value: string; name: string }[] {
394
+ const options = new Map<string, string>();
395
+ for (const item of pageItems(query)) {
396
+ const profile = object(item);
397
+ if (!profile || typeof profile.id !== "string") continue;
398
+ if (profile.available === false && profile.id !== current) continue;
399
+ options.set(profile.id, typeof profile.displayName === "string" ? profile.displayName : profile.id);
400
+ }
401
+ if (current === ACP_CUSTOM_MODEL_PRESET) options.set(ACP_CUSTOM_MODEL_PRESET, "Custom (current model)");
402
+ else if (!options.has(current)) options.set(current, current);
403
+ return [...options].map(([value, name]) => ({ value, name }));
404
+ }
405
+
334
406
  function modelConfigOptions(query: unknown, current: string | undefined): { value: string; name: string }[] {
335
407
  const options = new Map<string, string>();
336
408
  for (const item of pageItems(query)) {
@@ -348,9 +420,10 @@ const THINKING_CONFIG_OPTIONS = ["off", "minimal", "low", "medium", "high", "xhi
348
420
  name: value,
349
421
  }));
350
422
 
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) {
423
+ /** Maps live canonical SDK config and the selected model catalog into the ACP 1.2.1 session state surface. */
424
+ export function acpSessionStateFromConfig(query: unknown, modelCatalogQuery?: unknown, modelPreset?: string) {
353
425
  const values = configValues(query);
426
+ const useModelPresets = modelPreset !== undefined;
354
427
  const currentModeId = values.get(MODE_CONFIG_ID) === ACP_PLAN_MODE_ID ? ACP_PLAN_MODE_ID : ACP_DEFAULT_MODE_ID;
355
428
  return {
356
429
  configOptions: [
@@ -365,15 +438,28 @@ export function acpSessionStateFromConfig(query: unknown, modelsQuery?: unknown)
365
438
  ],
366
439
  },
367
440
  ...ACP_CONFIG_OPTIONS.flatMap(option => {
368
- const value = values.get(option.id);
441
+ const value =
442
+ option.id === MODEL_CONFIG_ID && useModelPresets
443
+ ? (values.get(MODEL_PRESET_CONFIG_KEY) ?? ACP_CUSTOM_MODEL_PRESET)
444
+ : values.get(option.id);
369
445
  if (value === undefined) return [];
370
446
  const options =
371
447
  option.id === MODEL_CONFIG_ID
372
- ? modelConfigOptions(modelsQuery, value)
448
+ ? useModelPresets
449
+ ? modelPresetConfigOptions(modelCatalogQuery, value)
450
+ : modelConfigOptions(modelCatalogQuery, value)
373
451
  : option.id === THINKING_CONFIG_ID
374
452
  ? THINKING_CONFIG_OPTIONS
375
453
  : [...option.options];
376
- return [{ ...option, type: "select" as const, currentValue: value, options }];
454
+ return [
455
+ {
456
+ ...option,
457
+ ...(option.id === MODEL_CONFIG_ID && useModelPresets ? { name: "Preset" } : {}),
458
+ type: "select" as const,
459
+ currentValue: value,
460
+ options,
461
+ },
462
+ ];
377
463
  }),
378
464
  ],
379
465
  modes: {
@@ -447,6 +533,34 @@ export function acpPromptPayload(blocks: PromptRequest["prompt"]): {
447
533
  return { text: text.join("\n"), images };
448
534
  }
449
535
 
536
+ /**
537
+ * `AcpSdkAdapterError.code` is an internal string, but the SDK only derives a
538
+ * JSON-RPC code from a `RequestError`. Everything else collapses to an opaque
539
+ * `-32603 Internal error`, which hides the reason and defeats client-side
540
+ * recovery (an ACP client cannot see that it must authenticate). Map the codes
541
+ * that have a defined ACP/JSON-RPC counterpart onto a real `RequestError`.
542
+ */
543
+ export function acpRequestFailure(error: unknown): unknown {
544
+ const code = typeof error === "object" && error !== null && "code" in error ? error.code : undefined;
545
+ if (typeof code !== "string") return error;
546
+ const message = error instanceof Error ? error.message : code;
547
+ switch (code) {
548
+ case "authentication_failed":
549
+ return RequestError.authRequired({ code, details: message }, message);
550
+ case "not_found":
551
+ return RequestError.resourceNotFound(message);
552
+ case "invalid_input":
553
+ case "unsupported":
554
+ case "unsupported_content":
555
+ return RequestError.invalidParams({ code, details: message }, message);
556
+ default:
557
+ // The remaining internal codes (conflict, unavailable, busy, …) have no ACP
558
+ // counterpart and stay -32603. Keep the discriminator in `data` so a client can
559
+ // branch on retry/reconnect instead of parsing an English message.
560
+ return RequestError.internalError({ code, details: message }, message);
561
+ }
562
+ }
563
+
450
564
  /** Registers a permission provider only when the ACP client requires prompts. */
451
565
  export function acpProviderRegistrations(
452
566
  capabilities: ClientCapabilities | undefined,
@@ -572,6 +686,7 @@ export class AcpAgent implements Agent {
572
686
  readonly #attaching = new Map<string, PendingAttachment>();
573
687
  readonly #resolvingExisting = new Map<string, PendingAttachment>();
574
688
  readonly #knownSessionCwds = new Map<string, string>();
689
+ readonly #knownSessionMetadata = new Map<string, { title?: string; updatedAt?: string }>();
575
690
  readonly #pendingCloseIdempotencyKeys = new Map<string, string>();
576
691
  readonly #sessionEpochs = new Map<string, number>();
577
692
  readonly #tearingDown = new Map<string, number>();
@@ -624,7 +739,14 @@ export class AcpAgent implements Agent {
624
739
  agentCapabilities: {
625
740
  loadSession: true,
626
741
  promptCapabilities: { embeddedContext: true, image: true },
627
- sessionCapabilities: { list: {}, fork: {}, resume: {}, close: {}, delete: {} },
742
+ mcpCapabilities: { http: true, sse: true },
743
+ sessionCapabilities: {
744
+ list: {},
745
+ fork: {},
746
+ resume: {},
747
+ close: {},
748
+ delete: {},
749
+ },
628
750
  },
629
751
  };
630
752
  }
@@ -636,16 +758,19 @@ export class AcpAgent implements Agent {
636
758
  }
637
759
 
638
760
  async newSession(params: NewSessionRequest): Promise<NewSessionResponse> {
639
- this.#assertNoMcpServers(params);
761
+ const mcpServers = this.#mcpServers(params);
640
762
  this.#assertAbsoluteCwd(params.cwd);
641
- const result = await (await this.#brokerAdapter()).global(
763
+ this.#assertNoAdditionalDirectories(params.additionalDirectories);
764
+ const result = await this.#launchSessionWithMcp(
642
765
  "session.create",
643
766
  {
644
767
  cwd: params.cwd,
645
768
  target: { path: params.cwd },
646
769
  ...(this.#startupOptions?.modelPreset ? { modelPreset: this.#startupOptions.modelPreset } : {}),
770
+ ...(mcpServers.length > 0 ? { mcpServers, readinessTimeoutMs: ACP_MCP_LIFECYCLE_TIMEOUT_MS } : {}),
647
771
  },
648
772
  randomUUID(),
773
+ mcpServers,
649
774
  );
650
775
  const id = sessionId(result);
651
776
  this.#knownSessionCwds.set(id, params.cwd);
@@ -653,7 +778,7 @@ export class AcpAgent implements Agent {
653
778
  await this.#attach(id, params.cwd, endpoint(result));
654
779
  await applyAcpStartupOptions(this.#adapter(id), this.#startupOptions);
655
780
  this.#scheduleBootstrap(id);
656
- return { sessionId: id, ...(await this.#sessionState(id)) };
781
+ return { sessionId: id, ...(await this.#sessionState(id, true)) };
657
782
  } catch (error) {
658
783
  await this.#discardNewSession(id);
659
784
  throw error;
@@ -661,35 +786,40 @@ export class AcpAgent implements Agent {
661
786
  }
662
787
 
663
788
  async loadSession(params: LoadSessionRequest): Promise<LoadSessionResponse> {
664
- this.#assertNoMcpServers(params);
789
+ const mcpServers = this.#mcpServers(params);
665
790
  this.#assertAbsoluteCwd(params.cwd);
666
- await this.#attachExisting(params.sessionId, params.cwd);
791
+ this.#assertNoAdditionalDirectories(params.additionalDirectories);
792
+ await this.#attachExisting(params.sessionId, params.cwd, mcpServers);
667
793
  await this.#replaySession(params.sessionId);
668
794
  this.#scheduleBootstrap(params.sessionId);
669
795
  return await this.#sessionState(params.sessionId);
670
796
  }
671
797
 
672
798
  async resumeSession(params: ResumeSessionRequest): Promise<ResumeSessionResponse> {
673
- this.#assertNoMcpServers(params);
799
+ const mcpServers = this.#mcpServers(params);
674
800
  this.#assertAbsoluteCwd(params.cwd);
675
- await this.#attachExisting(params.sessionId, params.cwd);
801
+ this.#assertNoAdditionalDirectories(params.additionalDirectories);
802
+ await this.#attachExisting(params.sessionId, params.cwd, mcpServers);
676
803
  this.#scheduleBootstrap(params.sessionId);
677
804
  return await this.#sessionState(params.sessionId);
678
805
  }
679
806
 
680
807
  async unstable_forkSession(params: ForkSessionRequest): Promise<ForkSessionResponse> {
681
- this.#assertNoMcpServers(params);
808
+ const mcpServers = this.#mcpServers(params);
682
809
  this.#assertAbsoluteCwd(params.cwd);
810
+ this.#assertNoAdditionalDirectories(params.additionalDirectories);
683
811
  const source = await this.#resolveSavedSession(params.sessionId, params.cwd);
684
- const result = await (await this.#brokerAdapter()).global(
812
+ const result = await this.#launchSessionWithMcp(
685
813
  "session.fork",
686
814
  {
687
815
  cwd: params.cwd,
688
816
  sourceSessionId: params.sessionId,
689
817
  sourceSessionPath: source,
690
818
  target: { path: params.cwd },
819
+ ...(mcpServers.length > 0 ? { mcpServers, readinessTimeoutMs: ACP_MCP_LIFECYCLE_TIMEOUT_MS } : {}),
691
820
  },
692
821
  randomUUID(),
822
+ mcpServers,
693
823
  );
694
824
  const id = sessionId(result);
695
825
  this.#knownSessionCwds.set(id, params.cwd);
@@ -730,7 +860,12 @@ export class AcpAgent implements Agent {
730
860
  this.#knownSessionCwds.set(candidate.sessionId, params.cwd);
731
861
  }
732
862
  }
733
- return paginateAcpSessions(listed, params.cwd ?? undefined, this.#cursor(params.cursor));
863
+ return paginateAcpSessions(
864
+ listed,
865
+ params.cwd ?? undefined,
866
+ this.#cursor(params.cursor),
867
+ this.#knownSessionMetadata,
868
+ );
734
869
  }
735
870
 
736
871
  closeSession(params: CloseSessionRequest): Promise<CloseSessionResponse> {
@@ -765,6 +900,7 @@ export class AcpAgent implements Agent {
765
900
  } catch (error) {
766
901
  if (error instanceof AcpSdkAdapterError && error.code === "not_found") {
767
902
  this.#knownSessionCwds.delete(params.sessionId);
903
+ this.#knownSessionMetadata.delete(params.sessionId);
768
904
  return {};
769
905
  }
770
906
  throw error;
@@ -775,6 +911,7 @@ export class AcpAgent implements Agent {
775
911
  this.#lifecycleIdempotencyKey(params.sessionId, "session.delete"),
776
912
  );
777
913
  this.#knownSessionCwds.delete(params.sessionId);
914
+ this.#knownSessionMetadata.delete(params.sessionId);
778
915
  return {};
779
916
  } finally {
780
917
  this.#finishTeardown(params.sessionId);
@@ -800,7 +937,11 @@ export class AcpAgent implements Agent {
800
937
  await this.setSessionMode({ sessionId: params.sessionId, modeId: params.value });
801
938
  break;
802
939
  case MODEL_CONFIG_ID:
803
- await this.#adapter(params.sessionId).setModel(params.value);
940
+ if (this.#startupOptions?.modelPreset === undefined) {
941
+ await this.#adapter(params.sessionId).setModel(params.value);
942
+ } else if (params.value !== ACP_CUSTOM_MODEL_PRESET) {
943
+ await this.#adapter(params.sessionId).control("model.profile.set", { id: params.value });
944
+ }
804
945
  break;
805
946
  case THINKING_CONFIG_ID:
806
947
  await this.#adapter(params.sessionId).control("thinking.set", { level: params.value });
@@ -823,6 +964,7 @@ export class AcpAgent implements Agent {
823
964
  const record = this.#sessions.get(params.sessionId);
824
965
  if (!record) throw new AcpSdkAdapterError("not_found", `Unsupported ACP session: ${params.sessionId}`);
825
966
  if (record.activePrompt) throw new AcpSdkAdapterError("conflict", "ACP session already has an active prompt.");
967
+ if (record.authFailure) throw new AcpSdkAdapterError("authentication_failed", record.authFailure);
826
968
  const payload = acpPromptPayload(params.prompt);
827
969
  let waiter!: PromptWaiter;
828
970
  const response = new Promise<PromptResponse>((resolve, reject) => {
@@ -833,6 +975,7 @@ export class AcpAgent implements Agent {
833
975
  steeringAtAcknowledgement: record.busy,
834
976
  boundary: record.inboundSequence,
835
977
  correlation: {},
978
+ emittedAssistantText: "",
836
979
  resolve,
837
980
  reject,
838
981
  };
@@ -874,18 +1017,33 @@ export class AcpAgent implements Agent {
874
1017
  }
875
1018
 
876
1019
  async extMethod(method: string, params: JsonObject): Promise<JsonObject> {
1020
+ // An unrecognized extension method is a protocol failure, not an application
1021
+ // result: it must reach the client as JSON-RPC -32601 rather than a resolved
1022
+ // payload. Recognized `_gjc/*` methods keep their `{ok:false}` result contract.
1023
+ if (
1024
+ method !== "session/set_model" &&
1025
+ method !== "_gjc/sdk/global" &&
1026
+ method !== "_gjc/sdk/control" &&
1027
+ method !== "_gjc/sdk/query"
1028
+ )
1029
+ throw RequestError.methodNotFound(method);
877
1030
  try {
1031
+ if (method === "session/set_model") {
1032
+ const sessionId = typeof params.sessionId === "string" ? params.sessionId : undefined;
1033
+ const modelId = typeof params.modelId === "string" ? params.modelId : undefined;
1034
+ if (!sessionId) throw new AcpSdkAdapterError("invalid_input", "sessionId is required.");
1035
+ if (!modelId) throw new AcpSdkAdapterError("invalid_input", "modelId is required.");
1036
+ await this.setSessionConfigOption({ sessionId, configId: MODEL_CONFIG_ID, value: modelId });
1037
+ return {};
1038
+ }
878
1039
  if (method === "_gjc/sdk/global") {
879
1040
  const result = await (await this.#brokerAdapter()).handle(method, params);
880
1041
  return object(result) ?? {};
881
1042
  }
882
- if (method === "_gjc/sdk/control" || method === "_gjc/sdk/query") {
883
- const id = typeof params.sessionId === "string" ? params.sessionId : undefined;
884
- if (!id) throw new AcpSdkAdapterError("invalid_input", "sessionId is required.");
885
- const result = await this.#adapter(id).handle(method, params);
886
- return object(result) ?? {};
887
- }
888
- throw new AcpSdkAdapterError("method_not_found", `Unknown ACP ext method: ${method}`);
1043
+ const id = typeof params.sessionId === "string" ? params.sessionId : undefined;
1044
+ if (!id) throw new AcpSdkAdapterError("invalid_input", "sessionId is required.");
1045
+ const result = await this.#adapter(id).handle(method, params);
1046
+ return object(result) ?? {};
889
1047
  } catch (error) {
890
1048
  const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "internal";
891
1049
  const message = error instanceof Error ? error.message : String(error);
@@ -946,12 +1104,14 @@ export class AcpAgent implements Agent {
946
1104
  return !["terminal_uncertain", "cleanup_pending", "broker_restarting", "unavailable"].includes(error.code);
947
1105
  }
948
1106
 
949
- async #attachExisting(id: string, cwd: string): Promise<void> {
1107
+ async #attachExisting(id: string, cwd: string, mcpServers: SessionLifecycleMcpServer[] = []): Promise<void> {
950
1108
  const epoch = this.#sessionEpoch(id);
951
1109
  const attached = this.#sessions.get(id);
952
1110
  if (attached) {
953
1111
  if (path.resolve(attached.cwd) !== path.resolve(cwd))
954
1112
  throw new AcpSdkAdapterError("conflict", `ACP session ${id} has conflicting cwd authority.`);
1113
+ if (mcpServers.length > 0)
1114
+ throw new AcpSdkAdapterError("conflict", `ACP session ${id} already has immutable MCP configuration.`);
955
1115
  return;
956
1116
  }
957
1117
  const knownCwd = this.#knownSessionCwds.get(id);
@@ -968,7 +1128,7 @@ export class AcpAgent implements Agent {
968
1128
  return;
969
1129
  }
970
1130
 
971
- const task = this.#resolveExistingAttachment(id, cwd, epoch);
1131
+ const task = this.#resolveExistingAttachment(id, cwd, epoch, mcpServers);
972
1132
  const pending = { epoch, task };
973
1133
  this.#resolvingExisting.set(id, pending);
974
1134
  try {
@@ -979,11 +1139,18 @@ export class AcpAgent implements Agent {
979
1139
  }
980
1140
  }
981
1141
 
982
- async #resolveExistingAttachment(id: string, cwd: string, epoch: number): Promise<void> {
1142
+ async #resolveExistingAttachment(
1143
+ id: string,
1144
+ cwd: string,
1145
+ epoch: number,
1146
+ mcpServers: SessionLifecycleMcpServer[],
1147
+ ): Promise<void> {
983
1148
  this.#assertSessionEpoch(id, epoch);
984
1149
  const indexed = await this.#scopedBrokerSession(id, cwd);
985
1150
  this.#assertSessionEpoch(id, epoch);
986
1151
  if (indexed?.live) {
1152
+ if (mcpServers.length > 0)
1153
+ throw new AcpSdkAdapterError("conflict", `ACP session ${id} already has immutable MCP configuration.`);
987
1154
  const result = await this.#brokerEndpoint(id, indexed.endpointGeneration);
988
1155
  this.#assertSessionEpoch(id, epoch);
989
1156
  await this.#attach(id, cwd, endpoint(result), epoch);
@@ -992,10 +1159,17 @@ export class AcpAgent implements Agent {
992
1159
 
993
1160
  const saved = await this.#resolveSavedSession(id, cwd);
994
1161
  this.#assertSessionEpoch(id, epoch);
995
- const result = await (await this.#brokerAdapter()).global(
1162
+ const result = await this.#launchSessionWithMcp(
996
1163
  "session.resume",
997
- { cwd, sessionId: id, sessionPath: saved, target: { path: cwd } },
1164
+ {
1165
+ cwd,
1166
+ sessionId: id,
1167
+ sessionPath: saved,
1168
+ target: { path: cwd },
1169
+ ...(mcpServers.length > 0 ? { mcpServers, readinessTimeoutMs: ACP_MCP_LIFECYCLE_TIMEOUT_MS } : {}),
1170
+ },
998
1171
  randomUUID(),
1172
+ mcpServers,
999
1173
  );
1000
1174
  this.#assertSessionEpoch(id, epoch);
1001
1175
  await this.#attach(id, cwd, endpoint(result), epoch);
@@ -1065,6 +1239,7 @@ export class AcpAgent implements Agent {
1065
1239
  frameTail: Promise.resolve(),
1066
1240
  inboundSequence: 0,
1067
1241
  busy: false,
1242
+ toolArgs: new Map(),
1068
1243
  };
1069
1244
  record.unsubscribe = adapter.onFrame(frame => this.#enqueueSdkFrame(id, adapter!, frame));
1070
1245
  record.reconnectUnsubscribe = adapter.onReconnectFailed(error =>
@@ -1117,6 +1292,7 @@ export class AcpAgent implements Agent {
1117
1292
  async #discardNewSession(id: string): Promise<void> {
1118
1293
  await this.#teardownSession(id, "discarded", true);
1119
1294
  this.#knownSessionCwds.delete(id);
1295
+ this.#knownSessionMetadata.delete(id);
1120
1296
  }
1121
1297
 
1122
1298
  async #closeOwnedSession(id: string): Promise<CloseSessionResponse> {
@@ -1128,6 +1304,7 @@ export class AcpAgent implements Agent {
1128
1304
  if (attaching) await Promise.allSettled([attaching.task]);
1129
1305
  await this.#teardownSession(id, "closed", true);
1130
1306
  this.#knownSessionCwds.delete(id);
1307
+ this.#knownSessionMetadata.delete(id);
1131
1308
  return {};
1132
1309
  } finally {
1133
1310
  this.#finishTeardown(id);
@@ -1307,13 +1484,23 @@ export class AcpAgent implements Agent {
1307
1484
  const received = receivedSdkEvent(frame);
1308
1485
  if (!received) return;
1309
1486
  const { event, wirePayload } = received;
1487
+ const toolCallId = typeof event.toolCallId === "string" ? event.toolCallId : undefined;
1488
+ if (
1489
+ toolCallId &&
1490
+ (event.type === "tool_execution_start" || event.type === "tool_execution_update") &&
1491
+ "args" in event
1492
+ ) {
1493
+ record.toolArgs.set(toolCallId, event.args);
1494
+ }
1310
1495
  const correlation = correlationFrom(frame, event);
1311
1496
  const activePrompt = record.activePrompt;
1312
1497
  // 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.
1498
+ // terminal waiter is resolved only after acknowledgement and publication
1499
+ // succeed. Fast hosts may emit the whole turn before acknowledging the
1500
+ // control request; retaining that activity prevents an idle ACP prompt from
1501
+ // remaining open forever.
1315
1502
  if (
1316
- activePrompt?.acknowledged &&
1503
+ activePrompt &&
1317
1504
  sequence > activePrompt.boundary &&
1318
1505
  !correlationsConflict(activePrompt.correlation, correlation)
1319
1506
  ) {
@@ -1322,6 +1509,8 @@ export class AcpAgent implements Agent {
1322
1509
  } else if (event.type === "agent_end") {
1323
1510
  if (
1324
1511
  correlationsMatch(activePrompt.correlation, correlation) ||
1512
+ correlation.commandId !== undefined ||
1513
+ correlation.turnId !== undefined ||
1325
1514
  activePrompt.activityObserved ||
1326
1515
  activePrompt.steeringAtAcknowledgement
1327
1516
  )
@@ -1331,17 +1520,69 @@ export class AcpAgent implements Agent {
1331
1520
  if (wirePayload) {
1332
1521
  for (const notification of mapAgentWireEventPayloadToAcpSessionUpdates(wirePayload as never, id, {
1333
1522
  cwd: record.cwd,
1523
+ getToolArgs: id => record.toolArgs.get(id),
1334
1524
  getMessageProgress: message => {
1335
1525
  if (!activePrompt || !object(message)) return undefined;
1336
1526
  activePrompt.messageProgress ??= { textEmitted: false, thoughtEmitted: false };
1337
1527
  return activePrompt.messageProgress;
1338
1528
  },
1339
- }))
1529
+ })) {
1530
+ if (
1531
+ activePrompt &&
1532
+ notification.update.sessionUpdate === "agent_message_chunk" &&
1533
+ notification.update.content.type === "text"
1534
+ )
1535
+ activePrompt.emittedAssistantText += notification.update.content.text;
1340
1536
  await this.#publishSessionUpdate(id, notification, adapter);
1537
+ }
1538
+ }
1539
+ if (toolCallId && event.type === "tool_execution_end") record.toolArgs.delete(toolCallId);
1540
+ if (event.type === "agent_start") {
1541
+ await this.#publishSessionUpdate(
1542
+ id,
1543
+ {
1544
+ sessionId: id,
1545
+ update: {
1546
+ sessionUpdate: "session_info_update",
1547
+ updatedAt: new Date().toISOString(),
1548
+ _meta: { gjcPhase: "working", running: true, gjcRunning: true },
1549
+ },
1550
+ },
1551
+ adapter,
1552
+ );
1341
1553
  }
1342
1554
  if (event.type === "message_end" && object(event.message)?.role === "assistant" && activePrompt)
1343
1555
  activePrompt.messageProgress = undefined;
1344
- if (event.type === "agent_end") await this.#emitEndOfTurnUpdates(id, adapter);
1556
+ if (event.type === "agent_end") {
1557
+ const finalText = typeof event.finalText === "string" ? event.finalText : "";
1558
+ if (activePrompt && finalText) {
1559
+ const resolution = resolveAcpFinalText(activePrompt.emittedAssistantText, finalText);
1560
+ if (resolution.kind === "emit") {
1561
+ activePrompt.emittedAssistantText += resolution.text;
1562
+ await this.#publishSessionUpdate(
1563
+ id,
1564
+ {
1565
+ sessionId: id,
1566
+ update: {
1567
+ sessionUpdate: "agent_message_chunk",
1568
+ content: { type: "text", text: resolution.text },
1569
+ ...(resolution.final.truncated ? { _meta: { gjcFinalTextTruncated: true } } : {}),
1570
+ },
1571
+ },
1572
+ adapter,
1573
+ );
1574
+ } else if (resolution.kind === "divergent") {
1575
+ logger.warn("acp_final_text_diverged", {
1576
+ sessionId: id,
1577
+ ...(activePrompt.correlation.commandId ? { commandId: activePrompt.correlation.commandId } : {}),
1578
+ ...(activePrompt.correlation.turnId ? { turnId: activePrompt.correlation.turnId } : {}),
1579
+ streamedLength: activePrompt.emittedAssistantText.length,
1580
+ finalLength: resolution.final.text.length,
1581
+ });
1582
+ }
1583
+ }
1584
+ await this.#emitEndOfTurnUpdates(id, adapter);
1585
+ }
1345
1586
  if (activePrompt) this.#settlePrompt(record, activePrompt);
1346
1587
  }
1347
1588
 
@@ -1360,6 +1601,7 @@ export class AcpAgent implements Agent {
1360
1601
 
1361
1602
  async #emitEndOfTurnUpdates(id: string, adapter: AcpSdkAdapter): Promise<void> {
1362
1603
  let usage: JsonObject | undefined;
1604
+ let title: string | undefined;
1363
1605
  try {
1364
1606
  const response = object(await adapter.query("context.get"));
1365
1607
  const result = object(response?.result) ?? response;
@@ -1367,6 +1609,15 @@ export class AcpAgent implements Agent {
1367
1609
  } catch {
1368
1610
  // Context usage is advisory ACP metadata; prompt completion remains authoritative.
1369
1611
  }
1612
+ try {
1613
+ const response = object(await adapter.query("session.metadata"));
1614
+ const result = object(response?.result) ?? response;
1615
+ const metadata = pageItems(result)[0];
1616
+ const item = object(metadata);
1617
+ if (typeof item?.name === "string" && item.name) title = item.name;
1618
+ } catch {
1619
+ // Session naming is advisory; prompt completion remains authoritative.
1620
+ }
1370
1621
  if (typeof usage?.tokens === "number" && typeof usage.contextWindow === "number") {
1371
1622
  await this.#publishSessionUpdate(
1372
1623
  id,
@@ -1381,13 +1632,16 @@ export class AcpAgent implements Agent {
1381
1632
  adapter,
1382
1633
  );
1383
1634
  }
1635
+ const updatedAt = new Date().toISOString();
1636
+ this.#knownSessionMetadata.set(id, { ...(title ? { title } : {}), updatedAt });
1384
1637
  await this.#publishSessionUpdate(
1385
1638
  id,
1386
1639
  {
1387
1640
  sessionId: id,
1388
1641
  update: {
1389
1642
  sessionUpdate: "session_info_update",
1390
- updatedAt: new Date().toISOString(),
1643
+ ...(title ? { title } : {}),
1644
+ updatedAt,
1391
1645
  _meta: { gjcPhase: "idle", running: false, gjcRunning: false },
1392
1646
  },
1393
1647
  },
@@ -1411,27 +1665,68 @@ export class AcpAgent implements Agent {
1411
1665
  }
1412
1666
  }
1413
1667
 
1414
- async #sessionState(id: string): Promise<Pick<NewSessionResponse, "configOptions" | "modes">> {
1668
+ async #sessionState(
1669
+ id: string,
1670
+ rejectUnavailableStartupPreset = false,
1671
+ ): Promise<Pick<NewSessionResponse, "configOptions" | "modes">> {
1415
1672
  const record = this.#sessions.get(id);
1416
1673
  if (!record) throw new AcpSdkAdapterError("not_found", `Unsupported ACP session: ${id}`);
1417
- const [config, models] = await Promise.all([
1674
+ const modelPreset = this.#startupOptions?.modelPreset;
1675
+ const [config, modelCatalog] = await Promise.all([
1418
1676
  record.adapter.query("config.list/get"),
1419
- record.adapter.query("models.list/current"),
1677
+ record.adapter.query(modelPreset === undefined ? "models.list/current" : "models.profiles.list"),
1420
1678
  ]);
1421
- return acpSessionStateFromConfig(config, models);
1679
+ record.authFailure = undefined;
1680
+ if (modelPreset !== undefined) {
1681
+ const activePreset = configValues(config).get(MODEL_PRESET_CONFIG_KEY);
1682
+ const activeProfile = pageItems(modelCatalog)
1683
+ .map(item => object(item))
1684
+ .find(item => item?.id === activePreset);
1685
+ if (activePreset && activeProfile?.available === false) {
1686
+ record.authFailure =
1687
+ `Model preset "${activePreset}" has no usable provider credentials. ` +
1688
+ "Authenticate the required provider in Gajae Code or select an available preset before prompting.";
1689
+ if (rejectUnavailableStartupPreset && activePreset === modelPreset)
1690
+ throw new AcpSdkAdapterError("authentication_failed", record.authFailure);
1691
+ }
1692
+ }
1693
+ return acpSessionStateFromConfig(config, modelCatalog, modelPreset);
1694
+ }
1695
+
1696
+ async #publishAvailableCommands(id: string, adapter: AcpSdkAdapter): Promise<void> {
1697
+ let skills: unknown;
1698
+ try {
1699
+ skills = await adapter.query("skill.list/state");
1700
+ } catch {
1701
+ // Builtins remain useful when an older SDK host cannot expose skill state.
1702
+ }
1703
+ await this.#publishSessionUpdate(
1704
+ id,
1705
+ {
1706
+ sessionId: id,
1707
+ update: {
1708
+ sessionUpdate: "available_commands_update",
1709
+ availableCommands: acpAvailableCommandsFromSkills(skills),
1710
+ },
1711
+ },
1712
+ adapter,
1713
+ );
1422
1714
  }
1423
1715
 
1424
1716
  async #replaySession(id: string): Promise<void> {
1425
1717
  const adapter = this.#adapter(id);
1718
+ const record = this.#sessions.get(id);
1719
+ if (!record) return;
1426
1720
  let cursor: string | undefined;
1427
1721
  let imageLimitationReported = false;
1722
+ const replayTools = new Map<string, { name: string; args: unknown }>();
1428
1723
  for (let pageCount = 0; pageCount < MAX_ACP_REPLAY_PAGES; pageCount++) {
1429
1724
  const response = object(await adapter.query("transcript.list", {}, cursor));
1430
1725
  const result = object(response?.result) ?? response;
1431
1726
  const page = object(result?.page);
1432
1727
  for (const item of Array.isArray(page?.items) ? page.items : []) {
1433
1728
  const message = object(item);
1434
- if (!message || (message.role !== "user" && message.role !== "assistant")) continue;
1729
+ if (!message) continue;
1435
1730
  const content = transcriptReplayContent(message);
1436
1731
  if (!imageLimitationReported) {
1437
1732
  imageLimitationReported = true;
@@ -1448,6 +1743,95 @@ export class AcpAgent implements Agent {
1448
1743
  );
1449
1744
  }
1450
1745
  const messageId = typeof message.id === "string" ? message.id : undefined;
1746
+ const richContent = Array.isArray(message.content) ? message.content : undefined;
1747
+ if ((message.role === "user" || message.role === "assistant") && richContent) {
1748
+ for (const rawBlock of richContent) {
1749
+ const block = object(rawBlock);
1750
+ if (!block || typeof block.type !== "string") continue;
1751
+ if (block.type === "text" && typeof block.text === "string" && block.text.length > 0) {
1752
+ await this.#publishSessionUpdate(
1753
+ id,
1754
+ {
1755
+ sessionId: id,
1756
+ update: {
1757
+ sessionUpdate: message.role === "user" ? "user_message_chunk" : "agent_message_chunk",
1758
+ content: { type: "text", text: block.text },
1759
+ ...(messageId ? { messageId } : {}),
1760
+ },
1761
+ },
1762
+ adapter,
1763
+ );
1764
+ } else if (
1765
+ message.role === "assistant" &&
1766
+ block.type === "thinking" &&
1767
+ typeof block.thinking === "string" &&
1768
+ block.thinking.length > 0
1769
+ ) {
1770
+ await this.#publishSessionUpdate(
1771
+ id,
1772
+ {
1773
+ sessionId: id,
1774
+ update: {
1775
+ sessionUpdate: "agent_thought_chunk",
1776
+ content: { type: "text", text: block.thinking },
1777
+ ...(messageId ? { messageId } : {}),
1778
+ },
1779
+ },
1780
+ adapter,
1781
+ );
1782
+ } else if (
1783
+ message.role === "assistant" &&
1784
+ block.type === "toolCall" &&
1785
+ typeof block.id === "string" &&
1786
+ typeof block.name === "string"
1787
+ ) {
1788
+ const args = block.arguments ?? {};
1789
+ replayTools.set(block.id, { name: block.name, args });
1790
+ await this.#publishSessionUpdate(
1791
+ id,
1792
+ {
1793
+ sessionId: id,
1794
+ update: buildToolCallStartUpdate({
1795
+ toolCallId: block.id,
1796
+ toolName: block.name,
1797
+ args,
1798
+ cwd: record.cwd,
1799
+ }),
1800
+ },
1801
+ adapter,
1802
+ );
1803
+ }
1804
+ }
1805
+ continue;
1806
+ }
1807
+ if (message.role === "toolResult" && typeof message.toolCallId === "string") {
1808
+ const replayTool = replayTools.get(message.toolCallId);
1809
+ const toolName = typeof message.toolName === "string" ? message.toolName : replayTool?.name;
1810
+ if (!toolName) continue;
1811
+ const resultContent = richContent
1812
+ ?.map(object)
1813
+ .filter(
1814
+ (block): block is JsonObject =>
1815
+ block !== undefined && block.type === "text" && typeof block.text === "string",
1816
+ )
1817
+ .map(block => ({ type: "text" as const, text: String(block.text) }));
1818
+ for (const notification of mapAgentSessionEventToAcpSessionUpdates(
1819
+ {
1820
+ type: "tool_execution_end",
1821
+ toolCallId: message.toolCallId,
1822
+ toolName,
1823
+ result: { content: resultContent ?? content.blocks },
1824
+ isError: message.isError === true,
1825
+ } as never,
1826
+ id,
1827
+ { cwd: record.cwd, getToolArgs: toolCallId => replayTools.get(toolCallId)?.args },
1828
+ )) {
1829
+ await this.#publishSessionUpdate(id, notification, adapter);
1830
+ }
1831
+ replayTools.delete(message.toolCallId);
1832
+ continue;
1833
+ }
1834
+ if (message.role !== "user" && message.role !== "assistant") continue;
1451
1835
  for (const block of content.blocks) {
1452
1836
  await this.#publishSessionUpdate(
1453
1837
  id,
@@ -1473,17 +1857,33 @@ export class AcpAgent implements Agent {
1473
1857
  setTimeout(() => {
1474
1858
  const record = this.#sessions.get(id);
1475
1859
  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 },
1860
+ void (async () => {
1861
+ await this.#publishAvailableCommands(id, record.adapter);
1862
+ if (record.authFailure) {
1863
+ await this.#publishSessionUpdate(
1864
+ id,
1865
+ {
1866
+ sessionId: id,
1867
+ update: {
1868
+ sessionUpdate: "agent_thought_chunk",
1869
+ content: { type: "text", text: `[error:auth] ${record.authFailure}\n` },
1870
+ },
1871
+ },
1872
+ record.adapter,
1873
+ );
1874
+ }
1875
+ await this.#publishSessionUpdate(
1876
+ id,
1877
+ {
1878
+ sessionId: id,
1879
+ update: {
1880
+ sessionUpdate: "session_info_update",
1881
+ _meta: { gjcPhase: "idle", running: false, gjcRunning: false },
1882
+ },
1483
1883
  },
1484
- },
1485
- record.adapter,
1486
- ).catch(() => undefined);
1884
+ record.adapter,
1885
+ );
1886
+ })().catch(() => undefined);
1487
1887
  }, ACP_BOOTSTRAP_RACE_GUARD_MS);
1488
1888
  }
1489
1889
 
@@ -1498,9 +1898,138 @@ export class AcpAgent implements Agent {
1498
1898
  if (!path.isAbsolute(cwd)) throw new Error(`ACP cwd must be an absolute path: ${cwd}`);
1499
1899
  }
1500
1900
 
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.");
1901
+ #assertNoAdditionalDirectories(directories: string[] | null | undefined): void {
1902
+ if (directories && directories.length > 0)
1903
+ throw new AcpSdkAdapterError("unsupported", "ACP additional directories are not supported.");
1904
+ }
1905
+
1906
+ #mcpServers(params: { mcpServers?: unknown[] }): SessionLifecycleMcpServer[] {
1907
+ const servers = params.mcpServers ?? [];
1908
+ if (servers.length > 64)
1909
+ throw new AcpSdkAdapterError("unsupported", "ACP supports at most 64 MCP servers per session.");
1910
+ const result: SessionLifecycleMcpServer[] = [];
1911
+ const names = new Set<string>();
1912
+ for (const value of servers) {
1913
+ if (typeof value !== "object" || value === null || Array.isArray(value))
1914
+ throw new AcpSdkAdapterError("invalid_input", "ACP MCP server definitions must be objects.");
1915
+ const server = value as Record<string, unknown>;
1916
+ if (typeof server.name !== "string" || !/^[A-Za-z0-9_.-]{1,100}$/.test(server.name) || names.has(server.name))
1917
+ throw new AcpSdkAdapterError("invalid_input", "ACP MCP servers must have unique safe names.");
1918
+ names.add(server.name);
1919
+ if (server.type === "http" || server.type === "sse") {
1920
+ if (
1921
+ typeof server.url !== "string" ||
1922
+ server.url.length > 8_192 ||
1923
+ !Array.isArray(server.headers) ||
1924
+ server.headers.length > 100
1925
+ )
1926
+ throw new AcpSdkAdapterError("invalid_input", "ACP remote MCP servers require a valid URL and headers.");
1927
+ let parsedUrl: URL;
1928
+ try {
1929
+ parsedUrl = new URL(server.url);
1930
+ } catch {
1931
+ throw new AcpSdkAdapterError("invalid_input", "ACP MCP URL is invalid.");
1932
+ }
1933
+ if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:")
1934
+ throw new AcpSdkAdapterError("invalid_input", "ACP MCP URLs must use HTTP or HTTPS.");
1935
+ const headers: Record<string, string> = {};
1936
+ for (const value of server.headers) {
1937
+ const header = object(value);
1938
+ if (
1939
+ typeof header?.name !== "string" ||
1940
+ header.name.length === 0 ||
1941
+ header.name.length > 256 ||
1942
+ header.name.includes("\r") ||
1943
+ header.name.includes("\n") ||
1944
+ typeof header.value !== "string" ||
1945
+ header.value.length > 8_192 ||
1946
+ header.value.includes("\r") ||
1947
+ header.value.includes("\n") ||
1948
+ Object.hasOwn(headers, header.name)
1949
+ )
1950
+ throw new AcpSdkAdapterError(
1951
+ "invalid_input",
1952
+ "ACP MCP headers must have unique valid names and values.",
1953
+ );
1954
+ headers[header.name] = header.value;
1955
+ }
1956
+ result.push({
1957
+ type: server.type,
1958
+ name: server.name,
1959
+ url: parsedUrl.toString(),
1960
+ ...(Object.keys(headers).length > 0 ? { headers } : {}),
1961
+ });
1962
+ continue;
1963
+ }
1964
+ if (
1965
+ (server.type !== undefined && server.type !== "stdio") ||
1966
+ typeof server.command !== "string" ||
1967
+ server.command.length > 4_096 ||
1968
+ !path.isAbsolute(server.command) ||
1969
+ !Array.isArray(server.args) ||
1970
+ server.args.length > 100 ||
1971
+ !server.args.every(argument => typeof argument === "string" && argument.length <= 8_192) ||
1972
+ !Array.isArray(server.env) ||
1973
+ server.env.length > 100
1974
+ )
1975
+ throw new AcpSdkAdapterError(
1976
+ "invalid_input",
1977
+ "ACP stdio MCP servers require an absolute command and bounded arguments and environment variables.",
1978
+ );
1979
+ const env: Record<string, string> = {};
1980
+ for (const value of server.env) {
1981
+ const variable = object(value);
1982
+ if (
1983
+ typeof variable?.name !== "string" ||
1984
+ !/^[A-Za-z_][A-Za-z0-9_]*$/.test(variable.name) ||
1985
+ typeof variable.value !== "string" ||
1986
+ variable.value.length > 32_768 ||
1987
+ Object.hasOwn(env, variable.name)
1988
+ )
1989
+ throw new AcpSdkAdapterError(
1990
+ "invalid_input",
1991
+ "ACP MCP environment variables must have unique valid names and string values.",
1992
+ );
1993
+ env[variable.name] = variable.value;
1994
+ }
1995
+ result.push({
1996
+ name: server.name,
1997
+ command: server.command,
1998
+ args: server.args as string[],
1999
+ ...(Object.keys(env).length > 0 ? { env } : {}),
2000
+ });
2001
+ }
2002
+ return result;
2003
+ }
2004
+
2005
+ async #launchSessionWithMcp(
2006
+ operation: "session.create" | "session.fork" | "session.resume",
2007
+ input: JsonObject,
2008
+ idempotencyKey: string,
2009
+ mcpServers: SessionLifecycleMcpServer[],
2010
+ ): Promise<unknown> {
2011
+ try {
2012
+ return await (await this.#brokerAdapter()).global(operation, input, idempotencyKey);
2013
+ } catch (error) {
2014
+ const code =
2015
+ typeof error === "object" && error !== null && "code" in error && typeof error.code === "string"
2016
+ ? error.code
2017
+ : undefined;
2018
+ if (
2019
+ mcpServers.length === 0 ||
2020
+ code === "invalid_input" ||
2021
+ code === "authentication_failed" ||
2022
+ code === "unknown_model_profile" ||
2023
+ code === "model_profile_registry_error"
2024
+ )
2025
+ throw error;
2026
+ const names = mcpServers
2027
+ .slice(0, 8)
2028
+ .map(server => server.name)
2029
+ .join(", ");
2030
+ const suffix = mcpServers.length > 8 ? `, and ${mcpServers.length - 8} more` : "";
2031
+ throw new AcpSdkAdapterError("unavailable", `MCP server request failed to start (${names}${suffix}).`);
2032
+ }
1504
2033
  }
1505
2034
 
1506
2035
  #beginDispose(): void {
@@ -1526,6 +2055,7 @@ export class AcpAgent implements Agent {
1526
2055
  this.#attaching.clear();
1527
2056
  this.#resolvingExisting.clear();
1528
2057
  this.#knownSessionCwds.clear();
2058
+ this.#knownSessionMetadata.clear();
1529
2059
  this.#pendingCloseIdempotencyKeys.clear();
1530
2060
  if (this.#closing.size === 0) this.#closing.clear();
1531
2061
  this.#tearingDown.clear();