@cohortapp/agent-sdk 2.16.0 → 2.18.4
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/.claude/settings.json +18 -0
- package/.env.example +23 -7
- package/README.md +1 -0
- package/bin/maestro.mjs +62 -0
- package/docs/guides/billing-console-keys.md +60 -0
- package/docs/guides/front-door-session.md +54 -9
- package/docs/guides/mac-mini.md +20 -25
- package/docs/guides/poller-daemon-setup.md +4 -1
- package/docs/guides/setup-wizard.md +1 -1
- package/docs/runbooks/fleet-rollout.md +156 -0
- package/docs/runbooks/mac-mini-bootstrap.md +12 -14
- package/lib/action-executor.js +19 -3
- package/lib/budget-guard.mjs +279 -3
- package/lib/channels/base-adapter.mjs +3 -1
- package/lib/channels/contract.mjs +2 -1
- package/lib/channels/inbox-item.mjs +8 -0
- package/lib/claude-bin.mjs +5 -6
- package/lib/cli/doctor-checks.mjs +141 -10
- package/lib/cli/global-setup-extras.mjs +5 -1
- package/lib/cli/inbox.mjs +100 -15
- package/lib/cli/seat-auth.mjs +463 -0
- package/lib/cli/session.mjs +80 -12
- package/lib/collective/capture.mjs +8 -6
- package/lib/collective/global-config.mjs +63 -1
- package/lib/collective/presence.mjs +142 -5
- package/lib/comms/send-gate.mjs +559 -1
- package/lib/context/budget.mjs +327 -0
- package/lib/context/history-scope.mjs +138 -0
- package/lib/diagnostics/alerts.mjs +49 -0
- package/lib/diagnostics/cadence-output-freshness.mjs +288 -0
- package/lib/engine/agents/definitions.mjs +343 -0
- package/lib/engine/agents/persist.mjs +275 -0
- package/lib/engine/agents/runtime.mjs +748 -0
- package/lib/engine/agents/usage.mjs +95 -0
- package/lib/engine/auth-status.mjs +139 -0
- package/lib/engine/budget.mjs +194 -0
- package/lib/engine/cli.mjs +1204 -0
- package/lib/engine/commands/index.mjs +269 -0
- package/lib/engine/context/budget.mjs +219 -0
- package/lib/engine/context/cache.mjs +125 -0
- package/lib/engine/context/child-env.mjs +215 -0
- package/lib/engine/context/compaction.mjs +342 -0
- package/lib/engine/context/images.mjs +90 -0
- package/lib/engine/context/instructions.mjs +327 -0
- package/lib/engine/context/lazy-instructions.mjs +169 -0
- package/lib/engine/context/manager.mjs +182 -0
- package/lib/engine/context/real-path.mjs +91 -0
- package/lib/engine/context/secret-values.mjs +163 -0
- package/lib/engine/context/settings.mjs +274 -0
- package/lib/engine/context/stream-input.mjs +159 -0
- package/lib/engine/guard.mjs +152 -0
- package/lib/engine/hooks.mjs +713 -0
- package/lib/engine/loop.mjs +560 -0
- package/lib/engine/mcp/client.mjs +254 -0
- package/lib/engine/mcp/config.mjs +301 -0
- package/lib/engine/mcp/http.mjs +201 -0
- package/lib/engine/mcp/index.mjs +146 -0
- package/lib/engine/mcp/jsonrpc.mjs +147 -0
- package/lib/engine/mcp/naming.mjs +66 -0
- package/lib/engine/mcp/resources.mjs +89 -0
- package/lib/engine/mcp/results.mjs +133 -0
- package/lib/engine/mcp/stdio.mjs +137 -0
- package/lib/engine/mcp/supervisor.mjs +116 -0
- package/lib/engine/messages.mjs +104 -0
- package/lib/engine/output/json.mjs +164 -0
- package/lib/engine/output/stream-json.mjs +266 -0
- package/lib/engine/permissions.mjs +845 -0
- package/lib/engine/process-identity.mjs +164 -0
- package/lib/engine/process-tree.mjs +551 -0
- package/lib/engine/prompt.mjs +60 -0
- package/lib/engine/session/store.mjs +299 -0
- package/lib/engine/session-runtime/args.mjs +97 -0
- package/lib/engine/session-runtime/host.mjs +143 -0
- package/lib/engine/session-runtime/inbox.mjs +122 -0
- package/lib/engine/session-runtime/notifications.mjs +129 -0
- package/lib/engine/session-runtime/registry.mjs +328 -0
- package/lib/engine/session-runtime/runner.mjs +344 -0
- package/lib/engine/session-runtime/socket.mjs +212 -0
- package/lib/engine/session-runtime/wakeup.mjs +115 -0
- package/lib/engine/skills/index.mjs +321 -0
- package/lib/engine/tools/bash-background.mjs +533 -0
- package/lib/engine/tools/bash.mjs +216 -0
- package/lib/engine/tools/edit.mjs +97 -0
- package/lib/engine/tools/glob.mjs +81 -0
- package/lib/engine/tools/grep.mjs +224 -0
- package/lib/engine/tools/index.mjs +84 -0
- package/lib/engine/tools/list-agents.mjs +32 -0
- package/lib/engine/tools/ls.mjs +127 -0
- package/lib/engine/tools/monitor.mjs +82 -0
- package/lib/engine/tools/notebook-edit.mjs +218 -0
- package/lib/engine/tools/read.mjs +103 -0
- package/lib/engine/tools/schedule-wakeup.mjs +45 -0
- package/lib/engine/tools/schema.mjs +144 -0
- package/lib/engine/tools/send-message.mjs +77 -0
- package/lib/engine/tools/session.mjs +70 -0
- package/lib/engine/tools/todo.mjs +144 -0
- package/lib/engine/tools/toolsearch.mjs +217 -0
- package/lib/engine/tools/walk.mjs +193 -0
- package/lib/engine/tools/web-switch.mjs +31 -0
- package/lib/engine/tools/webfetch-html.mjs +387 -0
- package/lib/engine/tools/webfetch-net.mjs +340 -0
- package/lib/engine/tools/webfetch.mjs +198 -0
- package/lib/engine/tools/websearch.mjs +91 -0
- package/lib/engine/tools/workflow.mjs +95 -0
- package/lib/engine/tools/write.mjs +76 -0
- package/lib/engine/tui/line-editor.mjs +137 -0
- package/lib/engine/tui/render.mjs +86 -0
- package/lib/engine/tui/tui.mjs +274 -0
- package/lib/engine/wire/anthropic-messages.mjs +263 -0
- package/lib/engine/wire/effort.mjs +36 -0
- package/lib/engine/wire/errors.mjs +496 -0
- package/lib/engine/wire/http.mjs +441 -0
- package/lib/engine/wire/index.mjs +76 -0
- package/lib/engine/wire/openai-chat.mjs +332 -0
- package/lib/engine/wire/prompt-cache.mjs +79 -0
- package/lib/engine/wire/search.mjs +140 -0
- package/lib/engine/wire/sse.mjs +114 -0
- package/lib/engine/wire/stall.mjs +349 -0
- package/lib/engine/wire/token-provider.mjs +175 -0
- package/lib/engine/wire/usage.mjs +192 -0
- package/lib/engine/workflow/host.mjs +524 -0
- package/lib/engine/workflow/journal.mjs +188 -0
- package/lib/engine/workflow/json-schema.mjs +171 -0
- package/lib/engine/workflow/meta.mjs +329 -0
- package/lib/engine/workflow/notifications.mjs +52 -0
- package/lib/engine/workflow/runtime.mjs +447 -0
- package/lib/engine/workflow/sandbox.mjs +534 -0
- package/lib/engine/workflow/worker.mjs +141 -0
- package/lib/engine/workflow/worktree.mjs +74 -0
- package/lib/execution/disposition.mjs +1 -1
- package/lib/execution/intake.mjs +10 -0
- package/lib/execution/surface-policy.mjs +15 -0
- package/lib/learning/curator.mjs +8 -6
- package/lib/learning/reflect.mjs +8 -6
- package/lib/model-router/catalog/cohort.yaml +137 -0
- package/lib/model-router/catalog.mjs +118 -1
- package/lib/model-router/economics.mjs +9 -0
- package/lib/model-router/failover.mjs +67 -16
- package/lib/model-router/llm-task.mjs +39 -3
- package/lib/model-router/resolve.mjs +95 -3
- package/lib/model-router/spawn.mjs +46 -47
- package/lib/model-router/taxonomy.mjs +126 -4
- package/lib/org/cost-sync.mjs +141 -11
- package/lib/org/inbound/broadcast.mjs +289 -0
- package/lib/org/inbound/collective.mjs +375 -0
- package/lib/org/inbound/directedness.mjs +96 -8
- package/lib/org/inbound/facts.mjs +82 -4
- package/lib/org/inbound/hydrate.mjs +555 -51
- package/lib/org/inbound/project.mjs +22 -0
- package/lib/org/inbound/surfaces.mjs +14 -0
- package/lib/org/llm-token.mjs +879 -0
- package/lib/org/mesh.mjs +61 -0
- package/lib/org/messaging.mjs +3 -1
- package/lib/org/protocol.checksum +1 -1
- package/lib/org/protocol.mjs +15 -0
- package/lib/org/quota.mjs +520 -0
- package/lib/org/tool-surface.mjs +104 -16
- package/lib/org/ui-parity.mjs +16 -1
- package/lib/org/work-ledger.mjs +37 -6
- package/lib/rate-guard.mjs +114 -1
- package/lib/resource-governor.mjs +41 -6
- package/lib/runtime/adapter.mjs +823 -0
- package/lib/runtime/child-env.mjs +191 -0
- package/lib/runtime/legacy-shell-guard.mjs +97 -0
- package/lib/runtime/seat-engine.mjs +162 -0
- package/lib/session/ask-ledger.mjs +271 -0
- package/lib/session/current-work.mjs +676 -0
- package/lib/session/feed-core.mjs +40 -3
- package/lib/session/launch-args.mjs +56 -4
- package/lib/session/status-summary.mjs +26 -9
- package/lib/session/upgrade-notice.mjs +42 -0
- package/lib/setup/claude-probe.mjs +117 -13
- package/lib/setup/enrich.mjs +13 -10
- package/lib/setup/sections/model.mjs +39 -13
- package/lib/telemetry/collect.mjs +208 -9
- package/lib/upgrade/ignored-drift.mjs +105 -0
- package/lib/voice/post-call-brief.mjs +30 -17
- package/package.json +15 -3
- package/plugins/maestro-skills/skills/board-work.md +5 -0
- package/plugins/maestro-skills/skills/inbound-triage.md +56 -15
- package/plugins/maestro-skills/skills/main-session.md +18 -7
- package/scripts/ci/check-tarball-fidelity.mjs +126 -2
- package/scripts/ci/run-tests.mjs +47 -19
- package/scripts/cohort-llm/api-key-helper.mjs +92 -0
- package/scripts/collective/hook-runner.mjs +29 -2
- package/scripts/continuous-monitor.sh +13 -0
- package/scripts/cost/track-claude-usage.mjs +15 -0
- package/scripts/daemon/agent-daemon.mjs +408 -20
- package/scripts/daemon/assurance.mjs +48 -12
- package/scripts/daemon/cadence-consumer.mjs +218 -68
- package/scripts/daemon/cadence-handlers.mjs +73 -4
- package/scripts/daemon/classifier.mjs +75 -26
- package/scripts/daemon/context-compiler.mjs +104 -59
- package/scripts/daemon/deliver.mjs +30 -1
- package/scripts/daemon/dispatcher.mjs +804 -157
- package/scripts/daemon/health.mjs +14 -1
- package/scripts/daemon/lib/session-router.mjs +310 -42
- package/scripts/daemon/maestro-daemon.mjs +11 -0
- package/scripts/daemon/prompt-builder.mjs +121 -12
- package/scripts/daemon/responder.mjs +315 -146
- package/scripts/daemon/sdk-version.mjs +98 -16
- package/scripts/eval/probe-gateway.mjs +635 -0
- package/scripts/eval/replay/extract.mjs +270 -0
- package/scripts/eval/replay/grade.mjs +260 -0
- package/scripts/eval/replay/lib/config.mjs +50 -0
- package/scripts/eval/replay/lib/effects.mjs +65 -0
- package/scripts/eval/replay/lib/fixture.mjs +188 -0
- package/scripts/eval/replay/lib/judge.mjs +72 -0
- package/scripts/eval/replay/lib/redact.mjs +136 -0
- package/scripts/eval/replay/lib/sandbox.mjs +170 -0
- package/scripts/eval/replay/lib/schema-check.mjs +63 -0
- package/scripts/eval/replay/lib/transcript.mjs +76 -0
- package/scripts/eval/replay/mcp-replay-stub.mjs +101 -0
- package/scripts/eval/replay/report.mjs +185 -0
- package/scripts/eval/replay/run.mjs +404 -0
- package/scripts/fleet/rollout.mjs +1094 -0
- package/scripts/hooks/pre-send-audit.sh +36 -245
- package/scripts/hooks/pre-write-yaml-validate.mjs +275 -0
- package/scripts/hooks/validate-state-yaml.sh +190 -0
- package/scripts/huddle/huddle-llm.mjs +361 -0
- package/scripts/huddle/huddle-server.mjs +46 -121
- package/scripts/local-triggers/autoupdate.sh +448 -78
- package/scripts/local-triggers/run-trigger.sh +13 -0
- package/scripts/maintenance/pin-integrity.mjs +364 -0
- package/scripts/poll-slack-events.sh +41 -9
- package/scripts/poller/slack-socket-mode.mjs +28 -3
- package/scripts/session/supervisor.mjs +80 -13
- package/scripts/spawn-session.sh +13 -0
- package/bin/maestro.test.mjs +0 -1574
- package/lib/action-executor.test.mjs +0 -871
- package/lib/archetype.test.mjs +0 -132
- package/lib/assurance/plan-note.test.mjs +0 -234
- package/lib/assurance/room-budget.test.mjs +0 -486
- package/lib/assurance/tier.test.mjs +0 -174
- package/lib/autonomy.test.mjs +0 -66
- package/lib/backlog.test.mjs +0 -302
- package/lib/backup/policy.test.mjs +0 -305
- package/lib/budget-escalate.test.mjs +0 -232
- package/lib/budget-guard.envelope.test.mjs +0 -476
- package/lib/budget-guard.test.mjs +0 -427
- package/lib/cadence-bus-requeue.test.mjs +0 -83
- package/lib/cadence-bus-schedule.test.mjs +0 -194
- package/lib/cadence-bus.test.mjs +0 -720
- package/lib/cadences.test.mjs +0 -230
- package/lib/capability/inventory.test.mjs +0 -232
- package/lib/capability.test.mjs +0 -78
- package/lib/channels/base-adapter.test.mjs +0 -590
- package/lib/channels/channels.test.mjs +0 -371
- package/lib/channels/contract.test.mjs +0 -162
- package/lib/channels/inbox-item.test.mjs +0 -368
- package/lib/channels/orgmail/adapter.test.mjs +0 -448
- package/lib/channels/pairing.test.mjs +0 -270
- package/lib/channels/repeat-suppressor.test.mjs +0 -134
- package/lib/channels/slack-adapter.test.mjs +0 -212
- package/lib/channels/telegram-adapter.test.mjs +0 -306
- package/lib/channels/voice/adapter.test.mjs +0 -278
- package/lib/channels/whatsapp/adapter-baileys.test.mjs +0 -359
- package/lib/channels/whatsapp/baileys-typing.test.mjs +0 -154
- package/lib/charter.test.mjs +0 -89
- package/lib/claude-bin.test.mjs +0 -131
- package/lib/cli/board.test.mjs +0 -227
- package/lib/cli/design.test.mjs +0 -270
- package/lib/cli/doctor-checks.test.mjs +0 -336
- package/lib/cli/global-setup-extras.test.mjs +0 -462
- package/lib/cli/inbox.test.mjs +0 -230
- package/lib/cli/session-ack.test.mjs +0 -63
- package/lib/cli/session.test.mjs +0 -613
- package/lib/collective/capture.test.mjs +0 -121
- package/lib/collective/cards.test.mjs +0 -114
- package/lib/collective/config.test.mjs +0 -123
- package/lib/collective/global-config.test.mjs +0 -220
- package/lib/collective/global-skills.test.mjs +0 -126
- package/lib/collective/presence.test.mjs +0 -95
- package/lib/collective/recall.test.mjs +0 -116
- package/lib/collective/vendor-skills.test.mjs +0 -306
- package/lib/comms/send-gate.test.mjs +0 -770
- package/lib/comms.test.mjs +0 -41
- package/lib/cost/ledger-row.test.mjs +0 -183
- package/lib/design/design-md.test.mjs +0 -318
- package/lib/design/fixtures/DESIGN.golden.md +0 -238
- package/lib/design/fixtures/PRODUCT.golden.md +0 -67
- package/lib/design/fixtures/foundation.json +0 -133
- package/lib/design/refresh-gate.test.mjs +0 -144
- package/lib/design/write.test.mjs +0 -241
- package/lib/diagnostics/alerts.test.mjs +0 -318
- package/lib/diagnostics/backup-freshness.test.mjs +0 -185
- package/lib/diagnostics/counters.test.mjs +0 -206
- package/lib/diagnostics/events.test.mjs +0 -290
- package/lib/diagnostics/otel.test.mjs +0 -196
- package/lib/diagnostics/trace.test.mjs +0 -251
- package/lib/env-compat.test.mjs +0 -104
- package/lib/execution/disposition.test.mjs +0 -553
- package/lib/execution/drive.test.mjs +0 -270
- package/lib/execution/effects.test.mjs +0 -344
- package/lib/execution/intake.test.mjs +0 -389
- package/lib/execution/journal.test.mjs +0 -261
- package/lib/execution/match.test.mjs +0 -235
- package/lib/execution/pipeline.test.mjs +0 -392
- package/lib/execution/route.test.mjs +0 -186
- package/lib/execution/surface-policy.test.mjs +0 -162
- package/lib/fs-atomic.test.mjs +0 -72
- package/lib/fs-ownership.test.mjs +0 -158
- package/lib/goals/admission.test.mjs +0 -164
- package/lib/goals/classify.test.mjs +0 -167
- package/lib/goals/collaborate.test.mjs +0 -336
- package/lib/goals/gaps.test.mjs +0 -284
- package/lib/goals/loop.test.mjs +0 -845
- package/lib/hooks/bus.test.mjs +0 -387
- package/lib/identity/persona.test.mjs +0 -142
- package/lib/kpi-sensors.test.mjs +0 -278
- package/lib/kpi.test.mjs +0 -244
- package/lib/learning/config.test.mjs +0 -75
- package/lib/learning/counters.test.mjs +0 -69
- package/lib/learning/curator-consolidate.test.mjs +0 -238
- package/lib/learning/curator.test.mjs +0 -106
- package/lib/learning/reflect.test.mjs +0 -0
- package/lib/learning/session-index.test.mjs +0 -125
- package/lib/learning/skill-writer.test.mjs +0 -210
- package/lib/mandate/audit.test.mjs +0 -195
- package/lib/mandate/contract.test.mjs +0 -185
- package/lib/mandate/derive.test.mjs +0 -274
- package/lib/mandate/model.test.mjs +0 -164
- package/lib/mandate/refresh.test.mjs +0 -389
- package/lib/mcp/server.test.mjs +0 -426
- package/lib/model-router/auth-profiles.test.mjs +0 -580
- package/lib/model-router/catalog.test.mjs +0 -385
- package/lib/model-router/economics.test.mjs +0 -438
- package/lib/model-router/failover.test.mjs +0 -439
- package/lib/model-router/health.test.mjs +0 -338
- package/lib/model-router/integration-coverage.test.mjs +0 -831
- package/lib/model-router/integration.test.mjs +0 -564
- package/lib/model-router/ledger.test.mjs +0 -415
- package/lib/model-router/llm-task.test.mjs +0 -392
- package/lib/model-router/org-credentials.test.mjs +0 -265
- package/lib/model-router/pricing-refresh.test.mjs +0 -286
- package/lib/model-router/reconcile.test.mjs +0 -316
- package/lib/model-router/repair.test.mjs +0 -180
- package/lib/model-router/spawn.test.mjs +0 -446
- package/lib/model-router/taxonomy.test.mjs +0 -410
- package/lib/model-router.test.mjs +0 -1207
- package/lib/org/activity.test.mjs +0 -134
- package/lib/org/approvals.test.mjs +0 -216
- package/lib/org/awareness.test.mjs +0 -159
- package/lib/org/board-mine-cache.test.mjs +0 -53
- package/lib/org/board.test.mjs +0 -187
- package/lib/org/bootstrap-context.test.mjs +0 -153
- package/lib/org/client.test.mjs +0 -1206
- package/lib/org/cohort-client.test.mjs +0 -126
- package/lib/org/cost-sync.test.mjs +0 -153
- package/lib/org/doctor.test.mjs +0 -346
- package/lib/org/engagement-ledger.test.mjs +0 -112
- package/lib/org/engagement.test.mjs +0 -739
- package/lib/org/handoff.test.mjs +0 -269
- package/lib/org/inbound/directedness.test.mjs +0 -668
- package/lib/org/inbound/facts.test.mjs +0 -471
- package/lib/org/inbound/hydrate.test.mjs +0 -453
- package/lib/org/inbound/index.test.mjs +0 -429
- package/lib/org/inbound/project.test.mjs +0 -287
- package/lib/org/integration-tools.test.mjs +0 -160
- package/lib/org/keys.test.mjs +0 -92
- package/lib/org/knowledge.test.mjs +0 -326
- package/lib/org/leases.test.mjs +0 -235
- package/lib/org/mesh-directives.test.mjs +0 -110
- package/lib/org/mesh-integration.test.mjs +0 -127
- package/lib/org/mesh.test.mjs +0 -400
- package/lib/org/messaging.test.mjs +0 -471
- package/lib/org/param-contract.test.mjs +0 -477
- package/lib/org/policy.test.mjs +0 -237
- package/lib/org/protocol.checksum.test.mjs +0 -90
- package/lib/org/protocol.test.mjs +0 -323
- package/lib/org/push.test.mjs +0 -792
- package/lib/org/registry.test.mjs +0 -100
- package/lib/org/resource-tools.test.mjs +0 -361
- package/lib/org/tool-access.test.mjs +0 -144
- package/lib/org/tool-surface-integration.test.mjs +0 -120
- package/lib/org/tool-surface.test.mjs +0 -1268
- package/lib/org/typing.test.mjs +0 -291
- package/lib/org/ui-parity.test.mjs +0 -560
- package/lib/org/verify.test.mjs +0 -194
- package/lib/org/work-ledger.test.mjs +0 -273
- package/lib/plan/adoption-e2e.test.mjs +0 -366
- package/lib/plan/budget-enforcement.test.mjs +0 -400
- package/lib/plan/compile.test.mjs +0 -382
- package/lib/plan/emit.test.mjs +0 -269
- package/lib/plan/explain.test.mjs +0 -188
- package/lib/prompts/parallelism.test.mjs +0 -177
- package/lib/rag/rag.test.mjs +0 -505
- package/lib/rate-guard.test.mjs +0 -272
- package/lib/reactive-gate.test.mjs +0 -57
- package/lib/render.test.mjs +0 -68
- package/lib/resource-governor.test.mjs +0 -488
- package/lib/scheduling/dynamic-jobs.test.mjs +0 -344
- package/lib/scheduling/jitter.test.mjs +0 -140
- package/lib/secrets/broker.test.mjs +0 -280
- package/lib/secrets/providers.test.mjs +0 -274
- package/lib/security/audit-engine.test.mjs +0 -424
- package/lib/security/coerce-args.test.mjs +0 -281
- package/lib/security/dangerous-tools.test.mjs +0 -68
- package/lib/security/external-content.test.mjs +0 -84
- package/lib/security/redact.test.mjs +0 -441
- package/lib/security/secret-equal.test.mjs +0 -55
- package/lib/session/config.test.mjs +0 -92
- package/lib/session/feed-core.test.mjs +0 -198
- package/lib/session/first-run.test.mjs +0 -121
- package/lib/session/frontdoor.test.mjs +0 -205
- package/lib/session/handoffs.test.mjs +0 -183
- package/lib/session/identity.test.mjs +0 -180
- package/lib/session/inbox-claims.test.mjs +0 -286
- package/lib/session/launch-args.test.mjs +0 -157
- package/lib/session/liveness.test.mjs +0 -100
- package/lib/session/status-summary.test.mjs +0 -118
- package/lib/session-permissions.test.mjs +0 -120
- package/lib/setup/claude-probe.test.mjs +0 -187
- package/lib/setup/completeness.test.mjs +0 -110
- package/lib/setup/context-pack.test.mjs +0 -89
- package/lib/setup/enrich.test.mjs +0 -115
- package/lib/setup/enroll-from-cohort.test.mjs +0 -300
- package/lib/setup/integration.test.mjs +0 -162
- package/lib/setup/io.test.mjs +0 -77
- package/lib/setup/runner.test.mjs +0 -132
- package/lib/setup/sections/identity.test.mjs +0 -234
- package/lib/setup/sections/inventory.test.mjs +0 -198
- package/lib/setup/sections/learning.test.mjs +0 -81
- package/lib/setup/sections/mandate.test.mjs +0 -388
- package/lib/setup/sections/messaging.test.mjs +0 -127
- package/lib/setup/sections/model.test.mjs +0 -240
- package/lib/setup/sections/org.test.mjs +0 -346
- package/lib/setup/sections/orgmail.test.mjs +0 -118
- package/lib/setup/sections/recovery.test.mjs +0 -98
- package/lib/setup/sections/subagents.test.mjs +0 -429
- package/lib/setup/sections/verify.test.mjs +0 -175
- package/lib/setup/sot.test.mjs +0 -81
- package/lib/setup/state.test.mjs +0 -115
- package/lib/singleton.test.mjs +0 -151
- package/lib/subagents/cli.test.mjs +0 -389
- package/lib/subagents/client.test.mjs +0 -309
- package/lib/subagents/gap.test.mjs +0 -234
- package/lib/subagents/lock.test.mjs +0 -248
- package/lib/subagents/manifest.test.mjs +0 -175
- package/lib/subagents/refs.test.mjs +0 -204
- package/lib/subagents/resolve.test.mjs +0 -422
- package/lib/subagents/schema.test.mjs +0 -328
- package/lib/telemetry/alerts.test.mjs +0 -109
- package/lib/telemetry/collect.test.mjs +0 -1274
- package/lib/tool-definitions-integration.test.mjs +0 -83
- package/lib/tool-definitions.test.mjs +0 -437
- package/lib/upgrade/global-refresh.test.mjs +0 -65
- package/lib/upgrade/launchd-reconcile.test.mjs +0 -272
- package/lib/upgrade/post-steps.test.mjs +0 -200
- package/lib/upgrade/verify.test.mjs +0 -164
- package/lib/util/fetch-timeout.test.mjs +0 -202
- package/lib/util/reconnect.test.mjs +0 -369
- package/lib/util/unhandled.test.mjs +0 -216
- package/lib/voice/outbound.test.mjs +0 -69
- package/lib/voice/session-rotation.test.mjs +0 -114
- package/lib/voice/stt.test.mjs +0 -226
- package/lib/voice/voice.test.mjs +0 -990
- package/scripts/cadence/enqueue-cadence-tick.test.mjs +0 -187
- package/scripts/ci/check-docs-accuracy.test.mjs +0 -409
- package/scripts/ci/check-durable-write-seam.test.mjs +0 -90
- package/scripts/ci/check-no-build-artifacts.test.mjs +0 -71
- package/scripts/ci/check-no-residual-identity.test.mjs +0 -202
- package/scripts/ci/check-skill-packs.test.mjs +0 -495
- package/scripts/ci/check-subagent-frontmatter.test.mjs +0 -124
- package/scripts/ci/check.test.mjs +0 -194
- package/scripts/ci/conformance-org-api.test.mjs +0 -425
- package/scripts/cloud-relay/voice/relay-identity.test.mjs +0 -96
- package/scripts/collective/hook-runner.test.mjs +0 -173
- package/scripts/cost/fleet-digest.test.mjs +0 -207
- package/scripts/cost/track-claude-usage-pricing.test.mjs +0 -183
- package/scripts/cost/track-claude-usage.test.mjs +0 -148
- package/scripts/daemon/agent-daemon-board-mine.test.mjs +0 -96
- package/scripts/daemon/agent-daemon-design.test.mjs +0 -238
- package/scripts/daemon/agent-daemon-frontdoor.test.mjs +0 -60
- package/scripts/daemon/agent-daemon.test.mjs +0 -995
- package/scripts/daemon/assurance-e2e.test.mjs +0 -613
- package/scripts/daemon/assurance.test.mjs +0 -1791
- package/scripts/daemon/board-mirror.test.mjs +0 -165
- package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +0 -393
- package/scripts/daemon/cadence-consumer-governance.test.mjs +0 -276
- package/scripts/daemon/cadence-consumer.test.mjs +0 -776
- package/scripts/daemon/cadence-handlers.test.mjs +0 -837
- package/scripts/daemon/classifier-identity.test.mjs +0 -137
- package/scripts/daemon/classifier.test.mjs +0 -266
- package/scripts/daemon/classify-kind.test.mjs +0 -40
- package/scripts/daemon/context-compiler.test.mjs +0 -300
- package/scripts/daemon/deliver.test.mjs +0 -564
- package/scripts/daemon/dispatcher-cooldown.test.mjs +0 -122
- package/scripts/daemon/dispatcher-governance.test.mjs +0 -1013
- package/scripts/daemon/dispatcher-resume.test.mjs +0 -166
- package/scripts/daemon/execution-ladder.test.mjs +0 -470
- package/scripts/daemon/goal-steward-cadence.test.mjs +0 -312
- package/scripts/daemon/inbox-deferral-session.test.mjs +0 -49
- package/scripts/daemon/inbox-deferral.test.mjs +0 -336
- package/scripts/daemon/inbox-wake.test.mjs +0 -199
- package/scripts/daemon/integration.test.mjs +0 -149
- package/scripts/daemon/lib/self-echo.test.mjs +0 -153
- package/scripts/daemon/lib/session-router.test.mjs +0 -295
- package/scripts/daemon/prompt-builder-preamble.test.mjs +0 -210
- package/scripts/daemon/prompt-builder.test.mjs +0 -344
- package/scripts/daemon/responder-cost.test.mjs +0 -68
- package/scripts/daemon/responder-history.test.mjs +0 -185
- package/scripts/daemon/sdk-version.test.mjs +0 -31
- package/scripts/daemon/session-lock.test.mjs +0 -252
- package/scripts/daemon/session-outcomes.test.mjs +0 -533
- package/scripts/daemon/typing-registry.test.mjs +0 -102
- package/scripts/hooks/pre-send-audit.test.mjs +0 -354
- package/scripts/huddle/huddle-prompt.test.mjs +0 -176
- package/scripts/local-triggers/autoupdate.test.mjs +0 -518
- package/scripts/local-triggers/generate-plists.test.mjs +0 -456
- package/scripts/media-generation/brand-clause.test.mjs +0 -135
- package/scripts/org/send-orgmail.first-contact.test.mjs +0 -102
- package/scripts/poller/inbox-privilege-injection.test.mjs +0 -167
- package/scripts/poller/inbox-scan-poller.test.mjs +0 -295
- package/scripts/poller/lib/cloud-relay-dedup.test.mjs +0 -133
- package/scripts/poller/slack-socket-mode.test.mjs +0 -805
- package/scripts/poller-launchd/install.test.mjs +0 -243
- package/scripts/restore-from-backup.test.mjs +0 -181
- package/scripts/session/feed.test.mjs +0 -196
- package/scripts/session/supervisor-sh.test.mjs +0 -218
- package/scripts/session/supervisor.test.mjs +0 -482
- package/scripts/setup/configure-macos.test.mjs +0 -306
- package/scripts/setup/gen-subagent-manifest.test.mjs +0 -124
- package/scripts/setup/generate-agent-package-json.test.mjs +0 -143
- package/scripts/setup/generate-capability.test.mjs +0 -134
- package/scripts/setup/init-agent.test.mjs +0 -370
- package/scripts/setup/init-skill-marketplace.test.mjs +0 -193
- package/scripts/vendor/sync-skill-packs.test.mjs +0 -103
- package/scripts/watchdog/memory-watchdog.test.mjs +0 -64
|
@@ -0,0 +1,748 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/engine/agents/runtime.mjs — running subagents: the Task tool (also named
|
|
3
|
+
* Agent), TaskOutput, TaskStop, background tasks and their notifications.
|
|
4
|
+
*
|
|
5
|
+
* A subagent run is a child loop (loop.mjs) with:
|
|
6
|
+
* · a fresh context: its first message is the Task prompt; its system prompt
|
|
7
|
+
* is the engine's base prompt, the run's instructions, and the definition's
|
|
8
|
+
* own prompt (see `subagentPrompt`);
|
|
9
|
+
* · the parent's rules, gates and hooks: its guard is built from the same
|
|
10
|
+
* settings, in the STRICTER of the parent's mode and the definition's
|
|
11
|
+
* `permissionMode` — a child can never loosen its parent's posture;
|
|
12
|
+
* · tools: the definition's list intersected with the parent's tools, minus
|
|
13
|
+
* its disallowed tools. A tool with `forChild()` (the session Bash, TodoWrite)
|
|
14
|
+
* is given as the child's own copy;
|
|
15
|
+
* · a tier from its `model` through the tier map (definitions.mjs);
|
|
16
|
+
* · depth: the main run is depth 0 and a child may start children only while
|
|
17
|
+
* depth + 1 < maxDepth (default 1: subagents cannot start subagents);
|
|
18
|
+
* · attribution headers on every model request: x-cohort-session-id (the main
|
|
19
|
+
* session), x-cohort-agent-id (this child), x-cohort-parent-agent-id (the
|
|
20
|
+
* main session id, or the agent that started it);
|
|
21
|
+
* · SubagentStop hooks consulted before it ends (a blocking hook gives the
|
|
22
|
+
* child another turn, exactly as Stop does for the main run); the input adds
|
|
23
|
+
* agent_id, agent_type and agent_transcript_path;
|
|
24
|
+
* · the permission check repeated on the RESOLVED definition name before a
|
|
25
|
+
* child starts (`checkSubagent`), so a padded or unqualified spelling cannot
|
|
26
|
+
* pass a `Task(<type>)` deny or ask rule the guard matched by spelling;
|
|
27
|
+
* · a sidechain transcript: <sessions>/subagents/<session id>/<agent id>.jsonl,
|
|
28
|
+
* whose header record carries isSidechain, parentSessionId, parentAgentId,
|
|
29
|
+
* parentToolUseId and agentType;
|
|
30
|
+
* · a usage record (tier, usage, cost micros, request ids) handed to the owner
|
|
31
|
+
* for the parent's result (usage.mjs); permission refusals are added to the
|
|
32
|
+
* parent's `permission_denials`. Model calls the child's run makes outside
|
|
33
|
+
* its turns (a compaction summary, WebFetch, WebSearch) reach the child
|
|
34
|
+
* loop's `recordModelCall`, so they are in that record too;
|
|
35
|
+
* · its own context (`prepareChild`, W3 integration): a context manager for
|
|
36
|
+
* the child's tier and tools, so a long child compacts on its own, and its
|
|
37
|
+
* own deferred-tool state when its MCP tools are deferred;
|
|
38
|
+
*
|
|
39
|
+
* Foreground: the Task call returns the child's final text. Background
|
|
40
|
+
* (`run_in_background`): the call returns a task id at once; when the child
|
|
41
|
+
* ends, a notification is queued and the loop delivers it as the next user
|
|
42
|
+
* message (`drainNotifications`). A loop whose model has finished while a
|
|
43
|
+
* background task is still running waits for it (`awaitNotifications`), within
|
|
44
|
+
* the run's time limit, so a headless run never ends with unreported work.
|
|
45
|
+
* TaskOutput {task_id, block?, timeout?} reads a task (or a background shell,
|
|
46
|
+
* or a workflow run) early; TaskStop {task_id} stops one. A task read or
|
|
47
|
+
* stopped by the model is not notified again.
|
|
48
|
+
*
|
|
49
|
+
* Surviving a restart (W4-E2, conformance row 23). Every task the Task tool
|
|
50
|
+
* starts keeps its state in <agent id>.state.json beside its transcript
|
|
51
|
+
* (persist.mjs): definition snapshot, prompt, the mode it was clamped to,
|
|
52
|
+
* status, final text, whether it was reported. A runtime created for a
|
|
53
|
+
* continued session reads its session's states back: finished tasks answer
|
|
54
|
+
* TaskOutput as before, and a task recorded as running by a process that is
|
|
55
|
+
* gone is `interrupted` — reported once as a notification and by TaskOutput,
|
|
56
|
+
* with the way to continue it. `Task {resume: <agent id>, prompt}` continues a
|
|
57
|
+
* task that is not running from its sidechain transcript: the same agent id and
|
|
58
|
+
* transcript, the definition of that name (or its snapshot), in the stricter of
|
|
59
|
+
* the mode it ran in and the mode this run would give it, with this run's tools
|
|
60
|
+
* at most. Workflow `agent()` children are not tasks: a workflow recovers from
|
|
61
|
+
* its journal (`resumeFromRunId`).
|
|
62
|
+
*
|
|
63
|
+
* Tool descriptions are original (see docs/engine/clean-room.md).
|
|
64
|
+
*
|
|
65
|
+
* @module lib/engine/agents/runtime
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
import { randomBytes } from "node:crypto";
|
|
69
|
+
import path from "node:path";
|
|
70
|
+
import { runLoop } from "../loop.mjs";
|
|
71
|
+
import { userText } from "../messages.mjs";
|
|
72
|
+
import { classifyStop } from "../output/json.mjs";
|
|
73
|
+
import { appendMessage, appendResult, openSession, sessionPath } from "../session/store.mjs";
|
|
74
|
+
import { AGENT_TOOL_NAMES, DEFAULT_TIER_MAP, childToolNames, clampPermissionMode, findAgent, resolveAgentModel } from "./definitions.mjs";
|
|
75
|
+
import { recordFromOutcome } from "./usage.mjs";
|
|
76
|
+
import { transcriptMessage } from "../context/images.mjs";
|
|
77
|
+
import { resumeHistory } from "../context/compaction.mjs";
|
|
78
|
+
import { TASK_STATE_VERSION, definitionForResume, isAgentId, readTaskState, readTaskStates, reconcileTaskState, subagentDir, taskOwnerState, writeTaskState } from "./persist.mjs";
|
|
79
|
+
import { PROCESS_NONCE, processAlive, processStartToken } from "../process-identity.mjs";
|
|
80
|
+
|
|
81
|
+
/** Agent ids with a child running in THIS process, whichever runtime started it (a host may rebuild one). */
|
|
82
|
+
const PROCESS_LIVE_TASKS = new Set();
|
|
83
|
+
|
|
84
|
+
export const DEFAULT_MAX_AGENT_DEPTH = 1;
|
|
85
|
+
export const TASK_OUTPUT_DEFAULT_TIMEOUT_MS = 30_000;
|
|
86
|
+
export const TASK_OUTPUT_MAX_TIMEOUT_MS = 600_000;
|
|
87
|
+
const STOP_WAIT_MS = 10_000;
|
|
88
|
+
const LISTED_DESCRIPTION_CHARS = 300;
|
|
89
|
+
const WORKFLOW_RUN_ID_RE = /^wf_[0-9a-f]{12}$/;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The part of a child's system prompt that makes it a subagent.
|
|
93
|
+
* Authorship: written for Cohort Engine by the Cohort team, 2026-09-14.
|
|
94
|
+
* @param {import('./definitions.mjs').AgentDefinition} def
|
|
95
|
+
*/
|
|
96
|
+
export function subagentPrompt(def) {
|
|
97
|
+
const own = def.prompt || def.description;
|
|
98
|
+
return `You are running as the "${def.name}" subagent: another agent handed you one task. It cannot see your working, only your final message, so end with a complete report of what you found or did.\n\n${own}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** @param {import('./definitions.mjs').AgentDefinition[]} agents */
|
|
102
|
+
export function formatAgentListing(agents) {
|
|
103
|
+
return agents
|
|
104
|
+
.map((a) => {
|
|
105
|
+
const d = a.description.length <= LISTED_DESCRIPTION_CHARS ? a.description : `${a.description.slice(0, LISTED_DESCRIPTION_CHARS - 1)}…`;
|
|
106
|
+
return `- ${a.name}: ${d}`;
|
|
107
|
+
})
|
|
108
|
+
.join("\n");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function newAgentId() {
|
|
112
|
+
return `agent-${randomBytes(8).toString("hex")}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* How the model continues an interrupted or finished task (pure).
|
|
117
|
+
* @param {string} id
|
|
118
|
+
*/
|
|
119
|
+
export function resumeHint(id) {
|
|
120
|
+
return `Task {"resume":"${id}","description":"<a short label>","prompt":"<what to do next>"} continues it from its transcript.`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @typedef {Object} WorkflowRuns the session's workflow runtime, as TaskOutput and TaskStop use it
|
|
125
|
+
* @property {(runId:string) => boolean} has
|
|
126
|
+
* @property {(runId:string, o:{block?:boolean, timeoutMs?:number, signal?:AbortSignal}) => Promise<any>} status
|
|
127
|
+
* @property {(runId:string) => Promise<{ok:true, content:string}|{ok:false, error:string}>} stop
|
|
128
|
+
* @property {(runId:string) => ({ok:true, content:string, running:boolean}|{ok:false, error:string})} [diskStatus]
|
|
129
|
+
*/
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @typedef {Object} AgentRuntimeDeps
|
|
133
|
+
* @property {import('./definitions.mjs').AgentDefinition[]} agents
|
|
134
|
+
* @property {string} sessionId
|
|
135
|
+
* @property {string} sessionsDir
|
|
136
|
+
* @property {string} cwd
|
|
137
|
+
* @property {string} wire
|
|
138
|
+
* @property {string} parentModel the tier of the run that owns this runtime
|
|
139
|
+
* @property {string} parentMode
|
|
140
|
+
* @property {string} parentAgentId the session id at depth 0
|
|
141
|
+
* @property {number} [depth]
|
|
142
|
+
* @property {number} [maxDepth]
|
|
143
|
+
* @property {(o:{model:string, headers:Record<string,string>, parentToolUseId:string|null}) => (turn:any)=>Promise<any>} createCaller
|
|
144
|
+
* @property {(o:{mode:string, agentId?:string, cwd?:string}) => any} createGuard
|
|
145
|
+
* @property {(o:{def:import('./definitions.mjs').AgentDefinition, toolNames:string[], notes?:string|null}) => string} buildSystem
|
|
146
|
+
* @property {(o:{headers:Record<string,string>, agentId:string}) => any} createToolContext the child's tool context; web tools attribute their requests with `headers`
|
|
147
|
+
* @property {(o:{def:import('./definitions.mjs').AgentDefinition, model:string, tools:any[], headers:Record<string,string>, agentId:string, parentToolUseId:string|null, onCompact:(message:any)=>void}) => ({tools:any[], sent:any[], notes:string|null, context:any}|null)} [prepareChild]
|
|
148
|
+
* the child's tool list (ToolSearch bound to its own state), the tools whose definitions are sent,
|
|
149
|
+
* system-prompt notes and the loop's `context`; null = the tools as given, no context manager
|
|
150
|
+
* @property {any} [hooks]
|
|
151
|
+
* @property {(message:any, o:{parentToolUseId:string|null, agentId:string}) => void} [onMessage]
|
|
152
|
+
* @property {(denials:any[], meta:{turn:number|null}) => void} [onDenials] `turn`: `currentTurn()` when the child started
|
|
153
|
+
* @property {() => number} [currentTurn]
|
|
154
|
+
* @property {(o:{toolName:string, input:any, mode:string}) => {behavior:'allow'|'deny'|'ask', reason:string}} [checkSubagent]
|
|
155
|
+
* the owner's permission evaluation for a Task call (permissions.mjs evaluatePermission)
|
|
156
|
+
* @property {(record:any) => void} [onRecord]
|
|
157
|
+
* @property {() => void} [onNotify] a background task's notification was queued (W4-A1: wakes an idle `cohort session`)
|
|
158
|
+
* @property {(line:string) => void} [log]
|
|
159
|
+
* @property {Record<string,string>} [tierMap]
|
|
160
|
+
* @property {number} [maxTurns]
|
|
161
|
+
* @property {import('../budget.mjs').BudgetMeter|null} [budget] the run's spend cap, shared with every child (W4-E1)
|
|
162
|
+
* @property {number} [wallClockMs]
|
|
163
|
+
* @property {() => number} [now]
|
|
164
|
+
* @property {{has:(id:string)=>boolean, read:(id:string, f?:string)=>any, wait:(id:string, ms:number, s?:AbortSignal)=>Promise<void>, kill:(id:string)=>Promise<any>}|null} [shells]
|
|
165
|
+
* @property {number} [pid] this process (default process.pid): recorded in task states
|
|
166
|
+
* @property {string} [nonce] this process's nonce (default process-identity.mjs PROCESS_NONCE)
|
|
167
|
+
* @property {(pid:number) => boolean} [isProcessAlive] whether a restored running task's process still lives
|
|
168
|
+
* @property {(pid:number) => string|null} [processStartToken] the OS start time of a PID, to catch a reused PID
|
|
169
|
+
* @property {boolean} [restore] read the session's task states back (default: at depth 0)
|
|
170
|
+
*/
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* @typedef {Object} BackgroundTask
|
|
174
|
+
* @property {string} id
|
|
175
|
+
* @property {string} agentType
|
|
176
|
+
* @property {string} description
|
|
177
|
+
* @property {'running'|'completed'|'failed'|'stopped'|'interrupted'} status
|
|
178
|
+
* @property {string} text
|
|
179
|
+
* @property {string|null} error
|
|
180
|
+
* @property {boolean} notified
|
|
181
|
+
* @property {boolean} stopRequested
|
|
182
|
+
* @property {AbortController} controller
|
|
183
|
+
* @property {Promise<void>} done
|
|
184
|
+
* @property {boolean} background
|
|
185
|
+
* @property {import('./persist.mjs').TaskState|null} state what is kept on disk
|
|
186
|
+
* @property {boolean} elsewhere restored as running in another live process: not this run's to wait for or stop
|
|
187
|
+
*/
|
|
188
|
+
|
|
189
|
+
/** @param {AgentRuntimeDeps} d */
|
|
190
|
+
export function createAgentRuntime(d) {
|
|
191
|
+
const depth = d.depth ?? 0;
|
|
192
|
+
const maxDepth = d.maxDepth ?? DEFAULT_MAX_AGENT_DEPTH;
|
|
193
|
+
const now = d.now ?? Date.now;
|
|
194
|
+
const pid = d.pid ?? process.pid;
|
|
195
|
+
const nonce = d.nonce ?? PROCESS_NONCE;
|
|
196
|
+
const isAlive = d.isProcessAlive ?? processAlive;
|
|
197
|
+
const startTokenOf = d.processStartToken ?? processStartToken;
|
|
198
|
+
/** How a recorded owner is judged (W4-E2 review: PID, nonce and start time, not the PID alone). */
|
|
199
|
+
const ownerCheck = { self: { pid, nonce }, probe: { isAlive, startToken: startTokenOf }, heldHere: (/** @type string */ agentId) => PROCESS_LIVE_TASKS.has(agentId) };
|
|
200
|
+
const iso = () => new Date(now()).toISOString();
|
|
201
|
+
const canSpawn = depth + 1 < maxDepth;
|
|
202
|
+
/** @type {Map<string, BackgroundTask>} */
|
|
203
|
+
const tasks = new Map();
|
|
204
|
+
/** Agent ids with a child running in this process (foreground or background). */
|
|
205
|
+
const live = new Set();
|
|
206
|
+
/** @type {Array<{taskId:string, text:string}>} */
|
|
207
|
+
const queue = [];
|
|
208
|
+
/** @type {Set<() => void>} */
|
|
209
|
+
const waiters = new Set();
|
|
210
|
+
/** Usage records of finished children (this runtime's and nested ones). */
|
|
211
|
+
const records = [];
|
|
212
|
+
/** @type {() => any[]} */
|
|
213
|
+
let parentTools = () => [];
|
|
214
|
+
/** @type {WorkflowRuns|null} */
|
|
215
|
+
let workflowRuns = null;
|
|
216
|
+
let saveFailureLogged = false;
|
|
217
|
+
|
|
218
|
+
const addRecord = (/** @type any */ r) => {
|
|
219
|
+
records.push(r);
|
|
220
|
+
d.onRecord?.(r);
|
|
221
|
+
};
|
|
222
|
+
const wake = () => {
|
|
223
|
+
for (const w of [...waiters]) w();
|
|
224
|
+
waiters.clear();
|
|
225
|
+
};
|
|
226
|
+
/** A task this run is responsible for waiting on and stopping: one it holds a live child for. @param {BackgroundTask} t */
|
|
227
|
+
const runningHere = (t) => t.status === "running" && !t.elsewhere && live.has(t.id);
|
|
228
|
+
/** @param {string} id */
|
|
229
|
+
const hold = (id) => {
|
|
230
|
+
live.add(id);
|
|
231
|
+
PROCESS_LIVE_TASKS.add(id);
|
|
232
|
+
};
|
|
233
|
+
/** @param {string} id */
|
|
234
|
+
const release = (id) => {
|
|
235
|
+
live.delete(id);
|
|
236
|
+
PROCESS_LIVE_TASKS.delete(id);
|
|
237
|
+
};
|
|
238
|
+
/** Where a task restored as running is running, for the model. @param {BackgroundTask} t */
|
|
239
|
+
const elsewhereText = (t) => (t.state?.pid === pid ? "in another session runtime of this engine process" : `in another engine process (pid ${t.state?.pid})`);
|
|
240
|
+
|
|
241
|
+
/** Write a task's state (W4-E2). A failed write is logged once and costs only recoverability. @param {BackgroundTask} t */
|
|
242
|
+
const save = (t) => {
|
|
243
|
+
if (!t.state) return;
|
|
244
|
+
t.state = { ...t.state, status: t.status, text: t.text, error: t.error, notified: t.notified, endedAt: t.status === "running" ? null : (t.state.endedAt ?? iso()) };
|
|
245
|
+
if (!writeTaskState(d.sessionsDir, t.state) && !saveFailureLogged) {
|
|
246
|
+
saveFailureLogged = true;
|
|
247
|
+
d.log?.(`subagent ${t.agentType}: its state could not be written under ${subagentDir(d.sessionsDir, d.sessionId)}; it will not be recoverable after a restart`);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* W4-A2 (workflow children): `extraTools` are added to the child's tools after the definition's
|
|
253
|
+
* filter (a StructuredOutput tool); `effort` overrides the run's --effort for this child; `cwd`
|
|
254
|
+
* runs the child in another directory (a git worktree) — its tools, guard, prompt and Bash.
|
|
255
|
+
* W4-E2: `resume` continues the child's existing transcript (its history is the child's
|
|
256
|
+
* conversation so far, then `prompt`); `ceilingMode` clamps the child's mode further.
|
|
257
|
+
* @param {{def:import('./definitions.mjs').AgentDefinition, prompt:string, description:string, parentToolUseId:string|null, signal?:AbortSignal, agentId?:string, background:boolean, extraTools?:any[], effort?:string|null, cwd?:string|null, resume?:boolean, ceilingMode?:string|null}} p
|
|
258
|
+
* @returns {Promise<{ok:boolean, agentId:string, text:string, error:string|null, stopKind:string, record:any}>}
|
|
259
|
+
*/
|
|
260
|
+
async function runChild({ def, prompt, description, parentToolUseId, signal, agentId = newAgentId(), background, extraTools = [], effort = null, cwd = null, resume = false, ceilingMode = null }) {
|
|
261
|
+
const at = cwd ? { cwd } : {};
|
|
262
|
+
const turn = d.currentTurn ? d.currentTurn() : null;
|
|
263
|
+
const { model, note } = resolveAgentModel(def.model, d.parentModel, d.tierMap ?? DEFAULT_TIER_MAP);
|
|
264
|
+
if (note) d.log?.(`subagent ${def.name}: ${note}`);
|
|
265
|
+
const clamped = clampPermissionMode(d.parentMode, def.permissionMode);
|
|
266
|
+
const mode = ceilingMode ? clampPermissionMode(clamped, ceilingMode) : clamped;
|
|
267
|
+
|
|
268
|
+
const opened = openSession({
|
|
269
|
+
dir: subagentDir(d.sessionsDir, d.sessionId),
|
|
270
|
+
sessionId: agentId,
|
|
271
|
+
mode: resume ? "resume" : "new",
|
|
272
|
+
meta: { cwd: cwd ?? d.cwd, model, wire: d.wire, isSidechain: true, parentSessionId: d.sessionId, parentAgentId: d.parentAgentId, parentToolUseId, agentType: def.name, description, background },
|
|
273
|
+
now,
|
|
274
|
+
});
|
|
275
|
+
if (!opened.ok && resume) {
|
|
276
|
+
return { ok: false, agentId, text: "", error: `its transcript could not be reopened (${opened.error.message})`, stopKind: "error", record: null };
|
|
277
|
+
}
|
|
278
|
+
const transcript = opened.ok ? opened.session : null;
|
|
279
|
+
if (!opened.ok) d.log?.(`subagent ${def.name}: transcript not written (${opened.error.message})`);
|
|
280
|
+
const history = resume && transcript ? resumeHistory(transcript.messages) : [];
|
|
281
|
+
|
|
282
|
+
const available = parentTools();
|
|
283
|
+
const { names, missing } = childToolNames(available.map((t) => t.name), def, { canSpawn });
|
|
284
|
+
if (missing.length) d.log?.(`subagent ${def.name}: not available to it (the parent does not have them): ${missing.join(", ")}`);
|
|
285
|
+
|
|
286
|
+
const nested = canSpawn && names.some((n) => AGENT_TOOL_NAMES.includes(n))
|
|
287
|
+
? createAgentRuntime({ ...d, depth: depth + 1, parentModel: model, parentMode: mode, parentAgentId: agentId, onRecord: addRecord, onDenials: d.onDenials, restore: false })
|
|
288
|
+
: null;
|
|
289
|
+
const nestedTools = new Map((nested?.tools() ?? []).map((t) => [t.name, t]));
|
|
290
|
+
const granted = names
|
|
291
|
+
.map((n) => {
|
|
292
|
+
if (AGENT_TOOL_NAMES.includes(n)) return nestedTools.get(n);
|
|
293
|
+
const t = available.find((x) => x.name === n);
|
|
294
|
+
return typeof t?.forChild === "function" ? t.forChild(cwd ? { cwd } : undefined) : t;
|
|
295
|
+
})
|
|
296
|
+
.filter(Boolean);
|
|
297
|
+
granted.push(...extraTools);
|
|
298
|
+
|
|
299
|
+
const headers = { "x-cohort-surface": "engine", "x-cohort-session-id": d.sessionId, "x-cohort-agent-id": agentId, "x-cohort-parent-agent-id": d.parentAgentId };
|
|
300
|
+
const callModel = d.createCaller({ model, headers, parentToolUseId, ...(effort ? { effort } : {}) });
|
|
301
|
+
const guard = d.createGuard({ mode, agentId, ...at });
|
|
302
|
+
|
|
303
|
+
const first = userText(prompt);
|
|
304
|
+
if (transcript) appendMessage(transcript, first, now);
|
|
305
|
+
|
|
306
|
+
// The child's own context: tools (with its own ToolSearch state), the definitions it is sent, a context manager.
|
|
307
|
+
const prepared = d.prepareChild
|
|
308
|
+
? d.prepareChild({ def, model, tools: granted, headers, agentId, parentToolUseId, onCompact: (m) => transcript && appendMessage(transcript, transcriptMessage(m), now) })
|
|
309
|
+
: null;
|
|
310
|
+
const tools = prepared?.tools ?? granted;
|
|
311
|
+
nested?.setParentTools(() => tools);
|
|
312
|
+
const system = d.buildSystem({ def, toolNames: (prepared?.sent ?? tools).map((t) => t.name), notes: prepared?.notes ?? null, ...at });
|
|
313
|
+
|
|
314
|
+
let outcome;
|
|
315
|
+
try {
|
|
316
|
+
outcome = await runLoop({
|
|
317
|
+
callModel,
|
|
318
|
+
system,
|
|
319
|
+
messages: [...history, first],
|
|
320
|
+
tools,
|
|
321
|
+
toolContext: d.createToolContext({ headers, agentId, ...at }),
|
|
322
|
+
maxTurns: def.maxTurns ?? d.maxTurns,
|
|
323
|
+
wallClockMs: d.wallClockMs,
|
|
324
|
+
signal,
|
|
325
|
+
now,
|
|
326
|
+
guard,
|
|
327
|
+
onMessage: (m) => {
|
|
328
|
+
if (transcript) appendMessage(transcript, transcriptMessage(m), now);
|
|
329
|
+
d.onMessage?.(m, { parentToolUseId, agentId });
|
|
330
|
+
},
|
|
331
|
+
onStop: d.hooks ? ({ stopHookActive }) => d.hooks.stop({ stopHookActive, event: "SubagentStop", fields: { agent_id: agentId, agent_type: def.name, agent_transcript_path: transcript?.path ?? null } }) : undefined,
|
|
332
|
+
drainNotifications: nested?.drainNotifications,
|
|
333
|
+
awaitNotifications: nested?.awaitNotifications,
|
|
334
|
+
context: prepared?.context ?? null,
|
|
335
|
+
// W4-E1 (row 24): the run's --max-budget-usd meter, shared by every child.
|
|
336
|
+
budget: d.budget ?? null,
|
|
337
|
+
});
|
|
338
|
+
} finally {
|
|
339
|
+
if (nested) await nested.stopAll();
|
|
340
|
+
if (guard.denials.length) d.onDenials?.(guard.denials, { turn });
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const record = { ...recordFromOutcome(outcome, model), agentId, agentType: def.name, background, stop: outcome.stop.kind, numTurns: outcome.numTurns };
|
|
344
|
+
addRecord(record);
|
|
345
|
+
const c = classifyStop(outcome.stop);
|
|
346
|
+
if (transcript) {
|
|
347
|
+
appendResult(
|
|
348
|
+
transcript,
|
|
349
|
+
{ type: "result", subtype: c.subtype, is_error: c.isError, result: c.isError ? c.message : outcome.resultText, agent_id: agentId, num_turns: outcome.numTurns, cohort: { modelTier: record.tier, costMicros: record.costMicros, usage: record.usage, requestIds: record.requestIds, stop: outcome.stop.kind } },
|
|
350
|
+
now,
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
return { ok: !c.isError, agentId, text: outcome.resultText, error: c.message, stopKind: outcome.stop.kind, record };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** @param {BackgroundTask} t */
|
|
357
|
+
const notificationText = (t) => {
|
|
358
|
+
const kind = t.background ? "Background task" : "Task";
|
|
359
|
+
if (t.status === "completed") return `[${kind} ${t.id} finished] The ${t.agentType} subagent working on "${t.description}" completed. Its report:\n\n${t.text || "(no final message)"}`;
|
|
360
|
+
if (t.status === "interrupted") return `[${kind} ${t.id} interrupted] The ${t.agentType} subagent working on "${t.description}" did not finish: ${t.error ?? "the engine stopped"}.${t.text ? `\n\nIts last message:\n${t.text}` : ""}\n\n${resumeHint(t.id)}`;
|
|
361
|
+
return `[${kind} ${t.id} ended] The ${t.agentType} subagent working on "${t.description}" did not finish: ${t.error ?? "unknown error"}${t.text ? `\n\nIts last message:\n${t.text}` : ""}`;
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
/** @param {BackgroundTask} t */
|
|
365
|
+
const consume = (t) => {
|
|
366
|
+
t.notified = true;
|
|
367
|
+
const i = queue.findIndex((q) => q.taskId === t.id);
|
|
368
|
+
if (i !== -1) queue.splice(i, 1);
|
|
369
|
+
save(t);
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
// ── W4-E2: the session's earlier tasks, read back when the session is continued ──
|
|
373
|
+
if (d.restore ?? depth === 0) {
|
|
374
|
+
const { states, errors } = readTaskStates(d.sessionsDir, d.sessionId);
|
|
375
|
+
for (const e of errors) d.log?.(`subagent state ${e}`);
|
|
376
|
+
for (const s0 of states) {
|
|
377
|
+
if (s0.parentAgentId !== d.parentAgentId) continue;
|
|
378
|
+
const { state, changed, elsewhere } = reconcileTaskState(s0, { ...ownerCheck, now: iso() });
|
|
379
|
+
/** @type {BackgroundTask} */
|
|
380
|
+
const t = { id: state.agentId, agentType: state.agentType, description: state.description, status: state.status, text: state.text ?? "", error: state.error ?? null, notified: state.notified === true, stopRequested: false, controller: new AbortController(), done: Promise.resolve(), background: state.background === true, state, elsewhere };
|
|
381
|
+
tasks.set(t.id, t);
|
|
382
|
+
if (changed) save(t);
|
|
383
|
+
// Reported once: a task cut off by the restart, and a background report the old process never delivered.
|
|
384
|
+
if (!t.notified && (t.status === "interrupted" || (t.background && (t.status === "completed" || t.status === "failed")))) queue.push({ taskId: t.id, text: notificationText(t) });
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const taskSchema = {
|
|
389
|
+
type: "object",
|
|
390
|
+
properties: {
|
|
391
|
+
description: { type: "string", description: "A short label for the task (three to five words)." },
|
|
392
|
+
prompt: { type: "string", description: "The complete task for the subagent, with everything it needs to know. With `resume`, what it should do next." },
|
|
393
|
+
subagent_type: { type: "string", description: "Which subagent to use, from the listed types. Not needed with `resume`." },
|
|
394
|
+
run_in_background: { type: "boolean", description: "Return a task id at once; the report arrives in a later message." },
|
|
395
|
+
resume: { type: "string", description: "The id of an earlier task of this session that is not running (finished, stopped or interrupted): continue that subagent from its transcript." },
|
|
396
|
+
},
|
|
397
|
+
required: ["description", "prompt"],
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Check a `resume` request (W4-E2).
|
|
402
|
+
* @param {string} id @param {any} input
|
|
403
|
+
* @returns {{ok:true, def:import('./definitions.mjs').AgentDefinition, state:import('./persist.mjs').TaskState}|{ok:false, error:string}}
|
|
404
|
+
*/
|
|
405
|
+
const prepareResume = (id, input) => {
|
|
406
|
+
if (!isAgentId(id)) return { ok: false, error: `resume takes the id of an earlier task (agent- and 16 hex digits), got "${id}".` };
|
|
407
|
+
if (live.has(id)) return { ok: false, error: `Task ${id} is still running; read it with TaskOutput or stop it with TaskStop before resuming it.` };
|
|
408
|
+
const state = readTaskState(d.sessionsDir, d.sessionId, id);
|
|
409
|
+
if (!state || state.parentAgentId !== d.parentAgentId) return { ok: false, error: `There is no task ${id} of this session to resume.` };
|
|
410
|
+
if (state.status === "running") {
|
|
411
|
+
const where = taskOwnerState(state, ownerCheck);
|
|
412
|
+
if (where === "elsewhere") return { ok: false, error: `Task ${id} is still running in another engine process (pid ${state.pid}); it cannot be resumed from here.` };
|
|
413
|
+
if (where === "here-elsewhere") return { ok: false, error: `Task ${id} is still running in another session runtime of this engine process; it cannot be resumed from here.` };
|
|
414
|
+
}
|
|
415
|
+
const def = definitionForResume(state, d.agents, findAgent);
|
|
416
|
+
if (typeof input.subagent_type === "string" && input.subagent_type.trim() !== "") {
|
|
417
|
+
const f = findAgent(d.agents, input.subagent_type);
|
|
418
|
+
const asked = f.ok ? f.agent.name : input.subagent_type.trim();
|
|
419
|
+
if (asked !== state.agentType) return { ok: false, error: `Task ${id} is a ${state.agentType} subagent; resume it without subagent_type, or with that type (got "${input.subagent_type}").` };
|
|
420
|
+
}
|
|
421
|
+
return { ok: true, def, state };
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
/** @param {'Task'|'Agent'} name */
|
|
425
|
+
const startTool = (name) => ({
|
|
426
|
+
name,
|
|
427
|
+
// Starting a subagent changes nothing by itself: every call the subagent
|
|
428
|
+
// makes passes the same guard. Several subagents in one reply run together.
|
|
429
|
+
readOnly: true,
|
|
430
|
+
description:
|
|
431
|
+
name === "Task"
|
|
432
|
+
? "Hand a self-contained piece of work to a subagent. It runs with a fresh context and its own tools, and returns one final report, which becomes this call's result. " +
|
|
433
|
+
"It cannot see this conversation: put everything it needs in `prompt`. Several Task calls in one reply run at the same time. " +
|
|
434
|
+
"With `run_in_background` the call returns a task id at once and the report arrives in a later message (TaskOutput reads it earlier, TaskStop stops it). " +
|
|
435
|
+
"`resume` with an earlier task's id continues that subagent, with its conversation so far, on a new `prompt`.\n\n" +
|
|
436
|
+
`Subagent types:\n${formatAgentListing(d.agents)}`
|
|
437
|
+
: "The same tool as Task under another name: start a subagent with a fresh context and receive its final report. See Task for the subagent types.",
|
|
438
|
+
input_schema: taskSchema,
|
|
439
|
+
/** @param {{description:string, prompt:string, subagent_type?:string, run_in_background?:boolean, resume?:string}} input @param {any} ctx */
|
|
440
|
+
async run(input, ctx) {
|
|
441
|
+
/** @type {import('./definitions.mjs').AgentDefinition} */
|
|
442
|
+
let def;
|
|
443
|
+
/** @type {import('./persist.mjs').TaskState|null} */
|
|
444
|
+
let resumeState = null;
|
|
445
|
+
if (typeof input.resume === "string" && input.resume.trim() !== "") {
|
|
446
|
+
const r = prepareResume(input.resume.trim(), input);
|
|
447
|
+
if (!r.ok) return r;
|
|
448
|
+
def = r.def;
|
|
449
|
+
resumeState = r.state;
|
|
450
|
+
} else {
|
|
451
|
+
if (typeof input.subagent_type !== "string" || input.subagent_type.trim() === "") return { ok: false, error: "subagent_type is required (or resume, with the id of an earlier task)." };
|
|
452
|
+
const found = findAgent(d.agents, input.subagent_type);
|
|
453
|
+
if (!found.ok) return { ok: false, error: found.error };
|
|
454
|
+
def = found.agent;
|
|
455
|
+
}
|
|
456
|
+
const parentToolUseId = ctx?.toolUseId ?? null;
|
|
457
|
+
if (d.checkSubagent) {
|
|
458
|
+
const v = d.checkSubagent({ toolName: name, input: { ...input, subagent_type: def.name }, mode: d.parentMode });
|
|
459
|
+
if (v.behavior !== "allow") {
|
|
460
|
+
d.onDenials?.([{ tool_name: name, tool_use_id: parentToolUseId ?? "", tool_input: input }], { turn: d.currentTurn ? d.currentTurn() : null });
|
|
461
|
+
return { ok: false, error: `Permission denied: the ${def.name} subagent ${v.behavior === "deny" ? "is refused" : "needs approval, which this run cannot ask for"} (${v.reason}).` };
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const background = Boolean(input.run_in_background);
|
|
466
|
+
const id = resumeState?.agentId ?? newAgentId();
|
|
467
|
+
const description = typeof input.description === "string" && input.description.trim() !== "" ? input.description : (resumeState?.description ?? def.name);
|
|
468
|
+
// The mode this child runs in; a resumed child never runs looser than it did before.
|
|
469
|
+
const clamped = clampPermissionMode(d.parentMode, def.permissionMode);
|
|
470
|
+
const ceiling = resumeState ? clampPermissionMode(clamped, resumeState.mode) : clamped;
|
|
471
|
+
const { model } = resolveAgentModel(def.model, d.parentModel, d.tierMap ?? DEFAULT_TIER_MAP);
|
|
472
|
+
const controller = new AbortController();
|
|
473
|
+
/** @type {BackgroundTask} */
|
|
474
|
+
const task = {
|
|
475
|
+
id,
|
|
476
|
+
agentType: def.name,
|
|
477
|
+
description,
|
|
478
|
+
status: "running",
|
|
479
|
+
text: "",
|
|
480
|
+
error: null,
|
|
481
|
+
notified: false,
|
|
482
|
+
stopRequested: false,
|
|
483
|
+
controller,
|
|
484
|
+
done: Promise.resolve(),
|
|
485
|
+
background,
|
|
486
|
+
elsewhere: false,
|
|
487
|
+
state: {
|
|
488
|
+
version: TASK_STATE_VERSION,
|
|
489
|
+
agentId: id,
|
|
490
|
+
sessionId: d.sessionId,
|
|
491
|
+
parentAgentId: d.parentAgentId,
|
|
492
|
+
parentToolUseId,
|
|
493
|
+
agentType: def.name,
|
|
494
|
+
definition: { ...def },
|
|
495
|
+
description,
|
|
496
|
+
prompt: resumeState?.prompt ?? input.prompt,
|
|
497
|
+
lastPrompt: resumeState ? input.prompt : null,
|
|
498
|
+
mode: ceiling,
|
|
499
|
+
model,
|
|
500
|
+
background,
|
|
501
|
+
transcriptPath: sessionPath(subagentDir(d.sessionsDir, d.sessionId), id),
|
|
502
|
+
status: "running",
|
|
503
|
+
text: "",
|
|
504
|
+
error: null,
|
|
505
|
+
notified: false,
|
|
506
|
+
pid,
|
|
507
|
+
nonce,
|
|
508
|
+
startToken: startTokenOf(pid),
|
|
509
|
+
resumes: resumeState ? (resumeState.resumes ?? 0) + 1 : 0,
|
|
510
|
+
startedAt: resumeState?.startedAt ?? iso(),
|
|
511
|
+
endedAt: null,
|
|
512
|
+
},
|
|
513
|
+
};
|
|
514
|
+
// A resumed task's restored entry is replaced by the running one (its notification, if queued, is dropped).
|
|
515
|
+
const earlier = tasks.get(id);
|
|
516
|
+
if (earlier) {
|
|
517
|
+
const i = queue.findIndex((q) => q.taskId === id);
|
|
518
|
+
if (i !== -1) queue.splice(i, 1);
|
|
519
|
+
tasks.delete(id);
|
|
520
|
+
}
|
|
521
|
+
hold(id);
|
|
522
|
+
save(task);
|
|
523
|
+
const child = { def, prompt: input.prompt, description, parentToolUseId, agentId: id, background, resume: Boolean(resumeState), ceilingMode: ceiling };
|
|
524
|
+
const resumedNote = resumeState ? ` (resumed from its transcript)` : "";
|
|
525
|
+
|
|
526
|
+
if (!background) {
|
|
527
|
+
/** @type {Awaited<ReturnType<typeof runChild>>} */
|
|
528
|
+
let r;
|
|
529
|
+
try {
|
|
530
|
+
r = await runChild({ ...child, signal: ctx?.signal });
|
|
531
|
+
} catch (e) {
|
|
532
|
+
task.status = "failed";
|
|
533
|
+
task.error = `the subagent failed unexpectedly (${e instanceof Error ? e.message : e})`;
|
|
534
|
+
throw e;
|
|
535
|
+
} finally {
|
|
536
|
+
release(id);
|
|
537
|
+
if (task.status === "running") {
|
|
538
|
+
task.text = r?.text ?? "";
|
|
539
|
+
task.error = r?.ok ? null : (r?.error ?? null);
|
|
540
|
+
task.status = r?.ok ? "completed" : ctx?.signal?.aborted ? "stopped" : "failed";
|
|
541
|
+
}
|
|
542
|
+
task.notified = true; // its result is this call's result
|
|
543
|
+
tasks.set(id, task);
|
|
544
|
+
save(task);
|
|
545
|
+
}
|
|
546
|
+
if (r.ok) return { ok: true, content: `${resumeState ? `[Task ${id}${resumedNote}]\n\n` : ""}${r.text || "(the subagent finished without a final message)"}` };
|
|
547
|
+
return { ok: false, error: `The ${def.name} subagent${resumeState ? ` (task ${id}, resumed)` : ""} did not finish: ${r.error}${r.text ? `\n\nIts last message:\n${r.text}` : ""}` };
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const outer = /** @type {AbortSignal|undefined} */ (ctx?.signal);
|
|
551
|
+
const onOuter = () => controller.abort();
|
|
552
|
+
if (outer?.aborted) controller.abort();
|
|
553
|
+
else outer?.addEventListener("abort", onOuter, { once: true });
|
|
554
|
+
tasks.set(id, task);
|
|
555
|
+
task.done = runChild({ ...child, signal: controller.signal })
|
|
556
|
+
.then(
|
|
557
|
+
(r) => {
|
|
558
|
+
task.text = r.text;
|
|
559
|
+
task.error = r.ok ? null : r.error;
|
|
560
|
+
task.status = task.stopRequested ? "stopped" : r.ok ? "completed" : "failed";
|
|
561
|
+
},
|
|
562
|
+
(e) => {
|
|
563
|
+
task.error = `the subagent failed unexpectedly (${e instanceof Error ? e.message : e})`;
|
|
564
|
+
task.status = task.stopRequested ? "stopped" : "failed";
|
|
565
|
+
},
|
|
566
|
+
)
|
|
567
|
+
.finally(() => {
|
|
568
|
+
outer?.removeEventListener("abort", onOuter);
|
|
569
|
+
release(id);
|
|
570
|
+
if (!task.notified && task.status !== "stopped") {
|
|
571
|
+
queue.push({ taskId: id, text: notificationText(task) });
|
|
572
|
+
d.onNotify?.();
|
|
573
|
+
}
|
|
574
|
+
save(task);
|
|
575
|
+
wake();
|
|
576
|
+
});
|
|
577
|
+
return {
|
|
578
|
+
ok: true,
|
|
579
|
+
content: `${resumeState ? "Resumed" : "Started"} background task ${id}: the ${def.name} subagent is working on "${description}"${resumedNote}. Its report arrives in a later message when it finishes. TaskOutput {"task_id":"${id}"} waits for it or reads it early; TaskStop {"task_id":"${id}"} stops it.`,
|
|
580
|
+
};
|
|
581
|
+
},
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
/** TaskOutput for a workflow run id (W4-E2). @param {string} runId @param {boolean} block @param {number} ms @param {AbortSignal} [signal] */
|
|
585
|
+
const workflowOutput = async (runId, block, ms, signal) => {
|
|
586
|
+
if (workflowRuns?.has(runId)) {
|
|
587
|
+
const r = await workflowRuns.status(runId, { block, timeoutMs: ms, signal });
|
|
588
|
+
if (!r.ok) return r;
|
|
589
|
+
const v = r.view;
|
|
590
|
+
return { ok: true, content: `Workflow ${runId} ("${v.name}") is ${v.status}.\n\n${JSON.stringify(v, null, 2)}${r.result !== null ? `\n\nResult:\n${r.result}` : ""}` };
|
|
591
|
+
}
|
|
592
|
+
const disk = workflowRuns?.diskStatus?.(runId);
|
|
593
|
+
return disk && disk.ok ? { ok: true, content: disk.content } : null;
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
const outputTool = {
|
|
597
|
+
name: "TaskOutput",
|
|
598
|
+
readOnly: true,
|
|
599
|
+
description:
|
|
600
|
+
"Read the result of a background task: a subagent started with run_in_background, a background shell, or a workflow run (its wf_ id). With `block` (the default) it waits until the task ends or `timeout` milliseconds pass " +
|
|
601
|
+
`(default ${TASK_OUTPUT_DEFAULT_TIMEOUT_MS}). A task read here is not reported again later. A subagent task that was interrupted by an engine restart says how to resume it.`,
|
|
602
|
+
input_schema: {
|
|
603
|
+
type: "object",
|
|
604
|
+
properties: {
|
|
605
|
+
task_id: { type: "string", description: "The task id (or shell id, or workflow run id)." },
|
|
606
|
+
block: { type: "boolean", description: "Wait for the task to end. Default true." },
|
|
607
|
+
timeout: { type: "integer", minimum: 0, maximum: TASK_OUTPUT_MAX_TIMEOUT_MS, description: "How long to wait, in milliseconds." },
|
|
608
|
+
},
|
|
609
|
+
required: ["task_id"],
|
|
610
|
+
},
|
|
611
|
+
/** @param {{task_id:string, block?:boolean, timeout?:number}} input @param {any} ctx */
|
|
612
|
+
async run(input, ctx) {
|
|
613
|
+
const ms = Math.min(input.timeout ?? TASK_OUTPUT_DEFAULT_TIMEOUT_MS, TASK_OUTPUT_MAX_TIMEOUT_MS);
|
|
614
|
+
const block = input.block !== false;
|
|
615
|
+
const task = tasks.get(input.task_id);
|
|
616
|
+
if (!task) {
|
|
617
|
+
if (d.shells?.has(input.task_id)) {
|
|
618
|
+
if (block) await d.shells.wait(input.task_id, ms, ctx?.signal);
|
|
619
|
+
const r = d.shells.read(input.task_id);
|
|
620
|
+
return r.ok ? { ok: true, content: r.content } : r;
|
|
621
|
+
}
|
|
622
|
+
if (WORKFLOW_RUN_ID_RE.test(input.task_id)) {
|
|
623
|
+
const w = await workflowOutput(input.task_id, block, ms, ctx?.signal);
|
|
624
|
+
if (w) return w;
|
|
625
|
+
}
|
|
626
|
+
return { ok: false, error: `There is no background task "${input.task_id}". Known: ${[...tasks.keys()].join(", ") || "none"}.` };
|
|
627
|
+
}
|
|
628
|
+
if (task.elsewhere && task.status === "running") return { ok: true, content: `Task ${task.id} (${task.agentType}, "${task.description}") is running ${elsewhereText(task)}; it can be read here once it has ended.` };
|
|
629
|
+
if (task.status === "running" && block) await waitFor(task.done, ms, ctx?.signal);
|
|
630
|
+
if (task.status === "running") return { ok: true, content: `Task ${task.id} (${task.agentType}, "${task.description}") is still running.` };
|
|
631
|
+
consume(task);
|
|
632
|
+
if (task.status === "completed") return { ok: true, content: `Task ${task.id} completed. Its report:\n\n${task.text || "(no final message)"}` };
|
|
633
|
+
if (task.status === "interrupted") return { ok: true, content: `Task ${task.id} (${task.agentType}, "${task.description}") was interrupted: ${task.error ?? "the engine stopped"}.${task.text ? `\n\nIts last message:\n${task.text}` : ""}\n\n${resumeHint(task.id)}` };
|
|
634
|
+
return { ok: true, content: `Task ${task.id} ${task.status === "stopped" ? "was stopped" : "did not finish"}: ${task.error ?? "stopped on request"}${task.text ? `\n\nIts last message:\n${task.text}` : ""}` };
|
|
635
|
+
},
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
const stopTool = {
|
|
639
|
+
name: "TaskStop",
|
|
640
|
+
// Stops only work this run started.
|
|
641
|
+
readOnly: true,
|
|
642
|
+
description: "Stop a background task: a subagent started with run_in_background, a background shell, or a workflow run (its wf_ id: the run's script and its agents are stopped).",
|
|
643
|
+
input_schema: {
|
|
644
|
+
type: "object",
|
|
645
|
+
properties: { task_id: { type: "string", description: "The task id (or shell id, or workflow run id)." } },
|
|
646
|
+
required: ["task_id"],
|
|
647
|
+
},
|
|
648
|
+
/** @param {{task_id:string}} input */
|
|
649
|
+
async run(input) {
|
|
650
|
+
const task = tasks.get(input.task_id);
|
|
651
|
+
if (!task) {
|
|
652
|
+
if (d.shells?.has(input.task_id)) return d.shells.kill(input.task_id);
|
|
653
|
+
if (WORKFLOW_RUN_ID_RE.test(input.task_id) && workflowRuns) {
|
|
654
|
+
if (workflowRuns.has(input.task_id)) return workflowRuns.stop(input.task_id);
|
|
655
|
+
const disk = workflowRuns.diskStatus?.(input.task_id);
|
|
656
|
+
if (disk?.ok) return disk.running ? { ok: false, error: `Workflow ${input.task_id} is not running in this process, so it cannot be stopped from here.\n\n${disk.content}` } : { ok: true, content: `Workflow ${input.task_id} is not running; nothing to stop.\n\n${disk.content}` };
|
|
657
|
+
}
|
|
658
|
+
return { ok: false, error: `There is no background task "${input.task_id}".` };
|
|
659
|
+
}
|
|
660
|
+
if (task.elsewhere && task.status === "running") return { ok: false, error: `Task ${task.id} is running ${elsewhereText(task)}; it cannot be stopped from here.` };
|
|
661
|
+
if (task.status !== "running") {
|
|
662
|
+
consume(task);
|
|
663
|
+
return { ok: true, content: `Task ${task.id} had already ${task.status === "completed" ? "completed" : task.status === "interrupted" ? `been interrupted; ${resumeHint(task.id)}` : "ended"}.` };
|
|
664
|
+
}
|
|
665
|
+
task.stopRequested = true;
|
|
666
|
+
consume(task);
|
|
667
|
+
task.controller.abort();
|
|
668
|
+
await waitFor(task.done, STOP_WAIT_MS);
|
|
669
|
+
return { ok: true, content: `Stopped task ${task.id} (${task.agentType}, "${task.description}").` };
|
|
670
|
+
},
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
const drainNotifications = () => {
|
|
674
|
+
const out = queue.splice(0);
|
|
675
|
+
for (const n of out) {
|
|
676
|
+
const t = tasks.get(n.taskId);
|
|
677
|
+
if (t) {
|
|
678
|
+
t.notified = true;
|
|
679
|
+
save(t);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return out.map((n) => n.text);
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
return {
|
|
686
|
+
/** The subagent tools, in a stable order. */
|
|
687
|
+
tools: () => [startTool("Task"), startTool("Agent"), outputTool, stopTool],
|
|
688
|
+
/** @param {() => any[]} fn the tool list the owning run exposes */
|
|
689
|
+
setParentTools(fn) {
|
|
690
|
+
parentTools = fn;
|
|
691
|
+
},
|
|
692
|
+
/** W4-E2: TaskOutput and TaskStop also take this session's workflow run ids. @param {WorkflowRuns|null} w */
|
|
693
|
+
setWorkflowRuns(w) {
|
|
694
|
+
workflowRuns = w;
|
|
695
|
+
},
|
|
696
|
+
drainNotifications,
|
|
697
|
+
/**
|
|
698
|
+
* Notifications ready now; otherwise, while a background task runs, wait
|
|
699
|
+
* for the next one to end (or the signal). Empty when nothing is running.
|
|
700
|
+
* @param {AbortSignal} [signal]
|
|
701
|
+
* @returns {Promise<string[]>}
|
|
702
|
+
*/
|
|
703
|
+
async awaitNotifications(signal) {
|
|
704
|
+
if (queue.length) return drainNotifications();
|
|
705
|
+
if (![...tasks.values()].some(runningHere)) return [];
|
|
706
|
+
await new Promise((resolve) => {
|
|
707
|
+
const done = () => {
|
|
708
|
+
signal?.removeEventListener("abort", done);
|
|
709
|
+
waiters.delete(done);
|
|
710
|
+
resolve(undefined);
|
|
711
|
+
};
|
|
712
|
+
waiters.add(done);
|
|
713
|
+
if (signal?.aborted) done();
|
|
714
|
+
else signal?.addEventListener("abort", done, { once: true });
|
|
715
|
+
});
|
|
716
|
+
return drainNotifications();
|
|
717
|
+
},
|
|
718
|
+
hasRunning: () => [...tasks.values()].some(runningHere),
|
|
719
|
+
/** Stop every running background task and wait for it to end. */
|
|
720
|
+
async stopAll() {
|
|
721
|
+
const running = [...tasks.values()].filter(runningHere);
|
|
722
|
+
for (const t of running) {
|
|
723
|
+
t.stopRequested = true;
|
|
724
|
+
t.notified = true;
|
|
725
|
+
t.controller.abort();
|
|
726
|
+
}
|
|
727
|
+
await Promise.all(running.map((t) => waitFor(t.done, STOP_WAIT_MS)));
|
|
728
|
+
},
|
|
729
|
+
/** Usage records of the children that finished since the last call. */
|
|
730
|
+
drainRecords: () => records.splice(0),
|
|
731
|
+
runChild,
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/** @param {Promise<unknown>} p @param {number} ms @param {AbortSignal} [signal] */
|
|
736
|
+
function waitFor(p, ms, signal) {
|
|
737
|
+
return new Promise((resolve) => {
|
|
738
|
+
const t = setTimeout(done, ms);
|
|
739
|
+
const onAbort = () => done();
|
|
740
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
741
|
+
p.then(done, done);
|
|
742
|
+
function done() {
|
|
743
|
+
clearTimeout(t);
|
|
744
|
+
signal?.removeEventListener("abort", onAbort);
|
|
745
|
+
resolve(undefined);
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
}
|