@ljwei-stak/dsh-model-route-bot 0.66.3 → 0.67.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dsh-plugin/bot-product.mjs +81 -5
- package/.dsh-plugin/client.js +17490 -1791
- package/README.i18n.yaml +4 -4
- package/README.md +409 -364
- package/README.zh.md +409 -364
- package/apps/integration-client/README.md +31 -7
- package/apps/integration-client/api.mjs +14 -3
- package/apps/integration-client/binding-editor.jsx +308 -109
- package/apps/integration-client/binding-model.mjs +262 -0
- package/apps/integration-client/binding.css +144 -0
- package/apps/integration-client/bot-model-status.mjs +18 -0
- package/apps/integration-client/bot-profile-identity.jsx +197 -0
- package/apps/integration-client/bot-profile.css +57 -0
- package/apps/integration-client/bot-profile.mjs +97 -0
- package/apps/integration-client/bot-settings-dialog.css +18 -0
- package/apps/integration-client/bot-settings-dialog.jsx +111 -0
- package/apps/integration-client/bot-thread-header-bridge.css +20 -0
- package/apps/integration-client/bot-thread-header.jsx +278 -0
- package/apps/integration-client/bot-threads-panel.jsx +378 -0
- package/apps/integration-client/bot-threads.css +169 -0
- package/apps/integration-client/chat-design.mjs +250 -0
- package/apps/integration-client/chat-ui.jsx +175 -0
- package/apps/integration-client/chat.css +477 -0
- package/apps/integration-client/composer-draft-binding.mjs +50 -0
- package/apps/integration-client/composer-draft-bound.jsx +14 -0
- package/apps/integration-client/composer-draft-host.jsx +142 -0
- package/apps/integration-client/composer-draft-session.jsx +287 -0
- package/apps/integration-client/composer-draft-witness.mjs +115 -0
- package/apps/integration-client/composer-draft.css +4 -0
- package/apps/integration-client/composer-draft.mjs +322 -0
- package/apps/integration-client/contact-attention.jsx +54 -0
- package/apps/integration-client/data-panel.jsx +439 -0
- package/apps/integration-client/data.css +69 -0
- package/apps/integration-client/desktop-bridge.mjs +1 -0
- package/apps/integration-client/desktop-http-socket.mjs +49 -0
- package/apps/integration-client/desktop-keyboard.mjs +32 -0
- package/apps/integration-client/desktop-pane.jsx +99 -30
- package/apps/integration-client/desktop-stream.mjs +13 -3
- package/apps/integration-client/desktop-tools.jsx +19 -0
- package/apps/integration-client/desktop-touch.mjs +27 -0
- package/apps/integration-client/desktop.css +41 -0
- package/apps/integration-client/independent-chat-controller.mjs +224 -0
- package/apps/integration-client/independent-chat-entry.jsx +61 -0
- package/apps/integration-client/independent-chat-navigation.mjs +21 -0
- package/apps/integration-client/independent-chat-page.jsx +148 -0
- package/apps/integration-client/independent-chat-rich.jsx +64 -0
- package/apps/integration-client/independent-chat-rich.mjs +30 -0
- package/apps/integration-client/independent-chat.css +73 -0
- package/apps/integration-client/index.jsx +24 -16
- package/apps/integration-client/library-panel.jsx +4 -1
- package/apps/integration-client/licenses/jsqr-1.4.0-LICENSE.txt +202 -0
- package/apps/integration-client/main-route-editor.jsx +94 -0
- package/apps/integration-client/main-route-model.mjs +100 -0
- package/apps/integration-client/model-picker.css +37 -0
- package/apps/integration-client/model-picker.jsx +83 -0
- package/apps/integration-client/native-activity.css +141 -0
- package/apps/integration-client/native-activity.jsx +276 -0
- package/apps/integration-client/native-activity.mjs +384 -0
- package/apps/integration-client/notifications-panel.jsx +186 -0
- package/apps/integration-client/notifications.css +126 -0
- package/apps/integration-client/pairing-qr.jsx +79 -0
- package/apps/integration-client/panels.jsx +126 -115
- package/apps/integration-client/product-slots.jsx +46 -0
- package/apps/integration-client/routine-recorder.mjs +23 -2
- package/apps/integration-client/secure-pairing.jsx +84 -0
- package/apps/integration-client/tasks-panel.jsx +32 -10
- package/apps/integration-client/ui-icons.jsx +17 -0
- package/apps/integration-client/usage-explorer.jsx +43 -0
- package/apps/integration-client/usage-report.mjs +71 -0
- package/apps/integration-client/usage.css +34 -0
- package/apps/integration-client/web-panel-model.mjs +140 -0
- package/apps/integration-client/web-panel-page.jsx +221 -0
- package/apps/integration-client/web-panel-pins-live.jsx +28 -0
- package/apps/integration-client/web-panel.css +139 -0
- package/apps/integration-client/workspace-model.mjs +67 -0
- package/apps/integration-client/workspace-navigation.mjs +56 -0
- package/apps/integration-client/workspace-ui.jsx +173 -0
- package/apps/integration-client/workspace.css +332 -0
- package/modules/bot-runtime/SOURCE.json +176 -50
- package/modules/bot-runtime/index.mjs +169 -13
- package/modules/bot-runtime/product-overlay/OVERLAY.json +128 -2
- package/modules/bot-runtime/product-overlay/base/src/client/header.js +244 -0
- package/modules/bot-runtime/product-overlay/base/src/client/learning-card.js +202 -0
- package/modules/bot-runtime/product-overlay/base/src/client/model-menu.js +371 -0
- package/modules/bot-runtime/product-overlay/base/src/client/secret-card.js +363 -0
- package/modules/bot-runtime/product-overlay/base/src/client/sources.js +68 -0
- package/modules/bot-runtime/product-overlay/base/src/client/turn-error.js +260 -0
- package/modules/bot-runtime/product-overlay/base/src/host/channel.js +647 -0
- package/modules/bot-runtime/product-overlay/base/src/host/memory.js +2050 -0
- package/modules/bot-runtime/product-overlay/base/src/host/models.js +314 -0
- package/modules/bot-runtime/product-overlay/base/src/host/parts.js +1473 -0
- package/modules/bot-runtime/product-overlay/base/src/host/privacy.js +368 -0
- package/modules/bot-runtime/product-overlay/base/src/host/roster.js +776 -0
- package/modules/bot-runtime/product-overlay/base/src/host/store.js +512 -0
- package/modules/bot-runtime/product-overlay/base/src/host/usage.js +653 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/details-drawer.js.patch +48 -2
- package/modules/bot-runtime/product-overlay/patches/src/client/header.js.patch +41 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/index.js.patch +33 -1
- package/modules/bot-runtime/product-overlay/patches/src/client/learning-card.js.patch +20 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/model-menu.js.patch +24 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/secret-card.js.patch +25 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/sources.js.patch +52 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/turn-error.js.patch +23 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/channel.js.patch +46 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/memory.js.patch +11 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/models.js.patch +16 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/parts.js.patch +95 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/privacy.js.patch +11 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/roster.js.patch +145 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/store.js.patch +68 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/usage.js.patch +11 -0
- package/modules/bot-runtime/read-model.mjs +19 -4
- package/modules/bot-runtime/vendor/ds-bot/src/client/details-drawer.js +15 -5
- package/modules/bot-runtime/vendor/ds-bot/src/client/header.js +9 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/index.js +9 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/learning-card.js +2 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/model-menu.js +5 -3
- package/modules/bot-runtime/vendor/ds-bot/src/client/secret-card.js +3 -3
- package/modules/bot-runtime/vendor/ds-bot/src/client/sources.js +25 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/turn-error.js +3 -2
- package/modules/bot-runtime/vendor/ds-bot/src/host/channel.js +14 -2
- package/modules/bot-runtime/vendor/ds-bot/src/host/memory.js +1 -1
- package/modules/bot-runtime/vendor/ds-bot/src/host/models.js +7 -0
- package/modules/bot-runtime/vendor/ds-bot/src/host/parts.js +26 -9
- package/modules/bot-runtime/vendor/ds-bot/src/host/privacy.js +1 -1
- package/modules/bot-runtime/vendor/ds-bot/src/host/roster.js +112 -3
- package/modules/bot-runtime/vendor/ds-bot/src/host/store.js +34 -1
- package/modules/bot-runtime/vendor/ds-bot/src/host/usage.js +1 -1
- package/modules/independent-runtime/index.mjs +152 -62
- package/modules/independent-runtime/remote-tools.mjs +8 -5
- package/modules/openmaus-protocol/NOTICE +10 -0
- package/modules/openmaus-protocol/README.md +15 -0
- package/modules/openmaus-protocol/SOURCE.json +68 -0
- package/modules/openmaus-protocol/dist/protocol.mjs +1203 -0
- package/modules/openmaus-protocol/index.mjs +1 -0
- package/modules/openmaus-protocol/transport.mjs +77 -0
- package/modules/openmaus-protocol/vendor/LICENSE +202 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/README.md +405 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/credentials.ts +128 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/descriptor.ts +84 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/noise.ts +456 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/phone.ts +104 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/session.ts +417 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.json +2565 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.sha256 +1 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/yamux.ts +335 -0
- package/package.json +20 -6
- package/packages/bot-acp/README.md +156 -0
- package/packages/bot-acp/licenses/LICENSE-OpenMausBot +202 -0
- package/packages/bot-acp/licenses/NOTICE.md +27 -0
- package/packages/bot-acp/src/acp-adapter.mjs +795 -0
- package/packages/bot-acp/src/acp-host.mjs +148 -0
- package/packages/bot-acp/src/native-launch.mjs +121 -0
- package/packages/bot-acp/src/permission-policy.mjs +283 -0
- package/packages/bot-acp/src/rpc-codec.mjs +265 -0
- package/packages/bot-acp/src/session-core.mjs +581 -0
- package/packages/bot-acp/src/stdio-rpc.mjs +329 -0
- package/packages/bot-engines/README.md +281 -0
- package/packages/bot-engines/licenses/LICENSE-OpenMausBot +202 -0
- package/packages/bot-engines/licenses/NOTICE.md +32 -0
- package/packages/bot-engines/src/claude-engine.mjs +772 -0
- package/packages/bot-engines/src/claude-llm-adapter.mjs +797 -0
- package/packages/bot-engines/src/claude-llm-turn.mjs +292 -0
- package/packages/bot-engines/src/claude-permission-broker.mjs +130 -0
- package/packages/bot-engines/src/claude-permission-proxy.mjs +87 -0
- package/packages/bot-engines/src/claude-protocol.mjs +177 -0
- package/packages/bot-engines/src/driver-catalog.mjs +334 -0
- package/packages/bot-engines/src/host-loop-request.mjs +95 -0
- package/packages/bot-engines/src/host-registrations.mjs +105 -0
- package/packages/capability-broker/src/cli-bootstrap.mjs +86 -16
- package/packages/capability-broker/src/cli-bridge-response.mjs +80 -0
- package/packages/capability-broker/src/cli-stdio-bridge.mjs +21 -8
- package/packages/cli-runtime/src/codex-session-core.mjs +4 -0
- package/packages/cli-runtime/src/native-cli-output.mjs +57 -0
- package/packages/cli-runtime/src/native-peer.ps1 +51 -3
- package/packages/cli-runtime/src/native-process.mjs +77 -14
- package/packages/model-router-plugin/README.md +3 -3
- package/packages/model-router-plugin/package.json +1 -1
- package/packages/orchestration/src/binding-product.mjs +19 -0
- package/packages/orchestration/src/bot-data-actions.mjs +45 -0
- package/packages/orchestration/src/bot-data-chart.mjs +109 -0
- package/packages/orchestration/src/bot-data-context.mjs +44 -0
- package/packages/orchestration/src/bot-data-engine.mjs +809 -0
- package/packages/orchestration/src/bot-data-io.mjs +186 -0
- package/packages/orchestration/src/bot-data-sql.mjs +195 -0
- package/packages/orchestration/src/bot-data-surface.mjs +144 -0
- package/packages/orchestration/src/bot-data-tools.mjs +68 -0
- package/packages/orchestration/src/bot-data-worker.mjs +228 -0
- package/packages/orchestration/src/bot-independent-execution.mjs +195 -0
- package/packages/orchestration/src/bot-independent-stream.mjs +74 -0
- package/packages/orchestration/src/bot-loop.mjs +152 -19
- package/packages/orchestration/src/bot-notifications.mjs +159 -0
- package/packages/orchestration/src/bot-policy.mjs +43 -4
- package/packages/orchestration/src/bot-profile.mjs +171 -0
- package/packages/orchestration/src/bot-routine.mjs +4 -4
- package/packages/orchestration/src/bot-runtime-assembly.mjs +255 -8
- package/packages/orchestration/src/bot-runtime-switch.mjs +13 -2
- package/packages/orchestration/src/bot-serving.mjs +164 -0
- package/packages/orchestration/src/bot-session-store.mjs +105 -9
- package/packages/orchestration/src/bot-settings.mjs +50 -11
- package/packages/orchestration/src/bot-thread-admission.mjs +84 -0
- package/packages/orchestration/src/bot-thread-host.mjs +187 -0
- package/packages/orchestration/src/bot-threads.mjs +790 -0
- package/packages/orchestration/src/data-product-access.mjs +37 -0
- package/packages/orchestration/src/data-tool-contract.mjs +291 -0
- package/packages/orchestration/src/desktop-http-carrier.mjs +120 -0
- package/packages/orchestration/src/desktop-proxy.mjs +311 -0
- package/packages/orchestration/src/desktop-sessions.mjs +43 -9
- package/packages/orchestration/src/ds-lifecycle.mjs +31 -4
- package/packages/orchestration/src/grant-refresh-queue.mjs +17 -0
- package/packages/orchestration/src/independent-chat-page.mjs +39 -0
- package/packages/orchestration/src/independent-chat-service.mjs +308 -0
- package/packages/orchestration/src/independent-context-state.mjs +44 -0
- package/packages/orchestration/src/independent-message-wire.mjs +84 -0
- package/packages/orchestration/src/independent-tool-catalog.mjs +175 -0
- package/packages/orchestration/src/independent-tool-families.mjs +73 -0
- package/packages/orchestration/src/independent-tool-host.mjs +344 -0
- package/packages/orchestration/src/main-bot-grant.mjs +15 -0
- package/packages/orchestration/src/main-model-routes.mjs +208 -0
- package/packages/orchestration/src/model-accounting.mjs +99 -35
- package/packages/orchestration/src/model-auxiliary-dispatch.mjs +187 -0
- package/packages/orchestration/src/model-loop-frame.mjs +179 -0
- package/packages/orchestration/src/model-prepared-generation.mjs +175 -0
- package/packages/orchestration/src/model-request-bounds.mjs +111 -0
- package/packages/orchestration/src/native-activity-contract.mjs +491 -0
- package/packages/orchestration/src/native-activity-service.mjs +102 -0
- package/packages/orchestration/src/native-activity-store.mjs +249 -0
- package/packages/orchestration/src/native-conversation-tools.mjs +78 -0
- package/packages/orchestration/src/native-data-tools.mjs +100 -0
- package/packages/orchestration/src/native-remote-actions.mjs +742 -0
- package/packages/orchestration/src/native-team-tools.mjs +73 -0
- package/packages/orchestration/src/pairing-invite.mjs +37 -0
- package/packages/orchestration/src/product-composition.mjs +1559 -1001
- package/packages/orchestration/src/remote-machines.mjs +82 -14
- package/packages/orchestration/src/routine-execution.mjs +5 -3
- package/packages/orchestration/src/routine-session.mjs +19 -13
- package/packages/orchestration/src/secure-pairing-host.mjs +112 -0
- package/packages/orchestration/src/task-journal.mjs +29 -0
- package/packages/orchestration/src/task-product.mjs +17 -0
- package/packages/orchestration/src/team-assignment-store.mjs +120 -0
- package/packages/orchestration/src/team-bridge.mjs +282 -0
- package/packages/orchestration/src/team-scheduler.mjs +83 -0
- package/packages/orchestration/src/team-sdk-tools.mjs +84 -0
- package/packages/orchestration/src/team-tool-contract.mjs +284 -0
- package/packages/orchestration/src/ui-host-namespace.mjs +23 -0
- package/packages/orchestration/src/web-panel-actions.mjs +103 -0
- package/packages/orchestration/src/web-panel-framing.mjs +227 -0
- package/packages/orchestration/src/web-panel-store.mjs +206 -0
- package/remote-computer-agent/README.md +39 -29
- package/remote-computer-agent/build.mjs +3 -0
- package/remote-computer-agent/dist/OPENMAUSBOT_LICENSE +202 -0
- package/remote-computer-agent/dist/OPENMAUSBOT_NOTICE +10 -0
- package/remote-computer-agent/dist/OPENMAUSBOT_SOURCE.json +68 -0
- package/remote-computer-agent/dist/cli-stdio-bridge.mjs +215 -25
- package/remote-computer-agent/dist/cli-stdio-bridge.mjs.map +4 -4
- package/remote-computer-agent/dist/cli.mjs +1688 -168
- package/remote-computer-agent/dist/cli.mjs.map +4 -4
- package/remote-computer-agent/dist/index.mjs +1579 -122
- package/remote-computer-agent/dist/index.mjs.map +4 -4
- package/remote-computer-agent/dist/native-peer.ps1 +51 -3
- package/remote-computer-agent/package-lock.json +308 -2
- package/remote-computer-agent/package.json +2 -1
- package/remote-computer-agent/pair-device.ps1 +20 -0
- package/remote-computer-agent/setup.ps1 +38 -16
- package/remote-computer-agent/src/cli.ts +24 -3
- package/remote-computer-agent/src/desktop-session.ts +11 -3
- package/remote-computer-agent/src/e2e-server.ts +81 -0
- package/remote-computer-agent/src/pairing-page.ts +35 -0
- package/remote-computer-agent/src/protocol.ts +25 -25
- package/remote-computer-agent/src/secure-pairing.ts +105 -0
- package/remote-computer-agent/src/store.ts +20 -5
- package/remote-computer-agent/src/transport.ts +39 -15
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
// Multi-turn ACP session over one agent process.
|
|
2
|
+
//
|
|
3
|
+
// Ported and re-grounded from OpenMausBot server/drivers/acp/core.ts,
|
|
4
|
+
// turn-state.ts, session-model-controls.ts and session-reports.ts at pin
|
|
5
|
+
// 1bc4749aa8307d7d6998bf86de4ef6478460dd93 (Apache-2.0); see licenses/NOTICE.md.
|
|
6
|
+
//
|
|
7
|
+
// Upstream invariants kept here: there is no turn/completed notification — the
|
|
8
|
+
// session/prompt result (stopReason plus usage) is the completion; session/load
|
|
9
|
+
// replays history as ordinary session/update notifications, so nothing is
|
|
10
|
+
// emitted before the prompt is sent; a Stop the agent answers with end_turn is
|
|
11
|
+
// the person's stop; and a model is only switched through an option the agent
|
|
12
|
+
// actually advertised.
|
|
13
|
+
//
|
|
14
|
+
// Boundaries this port adds: turns are strictly serial with an explicit busy
|
|
15
|
+
// refusal; a refused or unsupported resume never silently opens a new session
|
|
16
|
+
// and replays the old turn; an unknown stop reason is reported verbatim and is
|
|
17
|
+
// never presented as success; model, config and mode changes run only when the
|
|
18
|
+
// agent advertised them and the Host authorized the selection; the dispatch
|
|
19
|
+
// moment is reported to the caller through `onDispatched`, and every refusal
|
|
20
|
+
// raised before it is tagged `acpNotDispatched` so a caller can tell "nothing
|
|
21
|
+
// was sent" from "the peer may have acted".
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
ACP_METHODS, ACP_PROTOCOL_VERSION, clientCapabilities, readAgentCapabilities,
|
|
25
|
+
} from './rpc-codec.mjs'
|
|
26
|
+
import { createPermissionPolicy } from './permission-policy.mjs'
|
|
27
|
+
|
|
28
|
+
export const DEFAULT_LIMITS = Object.freeze({
|
|
29
|
+
initTimeoutMs: 300_000,
|
|
30
|
+
sessionTimeoutMs: 300_000,
|
|
31
|
+
loadTimeoutMs: 120_000,
|
|
32
|
+
promptTimeoutMs: 600_000,
|
|
33
|
+
promptIdleMs: 180_000,
|
|
34
|
+
/** A resident process runs a bounded number of turns before it is retired. */
|
|
35
|
+
maxTurns: 256,
|
|
36
|
+
/** An unanswered permission ask denies after this long. */
|
|
37
|
+
permissionTimeoutMs: 15 * 60_000,
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const MAX_PROMPT_BYTES = 64_000
|
|
41
|
+
|
|
42
|
+
export const STOP_REASONS = Object.freeze({
|
|
43
|
+
endTurn: 'end_turn',
|
|
44
|
+
cancelled: 'cancelled',
|
|
45
|
+
refusal: 'refusal',
|
|
46
|
+
maxTurn: 'max_turn_requests',
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
/** Merge one session/update into the running turn. Unknown updates count, they never invent text. */
|
|
50
|
+
export function mergeSessionUpdate(state, update, sessionId) {
|
|
51
|
+
const kind = update?.sessionUpdate
|
|
52
|
+
const entry = { kind, sessionId: sessionId ?? null }
|
|
53
|
+
switch (kind) {
|
|
54
|
+
case 'agent_message_chunk': {
|
|
55
|
+
const content = update.content
|
|
56
|
+
if (content?.type === 'text' && typeof content.text === 'string' && content.text) {
|
|
57
|
+
if (typeof update.messageId === 'string' && update.messageId) state.textMessageId = update.messageId
|
|
58
|
+
state.text += content.text
|
|
59
|
+
state.repliedText = true
|
|
60
|
+
entry.text = content.text
|
|
61
|
+
} else if (content?.type === 'image' && typeof content.data === 'string' && content.data) {
|
|
62
|
+
state.producedItem = true
|
|
63
|
+
entry.image = true
|
|
64
|
+
} else {
|
|
65
|
+
entry.ignored = true
|
|
66
|
+
}
|
|
67
|
+
break
|
|
68
|
+
}
|
|
69
|
+
case 'agent_thought_chunk': {
|
|
70
|
+
const content = update.content
|
|
71
|
+
if (content?.type === 'text' && typeof content.text === 'string' && content.text) {
|
|
72
|
+
entry.text = content.text
|
|
73
|
+
state.producedItem = true
|
|
74
|
+
} else entry.ignored = true
|
|
75
|
+
break
|
|
76
|
+
}
|
|
77
|
+
case 'tool_call':
|
|
78
|
+
case 'tool_call_update': {
|
|
79
|
+
const id = typeof update.toolCallId === 'string' && update.toolCallId ? update.toolCallId : null
|
|
80
|
+
const status = update.status ?? (kind === 'tool_call' ? 'pending' : undefined)
|
|
81
|
+
if (id) {
|
|
82
|
+
if (status === 'completed' || status === 'failed') state.runningTools.delete(id)
|
|
83
|
+
else if (status === 'pending' || status === 'in_progress') state.runningTools.add(id)
|
|
84
|
+
}
|
|
85
|
+
state.producedItem = true
|
|
86
|
+
entry.toolCallId = id
|
|
87
|
+
entry.status = status ?? null
|
|
88
|
+
break
|
|
89
|
+
}
|
|
90
|
+
case 'usage_update': {
|
|
91
|
+
const used = update.used ?? update.usage?.used
|
|
92
|
+
if (typeof used === 'number' && Number.isFinite(used) && used > 0) {
|
|
93
|
+
if (state.usagePeak !== null && used < state.usagePeak * 0.6) state.usageCompacted = true
|
|
94
|
+
if (state.usagePeak === null || used > state.usagePeak) state.usagePeak = used
|
|
95
|
+
state.usageLast = used
|
|
96
|
+
entry.contextTokens = used
|
|
97
|
+
if (typeof (update.size ?? update.usage?.size) === 'number') entry.contextWindow = update.size ?? update.usage?.size
|
|
98
|
+
} else entry.ignored = true
|
|
99
|
+
break
|
|
100
|
+
}
|
|
101
|
+
default:
|
|
102
|
+
entry.ignored = true
|
|
103
|
+
}
|
|
104
|
+
return entry
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Usage evidence. A figure the agent never reported stays null, never estimated. */
|
|
108
|
+
export function usageEvidence(state) {
|
|
109
|
+
return {
|
|
110
|
+
contextTokens: typeof state.usageLast === 'number' ? state.usageLast : null,
|
|
111
|
+
contextPeakTokens: typeof state.usagePeak === 'number' ? state.usagePeak : null,
|
|
112
|
+
compacted: state.usageCompacted === true,
|
|
113
|
+
inputTokens: null, outputTokens: null, cachedInputTokens: null,
|
|
114
|
+
reported: false,
|
|
115
|
+
status: 'unknown',
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const emptyTurnState = () => ({
|
|
120
|
+
settled: false, promptSent: false, text: '', textMessageId: null,
|
|
121
|
+
producedItem: false, repliedText: false, stopped: false, cancelling: false,
|
|
122
|
+
usagePeak: null, usageLast: null, usageCompacted: false,
|
|
123
|
+
runningTools: new Set(), updates: [],
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @param {object} options
|
|
128
|
+
* @param {object} options.host result of createAcpHost
|
|
129
|
+
* @param {object} options.link one host link (one session)
|
|
130
|
+
* @param {string} options.cwd Host-owned absolute workspace
|
|
131
|
+
* @param {{name: string, version: string}} options.clientInfo
|
|
132
|
+
* @param {{fileSystem?: boolean, terminal?: boolean, meta?: object}} [options.capabilities]
|
|
133
|
+
* @param {{pickMethod?: Function, onAuthFailure?: 'fail'|'continue'}} [options.auth]
|
|
134
|
+
* @param {(entry: object) => void} [options.onUpdate] merged stream, item by item
|
|
135
|
+
* @param {Function} [options.onPermissionRequest] trusted Host permission handler
|
|
136
|
+
* @param {{describe: Function, assertTool: Function}} [options.authority]
|
|
137
|
+
* @param {object} [options.subject]
|
|
138
|
+
* @param {(identity: object) => string|null} [options.operationTool]
|
|
139
|
+
* @param {(modelId: string) => void} [options.assertModelAuthorized] Host model authorization
|
|
140
|
+
* @param {Function} [options.log]
|
|
141
|
+
*/
|
|
142
|
+
export function createAcpSession({
|
|
143
|
+
host,
|
|
144
|
+
link,
|
|
145
|
+
cwd,
|
|
146
|
+
clientInfo,
|
|
147
|
+
capabilities = {},
|
|
148
|
+
auth = {},
|
|
149
|
+
onUpdate,
|
|
150
|
+
onPermissionRequest,
|
|
151
|
+
onServerRequest = null,
|
|
152
|
+
authority = null,
|
|
153
|
+
subject = null,
|
|
154
|
+
operationTool = null,
|
|
155
|
+
assertModelAuthorized = null,
|
|
156
|
+
limits = {},
|
|
157
|
+
signal,
|
|
158
|
+
log,
|
|
159
|
+
} = {}) {
|
|
160
|
+
if (!host || typeof host.once !== 'function') throw new TypeError('ACP_SESSION_HOST_REQUIRED')
|
|
161
|
+
if (!link || typeof link.request !== 'function') throw new TypeError('ACP_SESSION_LINK_REQUIRED')
|
|
162
|
+
if (typeof cwd !== 'string' || !cwd) throw new TypeError('ACP_SESSION_CWD_REQUIRED')
|
|
163
|
+
const bounds = { ...DEFAULT_LIMITS, ...limits }
|
|
164
|
+
if (typeof clientInfo?.name !== 'string' || !clientInfo.name) throw new TypeError('ACP_SESSION_CLIENT_INFO_REQUIRED')
|
|
165
|
+
|
|
166
|
+
const permissions = createPermissionPolicy({
|
|
167
|
+
onPermissionRequest, authority, subject, operationTool, timeoutMs: bounds.permissionTimeoutMs, signal, log,
|
|
168
|
+
})
|
|
169
|
+
let agentCapabilities = null
|
|
170
|
+
let initResult = null
|
|
171
|
+
let authenticated = false
|
|
172
|
+
let sessionId = null
|
|
173
|
+
let sessionConfigResult = null
|
|
174
|
+
let current = null
|
|
175
|
+
let turns = 0
|
|
176
|
+
let closed = false
|
|
177
|
+
let closingReason = null
|
|
178
|
+
let cancelRequestedAt = null
|
|
179
|
+
|
|
180
|
+
const sessionBusy = () => current !== null && !current.state.settled
|
|
181
|
+
|
|
182
|
+
const emitUpdate = entry => {
|
|
183
|
+
current?.state.updates.push(entry)
|
|
184
|
+
if (typeof onUpdate === 'function') {
|
|
185
|
+
try { onUpdate(entry) } catch (error) { log?.({ event: 'acp.update-listener-failed', message: error?.message ?? String(error) }) }
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ---- inbound ---------------------------------------------------------------
|
|
190
|
+
link.inbound = {
|
|
191
|
+
request: classified => {
|
|
192
|
+
// A Host-installed guard (fs/terminal) answers first; anything it does not
|
|
193
|
+
// own falls through to the fail-closed paths below.
|
|
194
|
+
if (typeof onServerRequest === 'function') {
|
|
195
|
+
const reply = onServerRequest(classified)
|
|
196
|
+
if (reply !== null && reply !== undefined) {
|
|
197
|
+
if (typeof reply?.then === 'function') {
|
|
198
|
+
void reply.then(value => link.respond(classified.id, value?.result ?? value), error => link.respondError(classified.id, -32602, error?.message ?? String(error)))
|
|
199
|
+
} else {
|
|
200
|
+
link.respond(classified.id, reply?.result ?? reply)
|
|
201
|
+
}
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (classified.method === ACP_METHODS.sessionRequestPermission) {
|
|
206
|
+
// Only while a turn is running: between turns nothing is brokered.
|
|
207
|
+
if (!sessionBusy()) {
|
|
208
|
+
link.respond(classified.id, { outcome: { outcome: 'cancelled' } })
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
void permissions.decide(classified).then(decision => {
|
|
212
|
+
link.respond(classified.id, decision.behavior === 'allow' && decision.optionId
|
|
213
|
+
? { outcome: { outcome: 'selected', optionId: decision.optionId } }
|
|
214
|
+
: { outcome: { outcome: 'cancelled' } })
|
|
215
|
+
emitUpdate({ kind: 'permission-resolved', behavior: decision.behavior, source: decision.source, reason: decision.reason, toolCallId: classified.params?.toolCall?.toolCallId ?? null })
|
|
216
|
+
})
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
// fs/* and terminal/* are refused by the guard upstream of this point; any
|
|
220
|
+
// other server request is answered fail-closed rather than blocking the agent.
|
|
221
|
+
link.respondError(classified.id, -32601, 'method not found')
|
|
222
|
+
},
|
|
223
|
+
notification: classified => {
|
|
224
|
+
if (classified.method !== ACP_METHODS.sessionUpdate) return
|
|
225
|
+
const params = classified.params ?? {}
|
|
226
|
+
if (params._meta?.isReplay === true) return
|
|
227
|
+
if (sessionId !== null && params.sessionId !== sessionId) return
|
|
228
|
+
const update = params.update ?? {}
|
|
229
|
+
if (!current || !current.state.promptSent) return
|
|
230
|
+
const entry = mergeSessionUpdate(current.state, update, sessionId)
|
|
231
|
+
emitUpdate(entry)
|
|
232
|
+
},
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ---- handshake -------------------------------------------------------------
|
|
236
|
+
const initialize = () => host.once('initialize', async () => {
|
|
237
|
+
const result = await link.request(ACP_METHODS.initialize, {
|
|
238
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
239
|
+
clientInfo,
|
|
240
|
+
clientCapabilities: clientCapabilities(capabilities),
|
|
241
|
+
}, { timeoutMs: bounds.initTimeoutMs })
|
|
242
|
+
if (!result || typeof result !== 'object') throw new Error('ACP initialize returned no result')
|
|
243
|
+
agentCapabilities = readAgentCapabilities(result)
|
|
244
|
+
if (agentCapabilities.protocolVersion !== ACP_PROTOCOL_VERSION) {
|
|
245
|
+
throw new Error(`ACP protocol version mismatch: agent reports ${agentCapabilities.protocolVersion ?? 'nothing'}, client speaks ${ACP_PROTOCOL_VERSION}`)
|
|
246
|
+
}
|
|
247
|
+
initResult = result
|
|
248
|
+
log?.({ event: 'acp.initialize', agent: agentCapabilities.agentName, version: agentCapabilities.agentVersion, loadSession: agentCapabilities.loadSession })
|
|
249
|
+
return agentCapabilities
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
const authenticate = () => host.once('authenticate', async () => {
|
|
253
|
+
if (authenticated) return true
|
|
254
|
+
const methods = Array.isArray(initResult?.authMethods) ? initResult.authMethods : []
|
|
255
|
+
const methodId = typeof auth.pickMethod === 'function' ? auth.pickMethod(methods) : null
|
|
256
|
+
if (typeof methodId !== 'string' || !methodId) {
|
|
257
|
+
if (auth.onAuthFailure === 'fail') throw new Error('ACP agent requires authentication')
|
|
258
|
+
return false
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
await link.request(ACP_METHODS.authenticate, { methodId }, { timeoutMs: bounds.initTimeoutMs })
|
|
262
|
+
authenticated = true
|
|
263
|
+
return true
|
|
264
|
+
} catch (error) {
|
|
265
|
+
if (auth.onAuthFailure === 'fail') throw error
|
|
266
|
+
return false
|
|
267
|
+
}
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
const handshake = async () => {
|
|
271
|
+
await initialize()
|
|
272
|
+
await authenticate()
|
|
273
|
+
return agentCapabilities
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ---- session lifecycle -----------------------------------------------------
|
|
277
|
+
/** Open a session. `resumeSessionId` is only honoured when the agent advertised loadSession. */
|
|
278
|
+
const openSession = async ({ resumeSessionId = null } = {}) => {
|
|
279
|
+
await handshake()
|
|
280
|
+
if (resumeSessionId !== null) {
|
|
281
|
+
if (!agentCapabilities.loadSession) {
|
|
282
|
+
return { ok: false, stopReason: 'resume-unsupported', reason: 'agent does not advertise loadSession' }
|
|
283
|
+
}
|
|
284
|
+
try {
|
|
285
|
+
const result = await link.request(
|
|
286
|
+
ACP_METHODS.sessionLoad,
|
|
287
|
+
{ sessionId: resumeSessionId, cwd, mcpServers: [] },
|
|
288
|
+
{ timeoutMs: bounds.loadTimeoutMs, owner: link.threadId },
|
|
289
|
+
)
|
|
290
|
+
const loaded = typeof result?.sessionId === 'string' && result.sessionId ? result.sessionId : resumeSessionId
|
|
291
|
+
sessionId = loaded
|
|
292
|
+
// The host routes by sessionId, so the link learns it too.
|
|
293
|
+
link.sessionId = loaded
|
|
294
|
+
sessionConfigResult = result ?? null
|
|
295
|
+
log?.({ event: 'acp.session-loaded', sessionId })
|
|
296
|
+
return { ok: true, sessionId, resumed: true }
|
|
297
|
+
} catch (error) {
|
|
298
|
+
// A refused resume is never papered over with a new session: the caller
|
|
299
|
+
// decides, and the old turn is not replayed into a fresh one.
|
|
300
|
+
log?.({ event: 'acp.session-load-refused', message: error?.message ?? String(error) })
|
|
301
|
+
return { ok: false, stopReason: 'resume-refused', reason: error?.message ?? String(error), error }
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const result = await link.request(
|
|
305
|
+
ACP_METHODS.sessionNew,
|
|
306
|
+
{ cwd, mcpServers: [] },
|
|
307
|
+
{ timeoutMs: bounds.sessionTimeoutMs, owner: link.threadId },
|
|
308
|
+
)
|
|
309
|
+
if (typeof result?.sessionId !== 'string' || !result.sessionId) {
|
|
310
|
+
throw new Error('ACP session/new returned no sessionId')
|
|
311
|
+
}
|
|
312
|
+
sessionId = result.sessionId
|
|
313
|
+
link.sessionId = result.sessionId
|
|
314
|
+
sessionConfigResult = result ?? null
|
|
315
|
+
turns = 0
|
|
316
|
+
log?.({ event: 'acp.session-new', sessionId })
|
|
317
|
+
return { ok: true, sessionId, resumed: false }
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Host-explicit fresh session. Replaying an earlier turn is the caller's decision, never ours. */
|
|
321
|
+
const resetSession = () => {
|
|
322
|
+
sessionId = null
|
|
323
|
+
link.sessionId = null
|
|
324
|
+
sessionConfigResult = null
|
|
325
|
+
turns = 0
|
|
326
|
+
}
|
|
327
|
+
// ---- model / config / mode -------------------------------------------------
|
|
328
|
+
const advertisedConfigOption = configId => {
|
|
329
|
+
const options = Array.isArray(sessionConfigResult?.configOptions) ? sessionConfigResult.configOptions : []
|
|
330
|
+
return options.find(option => option && typeof option === 'object' && option.id === configId && option.type === 'select') ?? null
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const advertisedLegacyModels = () => {
|
|
334
|
+
const models = sessionConfigResult?.models
|
|
335
|
+
return models && Array.isArray(models.availableModels) ? models : null
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Switch the session model. Only an advertised model is ever requested, the
|
|
340
|
+
* switch must be confirmed by the agent, and the Host authorizes the model
|
|
341
|
+
* before anything is sent. No prompt is sent on a refused switch.
|
|
342
|
+
*/
|
|
343
|
+
const selectModel = async modelId => {
|
|
344
|
+
if (typeof modelId !== 'string' || !modelId) throw new TypeError('ACP_MODEL_REQUIRED')
|
|
345
|
+
if (sessionId === null) throw new Error('ACP_NO_SESSION')
|
|
346
|
+
if (typeof assertModelAuthorized === 'function') assertModelAuthorized(modelId)
|
|
347
|
+
const option = advertisedConfigOption('model')
|
|
348
|
+
if (option) {
|
|
349
|
+
const offered = (Array.isArray(option.options) ? option.options : []).map(entry => entry?.value).filter(value => typeof value === 'string')
|
|
350
|
+
if (!offered.includes(modelId)) throw new Error(`ACP_MODEL_NOT_ADVERTISED: ${modelId}`)
|
|
351
|
+
const result = await link.request(
|
|
352
|
+
ACP_METHODS.sessionSetConfigOption,
|
|
353
|
+
{ sessionId, configId: option.id, value: modelId },
|
|
354
|
+
{ timeoutMs: bounds.sessionTimeoutMs },
|
|
355
|
+
)
|
|
356
|
+
const confirmed = Array.isArray(result?.configOptions)
|
|
357
|
+
? result.configOptions.find(entry => entry?.id === option.id)?.currentValue
|
|
358
|
+
: result?.currentValue
|
|
359
|
+
if (confirmed !== undefined && confirmed !== modelId) {
|
|
360
|
+
throw new Error(`ACP_MODEL_NOT_CONFIRMED: asked ${modelId}, agent kept ${String(confirmed)}`)
|
|
361
|
+
}
|
|
362
|
+
sessionConfigResult = result && typeof result === 'object' ? { ...sessionConfigResult, ...result } : sessionConfigResult
|
|
363
|
+
return { modelId, confirmed: confirmed === modelId || confirmed === undefined }
|
|
364
|
+
}
|
|
365
|
+
const legacy = advertisedLegacyModels()
|
|
366
|
+
if (legacy) {
|
|
367
|
+
const offered = legacy.availableModels.map(entry => entry?.modelId).filter(value => typeof value === 'string')
|
|
368
|
+
if (!offered.includes(modelId)) throw new Error(`ACP_MODEL_NOT_ADVERTISED: ${modelId}`)
|
|
369
|
+
await link.request(ACP_METHODS.sessionSetModel, { sessionId, modelId }, { timeoutMs: bounds.sessionTimeoutMs })
|
|
370
|
+
sessionConfigResult = { ...sessionConfigResult, models: { ...legacy, currentModelId: modelId } }
|
|
371
|
+
return { modelId, confirmed: true }
|
|
372
|
+
}
|
|
373
|
+
throw new Error('ACP_MODEL_SELECTION_UNAVAILABLE: agent advertises no model option')
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** Set a session mode only when the agent advertised that exact mode. */
|
|
377
|
+
const setMode = async modeId => {
|
|
378
|
+
if (typeof modeId !== 'string' || !modeId) throw new TypeError('ACP_MODE_REQUIRED')
|
|
379
|
+
if (sessionId === null) throw new Error('ACP_NO_SESSION')
|
|
380
|
+
const modes = Array.isArray(sessionConfigResult?.modes) ? sessionConfigResult.modes : []
|
|
381
|
+
const offered = modes.map(mode => mode?.id).filter(value => typeof value === 'string')
|
|
382
|
+
if (!offered.includes(modeId)) throw new Error(`ACP_MODE_NOT_ADVERTISED: ${modeId}`)
|
|
383
|
+
await link.request(ACP_METHODS.sessionSetMode, { sessionId, modeId }, { timeoutMs: bounds.sessionTimeoutMs })
|
|
384
|
+
return { modeId }
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ---- turns -----------------------------------------------------------------
|
|
388
|
+
/** Ask the agent to stop. This is a request, never a stop confirmation. */
|
|
389
|
+
const cancel = () => {
|
|
390
|
+
if (!current || current.state.settled) return false
|
|
391
|
+
current.state.stopped = true
|
|
392
|
+
cancelRequestedAt = Date.now()
|
|
393
|
+
if (sessionId !== null) link.notify(ACP_METHODS.sessionCancel, { sessionId })
|
|
394
|
+
log?.({ event: 'acp.cancel-requested', sessionId })
|
|
395
|
+
return true
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Run one turn. Text only: a turn payload can never carry a subject,
|
|
400
|
+
* executable or credential. The session/prompt result is the completion; an
|
|
401
|
+
* unknown stop reason is reported, never treated as success.
|
|
402
|
+
*
|
|
403
|
+
* `onDispatched` is called at the moment the request is issued — before any
|
|
404
|
+
* answer exists — so a caller can record dispatch evidence instead of counting
|
|
405
|
+
* completions. Every refusal raised before that point is tagged
|
|
406
|
+
* `acpNotDispatched`, which is the only honest proof that nothing was written
|
|
407
|
+
* to the peer.
|
|
408
|
+
*/
|
|
409
|
+
const prompt = async (input, { onDispatched } = {}) => {
|
|
410
|
+
const notDispatched = error => { throw Object.assign(error, { acpNotDispatched: true }) }
|
|
411
|
+
if (closed) notDispatched(new Error('ACP_SESSION_CLOSED'))
|
|
412
|
+
if (sessionId === null) notDispatched(new Error('ACP_NO_SESSION'))
|
|
413
|
+
if (sessionBusy()) notDispatched(new Error('ACP_SESSION_BUSY'))
|
|
414
|
+
if (turns >= bounds.maxTurns) notDispatched(new Error('ACP_SESSION_TURN_LIMIT'))
|
|
415
|
+
if (!input || typeof input !== 'object' || Array.isArray(input) || Object.keys(input).some(key => key !== 'text')) {
|
|
416
|
+
notDispatched(new TypeError('ACP_TURN_INPUT: only { text } is accepted'))
|
|
417
|
+
}
|
|
418
|
+
const promptText = input.text
|
|
419
|
+
if (typeof promptText !== 'string' || !promptText.trim() || promptText.includes('\u0000') || Buffer.byteLength(promptText) > MAX_PROMPT_BYTES) {
|
|
420
|
+
notDispatched(new TypeError('ACP_TURN_TEXT_INVALID'))
|
|
421
|
+
}
|
|
422
|
+
try { signal?.throwIfAborted() } catch (error) { notDispatched(error) }
|
|
423
|
+
|
|
424
|
+
const state = emptyTurnState()
|
|
425
|
+
const turn = { state, startedAt: Date.now() }
|
|
426
|
+
current = turn
|
|
427
|
+
turns++
|
|
428
|
+
|
|
429
|
+
try {
|
|
430
|
+
state.promptSent = true
|
|
431
|
+
// The request is issued here. This is the dispatch moment, recorded before
|
|
432
|
+
// any result exists.
|
|
433
|
+
if (typeof onDispatched === 'function') onDispatched()
|
|
434
|
+
// The abort race guarantees the turn settles even if the transport ignores
|
|
435
|
+
// the signal: a cancelled turn is never left waiting on the agent.
|
|
436
|
+
let onAbortRace = null
|
|
437
|
+
const abortRace = new Promise((resolve, reject) => {
|
|
438
|
+
onAbortRace = () => {
|
|
439
|
+
state.stopped = true
|
|
440
|
+
reject(Object.assign(new Error('ACP turn aborted'), { acpAborted: true }))
|
|
441
|
+
}
|
|
442
|
+
if (signal) signal.addEventListener('abort', onAbortRace, { once: true })
|
|
443
|
+
if (signal?.aborted) onAbortRace()
|
|
444
|
+
})
|
|
445
|
+
try {
|
|
446
|
+
const result = await Promise.race([
|
|
447
|
+
link.request(
|
|
448
|
+
ACP_METHODS.sessionPrompt,
|
|
449
|
+
{ sessionId, prompt: [{ type: 'text', text: promptText }] },
|
|
450
|
+
{
|
|
451
|
+
timeoutMs: bounds.promptTimeoutMs,
|
|
452
|
+
idleMs: bounds.promptIdleMs,
|
|
453
|
+
signal,
|
|
454
|
+
onReceive: value => {
|
|
455
|
+
// The agent may stream a final update before this continuation runs.
|
|
456
|
+
if (value && typeof value === 'object') turn.result = value
|
|
457
|
+
},
|
|
458
|
+
},
|
|
459
|
+
),
|
|
460
|
+
abortRace,
|
|
461
|
+
])
|
|
462
|
+
return settleTurn(turn, result, null)
|
|
463
|
+
} finally {
|
|
464
|
+
if (onAbortRace) signal?.removeEventListener?.('abort', onAbortRace)
|
|
465
|
+
}
|
|
466
|
+
} catch (error) {
|
|
467
|
+
return settleTurn(turn, turn.result ?? null, error)
|
|
468
|
+
} finally {
|
|
469
|
+
current = null
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const settleTurn = (turn, result, error) => {
|
|
474
|
+
const state = turn.state
|
|
475
|
+
state.settled = true
|
|
476
|
+
const usage = usageEvidence(state)
|
|
477
|
+
const rawUsage = result?.usage ?? result?._meta ?? null
|
|
478
|
+
if (rawUsage && typeof rawUsage === 'object') {
|
|
479
|
+
const input = typeof rawUsage.inputTokens === 'number' ? rawUsage.inputTokens : null
|
|
480
|
+
const output = typeof rawUsage.outputTokens === 'number' ? rawUsage.outputTokens : null
|
|
481
|
+
const cached = typeof rawUsage.cachedReadTokens === 'number' ? rawUsage.cachedReadTokens : null
|
|
482
|
+
usage.inputTokens = input
|
|
483
|
+
usage.outputTokens = output
|
|
484
|
+
usage.cachedInputTokens = cached
|
|
485
|
+
usage.reported = input !== null || output !== null
|
|
486
|
+
usage.status = usage.reported ? 'reported' : 'unknown'
|
|
487
|
+
}
|
|
488
|
+
const stopReason = typeof result?.stopReason === 'string' ? result.stopReason : null
|
|
489
|
+
const outcome = {
|
|
490
|
+
ok: false,
|
|
491
|
+
stopReason,
|
|
492
|
+
outcome: 'failed',
|
|
493
|
+
sessionId,
|
|
494
|
+
text: state.text,
|
|
495
|
+
producedItem: state.producedItem,
|
|
496
|
+
updates: state.updates.length,
|
|
497
|
+
usage,
|
|
498
|
+
// ACP reports no dollar figure: cost stays null/unknown here and flows to
|
|
499
|
+
// the Host ledger as evidence, never as an estimate made up in this adapter.
|
|
500
|
+
costUsd: null,
|
|
501
|
+
costStatus: 'unknown',
|
|
502
|
+
elapsedMs: Date.now() - turn.startedAt,
|
|
503
|
+
cancelRequestedAt,
|
|
504
|
+
errorText: null,
|
|
505
|
+
}
|
|
506
|
+
if (error) {
|
|
507
|
+
const aborted = error?.acpAborted === true || signal?.aborted === true || state.stopped === true
|
|
508
|
+
outcome.outcome = aborted ? 'cancelled' : 'failed'
|
|
509
|
+
outcome.stopReason = aborted ? 'cancelled' : (outcome.stopReason ?? 'rpc-error')
|
|
510
|
+
outcome.errorText = error?.message ?? String(error)
|
|
511
|
+
if (state.text || state.producedItem) outcome.partial = true
|
|
512
|
+
log?.({ event: 'acp.turn-failed', message: outcome.errorText })
|
|
513
|
+
return outcome
|
|
514
|
+
}
|
|
515
|
+
if (state.stopped || stopReason === STOP_REASONS.cancelled) {
|
|
516
|
+
outcome.ok = true
|
|
517
|
+
outcome.outcome = 'cancelled'
|
|
518
|
+
outcome.stopReason = 'cancelled'
|
|
519
|
+
return outcome
|
|
520
|
+
}
|
|
521
|
+
if (stopReason === STOP_REASONS.endTurn) {
|
|
522
|
+
if (!state.repliedText && !state.producedItem) {
|
|
523
|
+
// end_turn with nothing to show is a lost turn, not a success.
|
|
524
|
+
outcome.stopReason = 'empty_turn'
|
|
525
|
+
outcome.errorText = 'ACP agent ended the turn without producing any output'
|
|
526
|
+
return outcome
|
|
527
|
+
}
|
|
528
|
+
outcome.ok = true
|
|
529
|
+
outcome.outcome = 'succeeded'
|
|
530
|
+
return outcome
|
|
531
|
+
}
|
|
532
|
+
// refusal, max_turn_requests or a reason this client does not know: report it.
|
|
533
|
+
outcome.errorText = `ACP agent stopped with ${stopReason ?? 'no stopReason'}`
|
|
534
|
+
if (stopReason === null) outcome.stopReason = 'unknown'
|
|
535
|
+
log?.({ event: 'acp.turn-unfinished', stopReason })
|
|
536
|
+
return outcome
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const close = (reason = 'closed') => {
|
|
540
|
+
if (closed) return
|
|
541
|
+
closed = true
|
|
542
|
+
closingReason = reason
|
|
543
|
+
permissions.close(reason)
|
|
544
|
+
if (current && !current.state.settled) {
|
|
545
|
+
current.state.settled = true
|
|
546
|
+
current = null
|
|
547
|
+
}
|
|
548
|
+
log?.({ event: 'acp.session-closed', reason })
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
signal?.addEventListener('abort', () => close('aborted'), { once: true })
|
|
552
|
+
if (signal?.aborted) close('aborted')
|
|
553
|
+
|
|
554
|
+
return Object.freeze({
|
|
555
|
+
initialize,
|
|
556
|
+
handshake,
|
|
557
|
+
authenticate,
|
|
558
|
+
openSession,
|
|
559
|
+
resetSession,
|
|
560
|
+
prompt,
|
|
561
|
+
cancel,
|
|
562
|
+
selectModel,
|
|
563
|
+
setMode,
|
|
564
|
+
close,
|
|
565
|
+
permissions,
|
|
566
|
+
get sessionId() { return sessionId },
|
|
567
|
+
get agentCapabilities() { return agentCapabilities },
|
|
568
|
+
get busy() { return sessionBusy() },
|
|
569
|
+
get closed() { return closed },
|
|
570
|
+
get closingReason() { return closingReason },
|
|
571
|
+
get turns() { return turns },
|
|
572
|
+
get stats() {
|
|
573
|
+
return Object.freeze({
|
|
574
|
+
turns, sessionId, closed, cancelRequestedAt,
|
|
575
|
+
agent: agentCapabilities ? { name: agentCapabilities.agentName, version: agentCapabilities.agentVersion, loadSession: agentCapabilities.loadSession } : null,
|
|
576
|
+
permissions: permissions.stats,
|
|
577
|
+
limits: { ...bounds },
|
|
578
|
+
})
|
|
579
|
+
},
|
|
580
|
+
})
|
|
581
|
+
}
|