@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.
- package/ARCHITECTURE.md +192 -399
- package/README.md +127 -1149
- package/dist/agent/adapterCatalog.js +15 -2
- package/dist/agent/agent.js +23 -3
- package/dist/agent/argumentPolicy.js +7 -1
- package/dist/agent/connectionPlan.js +62 -0
- package/dist/agent/executionComponents.js +158 -0
- package/dist/agent/launchEnvironment.js +31 -3
- package/dist/agent/managedRuntimeEnvironment.js +3 -5
- package/dist/{turn/turn.js → agentRun/agentRun.js} +166 -109
- package/dist/{turn/turnIdentity.js → agentRun/runIdentity.js} +4 -4
- package/dist/brief/taskBrief.js +12 -0
- package/dist/cli/agentConfigurationPicker.js +13 -0
- package/dist/cli/commandCatalog.js +157 -70
- package/dist/cli/interactionCandidates.js +5 -5
- package/dist/cli/interactionPolicy.js +38 -8
- package/dist/cli/invocationRouter.js +1 -1
- package/dist/cli/managedDiagnostics.js +28 -0
- package/dist/cli/operatorWizard.js +1 -7
- package/dist/cli/roleOptionOrder.js +27 -0
- package/dist/cli/roleWizard.js +50 -14
- package/dist/cli/updateOrchestrator.js +1 -1
- package/dist/cli/updatePorts.js +3 -4
- package/dist/cli.js +178 -95
- package/dist/commands/agentCommands.js +72 -14
- package/dist/commands/capabilityCommands.js +9 -6
- package/dist/commands/configCommands.js +20 -20
- package/dist/commands/deliveryGuardPreflight.js +2 -2
- package/dist/commands/executionAuditCommands.js +24 -24
- package/dist/commands/globalRoleCommands.js +1 -1
- package/dist/commands/grantCommands.js +4 -4
- package/dist/commands/operatorCommands.js +1 -7
- package/dist/commands/projectCommands.js +4 -4
- package/dist/commands/resourcesCommands.js +2 -2
- package/dist/commands/roleConfiguration.js +25 -5
- package/dist/commands/roleRuntimeGuard.js +4 -5
- package/dist/commands/sessionCommands.js +3 -7
- package/dist/commands/taskActivationCommands.js +259 -0
- package/dist/commands/taskActor.js +28 -49
- package/dist/commands/taskCommands.js +1188 -710
- package/dist/commands/taskContextCommand.js +39 -583
- package/dist/commands/taskExecutionCommands.js +32 -32
- package/dist/commands/taskInputCommands.js +40 -104
- package/dist/commands/taskIntegrationCommands.js +3 -2
- package/dist/commands/taskIntegrationQueueCommands.js +1 -1
- package/dist/commands/taskNextActionCommand.js +8 -8
- package/dist/commands/taskOverviewCommand.js +33 -45
- package/dist/commands/taskRemoteDeliveryCommand.js +2 -2
- package/dist/commands/taskRoleRuntimeStatus.js +133 -102
- package/dist/commands/telemetryCommands.js +36 -38
- package/dist/config/configCatalog.js +4 -4
- package/dist/config/yuiConfig.js +8 -8
- package/dist/context/contextSnapshot.js +10 -10
- package/dist/context/dispatchContext.js +11 -11
- package/dist/context/roleSessionContext.js +6 -3
- package/dist/context/{turnContextPack.js → runContextPack.js} +146 -81
- package/dist/context/{turnInputContract.js → runInputContract.js} +73 -60
- package/dist/context/sessionBootstrapManifest.js +21 -2
- package/dist/context/sourceRunContext.js +30 -0
- package/dist/context/taskContext.js +458 -0
- package/dist/context/wakeNotification.js +27 -27
- package/dist/controller/agentRuntimeObserver.js +21 -24
- package/dist/controller/capabilityBridge.js +17 -6
- package/dist/controller/clientRuntime.js +65 -92
- package/dist/controller/controller.js +114 -188
- package/dist/controller/fileSchedulerStoreAdapter.js +787 -887
- package/dist/controller/jobControl.js +54 -85
- package/dist/controller/resourceInventory.js +8 -27
- package/dist/controller/resourceInventoryLinux.js +12 -13
- package/dist/controller/runtime.js +529 -479
- package/dist/controller/runtimeEventInbox.js +55 -25
- package/dist/controller/runtimeEventProcessor.js +22 -31
- package/dist/controller/{runtimeHookTurnFence.js → runtimeHookRunFence.js} +91 -115
- package/dist/controller/runtimeLaunchCoordinator.js +80 -426
- package/dist/controller/runtimeObservationHook.js +14 -18
- package/dist/controller/sessionNotify.js +16 -24
- package/dist/controller/sessionOwnerReconciliation.js +168 -50
- package/dist/controller/structuredProviderObservation.js +138 -99
- package/dist/coordination/workMailbox.js +3 -3
- package/dist/coordination/workMailboxQueue.js +36 -33
- package/dist/core/boundedRpc.js +8 -1
- package/dist/core/controllerClient.js +20 -1
- package/dist/core/controllerServer.js +4 -4
- package/dist/doctor/doctor.js +13 -2
- package/dist/domain/agentResultTransport.js +9 -9
- package/dist/execution/codexThreadNaming.js +2 -8
- package/dist/execution/executionHealth.js +51 -63
- package/dist/execution/reviewMainRun.js +137 -0
- package/dist/execution/workItemExecution.js +28 -29
- package/dist/execution/workItemExecutionProjection.js +99 -107
- package/dist/execution/workItemMainRun.js +141 -0
- package/dist/executor/agentAdapter.js +227 -20
- package/dist/executor/agentConfigurationCatalog.js +126 -4
- package/dist/executor/agentConfigurationProbe.js +162 -4
- package/dist/executor/agentExecutor.js +79 -78
- package/dist/executor/effectiveLaunch.js +105 -18
- package/dist/executor/executorRegistry.js +29 -44
- package/dist/executor/fileRoleLaunchPlanner.js +229 -154
- package/dist/executor/workspacePreflightClassification.js +16 -16
- package/dist/grant/capabilityGrant.js +6 -3
- package/dist/input/inputRequest.js +12 -10
- package/dist/integration/gitIntegrationService.js +4 -11
- package/dist/integration/integrationQueueService.js +4 -4
- package/dist/interaction/operatorPresentation.js +1 -1
- package/dist/kernel/builtinCapabilities.js +247 -12
- package/dist/kernel/capabilityRegistry.js +64 -18
- package/dist/kernel/instanceHost.js +12 -1
- package/dist/kernel/kernelPorts.js +2 -2
- package/dist/lifecycle/canonicalLifecycleEvent.js +44 -49
- package/dist/lifecycle/exactRunTerminalization.js +449 -0
- package/dist/message/message.js +62 -6
- package/dist/message/messageContinuation.js +204 -0
- package/dist/observability/executionAudit.js +70 -72
- package/dist/observability/faultClassification.js +2 -2
- package/dist/observability/orchestrationMetrics.js +8 -8
- package/dist/operator/operatorSessionHistory.js +1 -7
- package/dist/output/agentConfigurationPresentation.js +8 -3
- package/dist/output/agentRunConfigurationPresentation.js +128 -0
- package/dist/output/rolePresentation.js +54 -3
- package/dist/plugins/pluginChild.js +104 -0
- package/dist/plugins/pluginIntent.js +26 -0
- package/dist/plugins/pluginInterpreter.js +43 -0
- package/dist/plugins/pluginPackage.js +101 -0
- package/dist/plugins/pluginProcess.js +112 -0
- package/dist/plugins/pluginService.js +380 -0
- package/dist/profile/agentProfile.js +1 -1
- package/dist/repository/gitWorkspace.js +26 -4
- package/dist/repository/project.js +19 -4
- package/dist/repository/taskBaseFreshness.js +13 -13
- package/dist/repository/taskWorkspaceCoordinator.js +20 -27
- package/dist/repository/taskWorkspacePreparer.js +344 -83
- package/dist/resources/autoResourceGc.js +3 -3
- package/dist/resources/liveReferences.js +3 -3
- package/dist/resources/projectResource.js +123 -0
- package/dist/resources/projectResourceService.js +421 -0
- package/dist/resources/resourceDiscovery.js +6 -6
- package/dist/resources/resourceGc.js +1 -1
- package/dist/resources/resourceRegistrar.js +1 -1
- package/dist/resources/resourceTypes.js +1 -1
- package/dist/review/deltaRecheck.js +3 -3
- package/dist/review/reviewAcceptance.js +16 -16
- package/dist/review/reviewDecision.js +7 -7
- package/dist/review/reviewRound.js +21 -20
- package/dist/review/reviewerAvailability.js +2 -2
- package/dist/role/role.js +51 -7
- package/dist/role/taskRoleUpdate.js +30 -0
- package/dist/runtime/acpProtocol.js +425 -0
- package/dist/runtime/acpSession.js +731 -0
- package/dist/runtime/acpSessionConfiguration.js +260 -0
- package/dist/runtime/agentDriver.js +30 -11
- package/dist/runtime/agentEndpoint.js +278 -0
- package/dist/runtime/agentEndpointIdentity.js +86 -0
- package/dist/runtime/agentEndpointOwnership.js +239 -0
- package/dist/runtime/agentError.js +2 -10
- package/dist/runtime/agentHost.js +565 -314
- package/dist/runtime/agentRunConfiguration.js +258 -0
- package/dist/runtime/builtinAgentDrivers.js +134 -18
- package/dist/runtime/builtinAgentErrorMappers.js +55 -3
- package/dist/runtime/builtinTranscriptUsage.js +1 -1
- package/dist/runtime/claude-process-owner +0 -0
- package/dist/runtime/codexAppServerRuntime.js +38 -30
- package/dist/runtime/codexInteractiveHost.js +41 -6
- package/dist/runtime/continuationManager.js +2 -6
- package/dist/runtime/executionEnvironment.js +30 -0
- package/dist/runtime/firstProgressAdvisory.js +11 -11
- package/dist/runtime/index.js +4 -3
- package/dist/runtime/jsonLineChannel.js +109 -0
- package/dist/runtime/launchBroker.js +91 -16
- package/dist/runtime/launchDiagnostics.js +2 -2
- package/dist/runtime/lifecycleReservation.js +10 -18
- package/dist/runtime/managedCaller.js +61 -17
- package/dist/runtime/nativeSessionControl.js +102 -0
- package/dist/runtime/ports.js +6 -21
- package/dist/runtime/processExitObservation.js +8 -7
- package/dist/runtime/promptEnvelope.js +17 -6
- package/dist/runtime/providerContinuation.js +3 -9
- package/dist/runtime/providerContinuationReconciliationService.js +4 -13
- package/dist/runtime/providerControl.js +2 -7
- package/dist/runtime/providerRuntimeIdentity.js +110 -222
- package/dist/runtime/providerRuntimeReconciler.js +5 -9
- package/dist/runtime/runtimeBinding.js +0 -1
- package/dist/runtime/runtimeContinuationProjection.js +4 -7
- package/dist/runtime/runtimeDeadlines.js +9 -0
- package/dist/runtime/runtimeHealthPolicy.js +1 -1
- package/dist/runtime/runtimeObservation.js +29 -65
- package/dist/runtime/runtimeProjection.js +43 -51
- package/dist/runtime/runtimeSessionCandidate.js +1 -3
- package/dist/runtime/sessionLaunchRequest.js +3 -7
- package/dist/runtime/sessionOwnerIdentity.js +7 -54
- package/dist/runtime/sessionOwnerRegistry.js +22 -17
- package/dist/runtime/sessionReconciliation.js +4 -8
- package/dist/runtime/sessionTerminationGuard.js +70 -259
- package/dist/runtime/sessionTokenMetrics.js +5 -16
- package/dist/runtime/structuredProviderHost.js +237 -117
- package/dist/runtime/taskRuntimeIsolation.js +39 -122
- package/dist/runtime/tmuxAdapters.js +39 -86
- package/dist/scheduler/activeRoleRunDelivery.js +354 -0
- package/dist/scheduler/leaderWakeupProcessor.js +75 -266
- package/dist/scheduler/operatorInputNotificationProcessor.js +1 -1
- package/dist/scheduler/ports.js +80 -9
- package/dist/scheduler/{roleTurnLiveness.js → roleRunLiveness.js} +26 -30
- package/dist/scheduler/{roleTurnStall.js → roleRunStall.js} +128 -139
- package/dist/scheduler/taskExecutionProjection.js +120 -124
- package/dist/scheduler/taskObservabilityProjection.js +29 -29
- package/dist/scheduler/taskWake.js +11 -4
- package/dist/scheduler/wakeReason.js +9 -1
- package/dist/setup/setupCommand.js +1 -0
- package/dist/storage/migrations/agentRunContract.js +159 -0
- package/dist/storage/migrations/removeRuntimeGeneration.js +207 -0
- package/dist/storage/sqliteSchema.js +431 -5
- package/dist/storage/sqliteStore.js +375 -220
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +10 -5
- package/dist/storage/taskStore.js +13 -11
- package/dist/storage/upgrade/upgradeOrchestrator.js +5 -7
- package/dist/surface/surfaceContributions.js +102 -0
- package/dist/task/completionReadiness.js +32 -6
- package/dist/task/deliveryGuard.js +16 -16
- package/dist/task/draftPlan.js +72 -12
- package/dist/task/nextAction.js +144 -128
- package/dist/task/remoteDelivery.js +6 -6
- package/dist/task/task.js +184 -18
- package/dist/task/taskActivation.js +301 -0
- package/dist/task/taskActivationService.js +392 -0
- package/dist/task/taskRecordReference.js +5 -4
- package/dist/task/taskRecordRetirement.js +1 -1
- package/dist/telemetry/sqliteTelemetryStore.js +55 -68
- package/dist/telemetry/telemetryConfig.js +14 -14
- package/dist/telemetry/telemetryWiring.js +2 -2
- package/dist/web/assets/assetManifest.js +2 -0
- package/dist/web/assets/client/app.js +120 -20
- package/dist/web/assets/client/components.js +87 -54
- package/dist/web/assets/client/i18n.js +83 -41
- package/dist/web/assets/client/markdown.js +1 -1
- package/dist/web/assets/client/taskSurface.js +353 -0
- package/dist/web/assets/client/view.js +49 -44
- package/dist/web/assets/shell.js +1 -1
- package/dist/web/assets/styles/cards.js +22 -4
- package/dist/web/controllerWeb.js +60 -0
- package/dist/web/webMutation.js +28 -0
- package/dist/web/webServer.js +118 -8
- package/dist/web/webSnapshot.js +81 -74
- package/dist/web/webTaskSurface.js +64 -0
- package/dist/workItem/dependencyGate.js +1 -1
- package/dist/workItem/workItem.js +80 -48
- package/dist/workspace/workItemChangeSetManager.js +16 -9
- package/docs/agent-result-consumption.md +94 -0
- package/docs/agent-runtime-drivers.md +91 -0
- package/docs/architecture/README.md +38 -0
- package/docs/architecture/capabilities-and-resources.md +79 -0
- package/docs/managed-turn-and-session-runtime.md +222 -0
- package/docs/observability/README.md +81 -0
- package/docs/plugin-sdk.md +290 -0
- package/docs/provider-runtime.md +163 -0
- package/docs/release-workflow.md +303 -0
- package/docs/roles-and-configuration.md +113 -0
- package/docs/sqlite-control-plane-design.md +76 -0
- package/docs/task-dag-semantics.md +57 -0
- package/docs/task-delivery.md +103 -0
- package/docs/task-local-identity.md +6 -6
- package/docs/testing/verification-levels.md +86 -0
- package/i18n/README.zh-CN.md +98 -739
- package/package.json +2 -2
- package/skills/yui-leader/SKILL.md +130 -103
- package/skills/yui-leader/references/integration.md +39 -0
- package/skills/yui-leader/references/replicated-execution.md +42 -0
- package/skills/yui-leader/references/task-plugins.md +33 -0
- package/skills/yui-operator/SKILL.md +30 -59
- package/skills/yui-reviewer/SKILL.md +35 -36
- package/skills/yui-runtime/SKILL.md +88 -24
- package/skills/yui-runtime/references/publication.md +22 -0
- package/skills/yui-runtime/references/recovery.md +64 -0
- package/skills/yui-worker/SKILL.md +37 -39
- package/dist/cli/roleOptionCatalog.js +0 -68
- package/dist/context/sourceTurnContext.js +0 -30
- package/dist/execution/reviewMainTurn.js +0 -161
- package/dist/execution/workItemMainTurn.js +0 -164
- package/dist/lifecycle/exactTurnTerminalization.js +0 -407
- package/dist/runtime/preallocatedNativeSession.js +0 -13
- package/dist/runtime/runtimeStopReceipt.js +0 -42
- 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
|
+
}
|