@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,560 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/engine/loop.mjs — the agent loop.
|
|
3
|
+
*
|
|
4
|
+
* One run = model turn → (tool calls → tool results → model turn)* → answer.
|
|
5
|
+
*
|
|
6
|
+
* The loop owns ordering and stopping; it owns no I/O of its own. The model is
|
|
7
|
+
* an injected `callModel` (see `wire/index.mjs`) and tools are injected tool
|
|
8
|
+
* objects (see `tools/index.mjs`), so the whole loop runs in a test with a
|
|
9
|
+
* scripted fake model and no network.
|
|
10
|
+
*
|
|
11
|
+
* What it guarantees:
|
|
12
|
+
* · Stop conditions are a union (`LoopStop`): completed | max_turns |
|
|
13
|
+
* wall_clock | aborted | wire_error | loop_detected. Exactly one ends a run.
|
|
14
|
+
* · Every assistant `tool_use` is answered by a `tool_result` before the next
|
|
15
|
+
* model call or the end of the run, so the transcript is always resumable.
|
|
16
|
+
* · Read-only tool calls that arrive together run in parallel (bounded);
|
|
17
|
+
* anything that can mutate runs alone, in the order the model gave.
|
|
18
|
+
* · Tool-argument JSON that did not parse gets one repair pass; a repaired
|
|
19
|
+
* call is written back into the assistant message as valid JSON.
|
|
20
|
+
* · The same call (tool + identical input) made turn after turn is flagged in
|
|
21
|
+
* its result at `loopWarnAt` and ends the run at `loopStopAt`.
|
|
22
|
+
* · A tool that THROWS (a defect in the tool) becomes an error result; one
|
|
23
|
+
* buggy tool must not take down a seat's run.
|
|
24
|
+
* · A turn that stopped at the output token limit (`stopReason max_tokens`)
|
|
25
|
+
* runs none of its tool calls — a cut-off call can be "repaired" into a
|
|
26
|
+
* valid one that silently lost its trailing fields — and asks for a resend.
|
|
27
|
+
* · `costMicros` is exact or null: a request that failed after the gateway
|
|
28
|
+
* accepted it may be billed with no figure, so it nulls the sum.
|
|
29
|
+
* · The wall-clock timer and the outer abort listener are released even when
|
|
30
|
+
* a defect throws out of the loop.
|
|
31
|
+
* · With a `guard` (guard.mjs), no tool runs until permissions, in-process
|
|
32
|
+
* gates and PreToolUse hooks have passed it; a refusal is the call's error
|
|
33
|
+
* result. A guard that throws refuses the call (fail closed).
|
|
34
|
+
* · MCP tools (`passthroughInput`) get their input checked for shape and
|
|
35
|
+
* required keys only, and forwarded unchanged.
|
|
36
|
+
* · With `onStop`, a Stop hook that asks for more work turns a finished run
|
|
37
|
+
* into another turn, its reason sent as the next user message.
|
|
38
|
+
* · With `context` (context/manager.mjs), each turn boundary may replace the
|
|
39
|
+
* history (compaction), append user messages, and change the tools that
|
|
40
|
+
* are sent (deferred loading, MCP list changes); text it returns after a
|
|
41
|
+
* batch is appended to the tool-results message. Model calls made on the
|
|
42
|
+
* run's behalf outside the turn loop (a compaction summary, WebFetch,
|
|
43
|
+
* WebSearch) are reported through `toolContext.recordModelCall` and join
|
|
44
|
+
* the run's usage, request ids and cost exactly like turns do.
|
|
45
|
+
*
|
|
46
|
+
* The pure decisions (`planBatches`, `nextStreaks`, `repairToolUses`,
|
|
47
|
+
* `failureMayBeBilled`) are exported and tested on their own.
|
|
48
|
+
*
|
|
49
|
+
* @module lib/engine/loop
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
import { toolUses, messageText, stableStringify, userText } from "./messages.mjs";
|
|
53
|
+
import { validateInput, repairToolJson } from "./tools/schema.mjs";
|
|
54
|
+
import { toolDefinitions } from "./tools/index.mjs";
|
|
55
|
+
import { emptyUsage, addUsage, readFrameUsage } from "./wire/usage.mjs";
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @typedef {{kind:'completed'} | {kind:'max_turns', maxTurns:number} | {kind:'wall_clock', wallClockMs:number}
|
|
59
|
+
* | {kind:'aborted'} | {kind:'wire_error', error:any} | {kind:'loop_detected', tool:string, repeats:number}
|
|
60
|
+
* | {kind:'hook_stopped', reason:string} | {kind:'max_budget', maxBudgetUsd:number, spentMicros:string}} LoopStop
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
export const DEFAULTS = Object.freeze({
|
|
64
|
+
maxTurns: 50,
|
|
65
|
+
wallClockMs: 30 * 60_000,
|
|
66
|
+
maxParallel: 8,
|
|
67
|
+
loopWarnAt: 3,
|
|
68
|
+
loopStopAt: 5,
|
|
69
|
+
maxToolResultChars: 100_000,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Group tool calls into execution batches: runs of consecutive read-only calls
|
|
74
|
+
* (at most `maxParallel` each) run together; every other call is its own batch.
|
|
75
|
+
* @param {Array<{name:string}>} calls
|
|
76
|
+
* @param {(name:string) => boolean} isReadOnly
|
|
77
|
+
* @param {number} maxParallel
|
|
78
|
+
* @returns {number[][]} batches of indices into `calls`, in order
|
|
79
|
+
*/
|
|
80
|
+
export function planBatches(calls, isReadOnly, maxParallel) {
|
|
81
|
+
const batches = [];
|
|
82
|
+
let current = [];
|
|
83
|
+
calls.forEach((c, i) => {
|
|
84
|
+
if (isReadOnly(c.name)) {
|
|
85
|
+
if (current.length >= maxParallel) {
|
|
86
|
+
batches.push(current);
|
|
87
|
+
current = [];
|
|
88
|
+
}
|
|
89
|
+
current.push(i);
|
|
90
|
+
} else {
|
|
91
|
+
if (current.length > 0) batches.push(current);
|
|
92
|
+
current = [];
|
|
93
|
+
batches.push([i]);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
if (current.length > 0) batches.push(current);
|
|
97
|
+
return batches;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Consecutive-turn streaks per call signature. A signature absent from this
|
|
102
|
+
* turn resets to zero (it is simply not carried forward).
|
|
103
|
+
* @param {Map<string, number>} prev
|
|
104
|
+
* @param {string[]} signatures this turn's call signatures
|
|
105
|
+
* @returns {Map<string, number>}
|
|
106
|
+
*/
|
|
107
|
+
export function nextStreaks(prev, signatures) {
|
|
108
|
+
const next = new Map();
|
|
109
|
+
for (const s of new Set(signatures)) next.set(s, (prev.get(s) ?? 0) + 1);
|
|
110
|
+
return next;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** @param {{name:string, input:unknown}} call */
|
|
114
|
+
export function callSignature(call) {
|
|
115
|
+
return `${call.name}:${stableStringify(call.input ?? {})}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Repair unparseable tool arguments in an assistant message.
|
|
120
|
+
* @param {{role:string, content:any[]}} message
|
|
121
|
+
* @returns {{ message: {role:string, content:any[]}, unrepaired: Map<string, string> }}
|
|
122
|
+
* `unrepaired` maps tool_use id → the parse error to report back.
|
|
123
|
+
*/
|
|
124
|
+
export function repairToolUses(message) {
|
|
125
|
+
const unrepaired = new Map();
|
|
126
|
+
const content = message.content.map((b) => {
|
|
127
|
+
if (b.type !== "tool_use" || !b.inputError) return b;
|
|
128
|
+
const r = repairToolJson(b.rawInput ?? "");
|
|
129
|
+
if (r.ok) return { type: "tool_use", id: b.id, name: b.name, input: r.value };
|
|
130
|
+
unrepaired.set(b.id, b.inputError);
|
|
131
|
+
return b;
|
|
132
|
+
});
|
|
133
|
+
return { message: { ...message, content }, unrepaired };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* @param {object} p
|
|
138
|
+
* @param {(turn:{system:string, messages:any[], tools:any[], signal:AbortSignal}) => Promise<any>} p.callModel
|
|
139
|
+
* @param {string} p.system
|
|
140
|
+
* @param {any[]} p.messages full history INCLUDING the new user prompt
|
|
141
|
+
* @param {import('./tools/index.mjs').Tool[]} p.tools
|
|
142
|
+
* @param {import('./tools/index.mjs').ToolContext} p.toolContext its `signal` is replaced by the run's
|
|
143
|
+
* @param {number} [p.maxTurns]
|
|
144
|
+
* @param {number} [p.wallClockMs]
|
|
145
|
+
* @param {AbortSignal} [p.signal]
|
|
146
|
+
* @param {() => number} [p.now]
|
|
147
|
+
* @param {(message:any) => void} [p.onMessage] called for every message the run appends
|
|
148
|
+
* @param {number} [p.maxParallel]
|
|
149
|
+
* @param {number} [p.loopWarnAt]
|
|
150
|
+
* @param {number} [p.loopStopAt]
|
|
151
|
+
* @param {number} [p.maxToolResultChars]
|
|
152
|
+
* @param {ReturnType<typeof import('./guard.mjs').createToolGuard>|null} [p.guard]
|
|
153
|
+
* @param {(s:{stopHookActive:boolean}) => Promise<{continueWith:string|null}|null>} [p.onStop]
|
|
154
|
+
* @param {() => string[]} [p.drainNotifications] before each model call: messages to add as user turns
|
|
155
|
+
* @param {(signal:AbortSignal) => Promise<string[]>} [p.awaitNotifications] when the model has finished:
|
|
156
|
+
* waits for pending background work; a non-empty answer continues the run with those messages
|
|
157
|
+
* @param {{beforeTurn:(t:{system:string, messages:any[], signal:AbortSignal, recordModelCall:(res:any)=>void})=>Promise<{messages?:any[], append?:any[], tools?:any[], definitions?:any[]}|null>,
|
|
158
|
+
* observe?:(o:{system:string, messages:any[], tools:any[], res:any})=>void,
|
|
159
|
+
* afterTools?:(o:{calls:any[], results:any[], signal:AbortSignal})=>Promise<any[]|null>}|null} [p.context]
|
|
160
|
+
*/
|
|
161
|
+
export async function runLoop(p) {
|
|
162
|
+
const maxTurns = p.maxTurns ?? DEFAULTS.maxTurns;
|
|
163
|
+
const wallClockMs = p.wallClockMs ?? DEFAULTS.wallClockMs;
|
|
164
|
+
const maxParallel = p.maxParallel ?? DEFAULTS.maxParallel;
|
|
165
|
+
const loopWarnAt = p.loopWarnAt ?? DEFAULTS.loopWarnAt;
|
|
166
|
+
const loopStopAt = p.loopStopAt ?? DEFAULTS.loopStopAt;
|
|
167
|
+
const maxChars = p.maxToolResultChars ?? DEFAULTS.maxToolResultChars;
|
|
168
|
+
const now = p.now || Date.now;
|
|
169
|
+
const onMessage = p.onMessage || (() => {});
|
|
170
|
+
|
|
171
|
+
const messages = [...p.messages];
|
|
172
|
+
const newMessages = [];
|
|
173
|
+
const append = (m) => {
|
|
174
|
+
messages.push(m);
|
|
175
|
+
newMessages.push(m);
|
|
176
|
+
onMessage(m);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// One controller for the run: tripped by the caller's signal or the clock.
|
|
180
|
+
const controller = new AbortController();
|
|
181
|
+
let wallClockHit = false;
|
|
182
|
+
const timer = setTimeout(() => {
|
|
183
|
+
wallClockHit = true;
|
|
184
|
+
controller.abort();
|
|
185
|
+
}, wallClockMs);
|
|
186
|
+
const onOuterAbort = () => controller.abort();
|
|
187
|
+
if (p.signal?.aborted) controller.abort();
|
|
188
|
+
else p.signal?.addEventListener("abort", onOuterAbort, { once: true });
|
|
189
|
+
const signal = controller.signal;
|
|
190
|
+
|
|
191
|
+
// The timer and the outer listener are released however the run ends —
|
|
192
|
+
// including a defect that throws out of callModel, onMessage or a tool
|
|
193
|
+
// executor. A leaked 30-minute timer would hold the CLI process open.
|
|
194
|
+
try {
|
|
195
|
+
return await runTurns(p, {
|
|
196
|
+
signal,
|
|
197
|
+
append,
|
|
198
|
+
messages,
|
|
199
|
+
newMessages,
|
|
200
|
+
maxTurns,
|
|
201
|
+
wallClockMs,
|
|
202
|
+
maxParallel,
|
|
203
|
+
loopWarnAt,
|
|
204
|
+
loopStopAt,
|
|
205
|
+
maxChars,
|
|
206
|
+
isWallClock: () => wallClockHit,
|
|
207
|
+
});
|
|
208
|
+
} finally {
|
|
209
|
+
clearTimeout(timer);
|
|
210
|
+
p.signal?.removeEventListener("abort", onOuterAbort);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Did a failed model request possibly reach billing? Only a refusal the
|
|
216
|
+
* gateway answered BEFORE accepting the request (a non-2xx status) is known to
|
|
217
|
+
* be free. A failure after acceptance (`accepted:true` — a broken stream, an
|
|
218
|
+
* in-stream error, an abort mid-relay) or a transport fault with no status (the
|
|
219
|
+
* request may have been admitted before the socket died) may be billed, so the
|
|
220
|
+
* run's cost sum is no longer exact.
|
|
221
|
+
* @param {{accepted?:boolean, error?:{status?:number|null}}} res
|
|
222
|
+
*/
|
|
223
|
+
export function failureMayBeBilled(res) {
|
|
224
|
+
return res.accepted === true || res.error?.status == null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* The body of `runLoop`, separated so its caller can release the run's timer
|
|
229
|
+
* and listener in one `finally`.
|
|
230
|
+
* @param {Parameters<typeof runLoop>[0]} p
|
|
231
|
+
* @param {any} r
|
|
232
|
+
*/
|
|
233
|
+
async function runTurns(p, r) {
|
|
234
|
+
const { signal, append, messages, newMessages, maxTurns, wallClockMs, maxParallel, loopWarnAt, loopStopAt, maxChars } = r;
|
|
235
|
+
let toolsList = p.tools;
|
|
236
|
+
let toolsByName = new Map(toolsList.map((t) => [t.name, t]));
|
|
237
|
+
let defs = toolDefinitions(toolsList);
|
|
238
|
+
const ctx = { ...p.toolContext, signal };
|
|
239
|
+
|
|
240
|
+
let usage = emptyUsage("provider");
|
|
241
|
+
// CF-158: the gateway's OWN usage, off the terminal `event: cohort` frame,
|
|
242
|
+
// summed over this loop's requests. Deliberately a SECOND accumulator: it is
|
|
243
|
+
// the gateway's view of the same turns, near-equal to `usage` but not the
|
|
244
|
+
// same number, and conflating the two under one name is the CF-158 defect.
|
|
245
|
+
// Stays null while no frame reports any, so "not reported" ≠ "zero".
|
|
246
|
+
let gatewayUsage = null;
|
|
247
|
+
const requestIds = [];
|
|
248
|
+
let costTotal = 0n;
|
|
249
|
+
let costComplete = true;
|
|
250
|
+
let quota = null;
|
|
251
|
+
let modelTier = null;
|
|
252
|
+
let model = null;
|
|
253
|
+
let apiMs = 0;
|
|
254
|
+
let numTurns = 0;
|
|
255
|
+
let streaks = new Map();
|
|
256
|
+
let lastText = "";
|
|
257
|
+
let stopHookActive = false;
|
|
258
|
+
const guard = p.guard ?? null;
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* The model gave no tool calls. Ask the Stop hooks whether the run may end.
|
|
262
|
+
* @returns {Promise<LoopStop|null>} null = continue with another turn
|
|
263
|
+
*/
|
|
264
|
+
const finishOrContinue = async () => {
|
|
265
|
+
// Background work still running (agents/runtime.mjs): its report becomes
|
|
266
|
+
// the next user message instead of the run ending without it.
|
|
267
|
+
if (p.awaitNotifications && !signal.aborted) {
|
|
268
|
+
const notes = await p.awaitNotifications(signal);
|
|
269
|
+
if (signal.aborted) return interrupted();
|
|
270
|
+
if (notes.length > 0) {
|
|
271
|
+
for (const n of notes) append(userText(n));
|
|
272
|
+
if (numTurns >= maxTurns) return { kind: "max_turns", maxTurns };
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (!p.onStop || signal.aborted) return { kind: "completed" };
|
|
277
|
+
const s = await p.onStop({ stopHookActive });
|
|
278
|
+
if (!s?.continueWith) return { kind: "completed" };
|
|
279
|
+
stopHookActive = true;
|
|
280
|
+
append(userText(`Before finishing, a Stop hook asked for more:\n${s.continueWith}`));
|
|
281
|
+
if (numTurns >= maxTurns) return { kind: "max_turns", maxTurns };
|
|
282
|
+
return null;
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
/** @param {LoopStop} stop */
|
|
286
|
+
const done = (stop) => {
|
|
287
|
+
return {
|
|
288
|
+
stop,
|
|
289
|
+
messages,
|
|
290
|
+
newMessages,
|
|
291
|
+
resultText: lastText,
|
|
292
|
+
numTurns,
|
|
293
|
+
usage,
|
|
294
|
+
// The gateway's own per-request counts (CF-158), or null if it reported
|
|
295
|
+
// none. Never merged into `usage` — that would double-count every turn.
|
|
296
|
+
gatewayUsage,
|
|
297
|
+
requestIds,
|
|
298
|
+
costMicros: costComplete && numTurns > 0 ? costTotal.toString() : null,
|
|
299
|
+
// false when a request may have been billed with no figure; a run with no
|
|
300
|
+
// turn and costComplete true was never billed (agents/usage.mjs).
|
|
301
|
+
costComplete,
|
|
302
|
+
quota,
|
|
303
|
+
modelTier,
|
|
304
|
+
model,
|
|
305
|
+
apiMs,
|
|
306
|
+
};
|
|
307
|
+
};
|
|
308
|
+
const interrupted = () => (r.isWallClock() ? { kind: "wall_clock", wallClockMs } : { kind: "aborted" });
|
|
309
|
+
// W4-E1 (row 24): a spend cap shared with the run's subagents (budget.mjs). Asked before
|
|
310
|
+
// every model call this loop makes; the tool results of the last turn are always finished.
|
|
311
|
+
const budget = p.budget ?? null;
|
|
312
|
+
const overBudget = () =>
|
|
313
|
+
budget && budget.exhausted() ? { kind: "max_budget", maxBudgetUsd: Number(budget.maxMicros) / 1e6, spentMicros: budget.spentMicros().toString() } : null;
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Fold one gateway request into the run's accounting. `turn` is false for a
|
|
317
|
+
* request made on the run's behalf outside the turn loop (it counts toward
|
|
318
|
+
* usage, request ids and cost, but not turns, and does not name the tier).
|
|
319
|
+
* @param {any} res @param {boolean} turn
|
|
320
|
+
*/
|
|
321
|
+
const account = (res, turn) => {
|
|
322
|
+
budget?.record(res);
|
|
323
|
+
apiMs += res.apiMs ?? 0;
|
|
324
|
+
if (res.cohort) {
|
|
325
|
+
if (res.cohort.requestId) requestIds.push(res.cohort.requestId);
|
|
326
|
+
if (turn && res.cohort.modelTier) modelTier = res.cohort.modelTier;
|
|
327
|
+
if (turn && hasQuota(res.cohort.quota)) quota = res.cohort.quota;
|
|
328
|
+
// CF-158. Read through readFrameUsage again even though the wire already
|
|
329
|
+
// normalised it: this accumulator must never take a malformed object from
|
|
330
|
+
// a future call site, and an unusable one leaves the total untouched
|
|
331
|
+
// rather than adding zeros.
|
|
332
|
+
const gu = readFrameUsage(res.cohort.usage);
|
|
333
|
+
if (gu) gatewayUsage = addUsage(gatewayUsage ?? emptyUsage("provider"), gu);
|
|
334
|
+
}
|
|
335
|
+
if (!res.ok) {
|
|
336
|
+
// A request that may have been billed has no cost figure, so the sum of
|
|
337
|
+
// the earlier turns would under-report the run: report no total instead.
|
|
338
|
+
if (failureMayBeBilled(res)) costComplete = false;
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (res.value?.usage) usage = addUsage(usage, res.value.usage);
|
|
342
|
+
if (turn && res.value?.model) model = res.value.model;
|
|
343
|
+
if (res.cohort?.costMicros != null) costTotal += BigInt(res.cohort.costMicros);
|
|
344
|
+
else costComplete = false;
|
|
345
|
+
};
|
|
346
|
+
ctx.recordModelCall = (/** @type any */ res) => {
|
|
347
|
+
if (res && typeof res === "object") account(res, false);
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
for (;;) {
|
|
351
|
+
if (signal.aborted) return done(interrupted());
|
|
352
|
+
// Background tasks that ended since the last turn report as user messages
|
|
353
|
+
// (before the context manager measures the request, so they count toward it).
|
|
354
|
+
for (const n of p.drainNotifications?.() ?? []) append(userText(n));
|
|
355
|
+
{
|
|
356
|
+
const spent = overBudget();
|
|
357
|
+
if (spent) return done(spent);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (p.context) {
|
|
361
|
+
const prep = await p.context.beforeTurn({ system: p.system, messages, signal, recordModelCall: ctx.recordModelCall });
|
|
362
|
+
if (prep?.messages) messages.splice(0, messages.length, ...prep.messages);
|
|
363
|
+
for (const m of prep?.append ?? []) append(m);
|
|
364
|
+
if (prep?.tools && prep.tools !== toolsList) {
|
|
365
|
+
toolsList = prep.tools;
|
|
366
|
+
toolsByName = new Map(toolsList.map((t) => [t.name, t]));
|
|
367
|
+
}
|
|
368
|
+
defs = prep?.definitions ?? toolDefinitions(toolsList);
|
|
369
|
+
if (signal.aborted) return done(interrupted());
|
|
370
|
+
// A compaction summary is a model call too: it may have spent the rest.
|
|
371
|
+
const spent = overBudget();
|
|
372
|
+
if (spent) return done(spent);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const res = await p.callModel({ system: p.system, messages, tools: defs, signal });
|
|
376
|
+
p.context?.observe?.({ system: p.system, messages, tools: defs, res });
|
|
377
|
+
account(res, true);
|
|
378
|
+
if (!res.ok) {
|
|
379
|
+
if (res.error?.kind === "aborted" || signal.aborted) return done(interrupted());
|
|
380
|
+
return done({ kind: "wire_error", error: res.error });
|
|
381
|
+
}
|
|
382
|
+
numTurns++;
|
|
383
|
+
|
|
384
|
+
// Output cut off by the token limit: any tool call in this turn may be
|
|
385
|
+
// missing trailing fields (a `replace_all`, a `timeout`) that bracket repair
|
|
386
|
+
// would silently drop. Nothing runs; the model is asked to resend.
|
|
387
|
+
if (res.value.stopReason === "max_tokens") {
|
|
388
|
+
const message = res.value.message;
|
|
389
|
+
append(message);
|
|
390
|
+
const text = messageText(message);
|
|
391
|
+
if (text !== "") lastText = text;
|
|
392
|
+
const calls = toolUses(message);
|
|
393
|
+
if (calls.length === 0) {
|
|
394
|
+
const stop = await finishOrContinue();
|
|
395
|
+
if (stop) return done(stop);
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
streaks = nextStreaks(streaks, []);
|
|
399
|
+
append({ role: "user", content: calls.map((c) => errorResult(c.id, TRUNCATED_CALL)) });
|
|
400
|
+
if (signal.aborted) return done(interrupted());
|
|
401
|
+
if (numTurns >= maxTurns) return done({ kind: "max_turns", maxTurns });
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const { message, unrepaired } = repairToolUses(res.value.message);
|
|
406
|
+
append(message);
|
|
407
|
+
const text = messageText(message);
|
|
408
|
+
if (text !== "") lastText = text;
|
|
409
|
+
|
|
410
|
+
const calls = toolUses(message);
|
|
411
|
+
if (calls.length === 0) {
|
|
412
|
+
const stop = await finishOrContinue();
|
|
413
|
+
if (stop) return done(stop);
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
streaks = nextStreaks(streaks, calls.filter((c) => !unrepaired.has(c.id)).map(callSignature));
|
|
418
|
+
const worst = calls.reduce(
|
|
419
|
+
(acc, c) => {
|
|
420
|
+
const n = unrepaired.has(c.id) ? 0 : streaks.get(callSignature(c)) ?? 0;
|
|
421
|
+
return n > acc.n ? { n, tool: c.name } : acc;
|
|
422
|
+
},
|
|
423
|
+
{ n: 0, tool: "" },
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
/** @type {Array<{type:'tool_result', tool_use_id:string, content:string, is_error?:boolean}>} */
|
|
427
|
+
const results = new Array(calls.length);
|
|
428
|
+
if (worst.n >= loopStopAt) {
|
|
429
|
+
calls.forEach((c, i) => {
|
|
430
|
+
results[i] = errorResult(c.id, "Not run: this exact call was repeated too many times in a row and the run was stopped.");
|
|
431
|
+
});
|
|
432
|
+
append({ role: "user", content: results });
|
|
433
|
+
return done({ kind: "loop_detected", tool: worst.tool, repeats: worst.n });
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
for (const batch of planBatches(calls, (n) => Boolean(toolsByName.get(n)?.readOnly), maxParallel)) {
|
|
437
|
+
await Promise.all(
|
|
438
|
+
batch.map(async (i) => {
|
|
439
|
+
const c = calls[i];
|
|
440
|
+
const r = await executeCall(c, unrepaired.get(c.id), toolsByName, ctx, signal, guard);
|
|
441
|
+
const streak = unrepaired.has(c.id) ? 0 : streaks.get(callSignature(c)) ?? 0;
|
|
442
|
+
const note =
|
|
443
|
+
streak >= loopWarnAt
|
|
444
|
+
? `\n\n[This identical call has now been made ${streak} times in a row. The result will not change; try a different approach.]`
|
|
445
|
+
: "";
|
|
446
|
+
const body = clip((r.ok ? r.content : r.error) + note, maxChars);
|
|
447
|
+
const images = r.ok && Array.isArray(/** @type any */ (r).images) && /** @type any */ (r).images.length > 0 ? /** @type any */ (r).images : null;
|
|
448
|
+
results[i] = r.ok ? { type: "tool_result", tool_use_id: c.id, content: body, ...(images ? { images } : {}) } : errorResult(c.id, body);
|
|
449
|
+
}),
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
const extra = p.context?.afterTools ? await p.context.afterTools({ calls, results, signal }) : null;
|
|
453
|
+
append({ role: "user", content: extra && extra.length > 0 ? [...results, ...extra] : results });
|
|
454
|
+
|
|
455
|
+
if (signal.aborted) return done(interrupted());
|
|
456
|
+
if (guard?.stopReason) return done({ kind: "hook_stopped", reason: guard.stopReason });
|
|
457
|
+
if (numTurns >= maxTurns) return done({ kind: "max_turns", maxTurns });
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* The input check for a tool whose schema belongs to someone else (an MCP
|
|
463
|
+
* server): an object carrying its required keys, forwarded unchanged.
|
|
464
|
+
* @param {any} schema @param {unknown} input
|
|
465
|
+
* @returns {{ok:true, value:Record<string,unknown>}|{ok:false, error:string}}
|
|
466
|
+
*/
|
|
467
|
+
export function checkPassthroughInput(schema, input) {
|
|
468
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return { ok: false, error: "input must be a JSON object" };
|
|
469
|
+
const missing = (Array.isArray(schema?.required) ? schema.required : []).filter((k) => /** @type any */ (input)[k] === undefined || /** @type any */ (input)[k] === null);
|
|
470
|
+
if (missing.length > 0) return { ok: false, error: missing.map((k) => `missing required parameter "${k}"`).join("; ") };
|
|
471
|
+
return { ok: true, value: /** @type any */ (input) };
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export const TRUNCATED_CALL =
|
|
475
|
+
"Not run: your response reached the output token limit while this tool call was being written, so its arguments may be incomplete. " +
|
|
476
|
+
"Send the call again with shorter arguments; split large content across several calls.";
|
|
477
|
+
|
|
478
|
+
/** @param {any} q */
|
|
479
|
+
function hasQuota(q) {
|
|
480
|
+
return Boolean(q && (Object.keys(q.windows || {}).length > 0 || q.creditsRemainingMicros != null || q.funding != null));
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** @param {string} id @param {string} content */
|
|
484
|
+
function errorResult(id, content) {
|
|
485
|
+
return { type: "tool_result", tool_use_id: id, content, is_error: true };
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** @param {string} s @param {number} max */
|
|
489
|
+
function clip(s, max) {
|
|
490
|
+
return s.length <= max ? s : `${s.slice(0, max)}\n… [tool result truncated at ${max} characters]`;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* @param {{id:string, name:string, input:any}} call
|
|
495
|
+
* @param {string|undefined} parseError
|
|
496
|
+
* @param {Map<string, import('./tools/index.mjs').Tool>} toolsByName
|
|
497
|
+
* @param {import('./tools/index.mjs').ToolContext} ctx
|
|
498
|
+
* @param {AbortSignal} signal
|
|
499
|
+
* @param {ReturnType<typeof import('./guard.mjs').createToolGuard>|null} guard
|
|
500
|
+
* @returns {Promise<{ok:true, content:string}|{ok:false, error:string}>}
|
|
501
|
+
*/
|
|
502
|
+
async function executeCall(call, parseError, toolsByName, ctx, signal, guard) {
|
|
503
|
+
if (parseError !== undefined) {
|
|
504
|
+
return {
|
|
505
|
+
ok: false,
|
|
506
|
+
error: `The arguments for ${call.name} were not valid JSON (${parseError}). Send the call again with a JSON object that matches the tool's schema.`,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
const tool = toolsByName.get(call.name);
|
|
510
|
+
if (!tool) {
|
|
511
|
+
return { ok: false, error: `There is no tool named "${call.name}". Available tools: ${[...toolsByName.keys()].join(", ") || "none"}.` };
|
|
512
|
+
}
|
|
513
|
+
if (signal.aborted) return { ok: false, error: "Not run: the run was interrupted." };
|
|
514
|
+
const check = (/** @type unknown */ input) => (/** @type any */ (tool).passthroughInput ? checkPassthroughInput(tool.input_schema, input) : validateInput(tool.input_schema, input));
|
|
515
|
+
const v = check(call.input);
|
|
516
|
+
if (!v.ok) return { ok: false, error: `Invalid input for ${tool.name}: ${v.error}.` };
|
|
517
|
+
|
|
518
|
+
let input = v.value;
|
|
519
|
+
let hookContext = null;
|
|
520
|
+
if (guard) {
|
|
521
|
+
let g;
|
|
522
|
+
try {
|
|
523
|
+
g = await guard.before({ call, tool, input, signal });
|
|
524
|
+
} catch (e) {
|
|
525
|
+
return { ok: false, error: `Not run: the permission check for ${tool.name} failed unexpectedly (${e instanceof Error ? e.message : String(e)}).` };
|
|
526
|
+
}
|
|
527
|
+
if (!g.ok) return { ok: false, error: g.error };
|
|
528
|
+
if (g.input !== input) {
|
|
529
|
+
const again = check(g.input);
|
|
530
|
+
if (!again.ok) return { ok: false, error: `Invalid input for ${tool.name} after a hook changed it: ${again.error}.` };
|
|
531
|
+
input = again.value;
|
|
532
|
+
}
|
|
533
|
+
hookContext = g.context;
|
|
534
|
+
if (signal.aborted) return { ok: false, error: "Not run: the run was interrupted." };
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** @type {{ok:true, content:string}|{ok:false, error:string}} */
|
|
538
|
+
let r;
|
|
539
|
+
try {
|
|
540
|
+
r = await tool.run(input, { ...ctx, toolUseId: call.id }); // a subagent's events name the call that started it
|
|
541
|
+
} catch (e) {
|
|
542
|
+
// A tool throwing is a defect in that tool; contain it to this call so the
|
|
543
|
+
// model can route around it and the transcript stays well-formed.
|
|
544
|
+
r = { ok: false, error: `${tool.name} failed unexpectedly: ${e instanceof Error ? e.message : String(e)}` };
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const notes = [];
|
|
548
|
+
if (hookContext) notes.push(`[PreToolUse hook context]\n${hookContext}`);
|
|
549
|
+
if (guard) {
|
|
550
|
+
try {
|
|
551
|
+
const extra = await guard.after({ call, tool, input, result: r, signal });
|
|
552
|
+
if (extra) notes.push(extra);
|
|
553
|
+
} catch {
|
|
554
|
+
/* a PostToolUse defect cannot undo a call that already ran */
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
if (notes.length === 0) return r;
|
|
558
|
+
const tail = `\n\n${notes.join("\n\n")}`;
|
|
559
|
+
return r.ok ? { ...r, content: r.content + tail } : { ok: false, error: r.error + tail };
|
|
560
|
+
}
|