@zq-silk/yui 0.15.7 → 0.15.8

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 (281) hide show
  1. package/ARCHITECTURE.md +192 -399
  2. package/README.md +127 -1149
  3. package/dist/agent/adapterCatalog.js +15 -2
  4. package/dist/agent/agent.js +23 -3
  5. package/dist/agent/argumentPolicy.js +7 -1
  6. package/dist/agent/connectionPlan.js +62 -0
  7. package/dist/agent/executionComponents.js +158 -0
  8. package/dist/agent/launchEnvironment.js +31 -3
  9. package/dist/agent/managedRuntimeEnvironment.js +3 -5
  10. package/dist/{turn/turn.js → agentRun/agentRun.js} +166 -109
  11. package/dist/{turn/turnIdentity.js → agentRun/runIdentity.js} +4 -4
  12. package/dist/brief/taskBrief.js +12 -0
  13. package/dist/cli/agentConfigurationPicker.js +13 -0
  14. package/dist/cli/commandCatalog.js +157 -70
  15. package/dist/cli/interactionCandidates.js +5 -5
  16. package/dist/cli/interactionPolicy.js +38 -8
  17. package/dist/cli/invocationRouter.js +1 -1
  18. package/dist/cli/managedDiagnostics.js +28 -0
  19. package/dist/cli/operatorWizard.js +1 -7
  20. package/dist/cli/roleOptionOrder.js +27 -0
  21. package/dist/cli/roleWizard.js +50 -14
  22. package/dist/cli/updateOrchestrator.js +1 -1
  23. package/dist/cli/updatePorts.js +3 -4
  24. package/dist/cli.js +178 -95
  25. package/dist/commands/agentCommands.js +72 -14
  26. package/dist/commands/capabilityCommands.js +9 -6
  27. package/dist/commands/configCommands.js +20 -20
  28. package/dist/commands/deliveryGuardPreflight.js +2 -2
  29. package/dist/commands/executionAuditCommands.js +24 -24
  30. package/dist/commands/globalRoleCommands.js +1 -1
  31. package/dist/commands/grantCommands.js +4 -4
  32. package/dist/commands/operatorCommands.js +1 -7
  33. package/dist/commands/projectCommands.js +4 -4
  34. package/dist/commands/resourcesCommands.js +2 -2
  35. package/dist/commands/roleConfiguration.js +25 -5
  36. package/dist/commands/roleRuntimeGuard.js +4 -5
  37. package/dist/commands/sessionCommands.js +3 -7
  38. package/dist/commands/taskActivationCommands.js +259 -0
  39. package/dist/commands/taskActor.js +28 -49
  40. package/dist/commands/taskCommands.js +1188 -710
  41. package/dist/commands/taskContextCommand.js +39 -583
  42. package/dist/commands/taskExecutionCommands.js +32 -32
  43. package/dist/commands/taskInputCommands.js +40 -104
  44. package/dist/commands/taskIntegrationCommands.js +3 -2
  45. package/dist/commands/taskIntegrationQueueCommands.js +1 -1
  46. package/dist/commands/taskNextActionCommand.js +8 -8
  47. package/dist/commands/taskOverviewCommand.js +33 -45
  48. package/dist/commands/taskRemoteDeliveryCommand.js +2 -2
  49. package/dist/commands/taskRoleRuntimeStatus.js +133 -102
  50. package/dist/commands/telemetryCommands.js +36 -38
  51. package/dist/config/configCatalog.js +4 -4
  52. package/dist/config/yuiConfig.js +8 -8
  53. package/dist/context/contextSnapshot.js +10 -10
  54. package/dist/context/dispatchContext.js +11 -11
  55. package/dist/context/roleSessionContext.js +6 -3
  56. package/dist/context/{turnContextPack.js → runContextPack.js} +146 -81
  57. package/dist/context/{turnInputContract.js → runInputContract.js} +73 -60
  58. package/dist/context/sessionBootstrapManifest.js +21 -2
  59. package/dist/context/sourceRunContext.js +30 -0
  60. package/dist/context/taskContext.js +458 -0
  61. package/dist/context/wakeNotification.js +27 -27
  62. package/dist/controller/agentRuntimeObserver.js +21 -24
  63. package/dist/controller/capabilityBridge.js +17 -6
  64. package/dist/controller/clientRuntime.js +65 -92
  65. package/dist/controller/controller.js +114 -188
  66. package/dist/controller/fileSchedulerStoreAdapter.js +787 -887
  67. package/dist/controller/jobControl.js +54 -85
  68. package/dist/controller/resourceInventory.js +8 -27
  69. package/dist/controller/resourceInventoryLinux.js +12 -13
  70. package/dist/controller/runtime.js +529 -479
  71. package/dist/controller/runtimeEventInbox.js +55 -25
  72. package/dist/controller/runtimeEventProcessor.js +22 -31
  73. package/dist/controller/{runtimeHookTurnFence.js → runtimeHookRunFence.js} +91 -115
  74. package/dist/controller/runtimeLaunchCoordinator.js +80 -426
  75. package/dist/controller/runtimeObservationHook.js +14 -18
  76. package/dist/controller/sessionNotify.js +16 -24
  77. package/dist/controller/sessionOwnerReconciliation.js +168 -50
  78. package/dist/controller/structuredProviderObservation.js +138 -99
  79. package/dist/coordination/workMailbox.js +3 -3
  80. package/dist/coordination/workMailboxQueue.js +36 -33
  81. package/dist/core/boundedRpc.js +8 -1
  82. package/dist/core/controllerClient.js +20 -1
  83. package/dist/core/controllerServer.js +4 -4
  84. package/dist/doctor/doctor.js +13 -2
  85. package/dist/domain/agentResultTransport.js +9 -9
  86. package/dist/execution/codexThreadNaming.js +2 -8
  87. package/dist/execution/executionHealth.js +51 -63
  88. package/dist/execution/reviewMainRun.js +137 -0
  89. package/dist/execution/workItemExecution.js +28 -29
  90. package/dist/execution/workItemExecutionProjection.js +99 -107
  91. package/dist/execution/workItemMainRun.js +141 -0
  92. package/dist/executor/agentAdapter.js +227 -20
  93. package/dist/executor/agentConfigurationCatalog.js +126 -4
  94. package/dist/executor/agentConfigurationProbe.js +162 -4
  95. package/dist/executor/agentExecutor.js +79 -78
  96. package/dist/executor/effectiveLaunch.js +105 -18
  97. package/dist/executor/executorRegistry.js +29 -44
  98. package/dist/executor/fileRoleLaunchPlanner.js +229 -154
  99. package/dist/executor/workspacePreflightClassification.js +16 -16
  100. package/dist/grant/capabilityGrant.js +6 -3
  101. package/dist/input/inputRequest.js +12 -10
  102. package/dist/integration/gitIntegrationService.js +4 -11
  103. package/dist/integration/integrationQueueService.js +4 -4
  104. package/dist/interaction/operatorPresentation.js +1 -1
  105. package/dist/kernel/builtinCapabilities.js +247 -12
  106. package/dist/kernel/capabilityRegistry.js +64 -18
  107. package/dist/kernel/instanceHost.js +12 -1
  108. package/dist/kernel/kernelPorts.js +2 -2
  109. package/dist/lifecycle/canonicalLifecycleEvent.js +44 -49
  110. package/dist/lifecycle/exactRunTerminalization.js +449 -0
  111. package/dist/message/message.js +62 -6
  112. package/dist/message/messageContinuation.js +204 -0
  113. package/dist/observability/executionAudit.js +70 -72
  114. package/dist/observability/faultClassification.js +2 -2
  115. package/dist/observability/orchestrationMetrics.js +8 -8
  116. package/dist/operator/operatorSessionHistory.js +1 -7
  117. package/dist/output/agentConfigurationPresentation.js +8 -3
  118. package/dist/output/agentRunConfigurationPresentation.js +128 -0
  119. package/dist/output/rolePresentation.js +54 -3
  120. package/dist/plugins/pluginChild.js +104 -0
  121. package/dist/plugins/pluginIntent.js +26 -0
  122. package/dist/plugins/pluginInterpreter.js +43 -0
  123. package/dist/plugins/pluginPackage.js +101 -0
  124. package/dist/plugins/pluginProcess.js +112 -0
  125. package/dist/plugins/pluginService.js +380 -0
  126. package/dist/profile/agentProfile.js +1 -1
  127. package/dist/repository/gitWorkspace.js +26 -4
  128. package/dist/repository/project.js +19 -4
  129. package/dist/repository/taskBaseFreshness.js +13 -13
  130. package/dist/repository/taskWorkspaceCoordinator.js +20 -27
  131. package/dist/repository/taskWorkspacePreparer.js +344 -83
  132. package/dist/resources/autoResourceGc.js +3 -3
  133. package/dist/resources/liveReferences.js +3 -3
  134. package/dist/resources/projectResource.js +123 -0
  135. package/dist/resources/projectResourceService.js +421 -0
  136. package/dist/resources/resourceDiscovery.js +6 -6
  137. package/dist/resources/resourceGc.js +1 -1
  138. package/dist/resources/resourceRegistrar.js +1 -1
  139. package/dist/resources/resourceTypes.js +1 -1
  140. package/dist/review/deltaRecheck.js +3 -3
  141. package/dist/review/reviewAcceptance.js +16 -16
  142. package/dist/review/reviewDecision.js +7 -7
  143. package/dist/review/reviewRound.js +21 -20
  144. package/dist/review/reviewerAvailability.js +2 -2
  145. package/dist/role/role.js +51 -7
  146. package/dist/role/taskRoleUpdate.js +30 -0
  147. package/dist/runtime/acpProtocol.js +425 -0
  148. package/dist/runtime/acpSession.js +731 -0
  149. package/dist/runtime/acpSessionConfiguration.js +260 -0
  150. package/dist/runtime/agentDriver.js +30 -11
  151. package/dist/runtime/agentEndpoint.js +278 -0
  152. package/dist/runtime/agentEndpointIdentity.js +86 -0
  153. package/dist/runtime/agentEndpointOwnership.js +239 -0
  154. package/dist/runtime/agentError.js +2 -10
  155. package/dist/runtime/agentHost.js +565 -314
  156. package/dist/runtime/agentRunConfiguration.js +258 -0
  157. package/dist/runtime/builtinAgentDrivers.js +134 -18
  158. package/dist/runtime/builtinAgentErrorMappers.js +55 -3
  159. package/dist/runtime/builtinTranscriptUsage.js +1 -1
  160. package/dist/runtime/claude-process-owner +0 -0
  161. package/dist/runtime/codexAppServerRuntime.js +38 -30
  162. package/dist/runtime/codexInteractiveHost.js +41 -6
  163. package/dist/runtime/continuationManager.js +2 -6
  164. package/dist/runtime/executionEnvironment.js +30 -0
  165. package/dist/runtime/firstProgressAdvisory.js +11 -11
  166. package/dist/runtime/index.js +4 -3
  167. package/dist/runtime/jsonLineChannel.js +109 -0
  168. package/dist/runtime/launchBroker.js +91 -16
  169. package/dist/runtime/launchDiagnostics.js +2 -2
  170. package/dist/runtime/lifecycleReservation.js +10 -18
  171. package/dist/runtime/managedCaller.js +61 -17
  172. package/dist/runtime/nativeSessionControl.js +102 -0
  173. package/dist/runtime/ports.js +6 -21
  174. package/dist/runtime/processExitObservation.js +8 -7
  175. package/dist/runtime/promptEnvelope.js +17 -6
  176. package/dist/runtime/providerContinuation.js +3 -9
  177. package/dist/runtime/providerContinuationReconciliationService.js +4 -13
  178. package/dist/runtime/providerControl.js +2 -7
  179. package/dist/runtime/providerRuntimeIdentity.js +110 -222
  180. package/dist/runtime/providerRuntimeReconciler.js +5 -9
  181. package/dist/runtime/runtimeBinding.js +0 -1
  182. package/dist/runtime/runtimeContinuationProjection.js +4 -7
  183. package/dist/runtime/runtimeDeadlines.js +9 -0
  184. package/dist/runtime/runtimeHealthPolicy.js +1 -1
  185. package/dist/runtime/runtimeObservation.js +29 -65
  186. package/dist/runtime/runtimeProjection.js +43 -51
  187. package/dist/runtime/runtimeSessionCandidate.js +1 -3
  188. package/dist/runtime/sessionLaunchRequest.js +3 -7
  189. package/dist/runtime/sessionOwnerIdentity.js +7 -54
  190. package/dist/runtime/sessionOwnerRegistry.js +22 -17
  191. package/dist/runtime/sessionReconciliation.js +4 -8
  192. package/dist/runtime/sessionTerminationGuard.js +70 -259
  193. package/dist/runtime/sessionTokenMetrics.js +5 -16
  194. package/dist/runtime/structuredProviderHost.js +237 -117
  195. package/dist/runtime/taskRuntimeIsolation.js +39 -122
  196. package/dist/runtime/tmuxAdapters.js +39 -86
  197. package/dist/scheduler/activeRoleRunDelivery.js +354 -0
  198. package/dist/scheduler/leaderWakeupProcessor.js +75 -266
  199. package/dist/scheduler/operatorInputNotificationProcessor.js +1 -1
  200. package/dist/scheduler/ports.js +80 -9
  201. package/dist/scheduler/{roleTurnLiveness.js → roleRunLiveness.js} +26 -30
  202. package/dist/scheduler/{roleTurnStall.js → roleRunStall.js} +128 -139
  203. package/dist/scheduler/taskExecutionProjection.js +120 -124
  204. package/dist/scheduler/taskObservabilityProjection.js +29 -29
  205. package/dist/scheduler/taskWake.js +11 -4
  206. package/dist/scheduler/wakeReason.js +9 -1
  207. package/dist/setup/setupCommand.js +1 -0
  208. package/dist/storage/migrations/agentRunContract.js +159 -0
  209. package/dist/storage/migrations/removeRuntimeGeneration.js +207 -0
  210. package/dist/storage/sqliteSchema.js +431 -5
  211. package/dist/storage/sqliteStore.js +375 -220
  212. package/dist/storage/storageVersions.js +1 -1
  213. package/dist/storage/storeRpc.js +10 -5
  214. package/dist/storage/taskStore.js +13 -11
  215. package/dist/storage/upgrade/upgradeOrchestrator.js +5 -7
  216. package/dist/surface/surfaceContributions.js +102 -0
  217. package/dist/task/completionReadiness.js +32 -6
  218. package/dist/task/deliveryGuard.js +16 -16
  219. package/dist/task/draftPlan.js +72 -12
  220. package/dist/task/nextAction.js +144 -128
  221. package/dist/task/remoteDelivery.js +6 -6
  222. package/dist/task/task.js +184 -18
  223. package/dist/task/taskActivation.js +301 -0
  224. package/dist/task/taskActivationService.js +392 -0
  225. package/dist/task/taskRecordReference.js +5 -4
  226. package/dist/task/taskRecordRetirement.js +1 -1
  227. package/dist/telemetry/sqliteTelemetryStore.js +55 -68
  228. package/dist/telemetry/telemetryConfig.js +14 -14
  229. package/dist/telemetry/telemetryWiring.js +2 -2
  230. package/dist/web/assets/assetManifest.js +2 -0
  231. package/dist/web/assets/client/app.js +120 -20
  232. package/dist/web/assets/client/components.js +87 -54
  233. package/dist/web/assets/client/i18n.js +83 -41
  234. package/dist/web/assets/client/markdown.js +1 -1
  235. package/dist/web/assets/client/taskSurface.js +353 -0
  236. package/dist/web/assets/client/view.js +49 -44
  237. package/dist/web/assets/shell.js +1 -1
  238. package/dist/web/assets/styles/cards.js +22 -4
  239. package/dist/web/controllerWeb.js +60 -0
  240. package/dist/web/webMutation.js +28 -0
  241. package/dist/web/webServer.js +118 -8
  242. package/dist/web/webSnapshot.js +81 -74
  243. package/dist/web/webTaskSurface.js +64 -0
  244. package/dist/workItem/dependencyGate.js +1 -1
  245. package/dist/workItem/workItem.js +80 -48
  246. package/dist/workspace/workItemChangeSetManager.js +16 -9
  247. package/docs/agent-result-consumption.md +94 -0
  248. package/docs/agent-runtime-drivers.md +91 -0
  249. package/docs/architecture/README.md +38 -0
  250. package/docs/architecture/capabilities-and-resources.md +79 -0
  251. package/docs/managed-turn-and-session-runtime.md +222 -0
  252. package/docs/observability/README.md +81 -0
  253. package/docs/plugin-sdk.md +290 -0
  254. package/docs/provider-runtime.md +163 -0
  255. package/docs/release-workflow.md +303 -0
  256. package/docs/roles-and-configuration.md +113 -0
  257. package/docs/sqlite-control-plane-design.md +76 -0
  258. package/docs/task-dag-semantics.md +57 -0
  259. package/docs/task-delivery.md +103 -0
  260. package/docs/task-local-identity.md +6 -6
  261. package/docs/testing/verification-levels.md +86 -0
  262. package/i18n/README.zh-CN.md +98 -739
  263. package/package.json +2 -2
  264. package/skills/yui-leader/SKILL.md +130 -103
  265. package/skills/yui-leader/references/integration.md +39 -0
  266. package/skills/yui-leader/references/replicated-execution.md +42 -0
  267. package/skills/yui-leader/references/task-plugins.md +33 -0
  268. package/skills/yui-operator/SKILL.md +30 -59
  269. package/skills/yui-reviewer/SKILL.md +35 -36
  270. package/skills/yui-runtime/SKILL.md +88 -24
  271. package/skills/yui-runtime/references/publication.md +22 -0
  272. package/skills/yui-runtime/references/recovery.md +64 -0
  273. package/skills/yui-worker/SKILL.md +37 -39
  274. package/dist/cli/roleOptionCatalog.js +0 -68
  275. package/dist/context/sourceTurnContext.js +0 -30
  276. package/dist/execution/reviewMainTurn.js +0 -161
  277. package/dist/execution/workItemMainTurn.js +0 -164
  278. package/dist/lifecycle/exactTurnTerminalization.js +0 -407
  279. package/dist/runtime/preallocatedNativeSession.js +0 -13
  280. package/dist/runtime/runtimeStopReceipt.js +0 -42
  281. package/dist/scheduler/activeRoleTurnDelivery.js +0 -315
@@ -0,0 +1,731 @@
1
+ import { isAbsolute } from "node:path";
2
+ import { MAX_RUN_RESULT_OUTPUT_BYTES } from "../domain/agentResultTransport.js";
3
+ import { ACP_METHOD_NOT_FOUND_CODE, acpCancelNotification, acpDeclinePermission, acpInitializeRequest, acpNewSessionRequest, acpPermissionResult, acpPermissionSummary, acpPromptRequest, acpSetConfigOptionRequest, acpSetModeRequest, acpStopReasonDetail, acpTerminalStatus, asObject, optionalText, readAcpConfigOptions, readAcpInitializeResult, readAcpPermissionRequest, readAcpSessionConfiguration, readAcpSessionUpdate, readAcpStopReason } from "./acpProtocol.js";
4
+ import { ACP_CONFIGURATION_ORDER, acpRunConfigurationOptions, confirmAcpConfigurationStep, describeAcpConfigurationRejections, resolveAcpConfigurationField, verifyAcpConfiguration } from "./acpSessionConfiguration.js";
5
+ import { handshakeObservationFrom, unknownAgentRunConfiguration } from "./agentRunConfiguration.js";
6
+ import { JsonLineChannel, terminateProcessGroup } from "./jsonLineChannel.js";
7
+ import { ProviderDeliveryUnknownError, ProviderTurnRejectedError } from "./structuredProviderHost.js";
8
+ /**
9
+ * One Agent Client Protocol Session over an Agent's stdio.
10
+ *
11
+ * This class implements the protocol and nothing else. It never names a
12
+ * product: which executable sits on the other end is a launch descriptor fact,
13
+ * so a second ACP product reuses this code unchanged.
14
+ *
15
+ * Three identities are deliberately kept apart:
16
+ * - the JSON-RPC request id, which correlates one message pair on this pipe;
17
+ * - the Yui attemptId, which is the durable local request;
18
+ * - the ACP `sessionId`, which is the Agent's own native Session identity.
19
+ *
20
+ * ACP v1 defines no native *Turn* identity: a Turn is the `session/prompt`
21
+ * request itself, and its response is that Turn's terminal. Yui therefore
22
+ * reports no `nativeTurnId` for an ACP Turn rather than promoting a JSON-RPC
23
+ * request id or minting a value no Agent would recognize.
24
+ */
25
+ export class AcpStructuredProviderSession {
26
+ child;
27
+ exit;
28
+ processInstanceId;
29
+ channel;
30
+ onTerminal;
31
+ mirror;
32
+ adapterId = "acp";
33
+ #pending = new Map();
34
+ #sessionId = "";
35
+ #negotiated;
36
+ #nextRequestId = 1;
37
+ #activeAttemptId;
38
+ #promptRequestId;
39
+ #closed;
40
+ /**
41
+ * Settles the in-flight `submitTurn` call. ACP proves acceptance only by
42
+ * answering the prompt, so the promise this resolves is the caller's only
43
+ * honest acceptance signal, and it is deliberately left unresolved while the
44
+ * Agent is still working.
45
+ */
46
+ #promptSettle;
47
+ /**
48
+ * Text from `agent_message_chunk` for the Turn currently in flight, in
49
+ * arrival order. Reset per attempt so one Turn's answer can never be
50
+ * archived as another's, and never fed by thought or tool updates.
51
+ */
52
+ #answer = [];
53
+ #answerBytes = 0;
54
+ #answerOverflowed = false;
55
+ /**
56
+ * Roots this launch asked for that the Agent never advertised support for.
57
+ * Recorded rather than dropped silently: the workspace the Agent can actually
58
+ * reach is narrower than the one Yui scoped, and that is a fact the Session
59
+ * must be able to report instead of one the caller has to guess.
60
+ */
61
+ #unsentAdditionalDirectories = [];
62
+ /**
63
+ * Bootstrap still owed to the model, cleared once a prompt has carried it.
64
+ * ACP offers no system prompt, so the first prompt on each connection
65
+ * carries the manifest pointer. Later Turns on that connection do not repeat it.
66
+ */
67
+ #pendingBootstrap;
68
+ /**
69
+ * The Session's config options as the Agent last reported them, from setup,
70
+ * from a set call's answer or from an Agent-initiated update. ACP always sends
71
+ * the complete list, so this is replaced wholesale and never merged.
72
+ */
73
+ #configOptions = [];
74
+ /** Legacy writes invalidate current evidence until the peer reports it again. */
75
+ #unobservedConfigIds = new Set();
76
+ /**
77
+ * True when this Agent described its options through the legacy `modes` field
78
+ * only. Mode changes then go to `session/set_mode`, because an Agent that
79
+ * never advertised config options must not receive
80
+ * `session/set_config_option`.
81
+ */
82
+ #legacyModes = false;
83
+ /**
84
+ * What Yui asked this Session for and what the Agent did with it, recorded so
85
+ * a launch reports the configuration it actually runs under rather than the
86
+ * one it requested. Each entry carries how strongly it was confirmed, because
87
+ * a legacy mode-only peer cannot prove more than that it accepted the call.
88
+ */
89
+ #appliedConfiguration = [];
90
+ constructor(child, exit, processInstanceId, channel, onTerminal, mirror) {
91
+ this.child = child;
92
+ this.exit = exit;
93
+ this.processInstanceId = processInstanceId;
94
+ this.channel = channel;
95
+ this.onTerminal = onTerminal;
96
+ this.mirror = mirror;
97
+ }
98
+ static async open(input) {
99
+ const channel = new JsonLineChannel(input.child, input.mirror);
100
+ const session = new AcpStructuredProviderSession(input.child, input.exit, input.processInstanceId, channel, input.onTerminal, input.mirror);
101
+ channel.onMessage((message) => session.#receive(message));
102
+ channel.onClose((error) => session.#fail(error));
103
+ const negotiated = readAcpInitializeResult(await session.#request("initialize", acpInitializeRequest(input.clientVersion)));
104
+ session.#negotiated = negotiated;
105
+ const requested = input.additionalDirectories ?? [];
106
+ // ACP requires every workspace root to be absolute, and an Agent that never
107
+ // advertised the field must not receive it. Both are decided here, once,
108
+ // from what this Agent actually said during `initialize`.
109
+ for (const path of requested) {
110
+ if (!isAbsolute(path)) {
111
+ throw new Error(`ACP additional workspace root must be an absolute path: ${path}`);
112
+ }
113
+ }
114
+ session.#unsentAdditionalDirectories = negotiated.capabilities.additionalDirectories
115
+ ? []
116
+ : Object.freeze([...requested]);
117
+ session.#sessionId = input.nativeSessionId === undefined
118
+ ? await session.#create(input.cwd, requested)
119
+ : await session.#restore(input.nativeSessionId, input.cwd, negotiated, requested);
120
+ // The Session now exists, so its configurable surface is known — and no
121
+ // prompt has been sent yet, so nothing has run under the wrong settings.
122
+ // This is the only point where both are true.
123
+ if (input.desiredConfiguration !== undefined) {
124
+ await session.#applyConfiguration(input.desiredConfiguration,
125
+ // An unnamed component is the unidentified ACP product, which is exactly
126
+ // what an Agent Yui cannot identify should be treated as: it has no known
127
+ // bypass value, so such a request is refused rather than guessed.
128
+ input.component ?? "unknown-acp-agent");
129
+ }
130
+ // A restored Conversation may have disconnected before its first prompt.
131
+ // Repeat the short manifest pointer on attachment rather than assuming it
132
+ // was delivered or adding persistent acknowledgement state.
133
+ session.#pendingBootstrap = input.sessionBootstrap;
134
+ return session;
135
+ }
136
+ /** Capabilities the Agent actually advertised during `initialize`. */
137
+ get negotiated() {
138
+ if (this.#negotiated === undefined)
139
+ throw new Error("ACP Session is not initialized.");
140
+ return this.#negotiated;
141
+ }
142
+ get conversationId() {
143
+ return this.#sessionId;
144
+ }
145
+ get nativeSessionId() {
146
+ return this.#sessionId;
147
+ }
148
+ get activeTurnId() {
149
+ // ACP has no native Turn id. Returning the local attempt or a JSON-RPC
150
+ // request id here would present a Yui-owned value as a Provider identity.
151
+ return undefined;
152
+ }
153
+ /**
154
+ * Rebinding this Conversation from a later process means calling
155
+ * `session/load`, which ACP gates on `agentCapabilities.loadSession`. This
156
+ * Agent either advertised it during `initialize` or it did not, so the answer
157
+ * is a negotiated fact rather than an adapter-wide assumption.
158
+ */
159
+ get conversationRecoverability() {
160
+ return this.negotiated.capabilities.loadSession ? "recoverable" : "unknown";
161
+ }
162
+ async submitTurn(turn) {
163
+ if (this.#activeAttemptId !== undefined) {
164
+ throw new ProviderTurnRejectedError("ACP Session already has an unsettled Turn.", turn.attemptId);
165
+ }
166
+ const requestId = this.#nextRequestId;
167
+ this.#nextRequestId += 1;
168
+ // Reserve before the pipe write: a fast Agent can answer before the write
169
+ // callback runs, and an ambiguous write must retain the same occupancy so
170
+ // that no successor input is bound to this slot.
171
+ this.#activeAttemptId = turn.attemptId;
172
+ this.#promptRequestId = requestId;
173
+ this.#resetAnswer();
174
+ // Registered before the write so a response that arrives during the await
175
+ // below still finds someone to settle.
176
+ const settled = new Promise((resolvePromise) => {
177
+ this.#promptSettle = resolvePromise;
178
+ });
179
+ // The bootstrap rides the first prompt. It stays owed until the write
180
+ // completes, so a Turn that never left Yui does not silently consume the
181
+ // only instructions the Session was going to receive.
182
+ const bootstrap = this.#pendingBootstrap;
183
+ const text = bootstrap === undefined
184
+ ? turn.boundedText
185
+ : `${bootstrap}\n\n${turn.boundedText}`;
186
+ try {
187
+ await this.channel.send({
188
+ jsonrpc: "2.0",
189
+ id: requestId,
190
+ method: "session/prompt",
191
+ params: acpPromptRequest(this.#sessionId, text)
192
+ });
193
+ this.#pendingBootstrap = undefined;
194
+ }
195
+ catch (error) {
196
+ // The write itself failed, so the Agent may or may not have read it.
197
+ // Release the slot only through the unknown path: this Turn is never
198
+ // resent, and no successor may claim the slot as if it were free.
199
+ this.#promptSettle = undefined;
200
+ throw new ProviderDeliveryUnknownError(`ACP prompt write did not complete: ${error instanceof Error ? error.message : String(error)}`, turn.attemptId, { cause: error });
201
+ }
202
+ // A completed pipe write proves only that bytes left Yui. ACP acknowledges
203
+ // a prompt by answering it, so this call stays unresolved until that answer
204
+ // arrives: a long-running Turn is reported as pending by the caller's own
205
+ // deadline, and a transport that dies first is reported as unknown. Neither
206
+ // is an acceptance, and neither may be invented here.
207
+ const outcome = await settled;
208
+ if (outcome.kind === "unknown") {
209
+ throw new ProviderDeliveryUnknownError(outcome.message, turn.attemptId, {
210
+ ...(outcome.cause === undefined ? {} : { cause: outcome.cause })
211
+ });
212
+ }
213
+ return Object.freeze({
214
+ attemptId: turn.attemptId,
215
+ conversationId: this.#sessionId,
216
+ nativeSessionId: this.#sessionId,
217
+ acceptedAt: outcome.observedAt,
218
+ // The Agent answered this exact request: that is Provider acceptance,
219
+ // not merely a transport fact.
220
+ acceptance: "provider"
221
+ });
222
+ }
223
+ async steerTurn(turn) {
224
+ throw new ProviderTurnRejectedError("ACP v1 defines no mid-Turn steering: a Turn is one `session/prompt` request "
225
+ + "and accepts no further input until it answers.", turn.attemptId);
226
+ }
227
+ async cancelTurn(attemptId) {
228
+ if (this.#activeAttemptId !== attemptId)
229
+ return "not-active";
230
+ try {
231
+ await this.channel.send({
232
+ jsonrpc: "2.0",
233
+ method: "session/cancel",
234
+ params: acpCancelNotification(this.#sessionId)
235
+ });
236
+ }
237
+ catch {
238
+ // A notification carries no acknowledgement, so a failed write leaves it
239
+ // genuinely unknown whether the Agent ever saw the request.
240
+ return "unknown";
241
+ }
242
+ // `session/cancel` requests a stop; it does not prove one. ACP requires
243
+ // only that the Agent *answer* the prompt with `cancelled`, while halting
244
+ // the underlying work is a SHOULD. The Turn settles on that answer, and
245
+ // only that answer decides the terminal: this local intent is deliberately
246
+ // not remembered, because a request is not evidence of its own outcome.
247
+ return "requested";
248
+ }
249
+ waitForExit() {
250
+ return this.exit;
251
+ }
252
+ terminate(signal) {
253
+ terminateProcessGroup(this.child, signal);
254
+ }
255
+ async #create(cwd, additionalDirectories) {
256
+ await this.#request("session/new", acpNewSessionRequest(cwd, this.#sendableDirectories(additionalDirectories)), (response) => {
257
+ const setup = asObject(response);
258
+ const id = setup === null ? undefined : optionalText(setup.sessionId);
259
+ if (id === undefined)
260
+ throw new Error("ACP `session/new` returned no sessionId.");
261
+ this.#sessionId = id;
262
+ this.#readConfiguration(setup);
263
+ });
264
+ return this.#sessionId;
265
+ }
266
+ /**
267
+ * Reattach to an existing Agent Session. `session/load` is the only method
268
+ * ACP defines for this, and it is gated by `agentCapabilities.loadSession`;
269
+ * an Agent without it cannot restore the Conversation, and saying so is more
270
+ * useful than silently starting a different Session under the old id.
271
+ */
272
+ async #restore(sessionId, cwd, negotiated, additionalDirectories) {
273
+ if (!negotiated.capabilities.loadSession) {
274
+ throw new Error("ACP Agent does not support `session/load` (agentCapabilities.loadSession is "
275
+ + "false), so this native Session cannot be reattached. Start a new Session "
276
+ + "explicitly instead.");
277
+ }
278
+ // `session/load` replays the whole Conversation as `session/update`
279
+ // notifications before it answers. Those replays are history: the receive
280
+ // path only settles a Turn from a `session/prompt` response, so replayed
281
+ // content can never be mistaken for a new terminal.
282
+ this.#sessionId = sessionId;
283
+ await this.#request("session/load", {
284
+ sessionId,
285
+ ...acpNewSessionRequest(cwd, this.#sendableDirectories(additionalDirectories))
286
+ }, (response) => this.#readConfiguration(asObject(response)));
287
+ // A resumed Session reports its own current configuration, which is the
288
+ // Agent's state after the earlier launch — not this launch's request. Both
289
+ // are read the same way so a resume is configured from what is true now.
290
+ return sessionId;
291
+ }
292
+ /** Record the option list from a session setup or set-option result. */
293
+ #readConfiguration(result) {
294
+ const configuration = readAcpSessionConfiguration(result);
295
+ this.#configOptions = configuration.options;
296
+ this.#legacyModes = configuration.legacyModes;
297
+ }
298
+ /**
299
+ * Push this launch's requested run configuration and verify the Agent applied
300
+ * it, before any prompt exists to be affected by it.
301
+ *
302
+ * Ordering matters and is not incidental. ACP reports a Session's options only
303
+ * once the Session exists, so the request cannot be made at `initialize`; and
304
+ * a prompt sent before the options are set would run under the Agent's
305
+ * defaults while reporting the user's selection. So this sits exactly between
306
+ * the two, and any failure here stops the launch rather than degrading it.
307
+ *
308
+ * Within that window, each field is resolved against the option list the Agent
309
+ * reports at that moment, not against the list the Session opened with. This is
310
+ * what a single up-front plan cannot do: setting a model resets the reasoning
311
+ * effort on real Agents and can change which effort values exist at all, so a
312
+ * plan fixed before the model call would either re-send a now-stale value or
313
+ * reject a value that the new model does offer. Resolving one field at a time
314
+ * against the newest list handles both without guessing.
315
+ *
316
+ * The pass is bounded — each field is decided once, in a fixed order, and there
317
+ * is no retry or repair loop. What makes that sufficient is the final
318
+ * verification against the Agent's last complete list: it covers every
319
+ * explicitly requested value, so an axis moved by a later call is caught even
320
+ * though its own step had already been confirmed.
321
+ */
322
+ async #applyConfiguration(desired, component) {
323
+ const outcomes = [];
324
+ for (const field of ACP_CONFIGURATION_ORDER) {
325
+ const resolution = resolveAcpConfigurationField(field, desired, this.#configOptions, component);
326
+ if (resolution.kind === "unrequested")
327
+ continue;
328
+ if (resolution.kind === "rejection") {
329
+ // The user asked for something this Agent cannot deliver. Continuing
330
+ // would run the Turn under a configuration nobody chose, so the launch
331
+ // stops with the Agent's own enumeration in the message.
332
+ throw new Error(`ACP Agent cannot apply the requested run configuration. `
333
+ + describeAcpConfigurationRejections([resolution.rejection]));
334
+ }
335
+ if (resolution.kind === "satisfied") {
336
+ outcomes.push(Object.freeze({
337
+ field,
338
+ configId: resolution.step.configId,
339
+ value: resolution.step.value,
340
+ confirmation: "already"
341
+ }));
342
+ continue;
343
+ }
344
+ const step = resolution.step;
345
+ if (this.#legacyModes)
346
+ this.#unobservedConfigIds.add(step.configId);
347
+ // An Agent that only ever advertised legacy `modes` has no
348
+ // `session/set_config_option` handler, so mode changes must use the method
349
+ // it does implement. Sending the modern method to it would fail with
350
+ // method-not-found and read as the Agent refusing the value.
351
+ const result = this.#legacyModes
352
+ ? await this.#request("session/set_mode", acpSetModeRequest(this.#sessionId, step.value))
353
+ : await this.#request("session/set_config_option", acpSetConfigOptionRequest(this.#sessionId, step.configId, step.value), (response) => {
354
+ const options = readAcpConfigOptions(asObject(response)?.configOptions);
355
+ if (options !== undefined)
356
+ this.#configOptions = options;
357
+ });
358
+ if (this.#legacyModes) {
359
+ // `session/set_mode` answers with no options, so the only fact available
360
+ // is that the Agent accepted the call. Yui's own view is left untouched:
361
+ // writing the requested value into the cached list would manufacture the
362
+ // very confirmation the protocol withheld, and the final verification
363
+ // would then read Yui's own assumption back as the Agent's report.
364
+ outcomes.push(Object.freeze({
365
+ field,
366
+ configId: step.configId,
367
+ value: step.value,
368
+ confirmation: "acknowledged"
369
+ }));
370
+ continue;
371
+ }
372
+ // The answer is the whole option list, so it both confirms this step and
373
+ // carries any change the Agent made alongside it.
374
+ const options = readAcpConfigOptions(asObject(result)?.configOptions);
375
+ const mismatch = confirmAcpConfigurationStep(step, options ?? this.#configOptions);
376
+ if (mismatch !== undefined)
377
+ throw new Error(mismatch);
378
+ outcomes.push(Object.freeze({
379
+ field,
380
+ configId: step.configId,
381
+ value: step.value,
382
+ confirmation: "observed"
383
+ }));
384
+ }
385
+ // Every call has landed, so this is the configuration the prompt would run
386
+ // under. Re-checking all requested values here is what catches an axis that a
387
+ // later call reset after its own step had already been confirmed.
388
+ const failures = verifyAcpConfiguration(desired, this.#configOptions, component, outcomes);
389
+ if (failures.length > 0) {
390
+ throw new Error(`ACP Session run configuration did not hold. ${failures.join(" ")}`);
391
+ }
392
+ this.#appliedConfiguration = Object.freeze(outcomes);
393
+ }
394
+ /** The Session config options the Agent last reported. */
395
+ get configOptions() {
396
+ return this.#configOptions;
397
+ }
398
+ /** Requested configuration this Session confirmed, in application order. */
399
+ get appliedConfiguration() {
400
+ return this.#appliedConfiguration;
401
+ }
402
+ /**
403
+ * What this Session is actually running under, read fresh on every call.
404
+ *
405
+ * Two facts are combined and kept labelled. Each requested value carries the
406
+ * confirmation its own step earned, which is history and does not change. The
407
+ * current value beside it is read from the option list the Agent reports right
408
+ * now, so an axis the Agent has since moved shows as a different current value
409
+ * rather than as the launch's confirmed one. Reading the private field on every
410
+ * call rather than caching a projection is what makes that true: a snapshot
411
+ * taken at launch would keep reporting a configuration this Session has left.
412
+ *
413
+ * A legacy mode-only peer has no reported value to pair with its
414
+ * `acknowledged` step, because Yui deliberately never wrote the requested value
415
+ * into its own view. That absence is stated as `unobserved` instead of being
416
+ * filled in from the request.
417
+ */
418
+ get runConfiguration() {
419
+ if (this.#closed !== undefined) {
420
+ return unknownAgentRunConfiguration("The ACP connection is closed; its reports are no longer current.");
421
+ }
422
+ return Object.freeze({
423
+ status: "observed",
424
+ observedAt: new Date().toISOString(),
425
+ handshake: handshakeObservationFrom(this.negotiated),
426
+ requested: Object.freeze(this.#appliedConfiguration.map((outcome) => Object.freeze({
427
+ field: outcome.field,
428
+ key: outcome.configId,
429
+ value: outcome.value,
430
+ confirmation: outcome.confirmation,
431
+ current: this.#currentValue(outcome)
432
+ }))),
433
+ axes: Object.freeze(acpRunConfigurationOptions(this.#configOptions).map((option) => Object.freeze({
434
+ key: option.id,
435
+ ...(option.category === undefined ? {} : { category: option.category }),
436
+ current: this.#axisValue(option),
437
+ offered: Object.freeze(option.options.map(({ value }) => value))
438
+ })))
439
+ });
440
+ }
441
+ /**
442
+ * What the Agent reports for one listed axis.
443
+ *
444
+ * Almost always an observation: the option list ACP sends is the Agent's own
445
+ * report. The exception is an axis Yui changed on a legacy peer, whose value
446
+ * here still dates from the setup reply because `session/set_mode` returned
447
+ * nothing to refresh it with. That entry is listed — the axis is real and its
448
+ * enumeration is accurate — but its value is stale by construction, so it is
449
+ * reported as unobserved rather than as the Agent's current answer.
450
+ */
451
+ #axisValue(option) {
452
+ if (this.#unobservedConfigIds.has(option.id)) {
453
+ return Object.freeze({
454
+ status: "unobserved",
455
+ reason: "Yui set this axis with `session/set_mode`, which reports no "
456
+ + `configuration, so the Agent last reported \`${option.currentValue}\` before that call.`
457
+ });
458
+ }
459
+ return Object.freeze({ status: "observed", value: option.currentValue });
460
+ }
461
+ /** Read current evidence independently of the request's historical confirmation. */
462
+ #currentValue(outcome) {
463
+ const option = this.#configOptions.find((candidate) => candidate.id === outcome.configId);
464
+ if (option !== undefined) {
465
+ return this.#axisValue(option);
466
+ }
467
+ return Object.freeze({
468
+ status: "unobserved",
469
+ reason: `The Agent no longer reports an option \`${outcome.configId}\` in its configuration.`
470
+ });
471
+ }
472
+ /**
473
+ * The subset of requested roots this Agent is allowed to receive. Gating here
474
+ * rather than at the call sites keeps `session/new` and `session/load` from
475
+ * ever disagreeing about what this connection negotiated.
476
+ */
477
+ #sendableDirectories(requested) {
478
+ return this.negotiated.capabilities.additionalDirectories ? requested : [];
479
+ }
480
+ /** Requested roots this Agent cannot be told about. Empty when all were sent. */
481
+ get unsentAdditionalDirectories() {
482
+ return this.#unsentAdditionalDirectories;
483
+ }
484
+ #request(method, params, receive) {
485
+ return new Promise((resolvePromise, reject) => {
486
+ if (this.#closed !== undefined) {
487
+ reject(this.#closed);
488
+ return;
489
+ }
490
+ const id = this.#nextRequestId;
491
+ this.#nextRequestId += 1;
492
+ this.#pending.set(id, {
493
+ resolve: (result) => {
494
+ try {
495
+ // Apply response facts in wire order, before a following notification.
496
+ receive?.(result);
497
+ resolvePromise(result);
498
+ }
499
+ catch (error) {
500
+ reject(error instanceof Error ? error : new Error(String(error)));
501
+ }
502
+ },
503
+ reject
504
+ });
505
+ void this.channel.send({ jsonrpc: "2.0", id, method, params }).catch((error) => {
506
+ this.#pending.delete(id);
507
+ reject(error instanceof Error ? error : new Error(String(error)));
508
+ });
509
+ });
510
+ }
511
+ #receive(message) {
512
+ const method = optionalText(message.method);
513
+ if (method === undefined) {
514
+ this.#response(message);
515
+ return;
516
+ }
517
+ if (message.id === undefined)
518
+ this.#notification(method, message.params);
519
+ else
520
+ void this.#serve(method, message);
521
+ }
522
+ #response(message) {
523
+ const id = typeof message.id === "number" ? message.id : undefined;
524
+ if (id === undefined)
525
+ return;
526
+ if (id === this.#promptRequestId) {
527
+ this.#settlePrompt(message);
528
+ return;
529
+ }
530
+ const waiter = this.#pending.get(id);
531
+ if (waiter === undefined)
532
+ return;
533
+ this.#pending.delete(id);
534
+ const error = asObject(message.error);
535
+ if (error === null) {
536
+ waiter.resolve(message.result);
537
+ return;
538
+ }
539
+ waiter.reject(new Error(`ACP ${optionalText(error.message) ?? "request failed"}`
540
+ + `${typeof error.code === "number" ? ` (code ${error.code})` : ""}`));
541
+ }
542
+ /** A `session/prompt` response is the Turn's terminal; nothing else is. */
543
+ #settlePrompt(message) {
544
+ const attemptId = this.#activeAttemptId;
545
+ if (attemptId === undefined)
546
+ return;
547
+ this.#activeAttemptId = undefined;
548
+ this.#promptRequestId = undefined;
549
+ const answer = this.#takeAnswer();
550
+ const settle = this.#promptSettle;
551
+ this.#promptSettle = undefined;
552
+ const observedAt = new Date().toISOString();
553
+ // The Agent answered this exact request, so the submission is accepted
554
+ // whatever the answer says. A refusal or an error is an accepted Turn that
555
+ // finished badly, not a failed delivery.
556
+ settle?.({ kind: "answered", observedAt });
557
+ const base = {
558
+ conversationId: this.#sessionId,
559
+ nativeSessionId: this.#sessionId,
560
+ // The exact local request this response answers. ACP publishes no native
561
+ // Turn id, so `nativeTurnId` is absent rather than invented.
562
+ attemptId,
563
+ // The JSON-RPC id binds this response to this client's exact request.
564
+ clientOwned: true,
565
+ observedAt
566
+ };
567
+ const error = asObject(message.error);
568
+ if (error !== null) {
569
+ // A local cancel request does not reclassify a failure. ACP proves
570
+ // cancellation exactly one way — `stopReason: "cancelled"` on a
571
+ // successful response — and an error returned after a cancel request is
572
+ // still the error the Agent reported.
573
+ this.onTerminal?.({
574
+ ...base,
575
+ status: "failed",
576
+ error: optionalText(error.message) ?? "ACP prompt failed.",
577
+ rawError: JSON.stringify(error)
578
+ });
579
+ return;
580
+ }
581
+ const reason = readAcpStopReason(message.result);
582
+ if (reason === undefined) {
583
+ this.onTerminal?.({
584
+ ...base,
585
+ status: "failed",
586
+ error: "ACP prompt response carried no recognized stopReason."
587
+ });
588
+ return;
589
+ }
590
+ const status = acpTerminalStatus(reason);
591
+ this.onTerminal?.({
592
+ ...base,
593
+ status,
594
+ // The Agent's own answer for this attempt, assembled from the
595
+ // `agent_message_chunk` updates that preceded this response.
596
+ ...(answer === undefined ? {} : { output: answer }),
597
+ ...(status === "completed" ? {} : { error: acpStopReasonDetail(reason) })
598
+ });
599
+ }
600
+ #resetAnswer() {
601
+ this.#answer = [];
602
+ this.#answerBytes = 0;
603
+ this.#answerOverflowed = false;
604
+ }
605
+ /**
606
+ * The streamed answer for the attempt that just settled.
607
+ *
608
+ * Returns nothing when the Agent streamed no message text, so a Turn without
609
+ * an answer is reported as having none rather than as an empty one. An answer
610
+ * past the durable limit is dropped whole: the durable contract keeps Agent
611
+ * text exact or not at all, and a silently truncated answer would read as a
612
+ * complete one.
613
+ */
614
+ #takeAnswer() {
615
+ const chunks = this.#answer;
616
+ const overflowed = this.#answerOverflowed;
617
+ this.#resetAnswer();
618
+ if (overflowed || chunks.length === 0)
619
+ return undefined;
620
+ const text = chunks.join("");
621
+ return text.length === 0 ? undefined : text;
622
+ }
623
+ #notification(method, params) {
624
+ if (method !== "session/update")
625
+ return;
626
+ const update = readAcpSessionUpdate(params, this.#sessionId);
627
+ // An Agent may change the Session's configuration itself, and ACP sends the
628
+ // complete option list when it does. Tracking it keeps this Session's view
629
+ // of what it is running under accurate; it is not treated as a failure,
630
+ // because the Agent is entitled to do this and Yui's own requests were
631
+ // already confirmed before any prompt was sent.
632
+ if (update?.kind === "config-options") {
633
+ this.#configOptions = update.options;
634
+ this.#unobservedConfigIds.clear();
635
+ return;
636
+ }
637
+ if (update?.kind === "mode") {
638
+ for (const option of this.#configOptions) {
639
+ if (option.category === "mode")
640
+ this.#unobservedConfigIds.delete(option.id);
641
+ }
642
+ this.#configOptions = this.#configOptions.map((option) => option.category === "mode"
643
+ ? Object.freeze({ ...option, currentValue: update.modeId })
644
+ : option);
645
+ return;
646
+ }
647
+ // Streamed content is Provider-visible progress, mirrored for the Turn
648
+ // record. It never settles a Turn: only the prompt response does.
649
+ if (update?.kind !== "agent-message")
650
+ return;
651
+ this.mirror("stdout", update.text);
652
+ // Only `agent_message_chunk` reaches here: thought, tool-call and plan
653
+ // updates decode to other kinds and are deliberately not part of the
654
+ // Agent's answer. Chunks outside a Turn are history — most visibly the
655
+ // replay `session/load` performs — and are mirrored without being adopted
656
+ // as this attempt's output.
657
+ if (this.#activeAttemptId === undefined || this.#answerOverflowed)
658
+ return;
659
+ const bytes = Buffer.byteLength(update.text, "utf8");
660
+ if (this.#answerBytes + bytes > MAX_RUN_RESULT_OUTPUT_BYTES) {
661
+ // Stop retaining rather than keep a prefix: a truncated answer that still
662
+ // looked complete would be worse than an explicit absence.
663
+ this.#answerOverflowed = true;
664
+ this.#answer = [];
665
+ this.#answerBytes = 0;
666
+ return;
667
+ }
668
+ this.#answer.push(update.text);
669
+ this.#answerBytes += bytes;
670
+ }
671
+ async #serve(method, message) {
672
+ if (method === "session/request_permission") {
673
+ const request = readAcpPermissionRequest(message.params);
674
+ if (request === undefined) {
675
+ await this.#reply(message.id, undefined, {
676
+ code: ACP_METHOD_NOT_FOUND_CODE,
677
+ message: "ACP permission request was malformed."
678
+ });
679
+ return;
680
+ }
681
+ // Yui carries no interactive consent on this transport. Declining is the
682
+ // only answer that does not grant authority the user never gave.
683
+ const decision = acpDeclinePermission(request);
684
+ this.mirror("stderr", `${acpPermissionSummary(request, decision)}\n`);
685
+ await this.#reply(message.id, acpPermissionResult(decision), undefined);
686
+ return;
687
+ }
688
+ // Yui advertised no filesystem and no terminal capability, so such a call
689
+ // is outside what this client agreed to serve. A method-not-found error is
690
+ // the protocol's own way to say that plainly.
691
+ await this.#reply(message.id, undefined, {
692
+ code: ACP_METHOD_NOT_FOUND_CODE,
693
+ message: `Yui does not implement ACP method ${method}.`
694
+ });
695
+ }
696
+ async #reply(id, result, error) {
697
+ if (typeof id !== "number" && typeof id !== "string")
698
+ return;
699
+ try {
700
+ await this.channel.send({
701
+ jsonrpc: "2.0",
702
+ id,
703
+ ...(error === undefined ? { result: result ?? {} } : { error })
704
+ });
705
+ }
706
+ catch {
707
+ // The pipe is gone; the closure path already reports that fact.
708
+ }
709
+ }
710
+ #fail(error) {
711
+ this.#closed = error;
712
+ for (const [id, waiter] of [...this.#pending]) {
713
+ this.#pending.delete(id);
714
+ waiter.reject(error);
715
+ }
716
+ // A prompt in flight when the transport died has no known outcome: the
717
+ // Agent may have completed the work or never started it. Report that as
718
+ // unknown delivery so the Turn is never resent and never recorded as a
719
+ // result nobody observed. No terminal is emitted, because none was seen.
720
+ const settle = this.#promptSettle;
721
+ if (settle === undefined)
722
+ return;
723
+ this.#promptSettle = undefined;
724
+ this.#resetAnswer();
725
+ settle({
726
+ kind: "unknown",
727
+ message: `ACP transport closed while a Turn was in flight: ${error.message}`,
728
+ cause: error
729
+ });
730
+ }
731
+ }