@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,795 @@
|
|
|
1
|
+
// The generic ACP adapter the Host composition can wire.
|
|
2
|
+
//
|
|
3
|
+
// Ported and re-grounded from OpenMausBot server/drivers/acp/core.ts at pin
|
|
4
|
+
// 1bc4749aa8307d7d6998bf86de4ef6478460dd93 (Apache-2.0); see licenses/NOTICE.md.
|
|
5
|
+
// Upstream's ACP driver is embedded in a product server; this module is the
|
|
6
|
+
// same protocol core with every product decision pushed out to trusted Host
|
|
7
|
+
// state: authority, binding, entry, argv, environment, model routes, hooks.
|
|
8
|
+
//
|
|
9
|
+
// What this adapter is NOT: it is not wired into product composition. Being
|
|
10
|
+
// present in the tree does not make any ACP CLI a usable Bot engine, and no CLI
|
|
11
|
+
// is claimed to be verified here.
|
|
12
|
+
//
|
|
13
|
+
// Authority and money boundaries, matching packages/bot-engines:
|
|
14
|
+
// - a turn payload may carry `text` and nothing else;
|
|
15
|
+
// - model, tool, executable, endpoint and credential authority comes from the
|
|
16
|
+
// committed Host authority object, re-asserted every turn;
|
|
17
|
+
// - usage/cost figures the agent does not report stay null/unknown — this
|
|
18
|
+
// adapter keeps no second ledger and never estimates a dollar amount;
|
|
19
|
+
// - permission decisions come only from the Host handler against committed
|
|
20
|
+
// Tool/Task/operation grants;
|
|
21
|
+
// - a cancel request is recorded separately from a confirmed process stop.
|
|
22
|
+
//
|
|
23
|
+
// Turn-fence invariants this module enforces (see packages/bot-acp/README.md):
|
|
24
|
+
// - one turn slot per adapter, reserved synchronously before the first await,
|
|
25
|
+
// so a same-tick second submit is refused with zero extra admission, launch
|
|
26
|
+
// or prompt and no turn can be overwritten;
|
|
27
|
+
// - a turn is settled exactly once, and the durable Host settlement hook runs
|
|
28
|
+
// before the success is published or the slot released;
|
|
29
|
+
// - "nothing was dispatched" (not-run) and "it may have been dispatched"
|
|
30
|
+
// (unknown) are different answers and never merged;
|
|
31
|
+
// - `turnsDispatched` counts dispatches, never completions;
|
|
32
|
+
// - the session and the model identity a turn runs under cannot be changed
|
|
33
|
+
// while that turn is pending: a new binding means a new adapter.
|
|
34
|
+
|
|
35
|
+
import { EventEmitter } from 'node:events'
|
|
36
|
+
import { randomBytes } from 'node:crypto'
|
|
37
|
+
import { mkdir, realpath, stat } from 'node:fs/promises'
|
|
38
|
+
import { isAbsolute, join } from 'node:path'
|
|
39
|
+
|
|
40
|
+
import { ACP_METHODS } from './rpc-codec.mjs'
|
|
41
|
+
import { createChildPipe, createStdioRpc } from './stdio-rpc.mjs'
|
|
42
|
+
import { createAcpHost } from './acp-host.mjs'
|
|
43
|
+
import { createAcpSession } from './session-core.mjs'
|
|
44
|
+
import { createClientMethodGuard } from './permission-policy.mjs'
|
|
45
|
+
import { DEFAULT_LAUNCHER, acpProcessHome, createSupervisorPipe, launchAcpProcess, pinAcpEntry } from './native-launch.mjs'
|
|
46
|
+
|
|
47
|
+
export const DEFAULT_ADAPTER_LIMITS = Object.freeze({
|
|
48
|
+
deadlineMs: 120_000,
|
|
49
|
+
cancelTimeoutMs: 30_000,
|
|
50
|
+
stopGraceMs: 8_000,
|
|
51
|
+
guardIntervalMs: 100,
|
|
52
|
+
ttlMs: 600_000,
|
|
53
|
+
permissionTimeoutMs: 15 * 60_000,
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const identifier = value => typeof value === 'string' && /^[A-Za-z0-9_-]{1,128}$/.test(value)
|
|
57
|
+
const BILLING_KINDS = ['subscription', 'api', 'unknown']
|
|
58
|
+
/** Same prompt bound the session core enforces; decided before a slot is taken. */
|
|
59
|
+
const MAX_TURN_BYTES = 64_000
|
|
60
|
+
/** Transport refusals that provably never wrote a request to the peer. */
|
|
61
|
+
const PROVABLY_UNSENT = ['acpNotDispatched', 'acpInvalidRequest', 'acpPendingLimit', 'acpFrameLimit', 'acpBacklogLimit']
|
|
62
|
+
const provablyUnsent = error => PROVABLY_UNSENT.some(marker => error?.[marker] === true)
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {object} options every value is trusted Host state.
|
|
66
|
+
* @param {{describe: Function, assertModel: Function, assertTool?: Function}} options.authority
|
|
67
|
+
* @param {object} options.subject registered authority subject
|
|
68
|
+
* @param {{taskId: string, attemptId: string, machineId: string, workspaceId: string, location: 'local'|'remote'}} options.binding
|
|
69
|
+
* @param {string} options.operationId durable operation identity
|
|
70
|
+
* @param {{executable: string, sha256: string, version: string, script?: string, scriptSha256?: string}} options.entry pinned native entry
|
|
71
|
+
* @param {string[]} [options.spawnArgs] Host-constructed argv after the executable (e.g. ['acp'])
|
|
72
|
+
* @param {string[]} [options.versionArgs] Host-owned version probe args (default ['--version'])
|
|
73
|
+
* @param {string} options.runtimeRoot isolated absolute runtime root
|
|
74
|
+
* @param {string} options.workspace absolute conversation workspace (agent cwd)
|
|
75
|
+
* @param {{catalogProvider?: string, catalogModel: string}} [options.model]
|
|
76
|
+
* @param {string} [options.billing]
|
|
77
|
+
* @param {number} [options.deadlineMs] bounded lifetime for the whole adapter
|
|
78
|
+
* @param {AbortSignal} [options.signal]
|
|
79
|
+
* @param {{onBeforeRequest?: Function, onObserve?: Function, onSettle?: Function, onClose?: Function}} [options.hooks] the only funding/ledger recipients
|
|
80
|
+
* @param {Function} [options.onPermissionRequest] Host permission callback; absent denies every ask
|
|
81
|
+
* @param {(identity: object) => string|null} [options.operationTool] maps an ask onto a committed tool name
|
|
82
|
+
* @param {{fileSystem?: boolean, terminal?: boolean, roots?: string[], readTextFile?: Function, writeTextFile?: Function}} [options.clientCapabilities]
|
|
83
|
+
* @param {{pickMethod?: Function, onAuthFailure?: 'fail'|'continue'}} [options.auth]
|
|
84
|
+
* @param {string} [options.resumeSessionId] resume only when the agent advertises loadSession
|
|
85
|
+
* @param {{probe?: Function, protect?: Function, supervise?: Function}} [options.launcher] process-launcher seam (tests may substitute)
|
|
86
|
+
*/
|
|
87
|
+
export async function createAcpAdapter(options = {}) {
|
|
88
|
+
const {
|
|
89
|
+
authority, subject, binding, operationId,
|
|
90
|
+
entry, spawnArgs = [], versionArgs = ['--version'],
|
|
91
|
+
runtimeRoot, workspace,
|
|
92
|
+
model = {}, billing = 'subscription',
|
|
93
|
+
deadlineMs = DEFAULT_ADAPTER_LIMITS.deadlineMs,
|
|
94
|
+
signal, hooks = {}, onPermissionRequest, operationTool = null,
|
|
95
|
+
clientCapabilities = {}, auth = {}, resumeSessionId = null, expectedModelId = null,
|
|
96
|
+
launcher = DEFAULT_LAUNCHER, python,
|
|
97
|
+
} = options
|
|
98
|
+
const limits = { ...DEFAULT_ADAPTER_LIMITS, ...(options.limits ?? {}) }
|
|
99
|
+
|
|
100
|
+
if (!authority || typeof authority.describe !== 'function' || typeof authority.assertModel !== 'function') throw new TypeError('ACP_AUTHORITY_REQUIRED')
|
|
101
|
+
if (!subject || typeof subject !== 'object') throw new TypeError('ACP_SUBJECT_REQUIRED')
|
|
102
|
+
if (!identifier(operationId)) throw new TypeError('INVALID_ACP_OPERATION')
|
|
103
|
+
if (!isAbsolute(runtimeRoot ?? '') || !isAbsolute(workspace ?? '')) throw new TypeError('INVALID_ACP_PATHS')
|
|
104
|
+
const catalogProvider = model.catalogProvider ?? 'unknown'
|
|
105
|
+
const catalogModel = model.catalogModel
|
|
106
|
+
if (typeof catalogProvider !== 'string' || !catalogProvider || typeof catalogModel !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,99}$/.test(catalogModel)) throw new TypeError('INVALID_ACP_MODEL')
|
|
107
|
+
if (!BILLING_KINDS.includes(billing)) throw new TypeError('INVALID_ACP_BILLING')
|
|
108
|
+
if (!Number.isSafeInteger(deadlineMs) || deadlineMs < 1000 || deadlineMs > 7_200_000) throw new TypeError('INVALID_ACP_DEADLINE')
|
|
109
|
+
for (const key of ['onBeforeRequest', 'onObserve', 'onSettle', 'onClose']) {
|
|
110
|
+
if (hooks[key] !== undefined && typeof hooks[key] !== 'function') throw new TypeError(`ACP_HOOK_${key.toUpperCase()}_REQUIRED_FUNCTION`)
|
|
111
|
+
}
|
|
112
|
+
if (onPermissionRequest !== undefined && typeof onPermissionRequest !== 'function') throw new TypeError('ACP_PERMISSION_CALLBACK_REQUIRED_FUNCTION')
|
|
113
|
+
if (!Array.isArray(spawnArgs) || spawnArgs.some(arg => typeof arg !== 'string')) throw new TypeError('INVALID_ACP_SPAWN_ARGS')
|
|
114
|
+
|
|
115
|
+
const events = new EventEmitter()
|
|
116
|
+
const log = detail => events.emit('log', detail)
|
|
117
|
+
signal?.throwIfAborted()
|
|
118
|
+
|
|
119
|
+
const cwd = await realpath(workspace)
|
|
120
|
+
if (!(await stat(cwd)).isDirectory()) throw new Error('INVALID_ACP_WORKSPACE')
|
|
121
|
+
await mkdir(runtimeRoot, { recursive: true, mode: 0o700 })
|
|
122
|
+
const root = await realpath(runtimeRoot)
|
|
123
|
+
const home = join(root, `acp-${operationId}`)
|
|
124
|
+
await mkdir(home, { recursive: true, mode: 0o700 })
|
|
125
|
+
for (const folder of ['tmp', 'cache', 'config', 'data', 'appdata', 'local']) await mkdir(join(home, folder), { recursive: true, mode: 0o700 })
|
|
126
|
+
const probe = await launcher.probe({ home, python })
|
|
127
|
+
if (!probe?.available) throw new Error(`NATIVE_PEER_UNAVAILABLE: ${probe?.reason ?? 'unknown'}`)
|
|
128
|
+
await launcher.protect(probe.helper, home)
|
|
129
|
+
const pinned = await pinAcpEntry(entry, home, { versionArgs })
|
|
130
|
+
const deadlineEnd = Date.now() + deadlineMs
|
|
131
|
+
const { botId, revision } = authority.describe(subject)
|
|
132
|
+
|
|
133
|
+
let rpc = null, host = null, session = null, pipe = null
|
|
134
|
+
let currentProcess = null, stopPromise = null, stopped = false, fatalError = null
|
|
135
|
+
let stopRequestedAt = null, launches = 0, turnsDispatched = 0, turnsAdmitted = 0, turnsSettled = 0, resumeRefused = false
|
|
136
|
+
let sessionId = null, reportedModel = null, modelMismatch = false
|
|
137
|
+
// One generation per launch: a retired process never answers for the turn
|
|
138
|
+
// that replaced it, and a stop never leaves a late spawn behind.
|
|
139
|
+
let generation = 0, launchPromise = null, retiredSpawn = null
|
|
140
|
+
let guardTimer = null, deadlineTimer = null, cancelTimer = null
|
|
141
|
+
let currentTurn = null
|
|
142
|
+
let stopEvidenceValue = null
|
|
143
|
+
|
|
144
|
+
const clientGuard = createClientMethodGuard({
|
|
145
|
+
fileSystem: clientCapabilities.fileSystem === true,
|
|
146
|
+
terminal: clientCapabilities.terminal === true,
|
|
147
|
+
roots: Array.isArray(clientCapabilities.roots) ? clientCapabilities.roots : [],
|
|
148
|
+
readTextFile: clientCapabilities.readTextFile ?? null,
|
|
149
|
+
writeTextFile: clientCapabilities.writeTextFile ?? null,
|
|
150
|
+
log,
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
const assertModelAuthorized = modelId => {
|
|
154
|
+
if (typeof modelId !== 'string' || !modelId) throw new TypeError('ACP_MODEL_REQUIRED')
|
|
155
|
+
// A model switch is a Host decision on committed state, never a turn input.
|
|
156
|
+
authority.assertModel(subject, catalogProvider, modelId)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const fatal = (error, reason) => {
|
|
160
|
+
if (stopped) return
|
|
161
|
+
fatalError = error
|
|
162
|
+
void stop(reason ?? 'adapter-fatal')
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ---- turn settlement ------------------------------------------------------
|
|
166
|
+
// Settlement is two steps in a fixed order: the durable Host settlement hook
|
|
167
|
+
// first, and only then the published answer and the released slot. A slow hook
|
|
168
|
+
// keeps the turn observably pending (`busy === true`); a hook that refuses
|
|
169
|
+
// refuses the success — the caller receives the original native result with
|
|
170
|
+
// its unknown funding evidence, never a fabricated ok — and the adapter
|
|
171
|
+
// retires rather than let a new request run on a turn the ledger never
|
|
172
|
+
// recorded. Nothing here is caught and swallowed into a success.
|
|
173
|
+
let settleTail = Promise.resolve()
|
|
174
|
+
let settleInFlight = 0
|
|
175
|
+
let settleDrain = null
|
|
176
|
+
let settleDrainRelease = null
|
|
177
|
+
|
|
178
|
+
/** Resolves once no settlement is in flight. `stop` waits on this, never on a
|
|
179
|
+
* settlement that is itself waiting for the stop to finish. */
|
|
180
|
+
const settlementIdle = () => {
|
|
181
|
+
if (settleInFlight === 0) return Promise.resolve()
|
|
182
|
+
settleDrain ??= new Promise(resolve => { settleDrainRelease = resolve })
|
|
183
|
+
return settleDrain
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const releaseSettlement = () => {
|
|
187
|
+
settleInFlight--
|
|
188
|
+
if (settleInFlight === 0 && settleDrainRelease) {
|
|
189
|
+
const release = settleDrainRelease
|
|
190
|
+
settleDrainRelease = null
|
|
191
|
+
settleDrain = null
|
|
192
|
+
release()
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const recordSettlement = async (turn, evidence) => {
|
|
197
|
+
let hookError = null
|
|
198
|
+
try {
|
|
199
|
+
await hooks.onSettle?.(evidence)
|
|
200
|
+
} catch (error) { hookError = error }
|
|
201
|
+
// The durable record is in: the slot is released and the answer published.
|
|
202
|
+
if (currentTurn === turn) currentTurn = null
|
|
203
|
+
turn.settlement = evidence
|
|
204
|
+
if (hookError) {
|
|
205
|
+
fatalError = hookError
|
|
206
|
+
turn.reject(Object.assign(
|
|
207
|
+
new Error(`ACP_SETTLEMENT_FAILED: ${hookError?.message ?? String(hookError)}`),
|
|
208
|
+
{ evidence, cause: hookError, acpSettlementFailed: true },
|
|
209
|
+
))
|
|
210
|
+
events.emit('turn-settlement-failed', { turnId: turn.turnId, error: String(hookError?.message ?? hookError), evidence })
|
|
211
|
+
return 'settle-hook-failed'
|
|
212
|
+
}
|
|
213
|
+
events.emit('turn', evidence)
|
|
214
|
+
turn.resolve(evidence)
|
|
215
|
+
return null
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const settleTurn = (native, hostCause) => {
|
|
219
|
+
const turn = currentTurn
|
|
220
|
+
if (!turn || turn.settled) return
|
|
221
|
+
turn.settled = true
|
|
222
|
+
clearTimeout(turn.timer)
|
|
223
|
+
if (cancelTimer) { clearTimeout(cancelTimer); cancelTimer = null }
|
|
224
|
+
turnsSettled++
|
|
225
|
+
let ok = native.ok
|
|
226
|
+
let stopReason = native.stopReason
|
|
227
|
+
let outcome = native.outcome
|
|
228
|
+
if (hostCause) { ok = false; stopReason = hostCause.stopReason; outcome = hostCause.outcome }
|
|
229
|
+
const evidence = {
|
|
230
|
+
schemaVersion: 1, source: 'acp', driver: 'acp', botId, ...binding, operationId,
|
|
231
|
+
turnId: turn.turnId, ok, outcome, stopReason,
|
|
232
|
+
// Whether the prompt really reached the agent: a completion count is
|
|
233
|
+
// never used as dispatch evidence.
|
|
234
|
+
dispatched: turn.dispatched === true,
|
|
235
|
+
sessionId: sessionId ?? native.sessionId ?? null,
|
|
236
|
+
model: { catalogProvider, catalogModel, reportedModel, verification: modelMismatch ? 'mismatch' : reportedModel ? 'agent-reported' : 'unreported' },
|
|
237
|
+
usage: native.usage ?? null,
|
|
238
|
+
usageStatus: native.usage?.reported === true ? 'reported' : 'unknown',
|
|
239
|
+
// ACP reports no dollar figure. Nothing is estimated here; the Host ledger
|
|
240
|
+
// receives this evidence and remains the only place money is recorded.
|
|
241
|
+
costUsd: null, costStatus: 'unknown',
|
|
242
|
+
billing,
|
|
243
|
+
elapsedMs: native.elapsedMs ?? null,
|
|
244
|
+
cancelRequestedAt: native.cancelRequestedAt ?? null,
|
|
245
|
+
updates: native.updates ?? null,
|
|
246
|
+
text: typeof native.text === 'string' ? native.text.slice(0, 64_000) : null,
|
|
247
|
+
errorText: native.errorText ?? null,
|
|
248
|
+
entry: { path: pinned.path, sha256: pinned.sha256, version: pinned.version, script: pinned.script, scriptSha256: pinned.scriptSha256 },
|
|
249
|
+
}
|
|
250
|
+
settleInFlight++
|
|
251
|
+
const run = settleTail.then(() => recordSettlement(turn, evidence))
|
|
252
|
+
// The chain itself never rejects: one refused settlement must not cancel the
|
|
253
|
+
// settlements queued behind it.
|
|
254
|
+
settleTail = run.then(() => undefined, () => undefined)
|
|
255
|
+
void run.then(retireReason => {
|
|
256
|
+
releaseSettlement()
|
|
257
|
+
if (retireReason) void stop(retireReason)
|
|
258
|
+
}, error => {
|
|
259
|
+
releaseSettlement()
|
|
260
|
+
// A settlement that crashed still ends the turn: the caller is told, with
|
|
261
|
+
// the evidence that was built, instead of being left waiting on it.
|
|
262
|
+
turn.reject(Object.assign(new Error(`ACP_SETTLEMENT_CRASHED: ${error?.message ?? String(error)}`), { evidence }))
|
|
263
|
+
events.emit('turn-settlement-failed', { turnId: turn.turnId, error: String(error?.message ?? error), evidence })
|
|
264
|
+
void stop('settlement-crashed')
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const assertDispatchAuthorization = () => {
|
|
269
|
+
signal?.throwIfAborted()
|
|
270
|
+
// Every turn re-reads the Host's current authorization for this model.
|
|
271
|
+
authority.assertModel(subject, catalogProvider, catalogModel)
|
|
272
|
+
if (authority.describe(subject).revision !== revision) throw new Error('ACP_GRANT_CHANGED')
|
|
273
|
+
if (stopped) throw new Error(fatalError ? `ACP_STOPPED: ${fatalError.message}` : 'ACP_STOPPED')
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ---- process --------------------------------------------------------------
|
|
277
|
+
const launch = async () => {
|
|
278
|
+
if (stopped) throw new Error(fatalError ? `ACP_STOPPED: ${fatalError.message}` : 'ACP_STOPPED')
|
|
279
|
+
const remaining = deadlineEnd - Date.now()
|
|
280
|
+
if (remaining <= 1000) throw new Error('ACP_DEADLINE')
|
|
281
|
+
const own = ++generation
|
|
282
|
+
let started = null
|
|
283
|
+
try {
|
|
284
|
+
started = await launchAcpProcess({
|
|
285
|
+
entry: pinned, args: spawnArgs, cwd, home, ttlMs: Math.min(remaining, limits.ttlMs), launcher, python, probe,
|
|
286
|
+
})
|
|
287
|
+
} catch (error) {
|
|
288
|
+
if (stopped) throw new Error(`ACP_STOPPED: ${fatalError?.message ?? 'stopped during launch'}`)
|
|
289
|
+
throw error
|
|
290
|
+
}
|
|
291
|
+
const { supervisor, rootProcess } = started
|
|
292
|
+
if (stopped) {
|
|
293
|
+
// A stop raced this launch: the process must not outlive the stop, and the
|
|
294
|
+
// stop evidence has to name it instead of claiming there was no process.
|
|
295
|
+
const retired = { supervisor, rootProcess, dead: false, stopEvidence: null, exitInfo: null }
|
|
296
|
+
const onStopped = value => {
|
|
297
|
+
retired.stopEvidence = value
|
|
298
|
+
retired.dead = true
|
|
299
|
+
retired.exitInfo = { exitCode: value.exitCode, reason: value.reason }
|
|
300
|
+
}
|
|
301
|
+
supervisor.events.on('control', onStopped)
|
|
302
|
+
supervisor.events.once('closed', () => { retired.dead = true })
|
|
303
|
+
retiredSpawn = retired
|
|
304
|
+
supervisor.end()
|
|
305
|
+
await supervisor.exit.catch(() => {})
|
|
306
|
+
throw new Error(`ACP_STOPPED: ${fatalError?.message ?? 'stopped during launch'}`)
|
|
307
|
+
}
|
|
308
|
+
launches++
|
|
309
|
+
const state = { supervisor, rootProcess, dead: false, stopEvidence: null, exitInfo: null }
|
|
310
|
+
const relay = createSupervisorPipe(supervisor)
|
|
311
|
+
// One link owns this process, so any traffic is this session's traffic; a
|
|
312
|
+
// message naming another session is attributed to nobody.
|
|
313
|
+
const connection = createStdioRpc({
|
|
314
|
+
pipe: relay,
|
|
315
|
+
signal,
|
|
316
|
+
route: classified => {
|
|
317
|
+
const named = classified?.params?.sessionId
|
|
318
|
+
if (typeof named === 'string') return named === sessionInstance.sessionId ? operationId : null
|
|
319
|
+
return operationId
|
|
320
|
+
},
|
|
321
|
+
log,
|
|
322
|
+
})
|
|
323
|
+
const hostLink = createAcpHost({ rpc: connection, key: null, source: 'acp', onUnrouted: refuseUnrouted, log })
|
|
324
|
+
const acpLink = hostLink.open(operationId, cwd)
|
|
325
|
+
const sessionInstance = createAcpSession({
|
|
326
|
+
host: hostLink,
|
|
327
|
+
link: acpLink,
|
|
328
|
+
cwd,
|
|
329
|
+
clientInfo: { name: 'dsh-model-router', version: pinned.version },
|
|
330
|
+
capabilities: { fileSystem: clientGuard.capabilities.fileSystem, terminal: clientGuard.capabilities.terminal, meta: clientCapabilities.meta },
|
|
331
|
+
auth,
|
|
332
|
+
onUpdate: entry => {
|
|
333
|
+
if (entry.kind === 'agent_message_chunk' && typeof entry.text === 'string') {
|
|
334
|
+
events.emit('delta', { streamKind: 'assistant_text', delta: entry.text })
|
|
335
|
+
} else if (entry.kind === 'agent_thought_chunk' && typeof entry.text === 'string') {
|
|
336
|
+
events.emit('delta', { streamKind: 'reasoning_text', delta: entry.text })
|
|
337
|
+
}
|
|
338
|
+
events.emit('update', entry)
|
|
339
|
+
},
|
|
340
|
+
onPermissionRequest: ask => {
|
|
341
|
+
events.emit('permission-ask', ask)
|
|
342
|
+
if (!onPermissionRequest) return { behavior: 'deny' }
|
|
343
|
+
return Promise.resolve(onPermissionRequest(ask)).then(decision => {
|
|
344
|
+
if (!decision || typeof decision !== 'object') throw new TypeError('ACP_PERMISSION_DECISION_INVALID')
|
|
345
|
+
if (decision.behavior !== 'allow' && decision.behavior !== 'deny') throw new TypeError('ACP_PERMISSION_BEHAVIOR_INVALID')
|
|
346
|
+
return decision
|
|
347
|
+
})
|
|
348
|
+
},
|
|
349
|
+
onServerRequest: classified => {
|
|
350
|
+
if (classified.method === ACP_METHODS.fsReadTextFile || classified.method === ACP_METHODS.fsWriteTextFile || classified.method.startsWith('terminal/')) {
|
|
351
|
+
return clientGuard.handle(classified)
|
|
352
|
+
}
|
|
353
|
+
return null
|
|
354
|
+
},
|
|
355
|
+
authority, subject, operationTool,
|
|
356
|
+
assertModelAuthorized,
|
|
357
|
+
limits: {
|
|
358
|
+
initTimeoutMs: limits.initTimeoutMs,
|
|
359
|
+
sessionTimeoutMs: limits.sessionTimeoutMs,
|
|
360
|
+
loadTimeoutMs: limits.loadTimeoutMs,
|
|
361
|
+
promptTimeoutMs: limits.promptTimeoutMs,
|
|
362
|
+
promptIdleMs: limits.promptIdleMs,
|
|
363
|
+
maxTurns: limits.maxTurns,
|
|
364
|
+
permissionTimeoutMs: limits.permissionTimeoutMs,
|
|
365
|
+
},
|
|
366
|
+
signal,
|
|
367
|
+
log,
|
|
368
|
+
})
|
|
369
|
+
const onControl = value => {
|
|
370
|
+
if (value.control === 'spawned') state.rootProcess = value
|
|
371
|
+
if (value.control === 'stopped') { state.stopEvidence = value; markDead(value) }
|
|
372
|
+
}
|
|
373
|
+
const markDead = stoppedControl => {
|
|
374
|
+
if (state.dead) return
|
|
375
|
+
state.dead = true
|
|
376
|
+
state.exitInfo = stoppedControl ? { exitCode: stoppedControl.exitCode, reason: stoppedControl.reason } : null
|
|
377
|
+
supervisor.events.off('control', onControl)
|
|
378
|
+
supervisor.events.off('message', onMessage)
|
|
379
|
+
const live = currentProcess === state
|
|
380
|
+
if (live) currentProcess = null
|
|
381
|
+
if (sessionInstance && !sessionInstance.closed) sessionInstance.close('agent-exit')
|
|
382
|
+
// Only the live generation settles a turn: a late exit, error or response
|
|
383
|
+
// from a process the adapter already retired never answers for the turn
|
|
384
|
+
// that replaced it.
|
|
385
|
+
if (live && !stopped && currentTurn && !currentTurn.settled && currentTurn.generation === own) {
|
|
386
|
+
if (resumeRefused) settleTurn({ ok: false, stopReason: 'resume-refused', outcome: 'unknown', usage: null, elapsedMs: null, cancelRequestedAt: null }, { stopReason: 'resume-refused', outcome: 'unknown' })
|
|
387
|
+
else settleTurn({ ok: false, stopReason: 'exit-before-result', outcome: 'unknown', usage: null, elapsedMs: null, cancelRequestedAt: null }, { stopReason: 'exit-before-result', outcome: 'unknown' })
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
const onMessage = frame => {
|
|
391
|
+
// A retired process reports nothing: its late traffic is not evidence
|
|
392
|
+
// about the session that replaced it.
|
|
393
|
+
if (currentProcess !== state) return
|
|
394
|
+
// Session-level model confirmation: the agent names the model it runs.
|
|
395
|
+
let reported = null
|
|
396
|
+
if (frame?.method === ACP_METHODS.sessionUpdate) {
|
|
397
|
+
const update = frame.params?.update ?? {}
|
|
398
|
+
if (update.sessionUpdate === 'current_mode_update' && typeof update.currentModeId === 'string') reported = update.currentModeId
|
|
399
|
+
}
|
|
400
|
+
if (frame?.id !== undefined && frame.method === undefined) {
|
|
401
|
+
const params = frame.params ?? {}
|
|
402
|
+
if (typeof params.modelId === 'string') reported = params.modelId
|
|
403
|
+
if (typeof params.currentModelId === 'string') reported = params.currentModelId
|
|
404
|
+
}
|
|
405
|
+
if (typeof reported === 'string' && reported) {
|
|
406
|
+
reportedModel = reported
|
|
407
|
+
// Only a Host-declared expectation can make a report a mismatch.
|
|
408
|
+
if (typeof expectedModelId === 'string' && expectedModelId && reported !== expectedModelId) modelMismatch = true
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
supervisor.events.on('control', onControl)
|
|
412
|
+
supervisor.events.on('message', onMessage)
|
|
413
|
+
supervisor.events.once('failure', error => { if (!state.dead) fatal(error, 'native-failure'); markDead(null) })
|
|
414
|
+
supervisor.events.once('closed', () => markDead(null))
|
|
415
|
+
rpc = connection
|
|
416
|
+
host = hostLink
|
|
417
|
+
session = sessionInstance
|
|
418
|
+
pipe = relay
|
|
419
|
+
currentProcess = state
|
|
420
|
+
return state
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** Fail-closed answer for a request no session holds: never silence. */
|
|
424
|
+
const refuseUnrouted = (msg, send) => {
|
|
425
|
+
if (typeof msg?.id === 'number' || typeof msg?.id === 'string') {
|
|
426
|
+
send({ jsonrpc: '2.0', id: msg.id, error: { code: -32601, message: 'method not found' } })
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const launchOnce = () => {
|
|
431
|
+
if (!launchPromise) {
|
|
432
|
+
launchPromise = launch()
|
|
433
|
+
// A finished launch clears the slot: the next session starts a new one.
|
|
434
|
+
launchPromise.then(() => { launchPromise = null }, () => { launchPromise = null })
|
|
435
|
+
}
|
|
436
|
+
return launchPromise
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const ensureSession = async () => {
|
|
440
|
+
if (!session || session.closed) await launchOnce()
|
|
441
|
+
return session
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ---- stop -----------------------------------------------------------------
|
|
445
|
+
/**
|
|
446
|
+
* Real process stop plus evidence. A pending turn is settled first — before
|
|
447
|
+
* the tree is touched — with the honest split between "nothing was dispatched"
|
|
448
|
+
* (not-run) and "it may have been dispatched" (unknown). The native tree is
|
|
449
|
+
* then actually stopped, and only afterwards is a slow settlement hook waited
|
|
450
|
+
* on, so `onClose` never reports a stop whose turn the ledger has not recorded.
|
|
451
|
+
* A launch that raced this stop is awaited first: no late spawn outlives it.
|
|
452
|
+
*/
|
|
453
|
+
const stop = (reason = 'host-stop') => {
|
|
454
|
+
if (stopPromise) return stopPromise
|
|
455
|
+
stopRequestedAt = Date.now()
|
|
456
|
+
stopped = true
|
|
457
|
+
if (guardTimer) clearInterval(guardTimer)
|
|
458
|
+
if (deadlineTimer) clearTimeout(deadlineTimer)
|
|
459
|
+
if (cancelTimer) { clearTimeout(cancelTimer); cancelTimer = null }
|
|
460
|
+
if (currentTurn && !currentTurn.settled) {
|
|
461
|
+
const turn = currentTurn
|
|
462
|
+
const cancelled = reason === 'interrupt' || reason === 'cancel-timeout' || signal?.aborted === true
|
|
463
|
+
const outcome = turn.dispatched ? (cancelled ? 'cancelled' : 'unknown') : 'not-run'
|
|
464
|
+
settleTurn({ ok: false, stopReason: reason, outcome, usage: null, elapsedMs: Date.now() - turn.startedAt, cancelRequestedAt: turn.cancelRequestedAt }, { stopReason: reason, outcome })
|
|
465
|
+
}
|
|
466
|
+
stopPromise = (async () => {
|
|
467
|
+
if (launchPromise) { try { await launchPromise } catch {} }
|
|
468
|
+
// A spawn that raced this stop is reported as the process the stop ended.
|
|
469
|
+
const state = currentProcess ?? retiredSpawn
|
|
470
|
+
retiredSpawn = null
|
|
471
|
+
currentProcess = null
|
|
472
|
+
let forcedKill = false
|
|
473
|
+
if (state && !state.dead) {
|
|
474
|
+
state.supervisor.end()
|
|
475
|
+
const exited = await Promise.race([
|
|
476
|
+
state.supervisor.exit.then(() => true),
|
|
477
|
+
new Promise(resolve => { const timer = setTimeout(() => resolve(false), limits.stopGraceMs); timer.unref() }),
|
|
478
|
+
])
|
|
479
|
+
if (!exited) {
|
|
480
|
+
forcedKill = true
|
|
481
|
+
state.supervisor.child.kill()
|
|
482
|
+
await state.supervisor.exit
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
const native = state?.stopEvidence ?? null
|
|
486
|
+
session?.close(reason)
|
|
487
|
+
host?.close(reason)
|
|
488
|
+
// The tree is really stopped; now the durable turn records are waited for.
|
|
489
|
+
await settlementIdle()
|
|
490
|
+
const stopEvidence = {
|
|
491
|
+
stopRequestedAt, stoppedAt: Date.now(), reason,
|
|
492
|
+
pid: state?.rootProcess?.pid ?? native?.pid ?? null,
|
|
493
|
+
exitCode: native?.exitCode ?? state?.exitInfo?.exitCode ?? null,
|
|
494
|
+
nativeStopReason: native?.reason ?? state?.exitInfo?.reason ?? (state ? 'no-stopped-control' : 'no-process'),
|
|
495
|
+
containment: native?.containment ?? probe.supervisor ?? null,
|
|
496
|
+
// Only the launcher's own accounting (Windows job object) confirms a tree stop.
|
|
497
|
+
treeStopped: native?.treeStopped === true,
|
|
498
|
+
status: native?.treeStopped === true ? 'confirmed' : state ? 'unconfirmed' : 'no-process',
|
|
499
|
+
forcedKill,
|
|
500
|
+
// A cancel request was sent; this record is the confirmation, kept apart from it.
|
|
501
|
+
stopRequestedIsNotConfirmation: true,
|
|
502
|
+
cancelRequestedAt: session?.stats?.cancelRequestedAt ?? null,
|
|
503
|
+
turns: { admitted: turnsAdmitted, dispatched: turnsDispatched, settled: turnsSettled },
|
|
504
|
+
}
|
|
505
|
+
await hooks.onClose?.(stopEvidence)
|
|
506
|
+
stopEvidenceValue = stopEvidence
|
|
507
|
+
return stopEvidence
|
|
508
|
+
})()
|
|
509
|
+
return stopPromise
|
|
510
|
+
}
|
|
511
|
+
// ---- guards ---------------------------------------------------------------
|
|
512
|
+
guardTimer = setInterval(() => {
|
|
513
|
+
try {
|
|
514
|
+
if (stopped) return
|
|
515
|
+
authority.assertModel(subject, catalogProvider, catalogModel)
|
|
516
|
+
if (authority.describe(subject).revision !== revision) throw new Error('ACP_GRANT_CHANGED')
|
|
517
|
+
} catch (error) {
|
|
518
|
+
if (currentTurn && !currentTurn.settled) {
|
|
519
|
+
settleTurn({ ok: false, stopReason: 'grant-revoked', outcome: 'unknown', usage: null, elapsedMs: null, cancelRequestedAt: null }, { stopReason: 'grant-revoked', outcome: 'unknown' })
|
|
520
|
+
}
|
|
521
|
+
fatal(error instanceof Error ? error : new Error(String(error)), 'grant-revoked')
|
|
522
|
+
}
|
|
523
|
+
}, limits.guardIntervalMs)
|
|
524
|
+
guardTimer.unref()
|
|
525
|
+
deadlineTimer = setTimeout(() => fatal(new Error('ACP_DEADLINE'), 'deadline'), deadlineMs)
|
|
526
|
+
deadlineTimer.unref()
|
|
527
|
+
const onAbort = () => fatal(new Error('ACP_CANCELLED'), 'cancelled')
|
|
528
|
+
signal?.addEventListener('abort', onAbort, { once: true })
|
|
529
|
+
if (signal?.aborted) onAbort()
|
|
530
|
+
|
|
531
|
+
// ---- public surface -------------------------------------------------------
|
|
532
|
+
/**
|
|
533
|
+
* Start (or resume) the agent session. Resume is only attempted when the
|
|
534
|
+
* agent advertises loadSession; a refusal is reported and never replayed —
|
|
535
|
+
* the Host must call startFresh() for a new conversation on purpose.
|
|
536
|
+
* A pending turn fences this: the session a turn runs under is not replaced
|
|
537
|
+
* underneath it.
|
|
538
|
+
*/
|
|
539
|
+
const assertNoPendingTurn = action => {
|
|
540
|
+
if (currentTurn) throw new Error(`ACP_TURN_IN_PROGRESS: ${action} would change the session or model identity while a turn is pending`)
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const beginSession = async ({ fresh = false } = {}) => {
|
|
544
|
+
if (stopped) throw new Error(fatalError ? `ACP_STOPPED: ${fatalError.message}` : 'ACP_STOPPED')
|
|
545
|
+
if (resumeRefused && !fresh) {
|
|
546
|
+
throw new Error('ACP_RESUME_REFUSED: the agent refused the resume; call startFresh() for a new conversation, never a replay')
|
|
547
|
+
}
|
|
548
|
+
const live = await ensureSession()
|
|
549
|
+
const opened = await live.openSession({ resumeSessionId: fresh ? null : resumeSessionId })
|
|
550
|
+
if (!opened.ok) {
|
|
551
|
+
resumeRefused = true
|
|
552
|
+
sessionId = null
|
|
553
|
+
events.emit('session', { sessionId: null, resumed: false, refused: true, reason: opened.reason, stopReason: opened.stopReason })
|
|
554
|
+
return opened
|
|
555
|
+
}
|
|
556
|
+
resumeRefused = false
|
|
557
|
+
sessionId = opened.sessionId
|
|
558
|
+
events.emit('session', { sessionId: sessionId, resumed: opened.resumed === true, refused: false })
|
|
559
|
+
// Diagnostic only: this reports that a session exists. It is never a
|
|
560
|
+
// settlement and never carries money — settlement is `onSettle` alone.
|
|
561
|
+
await hooks.onObserve?.({ kind: 'session-confirmed', sessionId, model: catalogModel, resumed: opened.resumed === true, agent: live.agentCapabilities?.agentName ?? null })
|
|
562
|
+
return opened
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const start = async ({ fresh = false } = {}) => {
|
|
566
|
+
assertNoPendingTurn('start')
|
|
567
|
+
return beginSession({ fresh })
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/** A new conversation, explicitly: never a silent substitute for a refused resume. */
|
|
571
|
+
const startFresh = async () => {
|
|
572
|
+
assertNoPendingTurn('startFresh')
|
|
573
|
+
return beginSession({ fresh: true })
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Submit one conversation turn. The payload may carry `text` and nothing else.
|
|
578
|
+
*
|
|
579
|
+
* The turn slot is reserved synchronously, before the first await, so a second
|
|
580
|
+
* call in the same tick is refused with `ACP_TURN_IN_PROGRESS` and performs no
|
|
581
|
+
* admission, no launch and no prompt. Everything that follows the reservation
|
|
582
|
+
* — admission, session start, dispatch, the peer's result and the Host
|
|
583
|
+
* settlement — belongs to that one slot, and the Host grant is re-judged after
|
|
584
|
+
* every asynchronous boundary.
|
|
585
|
+
*/
|
|
586
|
+
const submitTurn = async input => {
|
|
587
|
+
if (stopped) throw new Error(fatalError ? `ACP_STOPPED: ${fatalError.message}` : 'ACP_STOPPED')
|
|
588
|
+
// The payload shape is decided before anything is reserved: a malformed turn
|
|
589
|
+
// never occupies the slot.
|
|
590
|
+
if (!input || typeof input !== 'object' || Array.isArray(input) || Object.keys(input).some(key => key !== 'text')) {
|
|
591
|
+
throw new TypeError('INVALID_ACP_TURN_INPUT: only { text } is accepted')
|
|
592
|
+
}
|
|
593
|
+
const promptText = input.text
|
|
594
|
+
if (typeof promptText !== 'string' || !promptText.trim() || promptText.includes('\u0000') || Buffer.byteLength(promptText) > MAX_TURN_BYTES) {
|
|
595
|
+
// Same code the session core uses, so a caller sees one stable error
|
|
596
|
+
// whichever layer catches the malformed text first.
|
|
597
|
+
throw new TypeError('ACP_TURN_TEXT_INVALID')
|
|
598
|
+
}
|
|
599
|
+
if (currentTurn) throw new Error('ACP_TURN_IN_PROGRESS')
|
|
600
|
+
const turnId = `t-${randomBytes(8).toString('hex')}`
|
|
601
|
+
let resolveTurn, rejectTurn
|
|
602
|
+
const promise = new Promise((resolve, reject) => { resolveTurn = resolve; rejectTurn = reject })
|
|
603
|
+
const timer = setTimeout(() => {
|
|
604
|
+
settleTurn({ ok: false, stopReason: 'turn-timeout', outcome: 'unknown', usage: null, elapsedMs: null, cancelRequestedAt: null }, { stopReason: 'turn-timeout', outcome: 'unknown' })
|
|
605
|
+
fatal(new Error('ACP_TURN_TIMEOUT'), 'turn-timeout')
|
|
606
|
+
}, Math.max(1000, deadlineEnd - Date.now()))
|
|
607
|
+
timer.unref()
|
|
608
|
+
const turn = {
|
|
609
|
+
turnId, settled: false, dispatched: false, generation: 0,
|
|
610
|
+
resolve: resolveTurn, reject: rejectTurn, timer, startedAt: Date.now(), cancelRequestedAt: null,
|
|
611
|
+
}
|
|
612
|
+
currentTurn = turn
|
|
613
|
+
promise.catch(() => {})
|
|
614
|
+
events.emit('turn-started', { turnId, sessionId })
|
|
615
|
+
// A turn that was settled underneath us (cancel, stop, revocation, peer exit)
|
|
616
|
+
// is never resumed: no late continuation dispatches a new prompt for it.
|
|
617
|
+
const assertTurnLive = () => {
|
|
618
|
+
if (turn.settled) {
|
|
619
|
+
throw new Error(`ACP_TURN_NOT_DISPATCHED: the turn was settled as ${turn.settlement?.stopReason ?? 'settled'} before dispatch`)
|
|
620
|
+
}
|
|
621
|
+
assertDispatchAuthorization()
|
|
622
|
+
}
|
|
623
|
+
try {
|
|
624
|
+
// Host admission first: a refusal here proves nothing was dispatched.
|
|
625
|
+
await hooks.onBeforeRequest?.({ turnId, botId, ...binding, operationId, source: 'acp', driver: 'acp', requestedModel: catalogModel, entry: { path: pinned.path, sha256: pinned.sha256, version: pinned.version } })
|
|
626
|
+
} catch (error) {
|
|
627
|
+
// The Host refused this request. The slot is released so the caller may
|
|
628
|
+
// call again, and the Host's own error is reported. Nothing was dispatched,
|
|
629
|
+
// so there is no settlement to record for it.
|
|
630
|
+
clearTimeout(turn.timer)
|
|
631
|
+
if (currentTurn === turn) currentTurn = null
|
|
632
|
+
events.emit('turn-refused', { turnId, reason: error?.message ?? String(error) })
|
|
633
|
+
throw error
|
|
634
|
+
}
|
|
635
|
+
turnsAdmitted++
|
|
636
|
+
try {
|
|
637
|
+
// The grant is re-judged across the admission boundary.
|
|
638
|
+
assertTurnLive()
|
|
639
|
+
// The session this turn runs under. Starting one may launch a new process,
|
|
640
|
+
// so the live session is read back afterwards.
|
|
641
|
+
if (!session || session.closed || session.sessionId === null) await beginSession({ fresh: false })
|
|
642
|
+
const live = session
|
|
643
|
+
if (!live || typeof live.prompt !== 'function') {
|
|
644
|
+
throw Object.assign(new Error('ACP_SESSION_UNAVAILABLE'), { acpNotDispatched: true })
|
|
645
|
+
}
|
|
646
|
+
assertTurnLive()
|
|
647
|
+
const result = await live.prompt(input, {
|
|
648
|
+
// Marked at the moment the request is issued, never at its completion.
|
|
649
|
+
onDispatched: () => { turn.dispatched = true; turn.generation = generation; turnsDispatched++ },
|
|
650
|
+
})
|
|
651
|
+
if (turn.settled) return promise
|
|
652
|
+
// A transport failure on a prompt that was issued is unknown: the frame may
|
|
653
|
+
// have reached the agent, so it is never reported as not-run and never
|
|
654
|
+
// replayed automatically.
|
|
655
|
+
if (result?.ok === false && result.outcome === 'failed' && result.stopReason === 'rpc-error') {
|
|
656
|
+
settleTurn({ ...result, stopReason: 'dispatch-unknown', outcome: 'unknown' }, { stopReason: 'dispatch-unknown', outcome: 'unknown' })
|
|
657
|
+
} else {
|
|
658
|
+
settleTurn(result)
|
|
659
|
+
}
|
|
660
|
+
} catch (error) {
|
|
661
|
+
if (currentTurn !== turn || turn.settled) throw error
|
|
662
|
+
// One terminal settlement. The dispatch marker decides: a prompt that was
|
|
663
|
+
// never issued is not-run, a prompt whose write provably never left is
|
|
664
|
+
// not-run too, and anything else is unknown — never replayed.
|
|
665
|
+
const notRun = turn.dispatched !== true || provablyUnsent(error)
|
|
666
|
+
const stopReason = notRun ? 'not-dispatched' : 'dispatch-unknown'
|
|
667
|
+
const outcome = notRun ? 'not-run' : 'unknown'
|
|
668
|
+
settleTurn({
|
|
669
|
+
ok: false, stopReason, outcome, usage: null,
|
|
670
|
+
elapsedMs: Date.now() - turn.startedAt, cancelRequestedAt: turn.cancelRequestedAt,
|
|
671
|
+
errorText: error?.message ?? String(error),
|
|
672
|
+
}, { stopReason, outcome })
|
|
673
|
+
throw error
|
|
674
|
+
}
|
|
675
|
+
return promise
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Ask the agent to stop the running turn. This records a cancel request; the
|
|
680
|
+
* turn settles on the agent's own answer, and an unanswered cancel ends in a
|
|
681
|
+
* real process stop whose evidence is reported separately.
|
|
682
|
+
*
|
|
683
|
+
* It works at every stage of a turn: while admission or a launch is still
|
|
684
|
+
* pending there is no prompt to cancel, so the reservation itself is settled
|
|
685
|
+
* as not-run — no prompt is ever sent for a cancelled turn.
|
|
686
|
+
*/
|
|
687
|
+
const cancel = () => {
|
|
688
|
+
const turn = currentTurn
|
|
689
|
+
if (!turn || turn.settled) return false
|
|
690
|
+
if (!turn.dispatched) {
|
|
691
|
+
turn.cancelRequestedAt = Date.now()
|
|
692
|
+
settleTurn({
|
|
693
|
+
ok: false, stopReason: 'cancelled-before-dispatch', outcome: 'not-run', usage: null,
|
|
694
|
+
elapsedMs: Date.now() - turn.startedAt, cancelRequestedAt: turn.cancelRequestedAt,
|
|
695
|
+
}, { stopReason: 'cancelled-before-dispatch', outcome: 'not-run' })
|
|
696
|
+
return true
|
|
697
|
+
}
|
|
698
|
+
if (!session || !session.busy) return false
|
|
699
|
+
const requested = session.cancel()
|
|
700
|
+
if (!requested) return false
|
|
701
|
+
turn.cancelRequestedAt = session.stats.cancelRequestedAt
|
|
702
|
+
events.emit('cancel-requested', { sessionId, at: session.stats.cancelRequestedAt })
|
|
703
|
+
if (cancelTimer) clearTimeout(cancelTimer)
|
|
704
|
+
cancelTimer = setTimeout(() => { void stop('cancel-timeout') }, limits.cancelTimeoutMs)
|
|
705
|
+
cancelTimer.unref()
|
|
706
|
+
return true
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Switch the session model. The model a turn runs under is the Host-committed
|
|
711
|
+
* `catalogModel`: a peer advertising another model is not authority, and
|
|
712
|
+
* selecting one anyway would make the turn evidence and the funded grant
|
|
713
|
+
* disagree. A different binding needs a rebuilt, re-authorized adapter — never
|
|
714
|
+
* a silent live switch, and never owner or permission data smuggled in through
|
|
715
|
+
* request JSON.
|
|
716
|
+
*/
|
|
717
|
+
const selectModel = async modelId => {
|
|
718
|
+
assertNoPendingTurn('selectModel')
|
|
719
|
+
if (typeof modelId !== 'string' || modelId !== catalogModel) {
|
|
720
|
+
throw new Error(`ACP_MODEL_BINDING_CHANGED: this adapter is committed to ${catalogModel}; a different model binding requires a rebuilt, re-authorized adapter`)
|
|
721
|
+
}
|
|
722
|
+
const live = await ensureSession()
|
|
723
|
+
// Re-judged across the launch boundary.
|
|
724
|
+
assertNoPendingTurn('selectModel')
|
|
725
|
+
assertModelAuthorized(modelId)
|
|
726
|
+
const selected = await live.selectModel(modelId)
|
|
727
|
+
reportedModel = modelId
|
|
728
|
+
events.emit('model-selected', { modelId, sessionId: live.sessionId })
|
|
729
|
+
return selected
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const setMode = async modeId => {
|
|
733
|
+
assertNoPendingTurn('setMode')
|
|
734
|
+
const live = await ensureSession()
|
|
735
|
+
// Re-judged across the launch boundary.
|
|
736
|
+
assertNoPendingTurn('setMode')
|
|
737
|
+
return live.setMode(modeId)
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const snapshot = () => Object.freeze({
|
|
741
|
+
schemaVersion: 1, source: 'acp', driver: 'acp', botId, ...binding, operationId,
|
|
742
|
+
entry: { path: pinned.path, sha256: pinned.sha256, version: pinned.version, versionOutput: pinned.versionOutput, script: pinned.script, scriptSha256: pinned.scriptSha256 },
|
|
743
|
+
spawnArgs: [...spawnArgs],
|
|
744
|
+
model: { catalogProvider, catalogModel, reportedModel, verification: modelMismatch ? 'mismatch' : reportedModel ? 'agent-reported' : 'unreported', mismatch: modelMismatch },
|
|
745
|
+
identity: { botId, operationId, taskId: binding.taskId, attemptId: binding.attemptId },
|
|
746
|
+
sessionId,
|
|
747
|
+
resumeRefused,
|
|
748
|
+
turnsAdmitted, turnsDispatched, turnsSettled, launches,
|
|
749
|
+
turn: currentTurn ? {
|
|
750
|
+
turnId: currentTurn.turnId, dispatched: currentTurn.dispatched, settled: currentTurn.settled,
|
|
751
|
+
cancelRequestedAt: currentTurn.cancelRequestedAt, elapsedMs: Date.now() - currentTurn.startedAt,
|
|
752
|
+
} : null,
|
|
753
|
+
settlement: { inFlight: settleInFlight },
|
|
754
|
+
generation,
|
|
755
|
+
session: session ? session.stats : null,
|
|
756
|
+
process: currentProcess ? { pid: currentProcess.rootProcess?.pid ?? null, dead: currentProcess.dead } : null,
|
|
757
|
+
permissions: session ? session.permissions.stats : null,
|
|
758
|
+
transport: { rpc: rpc?.stats ?? null, pipe: pipe?.stats ?? null },
|
|
759
|
+
clientCapabilities: clientGuard.capabilities,
|
|
760
|
+
billing: { kind: billing, amountUsd: null, amountStatus: 'unknown', note: 'ACP reports no dollar figure; cost evidence flows only to the Host ledger and is never estimated here' },
|
|
761
|
+
stopRequestedAt,
|
|
762
|
+
stopEvidence: stopEvidenceValue,
|
|
763
|
+
fatalError: fatalError ? String(fatalError.message) : null,
|
|
764
|
+
closed: stopped,
|
|
765
|
+
limitations: [
|
|
766
|
+
'not wired into product composition: presence in the tree is not product availability',
|
|
767
|
+
'no ACP CLI is verified end-to-end here; entry pinning is proven against offline fixtures only',
|
|
768
|
+
'session-scoped "allow always" approvals are deliberately not implemented: every ask is decided by the Host',
|
|
769
|
+
'ACP reports token/context usage and no dollar amount, so cost stays null/unknown',
|
|
770
|
+
'a live model switch away from the committed catalogModel is refused: a different binding needs a rebuilt, re-authorized adapter',
|
|
771
|
+
'this adapter proves no OS sandbox for a generic ACP CLI: the boundary here is the session/turn fence, not process isolation',
|
|
772
|
+
],
|
|
773
|
+
})
|
|
774
|
+
|
|
775
|
+
return Object.freeze({
|
|
776
|
+
driver: 'acp',
|
|
777
|
+
events,
|
|
778
|
+
start,
|
|
779
|
+
startFresh,
|
|
780
|
+
submitTurn,
|
|
781
|
+
cancel,
|
|
782
|
+
selectModel,
|
|
783
|
+
setMode,
|
|
784
|
+
stop,
|
|
785
|
+
close: stop,
|
|
786
|
+
snapshot,
|
|
787
|
+
get sessionId() { return sessionId },
|
|
788
|
+
get busy() { return currentTurn !== null },
|
|
789
|
+
get closed() { return stopped },
|
|
790
|
+
get agentCapabilities() { return session?.agentCapabilities ?? null },
|
|
791
|
+
get permissions() { return session?.permissions ?? null },
|
|
792
|
+
})
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
export { acpProcessHome, createChildPipe, createStdioRpc, createAcpHost, createAcpSession, createSupervisorPipe, pinAcpEntry, launchAcpProcess, DEFAULT_LAUNCHER }
|