@ljwei-stak/dsh-model-route-bot 0.66.4 → 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 +18 -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/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,120 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto'
|
|
2
|
+
import { mkdirSync } from 'node:fs'
|
|
3
|
+
import { dirname } from 'node:path'
|
|
4
|
+
import { TEAM_CODES } from './team-tool-contract.mjs'
|
|
5
|
+
|
|
6
|
+
const fail = code => { throw Object.assign(new Error(code), { code }) }
|
|
7
|
+
const hash = value => createHash('sha256').update(value).digest('hex')
|
|
8
|
+
const alive = pid => { try { process.kill(pid, 0); return true } catch (error) { return error.code !== 'ESRCH' } }
|
|
9
|
+
const terminal = row => ['settled', 'cancelled', 'unknown'].includes(row.state)
|
|
10
|
+
export const computeBriefHash = (mode, message, doneWhen, consult) => hash(JSON.stringify([mode, message, doneWhen ?? null, consult === true]))
|
|
11
|
+
export const standingThreadKey = (parent, target, origin = '') => `standing-${hash(JSON.stringify([parent, target, origin])).slice(0, 40)}`
|
|
12
|
+
export const workThreadConversationKey = (target, thread) => hash(JSON.stringify([target, thread])).slice(0, 48)
|
|
13
|
+
export const assertNoCycle = (lineage, target) => { if (lineage.includes(target)) fail(TEAM_CODES.CYCLE) }
|
|
14
|
+
// Upstream's depth is the number of delegation edges, not the root Bot itself.
|
|
15
|
+
export const assertDepth = (lineage, maxDepth) => { if (!Number.isSafeInteger(maxDepth) || maxDepth < 1) fail('TEAM_DEPTH_LIMIT_INVALID'); if (lineage.length > maxDepth) fail(TEAM_CODES.TOO_DEEP) }
|
|
16
|
+
|
|
17
|
+
/** Durable outbox, with one live Host owner. State writes never prove execution.
|
|
18
|
+
* The exact target request receipt is the only way to settle a dispatched row.
|
|
19
|
+
* A new schema leaves the original, unreleased Step journal intact for inspection.
|
|
20
|
+
*/
|
|
21
|
+
export function createTeamAssignmentStore({ Database, file, now = Date.now, maxRows = 65536, isProcessAlive = alive, ownerId = randomUUID() } = {}) {
|
|
22
|
+
if (typeof Database !== 'function' || typeof file !== 'string' || !file) fail('TEAM_STORE_HOST_REQUIRED')
|
|
23
|
+
if (!Number.isSafeInteger(maxRows) || maxRows < 1) fail('TEAM_STORE_INVALID_LIMIT')
|
|
24
|
+
if (file !== ':memory:') mkdirSync(dirname(file), { recursive: true })
|
|
25
|
+
const db = new Database(file)
|
|
26
|
+
let closed = false
|
|
27
|
+
try {
|
|
28
|
+
db.pragma('busy_timeout=5000'); db.pragma('synchronous=FULL')
|
|
29
|
+
if (file !== ':memory:' && db.pragma('journal_mode=WAL', { simple: true }) !== 'wal') fail('TEAM_STORE_WAL_REQUIRED')
|
|
30
|
+
db.exec(`CREATE TABLE IF NOT EXISTS bot_team_owner_v2 (slot INTEGER PRIMARY KEY CHECK(slot=1), token TEXT, pid INTEGER, active INTEGER NOT NULL);
|
|
31
|
+
CREATE TABLE IF NOT EXISTS bot_team_rows_v2 (id TEXT PRIMARY KEY, parent TEXT NOT NULL, origin TEXT NOT NULL, state TEXT NOT NULL, created INTEGER NOT NULL, data TEXT NOT NULL);
|
|
32
|
+
CREATE INDEX IF NOT EXISTS bot_team_rows_parent_v2 ON bot_team_rows_v2(parent,origin,created);
|
|
33
|
+
CREATE TABLE IF NOT EXISTS bot_team_batches_v2 (parent TEXT NOT NULL, origin TEXT NOT NULL, request TEXT NOT NULL, hash TEXT NOT NULL, ids TEXT NOT NULL, PRIMARY KEY(parent,origin,request));`)
|
|
34
|
+
if (db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='bot_team_assignments'").get() && db.prepare('SELECT count(*) n FROM bot_team_assignments').get().n) fail('TEAM_LEGACY_JOURNAL_RECONCILIATION_REQUIRED')
|
|
35
|
+
db.transaction(() => {
|
|
36
|
+
const owner = db.prepare('SELECT * FROM bot_team_owner_v2 WHERE slot=1').get()
|
|
37
|
+
if (owner?.active && isProcessAlive(owner.pid)) fail('TEAM_STORE_ALREADY_OWNED')
|
|
38
|
+
db.prepare('INSERT OR REPLACE INTO bot_team_owner_v2(slot,token,pid,active) VALUES(1,?,?,1)').run(ownerId, process.pid)
|
|
39
|
+
}).immediate()
|
|
40
|
+
} catch (error) { db.close(); throw error }
|
|
41
|
+
const assertOpen = () => {
|
|
42
|
+
if (closed) fail('TEAM_STORE_CLOSED')
|
|
43
|
+
if (db.prepare('SELECT token FROM bot_team_owner_v2 WHERE slot=1 AND active=1').get()?.token !== ownerId) fail('TEAM_STORE_OWNER_LOST')
|
|
44
|
+
}
|
|
45
|
+
const read = id => { const row = db.prepare('SELECT data FROM bot_team_rows_v2 WHERE id=?').get(id); return row ? JSON.parse(row.data) : null }
|
|
46
|
+
const all = () => { assertOpen(); return db.prepare('SELECT data FROM bot_team_rows_v2 ORDER BY created,id').all().map(row => JSON.parse(row.data)) }
|
|
47
|
+
const save = row => db.prepare('INSERT OR REPLACE INTO bot_team_rows_v2(id,parent,origin,state,created,data) VALUES(?,?,?,?,?,?)').run(row.assignmentId, row.parentBot, row.originKey, row.state, row.createdAt, JSON.stringify(row))
|
|
48
|
+
const mutate = (id, predicate, change) => {
|
|
49
|
+
assertOpen()
|
|
50
|
+
return db.transaction(() => { assertOpen(); const row = read(id); if (!row || !predicate(row)) return null; change(row); row.updatedAt = now(); save(row); return row }).immediate()
|
|
51
|
+
}
|
|
52
|
+
const createBatch = (specs, { parentBot = specs[0]?.parentBot, originKey = specs[0]?.originKey ?? '', requestKey, payloadHash } = {}) => {
|
|
53
|
+
assertOpen()
|
|
54
|
+
if (!Array.isArray(specs) || !specs.length || specs.length > 4 || !requestKey) fail(TEAM_CODES.INVALID_ARGS)
|
|
55
|
+
const identity = payloadHash ?? hash(JSON.stringify(specs.map(({ workThreadKey, ...spec }) => ({ ...spec, thread: spec.requestedThread ?? workThreadKey }))))
|
|
56
|
+
return db.transaction(() => {
|
|
57
|
+
assertOpen()
|
|
58
|
+
const batch = db.prepare('SELECT * FROM bot_team_batches_v2 WHERE parent=? AND origin=? AND request=?').get(parentBot, originKey, requestKey)
|
|
59
|
+
if (batch) return batch.hash === identity ? { status: 'replayed', assignments: JSON.parse(batch.ids).map(read) } : { status: 'conflict' }
|
|
60
|
+
if (db.prepare('SELECT count(*) n FROM bot_team_rows_v2').get().n + specs.length > maxRows) fail('TEAM_STORE_ROW_LIMIT')
|
|
61
|
+
const rows = all()
|
|
62
|
+
const decisions = specs.map(spec => {
|
|
63
|
+
const match = rows.slice().reverse().find(row => row.parentBot === parentBot && row.originKey === originKey && row.targetBot === spec.targetBot && row.workThreadKey === spec.workThreadKey && row.mode === spec.mode && (spec.rework || row.briefHash === spec.briefHash && (row.parentAssignmentId ?? row.createdByTurn) === (spec.parentAssignmentId ?? spec.createdByTurn)))
|
|
64
|
+
if (match && spec.rework && !terminal(match) && !match.outcome) fail(TEAM_CODES.REWORK_NOT_TERMINAL)
|
|
65
|
+
if (match && !spec.rework) return { row: match, duplicate: true }
|
|
66
|
+
const timestamp = now()
|
|
67
|
+
const assignmentId = `assign_${randomUUID()}`
|
|
68
|
+
return { row: { ...spec, requestKey, originKey, assignmentId, rootAssignmentId: spec.rootAssignmentId ?? assignmentId, reworkOf: match?.assignmentId ?? null, state: 'queued', outcome: null, resultText: null, resultCode: null, targetRef: null, targetRequestId: null, resumeState: spec.reply === 'none' ? 'none' : 'waiting', cancelRequestedAt: null, createdAt: timestamp, updatedAt: timestamp } }
|
|
69
|
+
})
|
|
70
|
+
// All decisions, including rework validity, are complete before the first insert.
|
|
71
|
+
for (const { row, duplicate } of decisions) if (!duplicate) save(row)
|
|
72
|
+
db.prepare('INSERT INTO bot_team_batches_v2(parent,origin,request,hash,ids) VALUES(?,?,?,?,?)').run(parentBot, originKey, requestKey, identity, JSON.stringify(decisions.map(({ row }) => row.assignmentId)))
|
|
73
|
+
return { status: 'created', assignments: decisions.map(({ row, duplicate }) => ({ ...row, ...(duplicate ? { duplicate: true } : {}) })) }
|
|
74
|
+
}).immediate()
|
|
75
|
+
}
|
|
76
|
+
const create = spec => { const result = createBatch([spec], { requestKey: spec.requestKey }); return { status: result.status, assignment: result.assignments?.[0] } }
|
|
77
|
+
return Object.freeze({
|
|
78
|
+
ready: true, create, createBatch,
|
|
79
|
+
get(id) { assertOpen(); return read(id) },
|
|
80
|
+
byRequestKey(parent, request, origin = '') { assertOpen(); const batch = db.prepare('SELECT ids FROM bot_team_batches_v2 WHERE parent=? AND origin=? AND request=?').get(parent, origin, request); return batch ? read(JSON.parse(batch.ids)[0]) : null },
|
|
81
|
+
listByParent: (parent, { includeTerminal = false, originKey } = {}) => all().filter(row => row.parentBot === parent && (originKey === undefined || row.originKey === originKey) && (includeTerminal || !terminal(row))),
|
|
82
|
+
threadsByParent(parent, originKey) { const seen = new Map(); for (const row of all().reverse().filter(row => row.parentBot === parent && (originKey === undefined || row.originKey === originKey))) if (!seen.has(row.workThreadKey)) seen.set(row.workThreadKey, row); return [...seen.values()] },
|
|
83
|
+
activeFor(botId, origin) {
|
|
84
|
+
const rows = all(), matches = ref => origin.sessionId ? ref?.sessionId === origin.sessionId : ref?.conversationId === origin.conversationId
|
|
85
|
+
const running = rows.find(row => row.state === 'running' && row.targetBot === botId && matches(row.targetRef))
|
|
86
|
+
if (running) return running
|
|
87
|
+
const uncertain = rows.find(row => row.targetActive === true && row.targetBot === botId && matches(row.targetRef))
|
|
88
|
+
if (uncertain) return uncertain
|
|
89
|
+
// A child result resumes its caller's old work request, even if that
|
|
90
|
+
// request already settled. This frame is durable across awaits and is
|
|
91
|
+
// independent of delivery state: a stop must not erase the execution gate.
|
|
92
|
+
const resume = rows.find(row => row.parentBot === botId && row.resumeActive === true && matches(row.origin))
|
|
93
|
+
if (!resume) return null
|
|
94
|
+
const parent = resume.parentAssignmentId ? read(resume.parentAssignmentId) : null
|
|
95
|
+
return { ...(parent ?? { targetBot: botId, parentBot: botId, lineage: [botId], rootBot: botId, depth: 0, mode: 'work', origin: resume.origin }),
|
|
96
|
+
resumeAssignmentId: resume.assignmentId, cancelRequestedAt: resume.cancelRequestedAt ?? parent?.cancelRequestedAt ?? null }
|
|
97
|
+
},
|
|
98
|
+
dispatchable: () => all().filter(row => row.state === 'queued'),
|
|
99
|
+
rowsForReconcile: () => all().filter(row => row.state === 'running'),
|
|
100
|
+
resumeDue: () => all().filter(row => row.state === 'waiting' && row.resumeState === 'waiting' && row.cancelRequestedAt === null),
|
|
101
|
+
all,
|
|
102
|
+
admit: id => mutate(id, row => row.state === 'queued', row => { row.state = 'admitting' }),
|
|
103
|
+
beginDispatch: (id, ref, requestId) => mutate(id, row => row.state === 'admitting' && !row.cancelRequestedAt, row => { row.state = 'running'; row.targetRef = ref; row.targetRequestId = requestId; row.targetActive = true }),
|
|
104
|
+
settle: (id, { outcome, resultText = null, resultCode = null }) => mutate(id, row => row.state === 'running', row => { row.state = row.reply === 'none' || row.cancelRequestedAt !== null ? 'settled' : 'waiting'; row.outcome = outcome; row.resultText = resultText; row.resultCode = resultCode; row.targetActive = false; if (row.cancelRequestedAt !== null) row.resumeState = 'withheld' }),
|
|
105
|
+
markResumeSending: id => Boolean(mutate(id, row => row.state === 'waiting' && row.resumeState === 'waiting' && row.cancelRequestedAt === null, row => { row.resumeState = 'sending'; row.resumeActive = true })),
|
|
106
|
+
completeResume: (id, state, { executionFinished = true } = {}) => mutate(id, row => row.state === 'waiting' && row.resumeActive === true, row => {
|
|
107
|
+
row.state = 'settled'; row.resumeState = row.cancelRequestedAt !== null ? 'withheld' : state
|
|
108
|
+
row.resumeActive = !executionFinished
|
|
109
|
+
if (!executionFinished) row.cancelRequestedAt ??= now()
|
|
110
|
+
}),
|
|
111
|
+
rewarmResume: id => Boolean(mutate(id, row => row.state === 'waiting' && row.resumeState === 'sending' && row.cancelRequestedAt === null, row => { row.resumeState = 'waiting'; row.resumeActive = false })),
|
|
112
|
+
rewarm: id => mutate(id, row => row.state === 'running' && row.cancelRequestedAt === null, row => { row.state = 'queued'; row.targetRequestId = null; row.targetRef = null; row.targetActive = false }),
|
|
113
|
+
cancel: (id, { resultCode = null } = {}) => mutate(id, row => ['queued', 'admitting'].includes(row.state), row => { row.state = 'cancelled'; row.outcome = 'cancelled'; row.resultCode = resultCode; row.resumeState = 'withheld' }),
|
|
114
|
+
requestCancel: id => mutate(id, row => !terminal(row), row => { row.cancelRequestedAt ??= now(); if (['queued', 'admitting'].includes(row.state)) { row.state = 'cancelled'; row.outcome = 'cancelled' }; row.resumeState = 'withheld' }),
|
|
115
|
+
fenceUnknown: (id, code, { executionFinished = false } = {}) => mutate(id, row => ['admitting', 'running'].includes(row.state), row => { row.state = 'unknown'; row.outcome = 'unknown'; row.resultCode = code ?? TEAM_CODES.OUTCOME_UNKNOWN; row.resumeState = 'withheld'; row.targetActive = row.targetActive === true && !executionFinished; if (row.targetActive) row.cancelRequestedAt ??= now() }),
|
|
116
|
+
recover() { let fenced = 0; for (const row of all()) { if (row.state === 'admitting') { this.fenceUnknown(row.assignmentId, 'TEAM_ADMISSION_INTERRUPTED'); fenced++ } else if (row.resumeActive === true) { mutate(row.assignmentId, () => true, current => { current.resumeActive = false; if (current.state === 'waiting') { current.state = 'settled'; current.resumeState = current.cancelRequestedAt !== null ? 'withheld' : 'uncertain' } }); fenced++ } }; return { fenced } },
|
|
117
|
+
close() { if (closed) return; try { db.prepare('UPDATE bot_team_owner_v2 SET active=0 WHERE slot=1 AND token=?').run(ownerId) } finally { closed = true; db.close() } },
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
export { STATE_UNKNOWN as TEAM_STATE_UNKNOWN } from './team-tool-contract.mjs'
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto'
|
|
2
|
+
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
3
|
+
import { TEAM_CODES, parseTeamToolArgs, projectCoordinateArgs } from './team-tool-contract.mjs'
|
|
4
|
+
import { createTeamAssignmentStore, computeBriefHash, standingThreadKey, workThreadConversationKey, assertNoCycle, assertDepth } from './team-assignment-store.mjs'
|
|
5
|
+
import { createTeamScheduler } from './team-scheduler.mjs'
|
|
6
|
+
|
|
7
|
+
const fail = (code, extra = {}) => { throw Object.assign(new Error(code), { code, ...extra }) }
|
|
8
|
+
const digest = value => createHash('sha256').update(JSON.stringify(value)).digest('hex')
|
|
9
|
+
const textOf = content => Array.isArray(content) ? content.filter(block => block.type === 'text').map(block => block.text ?? '').join('\n') : ''
|
|
10
|
+
const composeBrief = row => `[Team assignment from ${row.parentBot}; mode ${row.mode}]\n${row.doneWhen ? `Done when: ${row.doneWhen}\n` : ''}${row.brief}`
|
|
11
|
+
const composeResult = row => `[Team result from ${row.targetBot}; assignment ${row.assignmentId}; ${row.outcome}${row.resultCode ? ` (${row.resultCode})` : ''}]\n${row.resultText || '(no result text)'}`
|
|
12
|
+
const unknown = code => ({ outcome: 'unknown', code })
|
|
13
|
+
|
|
14
|
+
/** Fold installed SDK events, requiring the exact identified user message in one
|
|
15
|
+
* balanced turn. A later assignment, an injected extra user, or an unterminated
|
|
16
|
+
* request cannot supply this request's answer.
|
|
17
|
+
*/
|
|
18
|
+
export function teamReceiptFromEvents(events, requestId, role = 'assign-task') {
|
|
19
|
+
let turn = null, active = false, mixed = false, text = '', result = null, matches = 0
|
|
20
|
+
for (const event of events) {
|
|
21
|
+
if (event.type === 'turn/start') { turn = event.data?.turn; active = false; mixed = false; text = '' }
|
|
22
|
+
if (event.type === 'user/message') {
|
|
23
|
+
const source = event.data?.source
|
|
24
|
+
if (source?.kind === 'bot' && source.role === role && source.exchangeId === requestId) { matches++; active = true }
|
|
25
|
+
else if (active && !['snapshot', 'recall'].includes(source?.form)) mixed = true
|
|
26
|
+
}
|
|
27
|
+
if (event.type === 'assistant/message' && active) {
|
|
28
|
+
const content = event.data?.message?.content
|
|
29
|
+
if (!content?.some(block => block.type === 'tool-call')) text = textOf(content)
|
|
30
|
+
}
|
|
31
|
+
if (event.type === 'turn/end' && active && event.data?.turn === turn) {
|
|
32
|
+
const reason = event.data.reason
|
|
33
|
+
result = mixed ? unknown('TEAM_TARGET_MIXED_INPUT') : reason?.kind === 'completed' ? { outcome: 'succeeded', text } : reason?.kind === 'aborted' ? { outcome: 'cancelled', code: 'TEAM_TARGET_CANCELLED' } : { outcome: 'failed', code: 'TEAM_TARGET_FAILED' }
|
|
34
|
+
active = false
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return matches === 1 && result ? result : unknown(matches > 1 ? 'TEAM_TARGET_DUPLICATE_REQUEST' : 'TEAM_TARGET_NOT_TERMINAL')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createIntegratedTeamBridge({ rt, directory, authority, lifecycle, flush, threadHost = () => null, cancelWaitMs = 10000 } = {}) {
|
|
41
|
+
if (!rt || !directory || !authority || !lifecycle || typeof flush !== 'function') throw new TypeError('TEAM_BRIDGE_DEPENDENCIES_REQUIRED')
|
|
42
|
+
if (!Number.isSafeInteger(cancelWaitMs) || cancelWaitMs < 10 || cancelWaitMs > 60000) throw new TypeError('TEAM_CANCEL_WAIT_INVALID')
|
|
43
|
+
const subjectBot = agent => authority.describe(directory.forAgent(agent)).botId
|
|
44
|
+
const eventsFor = async (sessionId, signal) => {
|
|
45
|
+
if (typeof rt.eventsBackwards !== 'function') fail('TEAM_TARGET_HISTORY_UNAVAILABLE')
|
|
46
|
+
const events = []
|
|
47
|
+
for await (const event of rt.eventsBackwards(sessionId, signal)) { events.push(event); if (events.length > 16384) fail('TEAM_TARGET_HISTORY_LIMIT') }
|
|
48
|
+
return events.reverse()
|
|
49
|
+
}
|
|
50
|
+
const owns = (botId, sessionId) => {
|
|
51
|
+
const bot = lifecycle.committedBot(botId)
|
|
52
|
+
if (!bot) return false
|
|
53
|
+
if ((bot.sessionId ?? bot.id) === sessionId) return true
|
|
54
|
+
if (bot.threads?.some(thread => thread.sessionId === sessionId && !thread.archivedAt) !== true) return false
|
|
55
|
+
const host = threadHost()
|
|
56
|
+
if (host) {
|
|
57
|
+
try {
|
|
58
|
+
const found = host.service.threadBySession(sessionId)
|
|
59
|
+
const gate = found?.botId === botId ? host.service.probeSendable({ botId, threadId: found.threadId }) : null
|
|
60
|
+
return gate?.ok === true && gate.sessionId === sessionId
|
|
61
|
+
} catch { return false }
|
|
62
|
+
}
|
|
63
|
+
return true
|
|
64
|
+
}
|
|
65
|
+
const readTarget = async row => {
|
|
66
|
+
if (!row.targetRef?.sessionId || !row.requestId && !row.targetRequestId || !owns(row.targetBot, row.targetRef.sessionId)) return unknown('TEAM_TARGET_REFERENCE_UNCONFIRMED')
|
|
67
|
+
try { return teamReceiptFromEvents(await eventsFor(row.targetRef.sessionId, row.signal), row.requestId ?? row.targetRequestId) }
|
|
68
|
+
catch { return unknown('TEAM_TARGET_HISTORY_UNCONFIRMED') }
|
|
69
|
+
}
|
|
70
|
+
const resolve = async (botId, sessionId) => {
|
|
71
|
+
await rt.load(); await rt.ensureCapabilities?.(sessionId)
|
|
72
|
+
if (!owns(botId, sessionId)) fail('TEAM_SESSION_RETIRED', { started: false, retired: true })
|
|
73
|
+
const agent = await rt.resolveAgent(sessionId)
|
|
74
|
+
if (subjectBot(agent) !== botId) fail('TEAM_TARGET_SUBJECT_MISMATCH', { started: false })
|
|
75
|
+
if (typeof agent.whenIdle !== 'function') fail('TEAM_AGENT_API_UNAVAILABLE', { started: false })
|
|
76
|
+
return agent
|
|
77
|
+
}
|
|
78
|
+
const submit = async (row, sessionId, role, requestId, text) => {
|
|
79
|
+
// Followup is the actual public SDK admission. No owner RPC, fake Agent or
|
|
80
|
+
// arbitrary two-second completion timer. whenIdle is followed by a flush and
|
|
81
|
+
// exact request scan; quiescence alone is not a receipt.
|
|
82
|
+
let off = () => {}, agent, submitted = false, finished = false, receipt = null, failure = null
|
|
83
|
+
try {
|
|
84
|
+
agent = await resolve(role === 'assign-task' ? row.targetBot : row.parentBot, sessionId)
|
|
85
|
+
row.validate?.(); row.signal?.throwIfAborted()
|
|
86
|
+
if (agent.status !== 'idle' || rt.live?.get(sessionId)?.turn !== undefined) return { started: false, code: TEAM_CODES.TARGET_BUSY }
|
|
87
|
+
const prior = await eventsFor(sessionId, row.signal)
|
|
88
|
+
const existing = prior.some(event => event.type === 'user/message' && event.data?.source?.kind === 'bot' && event.data.source.exchangeId === requestId && event.data.source.role === role)
|
|
89
|
+
if (existing) return role === 'assign-task' ? { started: true, ...teamReceiptFromEvents(prior, requestId, role), executionFinished: true } : { resumed: true, replayed: true, executionFinished: true }
|
|
90
|
+
row.validate?.(); row.signal?.throwIfAborted()
|
|
91
|
+
const message = createUserMessage({ content: [{ type: 'text', text }], source: { kind: 'bot', form: 'relay', role, exchangeId: requestId, senderName: 'Team coordinator' } })
|
|
92
|
+
const aborted = new Promise(resolve => {
|
|
93
|
+
const stop = () => { try { agent.cancel({ kind: 'hook', reason: 'team-assignment-cancelled' }) } catch {}; resolve('aborted') }
|
|
94
|
+
row.signal?.addEventListener('abort', stop, { once: true }); off = () => row.signal?.removeEventListener('abort', stop)
|
|
95
|
+
})
|
|
96
|
+
submitted = true // conservative if public followup throws after acceptance
|
|
97
|
+
agent.followup(message)
|
|
98
|
+
if (await flush(agent.session) === false) fail('TEAM_SESSION_DURABILITY_UNAVAILABLE')
|
|
99
|
+
const state = await Promise.race([agent.whenIdle().then(() => 'idle'), aborted])
|
|
100
|
+
finished = state === 'idle'
|
|
101
|
+
if (await flush(agent.session) === false) fail('TEAM_SESSION_DURABILITY_UNAVAILABLE')
|
|
102
|
+
receipt = role === 'assign-result' ? { resumed: true, executionFinished: finished } : { started: true, ...(finished ? await readTarget({ ...row, requestId }) : unknown('TEAM_TARGET_STOP_UNCONFIRMED')), executionFinished: finished }
|
|
103
|
+
return receipt
|
|
104
|
+
} catch (error) { failure = error; if (!submitted) error.started = false; error.executionFinished = !submitted || finished; throw error }
|
|
105
|
+
finally {
|
|
106
|
+
// A result keeps its durable ancestry frame for the real resumed turn.
|
|
107
|
+
// Cancellation has a bounded convergence wait; a missing idle receipt
|
|
108
|
+
// preserves an uncertain execution fence instead of freeing that frame.
|
|
109
|
+
if (submitted && !finished) {
|
|
110
|
+
try { agent.cancel({ kind: 'hook', reason: 'team-assignment-cancelled' }) } catch {}
|
|
111
|
+
let timer
|
|
112
|
+
try { finished = await Promise.race([agent.whenIdle().then(() => true, () => false), new Promise(resolve => { timer = setTimeout(() => resolve(false), cancelWaitMs) })]) }
|
|
113
|
+
finally { clearTimeout(timer) }
|
|
114
|
+
}
|
|
115
|
+
if (receipt) receipt.executionFinished = finished
|
|
116
|
+
if (failure) failure.executionFinished = !submitted || finished
|
|
117
|
+
off()
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return Object.freeze({
|
|
121
|
+
hooks: { createSession: typeof rt.createSession === 'function', addThreadSession: typeof rt.addThreadSession === 'function' },
|
|
122
|
+
async resolveTargetRef(row) {
|
|
123
|
+
const bot = lifecycle.committedBot(row.targetBot)
|
|
124
|
+
if (!bot) fail(TEAM_CODES.TARGET_NOT_COMMITTED)
|
|
125
|
+
const host = threadHost()
|
|
126
|
+
if (host?.service?.create && host?.service?.ensureSession) {
|
|
127
|
+
const { thread } = host.service.create({ botId: row.targetBot, name: `Team: ${row.parentBot} → ${row.targetBot}`, requestKey: `team-${digest(row.workThreadKey).slice(0, 64)}` })
|
|
128
|
+
await host.service.ensureSession({ botId: row.targetBot, threadId: thread.threadId })
|
|
129
|
+
const gate = await host.sendGate({ botId: row.targetBot, threadId: thread.threadId })
|
|
130
|
+
if (!gate?.ok || !owns(row.targetBot, gate.sessionId)) fail(gate?.code ?? 'TEAM_SESSION_COMMIT_UNCONFIRMED')
|
|
131
|
+
return { mode: 'integration', sessionId: gate.sessionId, workThreadKey: row.workThreadKey, catalogThreadId: thread.threadId }
|
|
132
|
+
}
|
|
133
|
+
const prior = bot.threads?.find(thread => thread.threadId === row.workThreadKey && !thread.archivedAt)
|
|
134
|
+
if (prior) return { mode: 'integration', sessionId: prior.sessionId, workThreadKey: row.workThreadKey }
|
|
135
|
+
if (!rt.createSession || !rt.addThreadSession) fail('TEAM_SESSION_FACTORY_REQUIRED')
|
|
136
|
+
const title = `Team: ${row.parentBot} → ${row.targetBot}`
|
|
137
|
+
const sessionId = await rt.createSession(title, { dir: bot.workDir })
|
|
138
|
+
await rt.addThreadSession(row.targetBot, row.workThreadKey, sessionId, { name: title })
|
|
139
|
+
if (!owns(row.targetBot, sessionId)) fail('TEAM_SESSION_COMMIT_UNCONFIRMED')
|
|
140
|
+
return { mode: 'integration', sessionId, workThreadKey: row.workThreadKey }
|
|
141
|
+
},
|
|
142
|
+
dispatchTarget: row => submit(row, row.targetRef.sessionId, 'assign-task', row.requestId, composeBrief(row)),
|
|
143
|
+
readTarget,
|
|
144
|
+
captureOrigin({ callerBotId, sessionId }) { if (!owns(callerBotId, sessionId)) fail('TEAM_ORIGIN_UNCONFIRMED'); return { mode: 'integration', sessionId } },
|
|
145
|
+
originCurrent: (botId, origin) => origin.mode === 'integration' && owns(botId, origin.sessionId),
|
|
146
|
+
resumeSender(row) { if (!owns(row.parentBot, row.origin?.sessionId)) return { started: false, retired: true }; return submit(row, row.origin.sessionId, 'assign-result', `team-result-${row.assignmentId}`, composeResult(row)) },
|
|
147
|
+
async cancelTarget(row) { if (row.targetRef?.sessionId) { const agent = await resolve(row.targetBot, row.targetRef.sessionId); agent.cancel({ kind: 'hook', reason: 'team-assignment-cancelled' }) } },
|
|
148
|
+
isBusy: botId => rt.live?.get(rt.chatOf(botId))?.turn !== undefined,
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function createIndependentTeamBridge({ runtime } = {}) {
|
|
153
|
+
if (typeof runtime !== 'function') throw new TypeError('TEAM_BRIDGE_REQUIRES_RUNTIME')
|
|
154
|
+
const api = () => { const adapter = runtime(); if (!adapter?.workThreadTurn || !adapter?.resumeTeamTurn) fail('TEAM_WORKTHREAD_UNAVAILABLE'); return adapter }
|
|
155
|
+
const readTarget = row => {
|
|
156
|
+
const receipt = api().workThreadRead(row.targetBot, row.targetRef.workThreadKey, row.requestId ?? row.targetRequestId)
|
|
157
|
+
return receipt.hasConfirmedAnswer ? { outcome: 'succeeded', text: receipt.answer } : unknown(receipt.hasBrief ? 'TEAM_TARGET_ANSWER_UNCONFIRMED' : 'TEAM_TARGET_REQUEST_UNCONFIRMED')
|
|
158
|
+
}
|
|
159
|
+
return Object.freeze({
|
|
160
|
+
resolveTargetRef: row => ({ mode: 'independent', workThreadKey: row.workThreadKey, conversationId: `teamwt-${workThreadConversationKey(row.targetBot, row.workThreadKey)}` }),
|
|
161
|
+
async dispatchTarget(row) {
|
|
162
|
+
row.validate?.()
|
|
163
|
+
try { const result = await api().workThreadTurn({ botId: row.targetBot, workThreadId: row.targetRef.workThreadKey, conversationId: row.targetRef.conversationId, input: composeBrief(row), requestId: row.requestId, signal: row.signal, consult: row.mode === 'consult', validate: row.validate }); return { started: true, outcome: 'succeeded', text: result.answer } }
|
|
164
|
+
catch (error) { if (error.code === 'BOT_BUSY') return { started: false, code: TEAM_CODES.TARGET_BUSY }; return { started: true, ...readTarget(row), executionFinished: true } }
|
|
165
|
+
},
|
|
166
|
+
readTarget,
|
|
167
|
+
captureOrigin: row => api().captureTeamOrigin({ botId: row.callerBotId, conversationId: row.conversationId, workThreadId: row.workThreadId }),
|
|
168
|
+
originCurrent: (botId, origin) => api().teamOriginCurrent(botId, origin),
|
|
169
|
+
async resumeSender(row) {
|
|
170
|
+
try { return { ...await api().resumeTeamTurn({ botId: row.parentBot, origin: row.origin, requestId: `team-result-${row.assignmentId}`, input: composeResult(row), signal: row.signal, validate: row.validate }), executionFinished: true } }
|
|
171
|
+
catch (error) { error.executionFinished = true; throw error }
|
|
172
|
+
},
|
|
173
|
+
cancelTarget() {},
|
|
174
|
+
isBusy: botId => api().busyBots?.().includes(botId) === true,
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function createTeamBridge({ integrated, independent, serving }) {
|
|
179
|
+
const forBot = botId => { const bridge = serving.readMode(botId) === 'integration' ? integrated : serving.readMode(botId) === 'independent' ? independent : null; if (!bridge) fail('TEAM_BRIDGE_UNAVAILABLE'); return bridge }
|
|
180
|
+
// Recovery uses the recorded mode: moving a Bot cannot reinterpret an old receipt.
|
|
181
|
+
const forRef = ref => ref?.mode === 'integration' ? integrated : ref?.mode === 'independent' ? independent : null
|
|
182
|
+
return Object.freeze({
|
|
183
|
+
resolveTargetRef: row => forBot(row.targetBot).resolveTargetRef(row),
|
|
184
|
+
dispatchTarget: row => forRef(row.targetRef).dispatchTarget(row),
|
|
185
|
+
readTarget: row => forRef(row.targetRef)?.readTarget(row) ?? unknown('TEAM_BRIDGE_UNAVAILABLE'),
|
|
186
|
+
resumeSender: row => forRef(row.origin).resumeSender(row),
|
|
187
|
+
cancelTarget: row => forRef(row.targetRef)?.cancelTarget(row),
|
|
188
|
+
captureOrigin: row => forBot(row.callerBotId).captureOrigin(row),
|
|
189
|
+
originCurrent: (botId, origin) => serving.readMode(botId) === origin?.mode && forRef(origin)?.originCurrent(botId, origin) === true,
|
|
190
|
+
isBusy: botId => typeof serving.busy === 'function' ? serving.busy(botId) : forBot(botId).isBusy(botId),
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Both doors supply Host-resolved origin/turn/call metadata, never model args. */
|
|
195
|
+
export function createTeamCoordination({ Database, file, rt = null, directory, authority, lifecycle, serving, runtime = () => null, policy = null, sessionEvents = null, flush = null, integrated = null, independent = null, threadHost = () => null, now = Date.now, limits = {} } = {}) {
|
|
196
|
+
const maxDepth = limits.maxDepth ?? 4
|
|
197
|
+
const store = createTeamAssignmentStore({ Database, file, now })
|
|
198
|
+
const bridge = createTeamBridge({ integrated: integrated ?? (rt && flush ? createIntegratedTeamBridge({ rt, directory, authority, lifecycle, sessionEvents, flush, threadHost }) : null), independent: independent ?? createIndependentTeamBridge({ runtime }), serving })
|
|
199
|
+
const isMain = id => lifecycle.committedMainBotIds().includes(id)
|
|
200
|
+
const canUse = id => { try { if (!lifecycle.committedBot(id)) return false; authority.assertTool(directory.forBot(id), 'tool/team'); return true } catch { return false } }
|
|
201
|
+
const requireCaller = id => { if (!lifecycle.committedBot(id)) fail(TEAM_CODES.CALLER_NOT_COMMITTED); if (!canUse(id)) fail(TEAM_CODES.CALLER_NOT_AUTHORIZED) }
|
|
202
|
+
const reachable = (fromId, targetId) => {
|
|
203
|
+
const from = lifecycle.committedBot(fromId), target = lifecycle.committedBot(targetId)
|
|
204
|
+
if (!from || !target || fromId === targetId) return false
|
|
205
|
+
if (isMain(fromId)) return true
|
|
206
|
+
if (target.hidden || target.private || target.archivedAt || target.visibility !== undefined && target.visibility !== null && target.visibility !== 'everyone') return false
|
|
207
|
+
if (from.peers !== undefined && (!Array.isArray(from.peers) || !from.peers.includes(targetId))) return false
|
|
208
|
+
// A restricted sender may not disclose its brief into a broader audience.
|
|
209
|
+
if (from.visibility !== undefined && from.visibility !== null && from.visibility !== 'everyone') return false
|
|
210
|
+
return true
|
|
211
|
+
}
|
|
212
|
+
const accessAllowed = row => {
|
|
213
|
+
if (!canUse(row.parentBot) || !reachable(row.parentBot, row.targetBot)) return false
|
|
214
|
+
if (row.parentAssignmentId) { const parent = store.get(row.parentAssignmentId); if (!parent || parent.cancelRequestedAt !== null || parent.state === 'cancelled') return false }
|
|
215
|
+
try { authority.assertTool(directory.forBot(row.targetBot), 'model/infer'); return true } catch { return false }
|
|
216
|
+
}
|
|
217
|
+
const assertCanAct = (botId, { sessionId = null, conversationId = null } = {}) => {
|
|
218
|
+
const active = store.activeFor(botId, { sessionId, conversationId })
|
|
219
|
+
if (active?.mode === 'consult') fail('TEAM_CONSULT_READ_ONLY')
|
|
220
|
+
const resumed = active?.resumeAssignmentId ? store.get(active.resumeAssignmentId) : null
|
|
221
|
+
if (active && (active.cancelRequestedAt !== null || active.assignmentId && !accessAllowed(active) || resumed && !accessAllowed(resumed))) fail('TEAM_ACCESS_CHANGED')
|
|
222
|
+
}
|
|
223
|
+
const scheduler = createTeamScheduler({ store, bridge, checks: { committedBot: id => lifecycle.committedBot(id), accessAllowed, originAllowed: row => bridge.originCurrent(row.parentBot, row.origin) }, policy })
|
|
224
|
+
const scope = input => {
|
|
225
|
+
requireCaller(input.callerBotId)
|
|
226
|
+
const active = store.activeFor(input.callerBotId, { sessionId: input.sessionId, conversationId: input.conversationId })
|
|
227
|
+
const origin = bridge.captureOrigin({ ...input, workThreadId: active?.targetRef?.workThreadKey })
|
|
228
|
+
return { origin, originKey: digest(origin), active }
|
|
229
|
+
}
|
|
230
|
+
const shape = row => ({ assignmentId: row.assignmentId, requestKey: row.requestKey, targetBot: row.targetBot, workThreadKey: row.workThreadKey, threadId: row.workThreadKey, mode: row.mode, reply: row.reply, state: row.state, outcome: row.outcome, resultCode: row.resultCode, result: row.outcome === 'succeeded' ? row.resultText : null, resumeState: row.resumeState, reworkOf: row.reworkOf, depth: row.depth, cancelRequested: row.cancelRequestedAt !== null, ...(row.duplicate ? { duplicate: true } : {}) })
|
|
231
|
+
const coordinateBots = input => {
|
|
232
|
+
const { callerBotId, turnId, callId } = input
|
|
233
|
+
const { origin, originKey, active } = scope(input)
|
|
234
|
+
assertCanAct(callerBotId, input)
|
|
235
|
+
if (!turnId || !callId) fail('TEAM_HOST_TURN_REQUIRED')
|
|
236
|
+
const args = input.args
|
|
237
|
+
// Re-validate at the service door too; callers cannot bypass the shared parser.
|
|
238
|
+
parseTeamToolArgs('coordinate_bots', { bot_ids: args.targets, message: args.message, thread: args.thread ?? 'standing', reply: args.reply ?? 'resume', consult: args.consult ?? false, rework: args.rework ?? false, ...(args.doneWhen !== undefined && args.doneWhen !== null ? { done_when: args.doneWhen } : {}), ...(args.requestKey !== undefined ? { request_key: args.requestKey } : {}) })
|
|
239
|
+
const requestKey = args.requestKey ?? `call-${digest([turnId, callId]).slice(0, 60)}`
|
|
240
|
+
const mode = args.consult ? 'consult' : args.reply === 'none' ? 'notice' : 'work'
|
|
241
|
+
const lineage = active?.lineage ?? [callerBotId]
|
|
242
|
+
assertDepth(lineage, maxDepth)
|
|
243
|
+
const specs = args.targets.map(targetBot => {
|
|
244
|
+
if (targetBot === callerBotId) fail(TEAM_CODES.SELF_TARGET)
|
|
245
|
+
// A missing/private target produces the same refusal, without a name leak.
|
|
246
|
+
if (!reachable(callerBotId, targetBot)) fail(TEAM_CODES.CALLER_NOT_AUTHORIZED)
|
|
247
|
+
assertNoCycle(lineage, targetBot)
|
|
248
|
+
const requestedThread = args.thread ?? 'standing'
|
|
249
|
+
const workThreadKey = requestedThread === 'standing' ? standingThreadKey(callerBotId, targetBot, originKey) : requestedThread === 'new' ? `wt-${randomUUID()}` : requestedThread
|
|
250
|
+
if (!['new', 'standing'].includes(requestedThread) && !store.listByParent(callerBotId, { includeTerminal: true, originKey }).some(row => row.targetBot === targetBot && row.workThreadKey === workThreadKey)) fail(TEAM_CODES.THREAD_NOT_OWNED)
|
|
251
|
+
const spec = { parentBot: callerBotId, targetBot, requestedThread, workThreadKey, origin, originKey, mode, reply: args.reply ?? 'resume', brief: args.message, doneWhen: args.doneWhen ?? null, briefHash: computeBriefHash(mode, args.message, args.doneWhen, args.consult), rework: args.rework === true, lineage: [...lineage, targetBot], depth: lineage.length, rootBot: active?.rootBot ?? callerBotId, rootAssignmentId: active?.rootAssignmentId ?? active?.assignmentId ?? null, parentAssignmentId: active?.assignmentId ?? null, parentRequestId: active?.targetRequestId ?? null, createdByTurn: turnId, createdByCall: callId }
|
|
252
|
+
if (!accessAllowed(spec)) fail(TEAM_CODES.ACCESS_CHANGED)
|
|
253
|
+
return spec
|
|
254
|
+
})
|
|
255
|
+
const decision = store.createBatch(specs, { parentBot: callerBotId, originKey, requestKey, payloadHash: digest([args.targets, args.message, args.thread ?? 'standing', args.reply ?? 'resume', args.consult ?? false, args.rework ?? false, args.doneWhen ?? null]) })
|
|
256
|
+
if (decision.status === 'conflict') fail(TEAM_CODES.REQUEST_CONFLICT)
|
|
257
|
+
return { assignments: decision.assignments.map(row => ({ ...shape(row), ...(decision.status === 'replayed' ? { replayed: true } : {}) })) }
|
|
258
|
+
}
|
|
259
|
+
const rowsFor = input => { const scoped = scope(input); return store.listByParent(input.callerBotId, { includeTerminal: true, originKey: scoped.originKey }).filter(accessAllowed) }
|
|
260
|
+
return Object.freeze({
|
|
261
|
+
ready: true, canUse, assertCanAct, coordinateBots,
|
|
262
|
+
validateResult(input, value) {
|
|
263
|
+
requireCaller(input.callerBotId)
|
|
264
|
+
if (input.sessionId || input.conversationId) scope(input)
|
|
265
|
+
const targets = [...(value.assignments ?? []), ...(value.assignment ? [value.assignment] : []), ...(value.threads ?? [])].map(row => row.targetBot)
|
|
266
|
+
targets.push(...(value.bots ?? []).map(row => row.id))
|
|
267
|
+
if (targets.some(id => !reachable(input.callerBotId, id))) fail(TEAM_CODES.ACCESS_CHANGED)
|
|
268
|
+
},
|
|
269
|
+
assignmentStatus(input) {
|
|
270
|
+
const rows = rowsFor(input), args = input.args ?? {}
|
|
271
|
+
if (rows.some(row => row.createdByTurn === input.turnId)) fail('TEAM_STATUS_SAME_TURN')
|
|
272
|
+
if (args.id) { const row = rows.find(row => row.assignmentId === args.id); if (!row) fail(TEAM_CODES.ASSIGNMENT_NOT_FOUND); return { assignment: shape(row) } }
|
|
273
|
+
return { assignments: rows.filter(row => !['settled', 'unknown', 'cancelled'].includes(row.state)).map(shape) }
|
|
274
|
+
},
|
|
275
|
+
listBots({ callerBotId }) { requireCaller(callerBotId); return { bots: lifecycle.committedBots().filter(bot => reachable(callerBotId, bot.id)).map(bot => ({ id: bot.id, name: bot.name ?? bot.id, main: isMain(bot.id), model: bot.model ?? null, mode: serving.readMode(bot.id) })).sort((a, b) => a.id.localeCompare(b.id)) } },
|
|
276
|
+
listThreads(input) { const rows = rowsFor(input), seen = new Set(); return { threads: rows.reverse().filter(row => { if (seen.has(row.workThreadKey)) return false; seen.add(row.workThreadKey); return true }).map(row => ({ threadId: row.workThreadKey, targetBot: row.targetBot, title: row.brief.slice(0, 80), state: row.state, assignmentId: row.assignmentId, updatedAt: row.updatedAt })) } },
|
|
277
|
+
stop: botId => scheduler.stop(botId), store, scheduler,
|
|
278
|
+
start(options) { scheduler.start(options); return this }, tick: () => scheduler.tick(), whenIdle: () => scheduler.whenIdle(),
|
|
279
|
+
async drain(rounds = 24) { for (let index = 0; index < rounds; index++) { scheduler.tick(); await scheduler.whenIdle(); if (!store.dispatchable().length && !store.rowsForReconcile().length && !store.resumeDue().length) break }; return this },
|
|
280
|
+
close() { scheduler.close(); store.close() },
|
|
281
|
+
})
|
|
282
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
const safeCode = error => /^[A-Z][A-Z0-9_:-]{0,95}$/.test(error?.code ?? '') ? error.code : 'TEAM_OUTCOME_UNKNOWN'
|
|
4
|
+
const known = receipt => ['succeeded', 'failed', 'cancelled'].includes(receipt?.outcome)
|
|
5
|
+
|
|
6
|
+
/** Persist intent before effects. Running requests are read, never dispatched again.
|
|
7
|
+
* Sending resumes that lack a durable transport receipt are uncertain, never resent.
|
|
8
|
+
*/
|
|
9
|
+
export function createTeamScheduler({ store, bridge, checks, policy = null, pollMs = 200 } = {}) {
|
|
10
|
+
if (!store?.dispatchable || !bridge?.dispatchTarget || !checks?.accessAllowed) throw new TypeError('TEAM_SCHEDULER_DEPENDENCIES_REQUIRED')
|
|
11
|
+
if (!Number.isSafeInteger(pollMs) || pollMs < 5) throw new TypeError('TEAM_SCHEDULER_POLL_INVALID')
|
|
12
|
+
const inflight = new Map(), pending = new Set()
|
|
13
|
+
let closed = false, timer = null
|
|
14
|
+
const detached = operation => policy ? policy.runDetached(operation) : operation()
|
|
15
|
+
const allowed = row => !closed && row.cancelRequestedAt === null && checks.committedBot(row.parentBot) && checks.committedBot(row.targetBot) && checks.accessAllowed(row)
|
|
16
|
+
const launch = (row, kind, operation) => {
|
|
17
|
+
if (inflight.has(row.assignmentId) || closed) return
|
|
18
|
+
const controller = new AbortController()
|
|
19
|
+
inflight.set(row.assignmentId, { controller, kind })
|
|
20
|
+
const task = Promise.resolve().then(() => operation(controller.signal)).catch(() => {}).finally(() => { inflight.delete(row.assignmentId); pending.delete(task) })
|
|
21
|
+
pending.add(task)
|
|
22
|
+
}
|
|
23
|
+
const settle = (row, receipt) => {
|
|
24
|
+
if (closed) return
|
|
25
|
+
if (!known(receipt)) store.fenceUnknown(row.assignmentId, receipt?.code ?? 'TEAM_OUTCOME_UNKNOWN', { executionFinished: receipt?.executionFinished === true })
|
|
26
|
+
else store.settle(row.assignmentId, { outcome: receipt.outcome, resultText: receipt.text ?? null, resultCode: receipt.code ?? null })
|
|
27
|
+
}
|
|
28
|
+
const dispatch = async (row, signal) => {
|
|
29
|
+
if (!allowed(row)) { store.cancel(row.assignmentId, { resultCode: 'TEAM_ACCESS_CHANGED' }); return }
|
|
30
|
+
if (bridge.isBusy(row.targetBot)) return
|
|
31
|
+
if (!store.admit(row.assignmentId)) return
|
|
32
|
+
try {
|
|
33
|
+
const targetRef = await detached(() => bridge.resolveTargetRef(row))
|
|
34
|
+
const current = store.get(row.assignmentId)
|
|
35
|
+
if (signal.aborted || !allowed(current)) { store.cancel(row.assignmentId, { resultCode: 'TEAM_ACCESS_CHANGED' }); return }
|
|
36
|
+
const requestId = `teamreq_${randomUUID()}`
|
|
37
|
+
if (!store.beginDispatch(row.assignmentId, targetRef, requestId)) return
|
|
38
|
+
const receipt = await detached(() => bridge.dispatchTarget({ ...row, targetRef, requestId, signal, validate: () => { const current = store.get(row.assignmentId); if (signal.aborted || !allowed(current)) throw Object.assign(new Error('TEAM_ACCESS_CHANGED'), { code: 'TEAM_ACCESS_CHANGED' }) } }))
|
|
39
|
+
if (closed) return
|
|
40
|
+
if (receipt?.started === false) {
|
|
41
|
+
if (store.get(row.assignmentId).cancelRequestedAt === null) store.rewarm(row.assignmentId)
|
|
42
|
+
else store.fenceUnknown(row.assignmentId, 'TEAM_STOP_BEFORE_DISPATCH')
|
|
43
|
+
} else settle(row, receipt)
|
|
44
|
+
} catch (error) { if (!closed) store.fenceUnknown(row.assignmentId, safeCode(error), { executionFinished: error?.executionFinished === true }) }
|
|
45
|
+
}
|
|
46
|
+
const reconcile = async row => {
|
|
47
|
+
try { settle(row, await detached(() => bridge.readTarget({ ...row, requestId: row.targetRequestId }))) }
|
|
48
|
+
catch (error) { if (!closed) store.fenceUnknown(row.assignmentId, safeCode(error)) }
|
|
49
|
+
}
|
|
50
|
+
const resume = async (row, signal) => {
|
|
51
|
+
if (!allowed(row) || checks.originAllowed?.(row) === false) return
|
|
52
|
+
if (bridge.isBusy(row.parentBot) || !store.markResumeSending(row.assignmentId)) return
|
|
53
|
+
try {
|
|
54
|
+
const result = await detached(() => bridge.resumeSender({ ...row, signal, validate: () => { if (signal.aborted || !allowed(store.get(row.assignmentId)) || checks.originAllowed?.(row) === false) throw Object.assign(new Error('TEAM_ACCESS_CHANGED'), { code: 'TEAM_ACCESS_CHANGED', started: false, retired: true }) } }))
|
|
55
|
+
if (closed) return
|
|
56
|
+
if (result?.started === false) {
|
|
57
|
+
if (result.retired) store.completeResume(row.assignmentId, 'withheld')
|
|
58
|
+
else store.rewarmResume(row.assignmentId)
|
|
59
|
+
} else store.completeResume(row.assignmentId, result?.executionFinished === false ? 'uncertain' : 'delivered', { executionFinished: result?.executionFinished !== false })
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (closed) return
|
|
62
|
+
if (error?.started === false && !error.retired) store.rewarmResume(row.assignmentId)
|
|
63
|
+
else store.completeResume(row.assignmentId, error?.retired ? 'withheld' : 'uncertain', { executionFinished: error?.executionFinished === true || error?.started === false })
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const tick = () => {
|
|
67
|
+
if (closed) return
|
|
68
|
+
for (const row of store.rowsForReconcile()) launch(row, 'reconcile', () => reconcile(row))
|
|
69
|
+
for (const row of store.dispatchable()) launch(row, 'dispatch', signal => dispatch(row, signal))
|
|
70
|
+
for (const row of store.resumeDue()) launch(row, 'resume', signal => resume(row, signal))
|
|
71
|
+
}
|
|
72
|
+
return Object.freeze({
|
|
73
|
+
tick,
|
|
74
|
+
async whenIdle() { while (pending.size) await Promise.allSettled([...pending]) },
|
|
75
|
+
stop(botId) {
|
|
76
|
+
const rows = store.all().filter(row => !['settled', 'unknown', 'cancelled'].includes(row.state) && (row.lineage.includes(botId) || row.parentBot === botId))
|
|
77
|
+
for (const row of rows) { store.requestCancel(row.assignmentId); inflight.get(row.assignmentId)?.controller.abort(Object.assign(new Error('TEAM_SUBTREE_STOPPED'), { code: 'TEAM_SUBTREE_STOPPED' })); if (row.targetRef) Promise.resolve(detached(() => bridge.cancelTarget(row))).catch(() => {}) }
|
|
78
|
+
return { stopped: rows.map(row => row.assignmentId) }
|
|
79
|
+
},
|
|
80
|
+
start({ auto = true } = {}) { store.recover(); tick(); if (auto && !closed && !timer) { timer = setInterval(tick, pollMs); timer.unref?.() }; return this },
|
|
81
|
+
close() { if (closed) return; closed = true; clearInterval(timer); for (const { controller } of inflight.values()) controller.abort(Object.assign(new Error('TEAM_SCHEDULER_CLOSED'), { code: 'TEAM_SCHEDULER_CLOSED' })) },
|
|
82
|
+
})
|
|
83
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SDK (integrated Harness) door for the four teammate-coordination tools.
|
|
3
|
+
*
|
|
4
|
+
* Registered through `ctx.tools.register` exactly as the data tools are
|
|
5
|
+
* (bot-data-tools.mjs), reading names, descriptions and the SDK parameter map from the
|
|
6
|
+
* ONE shared contract (team-tool-contract.mjs) so the SDK and native doors cannot drift.
|
|
7
|
+
*
|
|
8
|
+
* Authority: the calling Bot comes from the live session through the injected
|
|
9
|
+
* `botIdFor(exec)` — never from an argument, which parseTeamToolArgs has already refused
|
|
10
|
+
* any `botId`/`owner`/route/session key in. The sender's Harness Session id is read from
|
|
11
|
+
* the live agent so a result returns into THIS conversation and resumes it; the target's
|
|
12
|
+
* own model, tools, permissions and funding are re-derived inside the target's frame, so
|
|
13
|
+
* nothing here lends the caller's authority to the target.
|
|
14
|
+
*
|
|
15
|
+
* Note for the parent: `list_bots` already exists in the vendored roster for integrated
|
|
16
|
+
* Bots. Register the NEW team tools (`coordinate_bots`, `assignment_status`,
|
|
17
|
+
* `list_threads`) here and keep the vendor `list_bots`; register `list_bots` from this
|
|
18
|
+
* family only on the native (independent) door, which has no roster tool of its own.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
22
|
+
import { TEAM_TOOL_ENTRIES, TEAM_CODES, parseTeamToolArgs, projectCoordinateArgs } from './team-tool-contract.mjs'
|
|
23
|
+
import { redactNativeToolValue } from './native-conversation-tools.mjs'
|
|
24
|
+
|
|
25
|
+
const JSON_OUTPUT = { schema: { type: 'json' }, render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }] }
|
|
26
|
+
const safeCode = code => /^[A-Z][A-Z0-9_:-]{0,95}$/.test(code ?? '') ? code : 'TEAM_CALL_FAILED'
|
|
27
|
+
// Installed SDK author DSL does not support array cardinality/uniqueness. The
|
|
28
|
+
// shared parser enforces these bounds before any service effect; MCP keeps them.
|
|
29
|
+
const sdkParameters = parameters => Object.fromEntries(Object.entries(parameters).map(([key, spec]) => {
|
|
30
|
+
const { minItems, maxItems, uniqueItems, minLength, maxLength, ...supported } = spec
|
|
31
|
+
return [key, supported]
|
|
32
|
+
}))
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {object} ctx Host tool registry ({ tools: { register } }).
|
|
36
|
+
* @param {{ service: object, botIdFor: (exec: object) => string | null, names?: string[] }} source
|
|
37
|
+
* @returns {() => void} release
|
|
38
|
+
*/
|
|
39
|
+
export function registerTeamTools(ctx, { service, botIdFor, names = ['coordinate_bots', 'assignment_status', 'list_threads'], redact = text => text } = {}) {
|
|
40
|
+
if (!service || typeof service.coordinateBots !== 'function') throw new TypeError('TEAM_TOOLS_REQUIRE_SERVICE')
|
|
41
|
+
if (typeof botIdFor !== 'function') throw new TypeError('TEAM_TOOLS_REQUIRE_BOT_ID_FOR')
|
|
42
|
+
if (!ctx || typeof ctx.tools?.register !== 'function') throw new TypeError('TEAM_TOOLS_REQUIRE_CTX')
|
|
43
|
+
const offered = new Set(Array.isArray(names) ? names : TEAM_TOOL_ENTRIES.map(entry => entry.name))
|
|
44
|
+
|
|
45
|
+
const asBot = async (exec, run) => {
|
|
46
|
+
let botId = null
|
|
47
|
+
try { botId = botIdFor(exec) ?? null } catch (error) {
|
|
48
|
+
return { error: { code: safeCode(error?.code), message: redact(String(error?.message ?? error)) } }
|
|
49
|
+
}
|
|
50
|
+
if (typeof botId !== 'string' || botId === '') return { error: { code: 'unknown_subject', message: 'The calling Bot could not be identified from the live session.' } }
|
|
51
|
+
const sessionId = exec?.agent?.session?.id ?? null
|
|
52
|
+
try {
|
|
53
|
+
exec.signal.throwIfAborted()
|
|
54
|
+
if (!service.canUse(botId)) throw Object.assign(new Error('TEAM_CALLER_NOT_AUTHORIZED'), { code: 'TEAM_CALLER_NOT_AUTHORIZED' })
|
|
55
|
+
const boundary = (exec.agent.ctx?.sessionProjections ?? ctx.sessionProjections)?.stateOf(exec.agent.session, 'turnBoundary')
|
|
56
|
+
if (boundary?.openTurnStartSeq === null || boundary?.openTurnStartSeq === undefined || !exec.callId) throw Object.assign(new Error('TEAM_HOST_TURN_REQUIRED'), { code: 'TEAM_HOST_TURN_REQUIRED' })
|
|
57
|
+
const request = { callerBotId: botId, sessionId, turnId: `sdk-${sessionId}-${boundary.openTurnStartSeq}`, callId: exec.callId, signal: exec.signal }
|
|
58
|
+
const value = await run(request)
|
|
59
|
+
exec.signal.throwIfAborted()
|
|
60
|
+
if (botIdFor(exec) !== botId || !service.canUse(botId)) throw Object.assign(new Error('TEAM_ACCESS_CHANGED'), { code: 'TEAM_ACCESS_CHANGED' })
|
|
61
|
+
service.validateResult?.(request, value)
|
|
62
|
+
return redactNativeToolValue(value, redact)
|
|
63
|
+
} catch (error) { return { error: { code: safeCode(error?.code), message: redact(String(error?.message ?? error)) } } }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const tools = TEAM_TOOL_ENTRIES.filter(entry => offered.has(entry.name)).map(entry => defineTool({
|
|
67
|
+
name: entry.name,
|
|
68
|
+
description: entry.description,
|
|
69
|
+
parameters: sdkParameters(entry.parameters),
|
|
70
|
+
output: JSON_OUTPUT,
|
|
71
|
+
execute: (args, exec) => asBot(exec, async request => {
|
|
72
|
+
const parsed = parseTeamToolArgs(entry.name, args)
|
|
73
|
+
if (entry.method === 'coordinateBots') { const value = await service.coordinateBots({ ...request, args: projectCoordinateArgs(parsed) }); exec.concludeTurn?.(); return value }
|
|
74
|
+
if (entry.method === 'assignmentStatus') return service.assignmentStatus({ ...request, args: parsed })
|
|
75
|
+
if (entry.method === 'listBots') return service.listBots(request)
|
|
76
|
+
return service.listThreads(request)
|
|
77
|
+
}),
|
|
78
|
+
}))
|
|
79
|
+
|
|
80
|
+
const releases = tools.map(tool => ctx.tools.register(tool))
|
|
81
|
+
return () => { for (const release of releases.reverse()) release() }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export { TEAM_CODES }
|