@vellumai/assistant 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +27 -0
- package/.env.example +22 -0
- package/Dockerfile +99 -0
- package/Dockerfile.sandbox +5 -0
- package/README.md +248 -0
- package/bun.lock +1723 -0
- package/bunfig.toml +2 -0
- package/docs/skills.md +158 -0
- package/drizzle/0000_dizzy_maggott.sql +301 -0
- package/drizzle/meta/0000_snapshot.json +1999 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +7 -0
- package/eslint.config.mjs +17 -0
- package/hook-templates/debug-prompt-logger/hook.json +7 -0
- package/hook-templates/debug-prompt-logger/run.sh +68 -0
- package/knip.json +9 -0
- package/package.json +70 -0
- package/scripts/capture-x-graphql.ts +545 -0
- package/scripts/ipc/check-contract-inventory.ts +104 -0
- package/scripts/ipc/check-swift-decoder-drift.ts +166 -0
- package/scripts/ipc/generate-swift.ts +492 -0
- package/scripts/test-filesystem-tools.sh +48 -0
- package/scripts/test.sh +127 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +2485 -0
- package/src/__tests__/account-registry.test.ts +245 -0
- package/src/__tests__/active-skill-tools.test.ts +378 -0
- package/src/__tests__/agent-heartbeat-service.test.ts +250 -0
- package/src/__tests__/agent-loop-thinking.test.ts +81 -0
- package/src/__tests__/agent-loop.test.ts +1135 -0
- package/src/__tests__/anthropic-provider.test.ts +778 -0
- package/src/__tests__/app-builder-tool-scripts.test.ts +290 -0
- package/src/__tests__/app-bundler.test.ts +292 -0
- package/src/__tests__/app-executors.test.ts +613 -0
- package/src/__tests__/app-git-history.test.ts +176 -0
- package/src/__tests__/app-git-service.test.ts +169 -0
- package/src/__tests__/app-open-proxy.test.ts +62 -0
- package/src/__tests__/asset-materialize-tool.test.ts +452 -0
- package/src/__tests__/asset-search-tool.test.ts +477 -0
- package/src/__tests__/assistant-attachment-directive.test.ts +401 -0
- package/src/__tests__/assistant-attachments.test.ts +437 -0
- package/src/__tests__/assistant-event-hub.test.ts +226 -0
- package/src/__tests__/assistant-event.test.ts +123 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +315 -0
- package/src/__tests__/attachments-store.test.ts +476 -0
- package/src/__tests__/attachments.test.ts +134 -0
- package/src/__tests__/audit-log-rotation.test.ts +154 -0
- package/src/__tests__/browser-fill-credential.test.ts +309 -0
- package/src/__tests__/browser-manager.test.ts +203 -0
- package/src/__tests__/browser-runtime-check.test.ts +55 -0
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +68 -0
- package/src/__tests__/browser-skill-endstate.test.ts +195 -0
- package/src/__tests__/bundle-scanner.test.ts +313 -0
- package/src/__tests__/call-bridge.test.ts +517 -0
- package/src/__tests__/call-constants.test.ts +40 -0
- package/src/__tests__/call-domain.test.ts +163 -0
- package/src/__tests__/call-orchestrator.test.ts +625 -0
- package/src/__tests__/call-recovery.test.ts +518 -0
- package/src/__tests__/call-routes-http.test.ts +699 -0
- package/src/__tests__/call-state-machine.test.ts +143 -0
- package/src/__tests__/call-state.test.ts +174 -0
- package/src/__tests__/call-store.test.ts +691 -0
- package/src/__tests__/channel-approval-routes.test.ts +2356 -0
- package/src/__tests__/channel-approval.test.ts +299 -0
- package/src/__tests__/channel-approvals.test.ts +521 -0
- package/src/__tests__/channel-delivery-store.test.ts +447 -0
- package/src/__tests__/channel-guardian.test.ts +1005 -0
- package/src/__tests__/checker.test.ts +3519 -0
- package/src/__tests__/clarification-resolver.test.ts +159 -0
- package/src/__tests__/classifier.test.ts +67 -0
- package/src/__tests__/claude-code-skill-regression.test.ts +127 -0
- package/src/__tests__/claude-code-tool-profiles.test.ts +88 -0
- package/src/__tests__/cli-discover.test.ts +85 -0
- package/src/__tests__/cli.test.ts +26 -0
- package/src/__tests__/clipboard.test.ts +80 -0
- package/src/__tests__/commit-guarantee.test.ts +335 -0
- package/src/__tests__/commit-message-enrichment-service.test.ts +550 -0
- package/src/__tests__/compaction.benchmark.test.ts +176 -0
- package/src/__tests__/computer-use-session-compaction.test.ts +132 -0
- package/src/__tests__/computer-use-session-lifecycle.test.ts +293 -0
- package/src/__tests__/computer-use-session-working-dir.test.ts +117 -0
- package/src/__tests__/computer-use-skill-baseline.test.ts +74 -0
- package/src/__tests__/computer-use-skill-endstate.test.ts +89 -0
- package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +217 -0
- package/src/__tests__/computer-use-skill-manifest-regression.test.ts +107 -0
- package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +54 -0
- package/src/__tests__/computer-use-tools.test.ts +250 -0
- package/src/__tests__/config-schema.test.ts +1462 -0
- package/src/__tests__/conflict-intent-tokenization.test.ts +141 -0
- package/src/__tests__/conflict-policy.test.ts +121 -0
- package/src/__tests__/conflict-store.test.ts +332 -0
- package/src/__tests__/connection-policy.test.ts +102 -0
- package/src/__tests__/contacts-tools.test.ts +331 -0
- package/src/__tests__/context-memory-e2e.test.ts +434 -0
- package/src/__tests__/context-token-estimator.test.ts +135 -0
- package/src/__tests__/context-window-manager.test.ts +376 -0
- package/src/__tests__/contradiction-checker.test.ts +314 -0
- package/src/__tests__/conversation-store.test.ts +612 -0
- package/src/__tests__/credential-broker-browser-fill.test.ts +517 -0
- package/src/__tests__/credential-broker-server-use.test.ts +554 -0
- package/src/__tests__/credential-broker.test.ts +167 -0
- package/src/__tests__/credential-host-pattern-match.test.ts +104 -0
- package/src/__tests__/credential-metadata-store.test.ts +779 -0
- package/src/__tests__/credential-policy-validate.test.ts +121 -0
- package/src/__tests__/credential-resolve.test.ts +328 -0
- package/src/__tests__/credential-security-e2e.test.ts +352 -0
- package/src/__tests__/credential-security-invariants.test.ts +583 -0
- package/src/__tests__/credential-selection.test.ts +354 -0
- package/src/__tests__/credential-vault-unit.test.ts +780 -0
- package/src/__tests__/credential-vault.test.ts +852 -0
- package/src/__tests__/daemon-assistant-events.test.ts +164 -0
- package/src/__tests__/daemon-server-session-init.test.ts +522 -0
- package/src/__tests__/date-context.test.ts +373 -0
- package/src/__tests__/db-schedule-syntax-migration.test.ts +129 -0
- package/src/__tests__/delete-managed-skill-tool.test.ts +97 -0
- package/src/__tests__/diff.test.ts +121 -0
- package/src/__tests__/domain-normalize.test.ts +112 -0
- package/src/__tests__/domain-policy.test.ts +124 -0
- package/src/__tests__/doordash-client.test.ts +186 -0
- package/src/__tests__/doordash-session.test.ts +152 -0
- package/src/__tests__/dynamic-page-surface.test.ts +91 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +132 -0
- package/src/__tests__/edit-engine.test.ts +180 -0
- package/src/__tests__/elevenlabs-client.test.ts +271 -0
- package/src/__tests__/email-cli.test.ts +283 -0
- package/src/__tests__/encrypted-store.test.ts +332 -0
- package/src/__tests__/entity-extractor.test.ts +190 -0
- package/src/__tests__/ephemeral-permissions.test.ts +362 -0
- package/src/__tests__/evaluate-typescript-tool.test.ts +286 -0
- package/src/__tests__/event-bus.test.ts +222 -0
- package/src/__tests__/file-edit-tool.test.ts +122 -0
- package/src/__tests__/file-ops-service.test.ts +330 -0
- package/src/__tests__/file-read-tool.test.ts +75 -0
- package/src/__tests__/file-write-tool.test.ts +113 -0
- package/src/__tests__/filesystem-tools.test.ts +579 -0
- package/src/__tests__/fixtures/credential-security-fixtures.ts +181 -0
- package/src/__tests__/fixtures/media-reuse-fixtures.ts +126 -0
- package/src/__tests__/fixtures/mock-signup-server.ts +387 -0
- package/src/__tests__/fixtures/proxy-fixtures.ts +147 -0
- package/src/__tests__/followup-tools.test.ts +303 -0
- package/src/__tests__/forbidden-legacy-symbols.test.ts +71 -0
- package/src/__tests__/fuzzy-match-property.test.ts +216 -0
- package/src/__tests__/fuzzy-match.test.ts +138 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +631 -0
- package/src/__tests__/gemini-image-service.test.ts +261 -0
- package/src/__tests__/gemini-provider.test.ts +651 -0
- package/src/__tests__/get-weather.test.ts +318 -0
- package/src/__tests__/gmail-integration.test.ts +73 -0
- package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +202 -0
- package/src/__tests__/handlers-cu-observation-blob.test.ts +352 -0
- package/src/__tests__/handlers-ipc-blob-probe.test.ts +191 -0
- package/src/__tests__/handlers-slack-config.test.ts +200 -0
- package/src/__tests__/handlers-task-submit-slash.test.ts +38 -0
- package/src/__tests__/handlers-telegram-config.test.ts +968 -0
- package/src/__tests__/handlers-twilio-config.test.ts +659 -0
- package/src/__tests__/handlers-twitter-config.test.ts +858 -0
- package/src/__tests__/headless-browser-interactions.test.ts +536 -0
- package/src/__tests__/headless-browser-navigate.test.ts +211 -0
- package/src/__tests__/headless-browser-read-tools.test.ts +261 -0
- package/src/__tests__/headless-browser-snapshot.test.ts +185 -0
- package/src/__tests__/history-repair-observability.test.ts +56 -0
- package/src/__tests__/history-repair.test.ts +510 -0
- package/src/__tests__/home-base-bootstrap.test.ts +82 -0
- package/src/__tests__/hooks-blocking.test.ts +128 -0
- package/src/__tests__/hooks-cli.test.ts +144 -0
- package/src/__tests__/hooks-config.test.ts +93 -0
- package/src/__tests__/hooks-discovery.test.ts +199 -0
- package/src/__tests__/hooks-integration.test.ts +189 -0
- package/src/__tests__/hooks-manager.test.ts +187 -0
- package/src/__tests__/hooks-runner.test.ts +182 -0
- package/src/__tests__/hooks-settings.test.ts +154 -0
- package/src/__tests__/hooks-templates.test.ts +137 -0
- package/src/__tests__/hooks-ts-runner.test.ts +125 -0
- package/src/__tests__/hooks-watch.test.ts +100 -0
- package/src/__tests__/host-file-edit-tool.test.ts +228 -0
- package/src/__tests__/host-file-read-tool.test.ts +123 -0
- package/src/__tests__/host-file-write-tool.test.ts +136 -0
- package/src/__tests__/host-shell-tool.test.ts +562 -0
- package/src/__tests__/ingress-reconcile.test.ts +581 -0
- package/src/__tests__/ingress-url-consistency.test.ts +214 -0
- package/src/__tests__/intent-routing.test.ts +259 -0
- package/src/__tests__/ipc-blob-store.test.ts +315 -0
- package/src/__tests__/ipc-contract-inventory.test.ts +54 -0
- package/src/__tests__/ipc-contract.test.ts +74 -0
- package/src/__tests__/ipc-protocol.test.ts +113 -0
- package/src/__tests__/ipc-roundtrip.benchmark.test.ts +237 -0
- package/src/__tests__/ipc-snapshot.test.ts +1769 -0
- package/src/__tests__/ipc-validate.test.ts +407 -0
- package/src/__tests__/key-migration.test.ts +206 -0
- package/src/__tests__/keychain.test.ts +258 -0
- package/src/__tests__/llm-usage-store.test.ts +221 -0
- package/src/__tests__/managed-skill-lifecycle.test.ts +257 -0
- package/src/__tests__/managed-store.test.ts +608 -0
- package/src/__tests__/media-generate-image.test.ts +238 -0
- package/src/__tests__/media-reuse-story.e2e.test.ts +676 -0
- package/src/__tests__/media-visibility-policy.test.ts +141 -0
- package/src/__tests__/memory-context-benchmark.benchmark.test.ts +235 -0
- package/src/__tests__/memory-lifecycle-e2e.test.ts +481 -0
- package/src/__tests__/memory-query-builder.test.ts +59 -0
- package/src/__tests__/memory-recall-quality.test.ts +846 -0
- package/src/__tests__/memory-regressions.experimental.test.ts +538 -0
- package/src/__tests__/memory-regressions.test.ts +4435 -0
- package/src/__tests__/memory-retrieval-budget.test.ts +49 -0
- package/src/__tests__/memory-retrieval.benchmark.test.ts +430 -0
- package/src/__tests__/migration-cli-flows.test.ts +169 -0
- package/src/__tests__/migration-ordering.test.ts +249 -0
- package/src/__tests__/mock-signup-server.test.ts +528 -0
- package/src/__tests__/oauth-callback-registry.test.ts +92 -0
- package/src/__tests__/oauth2-gateway-transport.test.ts +285 -0
- package/src/__tests__/onboarding-starter-tasks.test.ts +176 -0
- package/src/__tests__/onboarding-template-contract.test.ts +58 -0
- package/src/__tests__/openai-provider.test.ts +753 -0
- package/src/__tests__/parallel-tool.benchmark.test.ts +294 -0
- package/src/__tests__/parser.test.ts +472 -0
- package/src/__tests__/path-classifier.test.ts +73 -0
- package/src/__tests__/path-policy.test.ts +435 -0
- package/src/__tests__/platform-move-helper.test.ts +99 -0
- package/src/__tests__/platform-socket-path.test.ts +52 -0
- package/src/__tests__/platform-workspace-migration.test.ts +1000 -0
- package/src/__tests__/platform.test.ts +131 -0
- package/src/__tests__/playbook-execution.test.ts +502 -0
- package/src/__tests__/playbook-tools.test.ts +340 -0
- package/src/__tests__/prebuilt-home-base-seed.test.ts +75 -0
- package/src/__tests__/pricing.test.ts +256 -0
- package/src/__tests__/profile-compiler.test.ts +374 -0
- package/src/__tests__/provider-commit-message-generator.test.ts +342 -0
- package/src/__tests__/provider-registry-ollama.test.ts +16 -0
- package/src/__tests__/provider-streaming.benchmark.test.ts +773 -0
- package/src/__tests__/proxy-approval-callback.test.ts +601 -0
- package/src/__tests__/public-ingress-urls.test.ts +256 -0
- package/src/__tests__/qdrant-manager.test.ts +267 -0
- package/src/__tests__/ratelimit.test.ts +297 -0
- package/src/__tests__/recurrence-engine-rruleset.test.ts +175 -0
- package/src/__tests__/recurrence-engine.test.ts +78 -0
- package/src/__tests__/recurrence-types.test.ts +79 -0
- package/src/__tests__/registry.test.ts +494 -0
- package/src/__tests__/relay-server.test.ts +688 -0
- package/src/__tests__/reminder-store.test.ts +223 -0
- package/src/__tests__/reminder.test.ts +229 -0
- package/src/__tests__/request-file-tool.test.ts +158 -0
- package/src/__tests__/run-orchestrator-assistant-events.test.ts +227 -0
- package/src/__tests__/run-orchestrator.test.ts +425 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +189 -0
- package/src/__tests__/runtime-events-sse-parity.test.ts +343 -0
- package/src/__tests__/runtime-events-sse.test.ts +162 -0
- package/src/__tests__/runtime-runs-http.test.ts +438 -0
- package/src/__tests__/runtime-runs.test.ts +260 -0
- package/src/__tests__/sandbox-diagnostics.test.ts +408 -0
- package/src/__tests__/sandbox-host-parity.test.ts +950 -0
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +253 -0
- package/src/__tests__/schedule-store.test.ts +484 -0
- package/src/__tests__/schedule-tools.test.ts +783 -0
- package/src/__tests__/scheduler-recurrence.test.ts +430 -0
- package/src/__tests__/script-proxy-certs.test.ts +90 -0
- package/src/__tests__/script-proxy-connect-tunnel.test.ts +177 -0
- package/src/__tests__/script-proxy-decision-trace.test.ts +156 -0
- package/src/__tests__/script-proxy-http-forwarder.test.ts +281 -0
- package/src/__tests__/script-proxy-injection-runtime.test.ts +401 -0
- package/src/__tests__/script-proxy-mitm-handler.test.ts +407 -0
- package/src/__tests__/script-proxy-policy-runtime.test.ts +287 -0
- package/src/__tests__/script-proxy-policy.test.ts +310 -0
- package/src/__tests__/script-proxy-rewrite-specificity.test.ts +135 -0
- package/src/__tests__/script-proxy-router.test.ts +180 -0
- package/src/__tests__/script-proxy-session-manager.test.ts +382 -0
- package/src/__tests__/script-proxy-session-runtime.test.ts +113 -0
- package/src/__tests__/secret-allowlist.test.ts +230 -0
- package/src/__tests__/secret-ingress-handler.test.ts +110 -0
- package/src/__tests__/secret-onetime-send.test.ts +130 -0
- package/src/__tests__/secret-prompt-log-hygiene.test.ts +106 -0
- package/src/__tests__/secret-response-routing.test.ts +93 -0
- package/src/__tests__/secret-scanner-executor.test.ts +348 -0
- package/src/__tests__/secret-scanner.test.ts +900 -0
- package/src/__tests__/secure-keys.test.ts +323 -0
- package/src/__tests__/server-history-render.test.ts +431 -0
- package/src/__tests__/session-abort-tool-results.test.ts +240 -0
- package/src/__tests__/session-conflict-gate.test.ts +1136 -0
- package/src/__tests__/session-error.test.ts +369 -0
- package/src/__tests__/session-evictor.test.ts +188 -0
- package/src/__tests__/session-init.benchmark.test.ts +465 -0
- package/src/__tests__/session-load-history-repair.test.ts +222 -0
- package/src/__tests__/session-pre-run-repair.test.ts +213 -0
- package/src/__tests__/session-process-bridge.test.ts +242 -0
- package/src/__tests__/session-profile-injection.test.ts +444 -0
- package/src/__tests__/session-provider-retry-repair.test.ts +306 -0
- package/src/__tests__/session-queue.test.ts +1535 -0
- package/src/__tests__/session-runtime-assembly.test.ts +476 -0
- package/src/__tests__/session-runtime-workspace.test.ts +183 -0
- package/src/__tests__/session-skill-tools.test.ts +2431 -0
- package/src/__tests__/session-slash-known.test.ts +368 -0
- package/src/__tests__/session-slash-queue.test.ts +288 -0
- package/src/__tests__/session-slash-unknown.test.ts +271 -0
- package/src/__tests__/session-surfaces-task-progress.test.ts +104 -0
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +473 -0
- package/src/__tests__/session-tool-setup-memory-scope.test.ts +140 -0
- package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +140 -0
- package/src/__tests__/session-undo.test.ts +75 -0
- package/src/__tests__/session-workspace-cache-state.test.ts +246 -0
- package/src/__tests__/session-workspace-injection.test.ts +327 -0
- package/src/__tests__/session-workspace-tool-tracking.test.ts +240 -0
- package/src/__tests__/shared-filesystem-errors.test.ts +78 -0
- package/src/__tests__/shell-credential-ref.test.ts +187 -0
- package/src/__tests__/shell-identity.test.ts +256 -0
- package/src/__tests__/shell-parser-fuzz.test.ts +544 -0
- package/src/__tests__/shell-parser-property.test.ts +433 -0
- package/src/__tests__/shell-tool-proxy-mode.test.ts +272 -0
- package/src/__tests__/signup-e2e.test.ts +353 -0
- package/src/__tests__/size-guard.test.ts +117 -0
- package/src/__tests__/skill-include-graph.test.ts +303 -0
- package/src/__tests__/skill-load-tool.test.ts +409 -0
- package/src/__tests__/skill-projection.benchmark.test.ts +338 -0
- package/src/__tests__/skill-script-runner-host.test.ts +489 -0
- package/src/__tests__/skill-script-runner-sandbox.test.ts +349 -0
- package/src/__tests__/skill-script-runner.test.ts +159 -0
- package/src/__tests__/skill-tool-factory.test.ts +252 -0
- package/src/__tests__/skill-tool-manifest.test.ts +658 -0
- package/src/__tests__/skill-version-hash.test.ts +182 -0
- package/src/__tests__/skills.test.ts +680 -0
- package/src/__tests__/slash-commands-catalog.test.ts +86 -0
- package/src/__tests__/slash-commands-parser.test.ts +119 -0
- package/src/__tests__/slash-commands-resolver.test.ts +193 -0
- package/src/__tests__/slash-commands-rewrite.test.ts +39 -0
- package/src/__tests__/speaker-identification.test.ts +52 -0
- package/src/__tests__/starter-bundle.test.ts +136 -0
- package/src/__tests__/starter-task-flow.test.ts +143 -0
- package/src/__tests__/subagent-manager-notify.test.ts +404 -0
- package/src/__tests__/subagent-tools.test.ts +801 -0
- package/src/__tests__/subagent-types.test.ts +78 -0
- package/src/__tests__/swarm-orchestrator.test.ts +428 -0
- package/src/__tests__/swarm-plan-validator.test.ts +330 -0
- package/src/__tests__/swarm-recursion.test.ts +165 -0
- package/src/__tests__/swarm-router-planner.test.ts +208 -0
- package/src/__tests__/swarm-session-integration.test.ts +274 -0
- package/src/__tests__/swarm-tool.test.ts +145 -0
- package/src/__tests__/swarm-worker-backend.test.ts +129 -0
- package/src/__tests__/swarm-worker-runner.test.ts +272 -0
- package/src/__tests__/system-prompt.test.ts +439 -0
- package/src/__tests__/task-compiler.test.ts +284 -0
- package/src/__tests__/task-management-tools.test.ts +936 -0
- package/src/__tests__/task-runner.test.ts +216 -0
- package/src/__tests__/task-scheduler.test.ts +217 -0
- package/src/__tests__/task-tools.test.ts +595 -0
- package/src/__tests__/terminal-sandbox-docker.test.ts +1064 -0
- package/src/__tests__/terminal-sandbox.integration.test.ts +178 -0
- package/src/__tests__/terminal-sandbox.test.ts +202 -0
- package/src/__tests__/terminal-tools.test.ts +840 -0
- package/src/__tests__/test-support/browser-skill-harness.ts +90 -0
- package/src/__tests__/test-support/computer-use-skill-harness.ts +45 -0
- package/src/__tests__/tool-audit-listener.test.ts +113 -0
- package/src/__tests__/tool-domain-event-publisher.test.ts +253 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +500 -0
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +516 -0
- package/src/__tests__/tool-executor-redaction.test.ts +289 -0
- package/src/__tests__/tool-executor-shell-integration.test.ts +301 -0
- package/src/__tests__/tool-executor.test.ts +1989 -0
- package/src/__tests__/tool-metrics-listener.test.ts +225 -0
- package/src/__tests__/tool-notification-listener.test.ts +49 -0
- package/src/__tests__/tool-permission-simulate-handler.test.ts +336 -0
- package/src/__tests__/tool-policy.test.ts +54 -0
- package/src/__tests__/tool-profiling-listener.test.ts +268 -0
- package/src/__tests__/tool-result-truncation.test.ts +217 -0
- package/src/__tests__/tool-trace-listener.test.ts +226 -0
- package/src/__tests__/top-level-renderer.test.ts +121 -0
- package/src/__tests__/top-level-scanner.test.ts +141 -0
- package/src/__tests__/trace-emitter.test.ts +173 -0
- package/src/__tests__/trust-store.test.ts +1605 -0
- package/src/__tests__/turn-commit.test.ts +554 -0
- package/src/__tests__/twilio-provider.test.ts +329 -0
- package/src/__tests__/twilio-routes-elevenlabs.test.ts +375 -0
- package/src/__tests__/twilio-routes-twiml.test.ts +127 -0
- package/src/__tests__/twilio-routes.test.ts +577 -0
- package/src/__tests__/twitter-auth-handler.test.ts +667 -0
- package/src/__tests__/twitter-cli-error-shaping.test.ts +208 -0
- package/src/__tests__/twitter-cli-routing.test.ts +252 -0
- package/src/__tests__/twitter-oauth-client.test.ts +209 -0
- package/src/__tests__/url-safety.test.ts +418 -0
- package/src/__tests__/view-image-tool.test.ts +217 -0
- package/src/__tests__/weather-skill-regression.test.ts +225 -0
- package/src/__tests__/web-fetch.test.ts +869 -0
- package/src/__tests__/web-search.test.ts +584 -0
- package/src/__tests__/workspace-git-service.test.ts +1153 -0
- package/src/__tests__/workspace-heartbeat-service.test.ts +486 -0
- package/src/__tests__/workspace-lifecycle.test.ts +292 -0
- package/src/__tests__/workspace-policy.test.ts +213 -0
- package/src/agent/attachments.ts +35 -0
- package/src/agent/loop.ts +500 -0
- package/src/agent/message-types.ts +17 -0
- package/src/agent-heartbeat/agent-heartbeat-service.ts +155 -0
- package/src/autonomy/autonomy-resolver.ts +60 -0
- package/src/autonomy/autonomy-store.ts +122 -0
- package/src/autonomy/disposition-mapper.ts +31 -0
- package/src/autonomy/index.ts +11 -0
- package/src/autonomy/types.ts +39 -0
- package/src/bundler/app-bundler.ts +295 -0
- package/src/bundler/bundle-scanner.ts +535 -0
- package/src/bundler/bundle-signer.ts +124 -0
- package/src/bundler/manifest.ts +21 -0
- package/src/bundler/signature-verifier.ts +184 -0
- package/src/calls/call-bridge.ts +168 -0
- package/src/calls/call-constants.ts +48 -0
- package/src/calls/call-domain.ts +430 -0
- package/src/calls/call-orchestrator.ts +498 -0
- package/src/calls/call-recovery.ts +207 -0
- package/src/calls/call-state-machine.ts +68 -0
- package/src/calls/call-state.ts +87 -0
- package/src/calls/call-store.ts +422 -0
- package/src/calls/elevenlabs-client.ts +97 -0
- package/src/calls/elevenlabs-config.ts +31 -0
- package/src/calls/relay-server.ts +390 -0
- package/src/calls/speaker-identification.ts +213 -0
- package/src/calls/twilio-config.ts +45 -0
- package/src/calls/twilio-provider.ts +263 -0
- package/src/calls/twilio-rest.ts +156 -0
- package/src/calls/twilio-routes.ts +311 -0
- package/src/calls/types.ts +39 -0
- package/src/calls/voice-provider.ts +14 -0
- package/src/calls/voice-quality.ts +114 -0
- package/src/cli/autonomy.ts +188 -0
- package/src/cli/config-commands.ts +334 -0
- package/src/cli/contacts.ts +149 -0
- package/src/cli/core-commands.ts +784 -0
- package/src/cli/doordash.ts +1055 -0
- package/src/cli/email-guardrails.ts +200 -0
- package/src/cli/email.ts +405 -0
- package/src/cli/ipc-client.ts +82 -0
- package/src/cli/main-screen.tsx +53 -0
- package/src/cli/map.ts +270 -0
- package/src/cli/twitter.ts +754 -0
- package/src/cli.ts +918 -0
- package/src/commands/__tests__/cc-command-registry.test.ts +319 -0
- package/src/commands/cc-command-registry.ts +209 -0
- package/src/config/bundled-skills/.gitkeep +0 -0
- package/src/config/bundled-skills/agentmail/SKILL.md +128 -0
- package/src/config/bundled-skills/agentmail/icon.svg +21 -0
- package/src/config/bundled-skills/app-builder/SKILL.md +1404 -0
- package/src/config/bundled-skills/app-builder/TOOLS.json +279 -0
- package/src/config/bundled-skills/app-builder/icon.svg +9 -0
- package/src/config/bundled-skills/app-builder/tools/app-create.ts +15 -0
- package/src/config/bundled-skills/app-builder/tools/app-delete.ts +10 -0
- package/src/config/bundled-skills/app-builder/tools/app-file-edit.ts +11 -0
- package/src/config/bundled-skills/app-builder/tools/app-file-list.ts +10 -0
- package/src/config/bundled-skills/app-builder/tools/app-file-read.ts +18 -0
- package/src/config/bundled-skills/app-builder/tools/app-file-write.ts +11 -0
- package/src/config/bundled-skills/app-builder/tools/app-list.ts +10 -0
- package/src/config/bundled-skills/app-builder/tools/app-query.ts +10 -0
- package/src/config/bundled-skills/app-builder/tools/app-update.ts +20 -0
- package/src/config/bundled-skills/browser/SKILL.md +28 -0
- package/src/config/bundled-skills/browser/TOOLS.json +234 -0
- package/src/config/bundled-skills/browser/tools/browser-click.ts +9 -0
- package/src/config/bundled-skills/browser/tools/browser-close.ts +9 -0
- package/src/config/bundled-skills/browser/tools/browser-extract.ts +9 -0
- package/src/config/bundled-skills/browser/tools/browser-fill-credential.ts +9 -0
- package/src/config/bundled-skills/browser/tools/browser-navigate.ts +9 -0
- package/src/config/bundled-skills/browser/tools/browser-press-key.ts +9 -0
- package/src/config/bundled-skills/browser/tools/browser-screenshot.ts +9 -0
- package/src/config/bundled-skills/browser/tools/browser-snapshot.ts +9 -0
- package/src/config/bundled-skills/browser/tools/browser-type.ts +9 -0
- package/src/config/bundled-skills/browser/tools/browser-wait-for.ts +9 -0
- package/src/config/bundled-skills/claude-code/SKILL.md +50 -0
- package/src/config/bundled-skills/claude-code/TOOLS.json +40 -0
- package/src/config/bundled-skills/claude-code/tools/claude-code.ts +9 -0
- package/src/config/bundled-skills/computer-use/SKILL.md +17 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +326 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-click.ts +9 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-done.ts +9 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-double-click.ts +9 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-drag.ts +9 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-key.ts +9 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-open-app.ts +9 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-request-control.ts +9 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-respond.ts +9 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-right-click.ts +9 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-run-applescript.ts +9 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-scroll.ts +9 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-type-text.ts +9 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-wait.ts +9 -0
- package/src/config/bundled-skills/contacts/SKILL.md +39 -0
- package/src/config/bundled-skills/contacts/TOOLS.json +122 -0
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +57 -0
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +60 -0
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +66 -0
- package/src/config/bundled-skills/document/SKILL.md +26 -0
- package/src/config/bundled-skills/document/TOOLS.json +53 -0
- package/src/config/bundled-skills/document/tools/document-create.ts +9 -0
- package/src/config/bundled-skills/document/tools/document-update.ts +9 -0
- package/src/config/bundled-skills/doordash/SKILL.md +163 -0
- package/src/config/bundled-skills/followups/SKILL.md +32 -0
- package/src/config/bundled-skills/followups/TOOLS.json +100 -0
- package/src/config/bundled-skills/followups/icon.svg +24 -0
- package/src/config/bundled-skills/followups/tools/followup-create.ts +9 -0
- package/src/config/bundled-skills/followups/tools/followup-list.ts +9 -0
- package/src/config/bundled-skills/followups/tools/followup-resolve.ts +9 -0
- package/src/config/bundled-skills/google-calendar/SKILL.md +51 -0
- package/src/config/bundled-skills/google-calendar/TOOLS.json +108 -0
- package/src/config/bundled-skills/google-calendar/calendar-client.ts +165 -0
- package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +21 -0
- package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +42 -0
- package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +13 -0
- package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +30 -0
- package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +41 -0
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +18 -0
- package/src/config/bundled-skills/google-calendar/types.ts +97 -0
- package/src/config/bundled-skills/image-studio/SKILL.md +32 -0
- package/src/config/bundled-skills/image-studio/TOOLS.json +42 -0
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +115 -0
- package/src/config/bundled-skills/macos-automation/SKILL.md +66 -0
- package/src/config/bundled-skills/messaging/SKILL.md +153 -0
- package/src/config/bundled-skills/messaging/TOOLS.json +357 -0
- package/src/config/bundled-skills/messaging/tools/gmail-archive.ts +23 -0
- package/src/config/bundled-skills/messaging/tools/gmail-batch-archive.ts +23 -0
- package/src/config/bundled-skills/messaging/tools/gmail-batch-label.ts +25 -0
- package/src/config/bundled-skills/messaging/tools/gmail-draft.ts +26 -0
- package/src/config/bundled-skills/messaging/tools/gmail-label.ts +25 -0
- package/src/config/bundled-skills/messaging/tools/gmail-trash.ts +23 -0
- package/src/config/bundled-skills/messaging/tools/gmail-unsubscribe.ts +84 -0
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-activity.ts +18 -0
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +125 -0
- package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +16 -0
- package/src/config/bundled-skills/messaging/tools/messaging-draft.ts +49 -0
- package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +21 -0
- package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +25 -0
- package/src/config/bundled-skills/messaging/tools/messaging-read.ts +28 -0
- package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +32 -0
- package/src/config/bundled-skills/messaging/tools/messaging-search.ts +22 -0
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +31 -0
- package/src/config/bundled-skills/messaging/tools/shared.ts +76 -0
- package/src/config/bundled-skills/messaging/tools/slack-add-reaction.ts +25 -0
- package/src/config/bundled-skills/messaging/tools/slack-leave-channel.ts +23 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +533 -0
- package/src/config/bundled-skills/playbooks/SKILL.md +31 -0
- package/src/config/bundled-skills/playbooks/TOOLS.json +126 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +98 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +54 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +76 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +113 -0
- package/src/config/bundled-skills/public-ingress/SKILL.md +200 -0
- package/src/config/bundled-skills/reminder/SKILL.md +20 -0
- package/src/config/bundled-skills/reminder/TOOLS.json +67 -0
- package/src/config/bundled-skills/reminder/tools/reminder-cancel.ts +9 -0
- package/src/config/bundled-skills/reminder/tools/reminder-create.ts +9 -0
- package/src/config/bundled-skills/reminder/tools/reminder-list.ts +9 -0
- package/src/config/bundled-skills/schedule/SKILL.md +74 -0
- package/src/config/bundled-skills/schedule/TOOLS.json +135 -0
- package/src/config/bundled-skills/schedule/tools/schedule-create.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-delete.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-list.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-update.ts +9 -0
- package/src/config/bundled-skills/self-upgrade/SKILL.md +68 -0
- package/src/config/bundled-skills/start-the-day/SKILL.md +70 -0
- package/src/config/bundled-skills/start-the-day/icon.svg +13 -0
- package/src/config/bundled-skills/subagent/SKILL.md +25 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +107 -0
- package/src/config/bundled-skills/subagent/tools/subagent-abort.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-message.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-read.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-spawn.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-status.ts +9 -0
- package/src/config/bundled-skills/tasks/SKILL.md +28 -0
- package/src/config/bundled-skills/tasks/TOOLS.json +281 -0
- package/src/config/bundled-skills/tasks/tools/task-delete.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-add.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-show.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-update.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-queue-run.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-run.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-save.ts +9 -0
- package/src/config/bundled-skills/transcribe/SKILL.md +25 -0
- package/src/config/bundled-skills/transcribe/TOOLS.json +32 -0
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +370 -0
- package/src/config/bundled-skills/twitter/SKILL.md +220 -0
- package/src/config/bundled-skills/watcher/SKILL.md +27 -0
- package/src/config/bundled-skills/watcher/TOOLS.json +147 -0
- package/src/config/bundled-skills/watcher/tools/watcher-create.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-list.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-update.ts +9 -0
- package/src/config/bundled-skills/weather/SKILL.md +37 -0
- package/src/config/bundled-skills/weather/TOOLS.json +32 -0
- package/src/config/bundled-skills/weather/icon.svg +24 -0
- package/src/config/bundled-skills/weather/tools/get-weather.ts +9 -0
- package/src/config/computer-use-prompt.ts +97 -0
- package/src/config/defaults.ts +263 -0
- package/src/config/loader.ts +339 -0
- package/src/config/schema.ts +1436 -0
- package/src/config/skill-state.ts +95 -0
- package/src/config/skills.ts +972 -0
- package/src/config/system-prompt.ts +675 -0
- package/src/config/templates/BOOTSTRAP.md +70 -0
- package/src/config/templates/IDENTITY.md +25 -0
- package/src/config/templates/LOOKS.md +25 -0
- package/src/config/templates/SOUL.md +37 -0
- package/src/config/templates/USER.md +19 -0
- package/src/config/types.ts +42 -0
- package/src/config/vellum-skills/chatgpt-import/SKILL.md +24 -0
- package/src/config/vellum-skills/chatgpt-import/TOOLS.json +23 -0
- package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +284 -0
- package/src/config/vellum-skills/deploy-fullstack-vercel/SKILL.md +179 -0
- package/src/config/vellum-skills/document-writer/SKILL.md +195 -0
- package/src/config/vellum-skills/google-oauth-setup/SKILL.md +199 -0
- package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +153 -0
- package/src/config/vellum-skills/telegram-setup/SKILL.md +143 -0
- package/src/config/vellum-skills/twilio-setup/SKILL.md +213 -0
- package/src/contacts/contact-store.ts +410 -0
- package/src/contacts/index.ts +11 -0
- package/src/contacts/types.ts +28 -0
- package/src/context/token-estimator.ts +108 -0
- package/src/context/tool-result-truncation.ts +128 -0
- package/src/context/window-manager.ts +531 -0
- package/src/daemon/assistant-attachments.ts +691 -0
- package/src/daemon/classifier.ts +110 -0
- package/src/daemon/computer-use-session.ts +903 -0
- package/src/daemon/connection-policy.ts +41 -0
- package/src/daemon/date-context.ts +136 -0
- package/src/daemon/handlers/apps.ts +530 -0
- package/src/daemon/handlers/browser.ts +54 -0
- package/src/daemon/handlers/computer-use.ts +187 -0
- package/src/daemon/handlers/config.ts +1517 -0
- package/src/daemon/handlers/diagnostics.ts +338 -0
- package/src/daemon/handlers/documents.ts +173 -0
- package/src/daemon/handlers/home-base.ts +78 -0
- package/src/daemon/handlers/identity.ts +127 -0
- package/src/daemon/handlers/index.ts +129 -0
- package/src/daemon/handlers/misc.ts +331 -0
- package/src/daemon/handlers/open-bundle-handler.ts +80 -0
- package/src/daemon/handlers/publish.ts +187 -0
- package/src/daemon/handlers/sessions.ts +555 -0
- package/src/daemon/handlers/shared.ts +570 -0
- package/src/daemon/handlers/signing.ts +37 -0
- package/src/daemon/handlers/skills.ts +486 -0
- package/src/daemon/handlers/subagents.ts +210 -0
- package/src/daemon/handlers/twitter-auth.ts +198 -0
- package/src/daemon/handlers/work-items.ts +632 -0
- package/src/daemon/handlers/workspace-files.ts +75 -0
- package/src/daemon/handlers.ts +17 -0
- package/src/daemon/history-repair.ts +214 -0
- package/src/daemon/ipc-blob-store.ts +231 -0
- package/src/daemon/ipc-contract-inventory.json +495 -0
- package/src/daemon/ipc-contract-inventory.ts +126 -0
- package/src/daemon/ipc-contract.ts +2551 -0
- package/src/daemon/ipc-protocol.ts +75 -0
- package/src/daemon/ipc-validate.ts +188 -0
- package/src/daemon/lifecycle.ts +582 -0
- package/src/daemon/main.ts +21 -0
- package/src/daemon/media-visibility-policy.ts +57 -0
- package/src/daemon/ride-shotgun-handler.ts +309 -0
- package/src/daemon/server.ts +1215 -0
- package/src/daemon/session-agent-loop.ts +922 -0
- package/src/daemon/session-attachments.ts +196 -0
- package/src/daemon/session-conflict-gate.ts +184 -0
- package/src/daemon/session-dynamic-profile.ts +63 -0
- package/src/daemon/session-error.ts +290 -0
- package/src/daemon/session-evictor.ts +196 -0
- package/src/daemon/session-history.ts +437 -0
- package/src/daemon/session-lifecycle.ts +147 -0
- package/src/daemon/session-media-retry.ts +147 -0
- package/src/daemon/session-memory.ts +212 -0
- package/src/daemon/session-messaging.ts +145 -0
- package/src/daemon/session-notifiers.ts +193 -0
- package/src/daemon/session-process.ts +323 -0
- package/src/daemon/session-queue-manager.ts +82 -0
- package/src/daemon/session-runtime-assembly.ts +447 -0
- package/src/daemon/session-skill-tools.ts +356 -0
- package/src/daemon/session-slash.ts +305 -0
- package/src/daemon/session-surfaces.ts +702 -0
- package/src/daemon/session-tool-setup.ts +523 -0
- package/src/daemon/session-usage.ts +72 -0
- package/src/daemon/session-workspace.ts +19 -0
- package/src/daemon/session.ts +400 -0
- package/src/daemon/tls-certs.ts +189 -0
- package/src/daemon/trace-emitter.ts +82 -0
- package/src/daemon/video-thumbnail.ts +62 -0
- package/src/daemon/watch-handler.ts +274 -0
- package/src/doordash/client.ts +999 -0
- package/src/doordash/queries.ts +1311 -0
- package/src/doordash/query-extractor.ts +93 -0
- package/src/doordash/session.ts +82 -0
- package/src/email/provider.ts +117 -0
- package/src/email/providers/agentmail.ts +317 -0
- package/src/email/providers/index.ts +58 -0
- package/src/email/service.ts +303 -0
- package/src/email/types.ts +126 -0
- package/src/events/bus.ts +157 -0
- package/src/events/domain-events.ts +83 -0
- package/src/events/index.ts +18 -0
- package/src/events/tool-audit-listener.ts +80 -0
- package/src/events/tool-domain-event-publisher.ts +111 -0
- package/src/events/tool-metrics-listener.ts +159 -0
- package/src/events/tool-notification-listener.ts +17 -0
- package/src/events/tool-profiling-listener.ts +158 -0
- package/src/events/tool-trace-listener.ts +75 -0
- package/src/export/formatter.ts +98 -0
- package/src/followups/followup-store.ts +168 -0
- package/src/followups/index.ts +10 -0
- package/src/followups/types.ts +29 -0
- package/src/gallery/default-gallery.ts +795 -0
- package/src/gallery/gallery-manifest.ts +24 -0
- package/src/home-base/app-link-store.ts +82 -0
- package/src/home-base/bootstrap.ts +68 -0
- package/src/home-base/prebuilt/index.html +662 -0
- package/src/home-base/prebuilt/seed-metadata.json +21 -0
- package/src/home-base/prebuilt/seed.ts +112 -0
- package/src/home-base/prebuilt-home-base-updater.ts +30 -0
- package/src/hooks/cli.ts +163 -0
- package/src/hooks/config.ts +88 -0
- package/src/hooks/discovery.ts +110 -0
- package/src/hooks/manager.ts +124 -0
- package/src/hooks/runner.ts +123 -0
- package/src/hooks/templates.ts +52 -0
- package/src/hooks/types.ts +72 -0
- package/src/inbound/public-ingress-urls.ts +123 -0
- package/src/index.ts +81 -0
- package/src/instrument.ts +60 -0
- package/src/logfire.ts +99 -0
- package/src/media/gemini-image-service.ts +136 -0
- package/src/memory/account-store.ts +108 -0
- package/src/memory/admin.ts +211 -0
- package/src/memory/app-git-service.ts +295 -0
- package/src/memory/app-store.ts +577 -0
- package/src/memory/attachments-store.ts +397 -0
- package/src/memory/channel-delivery-store.ts +353 -0
- package/src/memory/channel-guardian-store.ts +669 -0
- package/src/memory/checkpoints.ts +52 -0
- package/src/memory/clarification-resolver.ts +298 -0
- package/src/memory/conflict-intent.ts +157 -0
- package/src/memory/conflict-policy.ts +73 -0
- package/src/memory/conflict-store.ts +350 -0
- package/src/memory/contradiction-checker.ts +358 -0
- package/src/memory/conversation-key-store.ts +122 -0
- package/src/memory/conversation-store.ts +470 -0
- package/src/memory/db.ts +1991 -0
- package/src/memory/embedding-backend.ts +229 -0
- package/src/memory/embedding-gemini.ts +52 -0
- package/src/memory/embedding-local.ts +65 -0
- package/src/memory/embedding-ollama.ts +55 -0
- package/src/memory/embedding-openai.ts +25 -0
- package/src/memory/entity-extractor.ts +474 -0
- package/src/memory/external-conversation-store.ts +234 -0
- package/src/memory/fingerprint.ts +20 -0
- package/src/memory/indexer.ts +156 -0
- package/src/memory/items-extractor.ts +461 -0
- package/src/memory/job-handlers/backfill.ts +139 -0
- package/src/memory/job-handlers/cleanup.ts +58 -0
- package/src/memory/job-handlers/conflict.ts +141 -0
- package/src/memory/job-handlers/embedding.ts +61 -0
- package/src/memory/job-handlers/extraction.ts +123 -0
- package/src/memory/job-handlers/index-maintenance.ts +54 -0
- package/src/memory/job-handlers/summarization.ts +286 -0
- package/src/memory/job-utils.ts +170 -0
- package/src/memory/jobs-store.ts +401 -0
- package/src/memory/jobs-worker.ts +313 -0
- package/src/memory/llm-request-log-store.ts +45 -0
- package/src/memory/llm-usage-store.ts +60 -0
- package/src/memory/message-content.ts +54 -0
- package/src/memory/profile-compiler.ts +160 -0
- package/src/memory/published-pages-store.ts +137 -0
- package/src/memory/qdrant-client.ts +366 -0
- package/src/memory/qdrant-manager.ts +242 -0
- package/src/memory/query-builder.ts +45 -0
- package/src/memory/retrieval-budget.ts +30 -0
- package/src/memory/retriever.ts +653 -0
- package/src/memory/runs-store.ts +305 -0
- package/src/memory/schema.ts +677 -0
- package/src/memory/search/entity.ts +298 -0
- package/src/memory/search/formatting.ts +207 -0
- package/src/memory/search/lexical.ts +227 -0
- package/src/memory/search/ranking.ts +401 -0
- package/src/memory/search/semantic.ts +121 -0
- package/src/memory/search/types.ts +137 -0
- package/src/memory/segmenter.ts +68 -0
- package/src/memory/shared-app-links-store.ts +138 -0
- package/src/memory/tool-usage-store.ts +62 -0
- package/src/messaging/activity-analyzer.ts +76 -0
- package/src/messaging/draft-store.ts +88 -0
- package/src/messaging/index.ts +3 -0
- package/src/messaging/provider-types.ts +80 -0
- package/src/messaging/provider.ts +52 -0
- package/src/messaging/providers/gmail/adapter.ts +193 -0
- package/src/messaging/providers/gmail/client.ts +204 -0
- package/src/messaging/providers/gmail/types.ts +90 -0
- package/src/messaging/providers/slack/adapter.ts +202 -0
- package/src/messaging/providers/slack/client.ts +198 -0
- package/src/messaging/providers/slack/types.ts +119 -0
- package/src/messaging/providers/telegram-bot/adapter.ts +162 -0
- package/src/messaging/providers/telegram-bot/client.ts +104 -0
- package/src/messaging/providers/telegram-bot/types.ts +15 -0
- package/src/messaging/registry.ts +35 -0
- package/src/messaging/style-analyzer.ts +159 -0
- package/src/messaging/thread-summarizer.ts +306 -0
- package/src/messaging/triage-engine.ts +323 -0
- package/src/messaging/types.ts +55 -0
- package/src/permissions/checker.ts +640 -0
- package/src/permissions/defaults.ts +254 -0
- package/src/permissions/prompter.ts +98 -0
- package/src/permissions/secret-prompter.ts +114 -0
- package/src/permissions/shell-identity.ts +227 -0
- package/src/permissions/trust-store.ts +607 -0
- package/src/permissions/types.ts +43 -0
- package/src/permissions/workspace-policy.ts +114 -0
- package/src/playbooks/index.ts +2 -0
- package/src/playbooks/playbook-compiler.ts +90 -0
- package/src/playbooks/types.ts +55 -0
- package/src/providers/anthropic/client.ts +751 -0
- package/src/providers/failover.ts +129 -0
- package/src/providers/fireworks/client.ts +20 -0
- package/src/providers/gemini/client.ts +285 -0
- package/src/providers/ollama/client.ts +30 -0
- package/src/providers/openai/client.ts +337 -0
- package/src/providers/openrouter/client.ts +20 -0
- package/src/providers/ratelimit.ts +93 -0
- package/src/providers/registry.ts +146 -0
- package/src/providers/retry.ts +81 -0
- package/src/providers/stream-timeout.ts +38 -0
- package/src/providers/types.ts +109 -0
- package/src/runtime/assistant-event-hub.ts +157 -0
- package/src/runtime/assistant-event.ts +82 -0
- package/src/runtime/channel-approval-parser.ts +60 -0
- package/src/runtime/channel-approval-types.ts +73 -0
- package/src/runtime/channel-approvals.ts +206 -0
- package/src/runtime/channel-guardian-service.ts +212 -0
- package/src/runtime/gateway-client.ts +58 -0
- package/src/runtime/http-server.ts +1076 -0
- package/src/runtime/http-types.ts +66 -0
- package/src/runtime/routes/app-routes.ts +174 -0
- package/src/runtime/routes/attachment-routes.ts +133 -0
- package/src/runtime/routes/call-routes.ts +190 -0
- package/src/runtime/routes/channel-routes.ts +1404 -0
- package/src/runtime/routes/conversation-routes.ts +352 -0
- package/src/runtime/routes/events-routes.ts +148 -0
- package/src/runtime/routes/run-routes.ts +257 -0
- package/src/runtime/routes/secret-routes.ts +76 -0
- package/src/runtime/run-orchestrator.ts +330 -0
- package/src/schedule/recurrence-engine.ts +162 -0
- package/src/schedule/recurrence-types.ts +67 -0
- package/src/schedule/schedule-store.ts +506 -0
- package/src/schedule/scheduler.ts +171 -0
- package/src/security/encrypted-store.ts +238 -0
- package/src/security/keychain.ts +252 -0
- package/src/security/oauth-callback-registry.ts +66 -0
- package/src/security/oauth2.ts +274 -0
- package/src/security/redaction.ts +89 -0
- package/src/security/secret-allowlist.ts +164 -0
- package/src/security/secret-ingress.ts +57 -0
- package/src/security/secret-scanner.ts +550 -0
- package/src/security/secure-keys.ts +180 -0
- package/src/security/token-manager.ts +141 -0
- package/src/services/published-app-updater.ts +69 -0
- package/src/services/vercel-deploy.ts +73 -0
- package/src/skills/active-skill-tools.ts +81 -0
- package/src/skills/clawhub.ts +414 -0
- package/src/skills/include-graph.ts +146 -0
- package/src/skills/managed-store.ts +233 -0
- package/src/skills/path-classifier.ts +128 -0
- package/src/skills/slash-commands.ts +174 -0
- package/src/skills/tool-manifest.ts +165 -0
- package/src/skills/version-hash.ts +110 -0
- package/src/slack/slack-webhook.ts +61 -0
- package/src/subagent/index.ts +19 -0
- package/src/subagent/manager.ts +511 -0
- package/src/subagent/types.ts +69 -0
- package/src/swarm/backend-claude-code.ts +145 -0
- package/src/swarm/index.ts +44 -0
- package/src/swarm/limits.ts +37 -0
- package/src/swarm/orchestrator.ts +279 -0
- package/src/swarm/plan-validator.ts +151 -0
- package/src/swarm/router-planner.ts +100 -0
- package/src/swarm/router-prompts.ts +36 -0
- package/src/swarm/synthesizer.ts +62 -0
- package/src/swarm/types.ts +62 -0
- package/src/swarm/worker-backend.ts +121 -0
- package/src/swarm/worker-prompts.ts +79 -0
- package/src/swarm/worker-runner.ts +164 -0
- package/src/tasks/SPEC.md +139 -0
- package/src/tasks/candidate-store.ts +86 -0
- package/src/tasks/ephemeral-permissions.ts +48 -0
- package/src/tasks/task-compiler.ts +199 -0
- package/src/tasks/task-runner.ts +90 -0
- package/src/tasks/task-scheduler.ts +21 -0
- package/src/tasks/task-store.ts +127 -0
- package/src/tasks/tool-sanitizer.ts +36 -0
- package/src/tools/apps/definitions.ts +59 -0
- package/src/tools/apps/executors.ts +313 -0
- package/src/tools/apps/open-proxy.ts +43 -0
- package/src/tools/apps/registry.ts +16 -0
- package/src/tools/assets/materialize.ts +218 -0
- package/src/tools/assets/search.ts +361 -0
- package/src/tools/browser/__tests__/auth-cache.test.ts +219 -0
- package/src/tools/browser/__tests__/auth-detector.test.ts +362 -0
- package/src/tools/browser/__tests__/jit-auth.test.ts +189 -0
- package/src/tools/browser/api-map.ts +293 -0
- package/src/tools/browser/auth-cache.ts +149 -0
- package/src/tools/browser/auth-detector.ts +347 -0
- package/src/tools/browser/auto-navigate.ts +270 -0
- package/src/tools/browser/browser-execution.ts +980 -0
- package/src/tools/browser/browser-handoff.ts +79 -0
- package/src/tools/browser/browser-manager.ts +715 -0
- package/src/tools/browser/browser-screencast.ts +217 -0
- package/src/tools/browser/headless-browser.ts +450 -0
- package/src/tools/browser/jit-auth.ts +51 -0
- package/src/tools/browser/network-recorder.ts +349 -0
- package/src/tools/browser/network-recording-types.ts +49 -0
- package/src/tools/browser/recording-store.ts +49 -0
- package/src/tools/browser/runtime-check.ts +43 -0
- package/src/tools/browser/x-auto-navigate.ts +207 -0
- package/src/tools/calls/call-end.ts +67 -0
- package/src/tools/calls/call-start.ts +81 -0
- package/src/tools/calls/call-status.ts +81 -0
- package/src/tools/claude-code/claude-code.ts +428 -0
- package/src/tools/computer-use/definitions.ts +443 -0
- package/src/tools/computer-use/registry.ts +22 -0
- package/src/tools/computer-use/request-computer-control.ts +53 -0
- package/src/tools/computer-use/skill-proxy-bridge.ts +28 -0
- package/src/tools/credentials/account-registry.ts +127 -0
- package/src/tools/credentials/broker-types.ts +107 -0
- package/src/tools/credentials/broker.ts +372 -0
- package/src/tools/credentials/domain-policy.ts +51 -0
- package/src/tools/credentials/host-pattern-match.ts +60 -0
- package/src/tools/credentials/metadata-store.ts +335 -0
- package/src/tools/credentials/policy-types.ts +52 -0
- package/src/tools/credentials/policy-validate.ts +80 -0
- package/src/tools/credentials/resolve.ts +122 -0
- package/src/tools/credentials/selection.ts +159 -0
- package/src/tools/credentials/tool-policy.ts +25 -0
- package/src/tools/credentials/vault.ts +657 -0
- package/src/tools/document/document-tool.ts +92 -0
- package/src/tools/document/editor-template.ts +237 -0
- package/src/tools/execution-target.ts +21 -0
- package/src/tools/execution-timeout.ts +49 -0
- package/src/tools/executor.ts +815 -0
- package/src/tools/filesystem/edit.ts +127 -0
- package/src/tools/filesystem/fuzzy-match.ts +202 -0
- package/src/tools/filesystem/read.ts +71 -0
- package/src/tools/filesystem/view-image.ts +199 -0
- package/src/tools/filesystem/write.ts +79 -0
- package/src/tools/followups/followup_create.ts +76 -0
- package/src/tools/followups/followup_list.ts +60 -0
- package/src/tools/followups/followup_resolve.ts +56 -0
- package/src/tools/host-filesystem/edit.ts +125 -0
- package/src/tools/host-filesystem/read.ts +80 -0
- package/src/tools/host-filesystem/write.ts +76 -0
- package/src/tools/host-terminal/cli-discover.ts +180 -0
- package/src/tools/host-terminal/host-shell.ts +191 -0
- package/src/tools/memory/definitions.ts +69 -0
- package/src/tools/memory/handlers.ts +246 -0
- package/src/tools/memory/register.ts +66 -0
- package/src/tools/network/__tests__/web-search.test.ts +427 -0
- package/src/tools/network/domain-normalize.ts +85 -0
- package/src/tools/network/script-proxy/__tests__/logging.test.ts +248 -0
- package/src/tools/network/script-proxy/__tests__/policy.test.ts +234 -0
- package/src/tools/network/script-proxy/__tests__/router.test.ts +76 -0
- package/src/tools/network/script-proxy/certs.ts +237 -0
- package/src/tools/network/script-proxy/connect-tunnel.ts +82 -0
- package/src/tools/network/script-proxy/http-forwarder.ts +151 -0
- package/src/tools/network/script-proxy/index.ts +28 -0
- package/src/tools/network/script-proxy/logging.ts +196 -0
- package/src/tools/network/script-proxy/mitm-handler.ts +269 -0
- package/src/tools/network/script-proxy/policy.ts +152 -0
- package/src/tools/network/script-proxy/router.ts +60 -0
- package/src/tools/network/script-proxy/server.ts +136 -0
- package/src/tools/network/script-proxy/session-manager.ts +534 -0
- package/src/tools/network/script-proxy/types.ts +125 -0
- package/src/tools/network/url-safety.ts +227 -0
- package/src/tools/network/web-fetch.ts +713 -0
- package/src/tools/network/web-search.ts +296 -0
- package/src/tools/policy-context.ts +29 -0
- package/src/tools/registry.ts +295 -0
- package/src/tools/reminder/reminder-store.ts +148 -0
- package/src/tools/reminder/reminder.ts +80 -0
- package/src/tools/schedule/create.ts +81 -0
- package/src/tools/schedule/delete.ts +28 -0
- package/src/tools/schedule/list.ts +69 -0
- package/src/tools/schedule/update.ts +97 -0
- package/src/tools/shared/filesystem/edit-engine.ts +56 -0
- package/src/tools/shared/filesystem/errors.ts +85 -0
- package/src/tools/shared/filesystem/file-ops-service.ts +215 -0
- package/src/tools/shared/filesystem/format-diff.ts +35 -0
- package/src/tools/shared/filesystem/path-policy.ts +125 -0
- package/src/tools/shared/filesystem/size-guard.ts +41 -0
- package/src/tools/shared/filesystem/types.ts +80 -0
- package/src/tools/shared/shell-output.ts +52 -0
- package/src/tools/skills/delete-managed.ts +60 -0
- package/src/tools/skills/load.ts +139 -0
- package/src/tools/skills/sandbox-runner.ts +279 -0
- package/src/tools/skills/scaffold-managed.ts +150 -0
- package/src/tools/skills/script-contract.ts +6 -0
- package/src/tools/skills/skill-script-runner.ts +86 -0
- package/src/tools/skills/skill-tool-factory.ts +64 -0
- package/src/tools/skills/vellum-catalog.ts +217 -0
- package/src/tools/subagent/abort.ts +33 -0
- package/src/tools/subagent/message.ts +39 -0
- package/src/tools/subagent/read.ts +67 -0
- package/src/tools/subagent/spawn.ts +46 -0
- package/src/tools/subagent/status.ts +45 -0
- package/src/tools/swarm/delegate.ts +183 -0
- package/src/tools/system/request-permission.ts +98 -0
- package/src/tools/system/version.ts +43 -0
- package/src/tools/tasks/index.ts +27 -0
- package/src/tools/tasks/task-delete.ts +82 -0
- package/src/tools/tasks/task-list.ts +44 -0
- package/src/tools/tasks/task-run.ts +97 -0
- package/src/tools/tasks/task-save.ts +47 -0
- package/src/tools/tasks/work-item-enqueue.ts +234 -0
- package/src/tools/tasks/work-item-list.ts +55 -0
- package/src/tools/tasks/work-item-remove.ts +60 -0
- package/src/tools/tasks/work-item-run.ts +78 -0
- package/src/tools/tasks/work-item-update.ts +114 -0
- package/src/tools/terminal/backends/docker.ts +372 -0
- package/src/tools/terminal/backends/native.ts +190 -0
- package/src/tools/terminal/backends/types.ts +26 -0
- package/src/tools/terminal/evaluate-typescript.ts +275 -0
- package/src/tools/terminal/parser.ts +413 -0
- package/src/tools/terminal/safe-env.ts +37 -0
- package/src/tools/terminal/sandbox-diagnostics.ts +149 -0
- package/src/tools/terminal/sandbox.ts +44 -0
- package/src/tools/terminal/shell.ts +257 -0
- package/src/tools/tool-manifest.ts +198 -0
- package/src/tools/types.ts +176 -0
- package/src/tools/ui-surface/definitions.ts +244 -0
- package/src/tools/ui-surface/registry.ts +14 -0
- package/src/tools/watch/screen-watch.ts +130 -0
- package/src/tools/watch/watch-state.ts +119 -0
- package/src/tools/watcher/create.ts +64 -0
- package/src/tools/watcher/delete.ts +27 -0
- package/src/tools/watcher/digest.ts +50 -0
- package/src/tools/watcher/list.ts +60 -0
- package/src/tools/watcher/update.ts +56 -0
- package/src/tools/weather/service.ts +551 -0
- package/src/twitter/client.ts +690 -0
- package/src/twitter/oauth-client.ts +102 -0
- package/src/twitter/router.ts +101 -0
- package/src/twitter/session.ts +91 -0
- package/src/usage/actors.ts +24 -0
- package/src/usage/types.ts +37 -0
- package/src/util/clipboard.ts +33 -0
- package/src/util/content-id.ts +16 -0
- package/src/util/debounce.ts +88 -0
- package/src/util/diff.ts +181 -0
- package/src/util/errors.ts +129 -0
- package/src/util/logger.ts +243 -0
- package/src/util/network-info.ts +47 -0
- package/src/util/platform.ts +632 -0
- package/src/util/pricing.ts +150 -0
- package/src/util/promise-guard.ts +37 -0
- package/src/util/retry.ts +98 -0
- package/src/util/spinner.ts +51 -0
- package/src/util/time.ts +16 -0
- package/src/util/truncate.ts +6 -0
- package/src/util/xml.ts +4 -0
- package/src/version.ts +3 -0
- package/src/watcher/constants.ts +11 -0
- package/src/watcher/engine.ts +199 -0
- package/src/watcher/provider-registry.ts +15 -0
- package/src/watcher/provider-types.ts +48 -0
- package/src/watcher/providers/gmail.ts +198 -0
- package/src/watcher/providers/google-calendar.ts +228 -0
- package/src/watcher/providers/slack.ts +129 -0
- package/src/watcher/watcher-store.ts +419 -0
- package/src/work-items/work-item-runner.ts +171 -0
- package/src/work-items/work-item-store.ts +325 -0
- package/src/workspace/commit-message-enrichment-service.ts +284 -0
- package/src/workspace/commit-message-provider.ts +95 -0
- package/src/workspace/git-service.ts +857 -0
- package/src/workspace/heartbeat-service.ts +345 -0
- package/src/workspace/provider-commit-message-generator.ts +285 -0
- package/src/workspace/top-level-renderer.ts +19 -0
- package/src/workspace/top-level-scanner.ts +41 -0
- package/src/workspace/turn-commit.ts +175 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,1135 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import { AgentLoop } from '../agent/loop.js';
|
|
3
|
+
import type { AgentEvent, CheckpointInfo, CheckpointDecision } from '../agent/loop.js';
|
|
4
|
+
import type {
|
|
5
|
+
Provider,
|
|
6
|
+
Message,
|
|
7
|
+
ProviderResponse,
|
|
8
|
+
SendMessageOptions,
|
|
9
|
+
ToolDefinition,
|
|
10
|
+
ContentBlock,
|
|
11
|
+
} from '../providers/types.js';
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Helpers
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
/** A mock provider that returns pre-configured responses in sequence. */
|
|
18
|
+
function createMockProvider(
|
|
19
|
+
responses: ProviderResponse[],
|
|
20
|
+
): { provider: Provider; calls: { messages: Message[]; tools?: ToolDefinition[]; systemPrompt?: string }[] } {
|
|
21
|
+
const calls: { messages: Message[]; tools?: ToolDefinition[]; systemPrompt?: string }[] = [];
|
|
22
|
+
let callIndex = 0;
|
|
23
|
+
|
|
24
|
+
const provider: Provider = {
|
|
25
|
+
name: 'mock',
|
|
26
|
+
async sendMessage(
|
|
27
|
+
messages: Message[],
|
|
28
|
+
tools?: ToolDefinition[],
|
|
29
|
+
systemPrompt?: string,
|
|
30
|
+
options?: SendMessageOptions,
|
|
31
|
+
): Promise<ProviderResponse> {
|
|
32
|
+
calls.push({ messages: [...messages], tools, systemPrompt });
|
|
33
|
+
const response = responses[callIndex] ?? responses[responses.length - 1];
|
|
34
|
+
callIndex++;
|
|
35
|
+
|
|
36
|
+
// Emit streaming events if the response has text blocks
|
|
37
|
+
if (options?.onEvent) {
|
|
38
|
+
for (const block of response.content) {
|
|
39
|
+
if (block.type === 'text') {
|
|
40
|
+
options.onEvent({ type: 'text_delta', text: block.text });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return response;
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return { provider, calls };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function textResponse(text: string): ProviderResponse {
|
|
53
|
+
return {
|
|
54
|
+
content: [{ type: 'text', text }],
|
|
55
|
+
model: 'mock-model',
|
|
56
|
+
usage: { inputTokens: 10, outputTokens: 5 },
|
|
57
|
+
stopReason: 'end_turn',
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function toolUseResponse(id: string, name: string, input: Record<string, unknown>): ProviderResponse {
|
|
62
|
+
return {
|
|
63
|
+
content: [{ type: 'tool_use', id, name, input }],
|
|
64
|
+
model: 'mock-model',
|
|
65
|
+
usage: { inputTokens: 10, outputTokens: 5 },
|
|
66
|
+
stopReason: 'tool_use',
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const dummyTools: ToolDefinition[] = [
|
|
71
|
+
{ name: 'read_file', description: 'Read a file', input_schema: { type: 'object', properties: { path: { type: 'string' } } } },
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
const userMessage: Message = {
|
|
75
|
+
role: 'user',
|
|
76
|
+
content: [{ type: 'text', text: 'Hello' }],
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
function collectEvents(events: AgentEvent[]): (event: AgentEvent) => void {
|
|
80
|
+
return (event) => events.push(event);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Tests
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
describe('AgentLoop', () => {
|
|
88
|
+
// 1. Basic text response
|
|
89
|
+
test('returns history with assistant message for simple text response', async () => {
|
|
90
|
+
const { provider } = createMockProvider([textResponse('Hi there!')]);
|
|
91
|
+
const loop = new AgentLoop(provider, 'system prompt');
|
|
92
|
+
|
|
93
|
+
const events: AgentEvent[] = [];
|
|
94
|
+
const history = await loop.run([userMessage], collectEvents(events));
|
|
95
|
+
|
|
96
|
+
// History should contain original user message + assistant response
|
|
97
|
+
expect(history).toHaveLength(2);
|
|
98
|
+
expect(history[0]).toEqual(userMessage);
|
|
99
|
+
expect(history[1].role).toBe('assistant');
|
|
100
|
+
expect(history[1].content).toEqual([{ type: 'text', text: 'Hi there!' }]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// 2. Tool execution — provider returns tool_use, verify tool executor is called
|
|
104
|
+
test('executes tool and passes result back to provider', async () => {
|
|
105
|
+
const toolCallId = 'tool-1';
|
|
106
|
+
const { provider, calls } = createMockProvider([
|
|
107
|
+
toolUseResponse(toolCallId, 'read_file', { path: '/tmp/test.txt' }),
|
|
108
|
+
textResponse('File contents received.'),
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
const toolCalls: { name: string; input: Record<string, unknown> }[] = [];
|
|
112
|
+
const toolExecutor = async (name: string, input: Record<string, unknown>) => {
|
|
113
|
+
toolCalls.push({ name, input });
|
|
114
|
+
return { content: 'file data here', isError: false };
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const loop = new AgentLoop(provider, 'system', {}, dummyTools, toolExecutor);
|
|
118
|
+
const events: AgentEvent[] = [];
|
|
119
|
+
const history = await loop.run([userMessage], collectEvents(events));
|
|
120
|
+
|
|
121
|
+
// Tool executor was called with correct args
|
|
122
|
+
expect(toolCalls).toHaveLength(1);
|
|
123
|
+
expect(toolCalls[0].name).toBe('read_file');
|
|
124
|
+
expect(toolCalls[0].input).toEqual({ path: '/tmp/test.txt' });
|
|
125
|
+
|
|
126
|
+
// Provider was called twice (initial + after tool result)
|
|
127
|
+
expect(calls).toHaveLength(2);
|
|
128
|
+
|
|
129
|
+
// Second call should include the tool result as a user message
|
|
130
|
+
const secondCallMessages = calls[1].messages;
|
|
131
|
+
const lastMsg = secondCallMessages[secondCallMessages.length - 1];
|
|
132
|
+
expect(lastMsg.role).toBe('user');
|
|
133
|
+
|
|
134
|
+
const toolResultBlock = lastMsg.content.find(
|
|
135
|
+
(b): b is Extract<ContentBlock, { type: 'tool_result' }> => b.type === 'tool_result',
|
|
136
|
+
);
|
|
137
|
+
expect(toolResultBlock).toBeDefined();
|
|
138
|
+
expect(toolResultBlock!.tool_use_id).toBe(toolCallId);
|
|
139
|
+
expect(toolResultBlock!.content).toBe('file data here');
|
|
140
|
+
expect(toolResultBlock!.is_error).toBe(false);
|
|
141
|
+
|
|
142
|
+
// Final history: user, assistant(tool_use), user(tool_result), assistant(text)
|
|
143
|
+
expect(history).toHaveLength(4);
|
|
144
|
+
expect(history[3].role).toBe('assistant');
|
|
145
|
+
expect(history[3].content).toEqual([{ type: 'text', text: 'File contents received.' }]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// 3. Multi-turn tool loop
|
|
149
|
+
test('supports multi-turn tool execution', async () => {
|
|
150
|
+
const { provider, calls } = createMockProvider([
|
|
151
|
+
toolUseResponse('t1', 'read_file', { path: '/a.txt' }),
|
|
152
|
+
toolUseResponse('t2', 'read_file', { path: '/b.txt' }),
|
|
153
|
+
textResponse('Done reading both files.'),
|
|
154
|
+
]);
|
|
155
|
+
|
|
156
|
+
const toolExecutor = async (name: string, input: Record<string, unknown>) => {
|
|
157
|
+
return { content: `contents of ${(input as { path: string }).path}`, isError: false };
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const loop = new AgentLoop(provider, 'system', {}, dummyTools, toolExecutor);
|
|
161
|
+
const history = await loop.run([userMessage], () => {});
|
|
162
|
+
|
|
163
|
+
// Provider called 3 times (two tool rounds + final text)
|
|
164
|
+
expect(calls).toHaveLength(3);
|
|
165
|
+
|
|
166
|
+
// History: user, assistant(t1), user(result1), assistant(t2), user(result2), assistant(text)
|
|
167
|
+
expect(history).toHaveLength(6);
|
|
168
|
+
expect(history[5].content).toEqual([{ type: 'text', text: 'Done reading both files.' }]);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// 4. Loop stops when provider returns tool_use but no executor is configured
|
|
172
|
+
test('stops when tool_use returned but no tool executor configured', async () => {
|
|
173
|
+
const { provider } = createMockProvider([
|
|
174
|
+
toolUseResponse('t1', 'read_file', { path: '/a.txt' }),
|
|
175
|
+
]);
|
|
176
|
+
|
|
177
|
+
// No tool executor provided
|
|
178
|
+
const loop = new AgentLoop(provider, 'system', {}, dummyTools);
|
|
179
|
+
const history = await loop.run([userMessage], () => {});
|
|
180
|
+
|
|
181
|
+
// Should stop after first response (no executor to handle tool use)
|
|
182
|
+
expect(history).toHaveLength(2);
|
|
183
|
+
expect(history[1].role).toBe('assistant');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// 5. Error handling — provider throws, verify error event and loop stops
|
|
187
|
+
test('emits error event and stops when provider throws', async () => {
|
|
188
|
+
const error = new Error('API rate limit exceeded');
|
|
189
|
+
const provider: Provider = {
|
|
190
|
+
name: 'mock',
|
|
191
|
+
async sendMessage(): Promise<ProviderResponse> {
|
|
192
|
+
throw error;
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const loop = new AgentLoop(provider, 'system');
|
|
197
|
+
const events: AgentEvent[] = [];
|
|
198
|
+
const history = await loop.run([userMessage], collectEvents(events));
|
|
199
|
+
|
|
200
|
+
// Only the original message remains (no assistant message added on error)
|
|
201
|
+
expect(history).toHaveLength(1);
|
|
202
|
+
|
|
203
|
+
// Error event was emitted
|
|
204
|
+
const errorEvents = events.filter((e) => e.type === 'error');
|
|
205
|
+
expect(errorEvents).toHaveLength(1);
|
|
206
|
+
expect((errorEvents[0] as { type: 'error'; error: Error }).error.message).toBe('API rate limit exceeded');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// 6. Abort signal — verify the loop respects AbortSignal
|
|
210
|
+
test('stops when abort signal is triggered before provider call', async () => {
|
|
211
|
+
const controller = new AbortController();
|
|
212
|
+
controller.abort(); // abort immediately
|
|
213
|
+
|
|
214
|
+
const { provider } = createMockProvider([textResponse('Should not reach')]);
|
|
215
|
+
const loop = new AgentLoop(provider, 'system');
|
|
216
|
+
const history = await loop.run([userMessage], () => {}, controller.signal);
|
|
217
|
+
|
|
218
|
+
// Loop should exit immediately, returning only original messages
|
|
219
|
+
expect(history).toHaveLength(1);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('stops when abort signal is triggered between turns', async () => {
|
|
223
|
+
const controller = new AbortController();
|
|
224
|
+
let turnCount = 0;
|
|
225
|
+
|
|
226
|
+
const { provider } = createMockProvider([
|
|
227
|
+
toolUseResponse('t1', 'read_file', { path: '/a.txt' }),
|
|
228
|
+
toolUseResponse('t2', 'read_file', { path: '/b.txt' }),
|
|
229
|
+
textResponse('Should not reach'),
|
|
230
|
+
]);
|
|
231
|
+
|
|
232
|
+
const toolExecutor = async () => {
|
|
233
|
+
turnCount++;
|
|
234
|
+
if (turnCount === 1) {
|
|
235
|
+
// Abort after the first tool turn completes
|
|
236
|
+
controller.abort();
|
|
237
|
+
}
|
|
238
|
+
return { content: 'data', isError: false };
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const loop = new AgentLoop(provider, 'system', {}, dummyTools, toolExecutor);
|
|
242
|
+
const history = await loop.run([userMessage], () => {}, controller.signal);
|
|
243
|
+
|
|
244
|
+
// After the first tool turn, abort fires. The while loop checks signal at the
|
|
245
|
+
// top and breaks. History: user, assistant(t1), user(result1)
|
|
246
|
+
// The second provider call may or may not happen depending on when the abort
|
|
247
|
+
// check triggers, but the loop should eventually stop.
|
|
248
|
+
// At minimum, verify it doesn't run all 3 provider calls.
|
|
249
|
+
expect(history.length).toBeLessThanOrEqual(4);
|
|
250
|
+
|
|
251
|
+
// Verify the loop didn't reach the final text response
|
|
252
|
+
const lastAssistant = [...history].reverse().find(m => m.role === 'assistant');
|
|
253
|
+
expect(lastAssistant).toBeDefined();
|
|
254
|
+
const hasToolUse = lastAssistant!.content.some(b => b.type === 'tool_use');
|
|
255
|
+
// The last assistant message should be a tool_use, not the final text
|
|
256
|
+
expect(hasToolUse).toBe(true);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// 6b. Abort signal during long-running tool execution — loop exits immediately
|
|
260
|
+
test('stops immediately when abort fires during a stuck tool execution', async () => {
|
|
261
|
+
const controller = new AbortController();
|
|
262
|
+
|
|
263
|
+
const { provider } = createMockProvider([
|
|
264
|
+
toolUseResponse('t1', 'read_file', { path: '/stuck.txt' }),
|
|
265
|
+
textResponse('Should not reach'),
|
|
266
|
+
]);
|
|
267
|
+
|
|
268
|
+
// Simulate a stuck tool that never resolves — abort fires while it's running
|
|
269
|
+
const toolExecutor = async () => {
|
|
270
|
+
// Abort from a timer while this tool is "stuck"
|
|
271
|
+
setTimeout(() => controller.abort(), 50);
|
|
272
|
+
// Simulate being stuck for a long time
|
|
273
|
+
await new Promise(resolve => setTimeout(resolve, 10_000));
|
|
274
|
+
return { content: 'should never return', isError: false };
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const loop = new AgentLoop(provider, 'system', {}, dummyTools, toolExecutor);
|
|
278
|
+
const start = Date.now();
|
|
279
|
+
const history = await loop.run([userMessage], () => {}, controller.signal);
|
|
280
|
+
const elapsed = Date.now() - start;
|
|
281
|
+
|
|
282
|
+
// The loop should exit quickly (~50ms for abort), not wait 10s for the tool
|
|
283
|
+
expect(elapsed).toBeLessThan(2000);
|
|
284
|
+
|
|
285
|
+
// User message + assistant tool_use + synthesized cancellation tool_result
|
|
286
|
+
expect(history).toHaveLength(3);
|
|
287
|
+
const lastMsg = history[2];
|
|
288
|
+
expect(lastMsg.role).toBe('user');
|
|
289
|
+
expect(lastMsg.content).toHaveLength(1);
|
|
290
|
+
expect(lastMsg.content[0].type).toBe('tool_result');
|
|
291
|
+
expect((lastMsg.content[0] as { type: 'tool_result'; tool_use_id: string; content: string; is_error: boolean }).content).toBe('Cancelled by user');
|
|
292
|
+
expect((lastMsg.content[0] as { type: 'tool_result'; tool_use_id: string; content: string; is_error: boolean }).is_error).toBe(true);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// 7. Events — verify text_delta and other events are emitted
|
|
296
|
+
test('emits text_delta events during streaming', async () => {
|
|
297
|
+
const { provider } = createMockProvider([textResponse('Hello world')]);
|
|
298
|
+
const loop = new AgentLoop(provider, 'system');
|
|
299
|
+
|
|
300
|
+
const events: AgentEvent[] = [];
|
|
301
|
+
await loop.run([userMessage], collectEvents(events));
|
|
302
|
+
|
|
303
|
+
const textDeltas = events.filter((e) => e.type === 'text_delta');
|
|
304
|
+
expect(textDeltas).toHaveLength(1);
|
|
305
|
+
expect((textDeltas[0] as { type: 'text_delta'; text: string }).text).toBe('Hello world');
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test('emits usage events', async () => {
|
|
309
|
+
const { provider } = createMockProvider([textResponse('Hi')]);
|
|
310
|
+
const loop = new AgentLoop(provider, 'system');
|
|
311
|
+
|
|
312
|
+
const events: AgentEvent[] = [];
|
|
313
|
+
await loop.run([userMessage], collectEvents(events));
|
|
314
|
+
|
|
315
|
+
const usageEvents = events.filter((e) => e.type === 'usage');
|
|
316
|
+
expect(usageEvents).toHaveLength(1);
|
|
317
|
+
const usage = usageEvents[0] as Extract<AgentEvent, { type: 'usage' }>;
|
|
318
|
+
expect(usage.type).toBe('usage');
|
|
319
|
+
expect(usage.inputTokens).toBe(10);
|
|
320
|
+
expect(usage.outputTokens).toBe(5);
|
|
321
|
+
expect(usage.model).toBe('mock-model');
|
|
322
|
+
expect(typeof usage.providerDurationMs).toBe('number');
|
|
323
|
+
expect(usage.providerDurationMs).toBeGreaterThanOrEqual(0);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test('emits message_complete events', async () => {
|
|
327
|
+
const { provider } = createMockProvider([textResponse('Done')]);
|
|
328
|
+
const loop = new AgentLoop(provider, 'system');
|
|
329
|
+
|
|
330
|
+
const events: AgentEvent[] = [];
|
|
331
|
+
await loop.run([userMessage], collectEvents(events));
|
|
332
|
+
|
|
333
|
+
const completeEvents = events.filter((e) => e.type === 'message_complete');
|
|
334
|
+
expect(completeEvents).toHaveLength(1);
|
|
335
|
+
expect((completeEvents[0] as { type: 'message_complete'; message: Message }).message.role).toBe('assistant');
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test('emits tool_use and tool_result events during tool execution', async () => {
|
|
339
|
+
const { provider } = createMockProvider([
|
|
340
|
+
toolUseResponse('t1', 'read_file', { path: '/test.txt' }),
|
|
341
|
+
textResponse('Done'),
|
|
342
|
+
]);
|
|
343
|
+
|
|
344
|
+
const toolExecutor = async () => ({ content: 'file data', isError: false });
|
|
345
|
+
const loop = new AgentLoop(provider, 'system', {}, dummyTools, toolExecutor);
|
|
346
|
+
|
|
347
|
+
const events: AgentEvent[] = [];
|
|
348
|
+
await loop.run([userMessage], collectEvents(events));
|
|
349
|
+
|
|
350
|
+
const toolUseEvents = events.filter((e) => e.type === 'tool_use');
|
|
351
|
+
expect(toolUseEvents).toHaveLength(1);
|
|
352
|
+
expect(toolUseEvents[0]).toEqual({
|
|
353
|
+
type: 'tool_use',
|
|
354
|
+
id: 't1',
|
|
355
|
+
name: 'read_file',
|
|
356
|
+
input: { path: '/test.txt' },
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
const toolResultEvents = events.filter((e) => e.type === 'tool_result');
|
|
360
|
+
expect(toolResultEvents).toHaveLength(1);
|
|
361
|
+
expect((toolResultEvents[0] as Extract<AgentEvent, { type: 'tool_result' }>).toolUseId).toBe('t1');
|
|
362
|
+
expect((toolResultEvents[0] as Extract<AgentEvent, { type: 'tool_result' }>).content).toBe('file data');
|
|
363
|
+
expect((toolResultEvents[0] as Extract<AgentEvent, { type: 'tool_result' }>).isError).toBe(false);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// 8. Progress reminder injection every 5 tool-use turns
|
|
367
|
+
test('injects progress reminder after every 5 tool-use turns', async () => {
|
|
368
|
+
// Create 6 tool responses followed by a text response
|
|
369
|
+
const responses: ProviderResponse[] = [];
|
|
370
|
+
for (let i = 0; i < 6; i++) {
|
|
371
|
+
responses.push(toolUseResponse(`t${i}`, 'read_file', { path: `/file${i}.txt` }));
|
|
372
|
+
}
|
|
373
|
+
responses.push(textResponse('Finally done'));
|
|
374
|
+
|
|
375
|
+
const { provider, calls } = createMockProvider(responses);
|
|
376
|
+
const toolExecutor = async () => ({ content: 'data', isError: false });
|
|
377
|
+
const loop = new AgentLoop(provider, 'system', {}, dummyTools, toolExecutor);
|
|
378
|
+
|
|
379
|
+
await loop.run([userMessage], () => {});
|
|
380
|
+
|
|
381
|
+
// After the 5th tool-use turn, the user message should contain a progress reminder
|
|
382
|
+
// calls[5] is the 6th provider call; its messages[-1] should have the reminder
|
|
383
|
+
const fifthTurnResultMsg = calls[5].messages[calls[5].messages.length - 1];
|
|
384
|
+
const reminderBlock = fifthTurnResultMsg.content.find(
|
|
385
|
+
(b): b is Extract<ContentBlock, { type: 'text' }> =>
|
|
386
|
+
b.type === 'text' && b.text.includes('making meaningful progress'),
|
|
387
|
+
);
|
|
388
|
+
expect(reminderBlock).toBeDefined();
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
test('stops after configured maxToolUseTurns to prevent runaway loops', async () => {
|
|
392
|
+
const responses: ProviderResponse[] = [
|
|
393
|
+
toolUseResponse('t1', 'read_file', { path: '/one.txt' }),
|
|
394
|
+
toolUseResponse('t2', 'read_file', { path: '/two.txt' }),
|
|
395
|
+
toolUseResponse('t3', 'read_file', { path: '/three.txt' }),
|
|
396
|
+
textResponse('Should never be requested'),
|
|
397
|
+
];
|
|
398
|
+
const { provider, calls } = createMockProvider(responses);
|
|
399
|
+
const toolExecutor = async () => ({ content: 'data', isError: false });
|
|
400
|
+
const loop = new AgentLoop(
|
|
401
|
+
provider,
|
|
402
|
+
'system',
|
|
403
|
+
{ maxToolUseTurns: 3 },
|
|
404
|
+
dummyTools,
|
|
405
|
+
toolExecutor,
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
const events: AgentEvent[] = [];
|
|
409
|
+
const history = await loop.run([userMessage], collectEvents(events));
|
|
410
|
+
|
|
411
|
+
// The loop should stop immediately after the 3rd tool-use turn, before the next provider call.
|
|
412
|
+
expect(calls).toHaveLength(3);
|
|
413
|
+
|
|
414
|
+
const errorEvents = events.filter(
|
|
415
|
+
(e): e is Extract<AgentEvent, { type: 'error' }> => e.type === 'error',
|
|
416
|
+
);
|
|
417
|
+
expect(errorEvents).toHaveLength(1);
|
|
418
|
+
expect(errorEvents[0].error.message).toContain('Tool-use turn limit reached (3)');
|
|
419
|
+
|
|
420
|
+
const lastMessage = history[history.length - 1];
|
|
421
|
+
expect(lastMessage.role).toBe('user');
|
|
422
|
+
const limitText = lastMessage.content.find(
|
|
423
|
+
(b): b is Extract<ContentBlock, { type: 'text' }> =>
|
|
424
|
+
b.type === 'text' && b.text.includes('Tool-use turn limit reached (3)'),
|
|
425
|
+
);
|
|
426
|
+
expect(limitText).toBeDefined();
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// 9. Tool executor error results are forwarded correctly
|
|
430
|
+
test('forwards tool error results to provider', async () => {
|
|
431
|
+
const { provider, calls } = createMockProvider([
|
|
432
|
+
toolUseResponse('t1', 'read_file', { path: '/nonexistent.txt' }),
|
|
433
|
+
textResponse('File not found, sorry.'),
|
|
434
|
+
]);
|
|
435
|
+
|
|
436
|
+
const toolExecutor = async () => ({ content: 'ENOENT: file not found', isError: true });
|
|
437
|
+
const loop = new AgentLoop(provider, 'system', {}, dummyTools, toolExecutor);
|
|
438
|
+
|
|
439
|
+
await loop.run([userMessage], () => {});
|
|
440
|
+
|
|
441
|
+
const secondCallMessages = calls[1].messages;
|
|
442
|
+
const lastMsg = secondCallMessages[secondCallMessages.length - 1];
|
|
443
|
+
const toolResultBlock = lastMsg.content.find(
|
|
444
|
+
(b): b is Extract<ContentBlock, { type: 'tool_result' }> => b.type === 'tool_result',
|
|
445
|
+
);
|
|
446
|
+
expect(toolResultBlock).toBeDefined();
|
|
447
|
+
expect(toolResultBlock!.is_error).toBe(true);
|
|
448
|
+
expect(toolResultBlock!.content).toBe('ENOENT: file not found');
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// 10. Tool output chunks are forwarded via onEvent
|
|
452
|
+
test('emits tool_output_chunk events during tool execution', async () => {
|
|
453
|
+
const { provider } = createMockProvider([
|
|
454
|
+
toolUseResponse('t1', 'read_file', { path: '/test.txt' }),
|
|
455
|
+
textResponse('Done'),
|
|
456
|
+
]);
|
|
457
|
+
|
|
458
|
+
const toolExecutor = async (
|
|
459
|
+
_name: string,
|
|
460
|
+
_input: Record<string, unknown>,
|
|
461
|
+
onOutput?: (chunk: string) => void,
|
|
462
|
+
) => {
|
|
463
|
+
onOutput?.('chunk1');
|
|
464
|
+
onOutput?.('chunk2');
|
|
465
|
+
return { content: 'full output', isError: false };
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
const loop = new AgentLoop(provider, 'system', {}, dummyTools, toolExecutor);
|
|
469
|
+
const events: AgentEvent[] = [];
|
|
470
|
+
await loop.run([userMessage], collectEvents(events));
|
|
471
|
+
|
|
472
|
+
const chunkEvents = events.filter((e) => e.type === 'tool_output_chunk');
|
|
473
|
+
expect(chunkEvents).toHaveLength(2);
|
|
474
|
+
expect((chunkEvents[0] as Extract<AgentEvent, { type: 'tool_output_chunk' }>).chunk).toBe('chunk1');
|
|
475
|
+
expect((chunkEvents[1] as Extract<AgentEvent, { type: 'tool_output_chunk' }>).chunk).toBe('chunk2');
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
// 11. System prompt and tools are passed to provider
|
|
479
|
+
test('passes system prompt and tools to provider', async () => {
|
|
480
|
+
const { provider, calls } = createMockProvider([textResponse('Hi')]);
|
|
481
|
+
const loop = new AgentLoop(provider, 'My system prompt', {}, dummyTools);
|
|
482
|
+
|
|
483
|
+
await loop.run([userMessage], () => {});
|
|
484
|
+
|
|
485
|
+
expect(calls[0].systemPrompt).toBe('My system prompt');
|
|
486
|
+
expect(calls[0].tools).toEqual(dummyTools);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
// 12. No tools configured — tools are not passed to provider
|
|
490
|
+
test('does not pass tools to provider when none are configured', async () => {
|
|
491
|
+
const { provider, calls } = createMockProvider([textResponse('Hi')]);
|
|
492
|
+
const loop = new AgentLoop(provider, 'system');
|
|
493
|
+
|
|
494
|
+
await loop.run([userMessage], () => {});
|
|
495
|
+
|
|
496
|
+
expect(calls[0].tools).toBeUndefined();
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
// 13. Parallel tool execution — multiple tool_use blocks in a single response
|
|
500
|
+
test('executes multiple tools in parallel', async () => {
|
|
501
|
+
const { provider, calls } = createMockProvider([
|
|
502
|
+
// Provider returns 3 tool_use blocks in a single response
|
|
503
|
+
{
|
|
504
|
+
content: [
|
|
505
|
+
{ type: 'tool_use' as const, id: 't1', name: 'read_file', input: { path: '/a.txt' } },
|
|
506
|
+
{ type: 'tool_use' as const, id: 't2', name: 'read_file', input: { path: '/b.txt' } },
|
|
507
|
+
{ type: 'tool_use' as const, id: 't3', name: 'read_file', input: { path: '/c.txt' } },
|
|
508
|
+
],
|
|
509
|
+
model: 'mock-model',
|
|
510
|
+
usage: { inputTokens: 10, outputTokens: 5 },
|
|
511
|
+
stopReason: 'tool_use' as const,
|
|
512
|
+
},
|
|
513
|
+
textResponse('Got all three files.'),
|
|
514
|
+
]);
|
|
515
|
+
|
|
516
|
+
const executionLog: { path: string; start: number; end: number }[] = [];
|
|
517
|
+
const toolExecutor = async (_name: string, input: Record<string, unknown>) => {
|
|
518
|
+
const start = Date.now();
|
|
519
|
+
// Simulate async work — all tools should overlap in time
|
|
520
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
521
|
+
const end = Date.now();
|
|
522
|
+
executionLog.push({ path: (input as { path: string }).path, start, end });
|
|
523
|
+
return { content: `contents of ${(input as { path: string }).path}`, isError: false };
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
const loop = new AgentLoop(provider, 'system', {}, dummyTools, toolExecutor);
|
|
527
|
+
const events: AgentEvent[] = [];
|
|
528
|
+
const history = await loop.run([userMessage], collectEvents(events));
|
|
529
|
+
|
|
530
|
+
// All 3 tools should have been called
|
|
531
|
+
expect(executionLog).toHaveLength(3);
|
|
532
|
+
|
|
533
|
+
// Verify parallel execution: all tools should start before any finishes
|
|
534
|
+
// (with 50ms delay each, sequential would take 150ms+, parallel ~50ms)
|
|
535
|
+
const allStarts = executionLog.map(e => e.start);
|
|
536
|
+
const allEnds = executionLog.map(e => e.end);
|
|
537
|
+
const firstEnd = Math.min(...allEnds);
|
|
538
|
+
const lastStart = Math.max(...allStarts);
|
|
539
|
+
// In parallel execution, the last tool starts before the first tool ends
|
|
540
|
+
expect(lastStart).toBeLessThanOrEqual(firstEnd);
|
|
541
|
+
|
|
542
|
+
// Provider should have been called twice (tool batch + final text)
|
|
543
|
+
expect(calls).toHaveLength(2);
|
|
544
|
+
|
|
545
|
+
// Second call should contain 3 tool_result blocks in order
|
|
546
|
+
const secondCallMessages = calls[1].messages;
|
|
547
|
+
const lastMsg = secondCallMessages[secondCallMessages.length - 1];
|
|
548
|
+
const toolResultBlocks = lastMsg.content.filter(
|
|
549
|
+
(b): b is Extract<ContentBlock, { type: 'tool_result' }> => b.type === 'tool_result',
|
|
550
|
+
);
|
|
551
|
+
expect(toolResultBlocks).toHaveLength(3);
|
|
552
|
+
expect(toolResultBlocks[0].tool_use_id).toBe('t1');
|
|
553
|
+
expect(toolResultBlocks[1].tool_use_id).toBe('t2');
|
|
554
|
+
expect(toolResultBlocks[2].tool_use_id).toBe('t3');
|
|
555
|
+
|
|
556
|
+
// All tool_use events should be emitted before any tool_result events
|
|
557
|
+
let lastToolUseIdx = -1;
|
|
558
|
+
let firstToolResultIdx = events.length;
|
|
559
|
+
events.forEach((e, i) => {
|
|
560
|
+
if (e.type === 'tool_use') lastToolUseIdx = i;
|
|
561
|
+
if (e.type === 'tool_result' && i < firstToolResultIdx) firstToolResultIdx = i;
|
|
562
|
+
});
|
|
563
|
+
expect(lastToolUseIdx).toBeLessThan(firstToolResultIdx);
|
|
564
|
+
|
|
565
|
+
// Final history: user, assistant(3 tool_use), user(3 tool_result), assistant(text)
|
|
566
|
+
expect(history).toHaveLength(4);
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
// 14. Abort before parallel tool execution synthesizes cancelled results
|
|
570
|
+
test('synthesizes cancelled results when aborted before tool execution', async () => {
|
|
571
|
+
const controller = new AbortController();
|
|
572
|
+
|
|
573
|
+
const { provider } = createMockProvider([
|
|
574
|
+
{
|
|
575
|
+
content: [
|
|
576
|
+
{ type: 'tool_use' as const, id: 't1', name: 'read_file', input: { path: '/a.txt' } },
|
|
577
|
+
{ type: 'tool_use' as const, id: 't2', name: 'read_file', input: { path: '/b.txt' } },
|
|
578
|
+
],
|
|
579
|
+
model: 'mock-model',
|
|
580
|
+
usage: { inputTokens: 10, outputTokens: 5 },
|
|
581
|
+
stopReason: 'tool_use' as const,
|
|
582
|
+
},
|
|
583
|
+
]);
|
|
584
|
+
|
|
585
|
+
// Abort during the provider call so the signal is already aborted
|
|
586
|
+
// before tool execution begins
|
|
587
|
+
const originalSendMessage = provider.sendMessage.bind(provider);
|
|
588
|
+
provider.sendMessage = async (...args: Parameters<typeof provider.sendMessage>) => {
|
|
589
|
+
const result = await originalSendMessage(...args);
|
|
590
|
+
controller.abort();
|
|
591
|
+
return result;
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
const toolCalls: string[] = [];
|
|
595
|
+
const toolExecutor = async (_name: string, input: Record<string, unknown>) => {
|
|
596
|
+
toolCalls.push((input as { path: string }).path);
|
|
597
|
+
return { content: 'data', isError: false };
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
const loop = new AgentLoop(provider, 'system', {}, dummyTools, toolExecutor);
|
|
601
|
+
const events: AgentEvent[] = [];
|
|
602
|
+
const history = await loop.run([userMessage], collectEvents(events), controller.signal);
|
|
603
|
+
|
|
604
|
+
// No tools should have been executed
|
|
605
|
+
expect(toolCalls).toHaveLength(0);
|
|
606
|
+
|
|
607
|
+
// History should contain cancelled tool_result blocks
|
|
608
|
+
const lastMsg = history[history.length - 1];
|
|
609
|
+
expect(lastMsg.role).toBe('user');
|
|
610
|
+
const toolResultBlocks = lastMsg.content.filter(
|
|
611
|
+
(b): b is Extract<ContentBlock, { type: 'tool_result' }> => b.type === 'tool_result',
|
|
612
|
+
);
|
|
613
|
+
expect(toolResultBlocks).toHaveLength(2);
|
|
614
|
+
expect(toolResultBlocks[0].tool_use_id).toBe('t1');
|
|
615
|
+
expect(toolResultBlocks[0].content).toBe('Cancelled by user');
|
|
616
|
+
expect(toolResultBlocks[0].is_error).toBe(true);
|
|
617
|
+
expect(toolResultBlocks[1].tool_use_id).toBe('t2');
|
|
618
|
+
expect(toolResultBlocks[1].content).toBe('Cancelled by user');
|
|
619
|
+
expect(toolResultBlocks[1].is_error).toBe(true);
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
// 15. Parallel tool_result events are emitted in deterministic tool_use order
|
|
623
|
+
test('emits tool_result events in tool_use order regardless of completion timing', async () => {
|
|
624
|
+
const { provider } = createMockProvider([
|
|
625
|
+
{
|
|
626
|
+
content: [
|
|
627
|
+
{ type: 'tool_use' as const, id: 't1', name: 'read_file', input: { path: '/slow.txt' } },
|
|
628
|
+
{ type: 'tool_use' as const, id: 't2', name: 'read_file', input: { path: '/fast.txt' } },
|
|
629
|
+
{ type: 'tool_use' as const, id: 't3', name: 'read_file', input: { path: '/medium.txt' } },
|
|
630
|
+
],
|
|
631
|
+
model: 'mock-model',
|
|
632
|
+
usage: { inputTokens: 10, outputTokens: 5 },
|
|
633
|
+
stopReason: 'tool_use' as const,
|
|
634
|
+
},
|
|
635
|
+
textResponse('Done'),
|
|
636
|
+
]);
|
|
637
|
+
|
|
638
|
+
// Tools complete in different order than they were called: t2 first, t3 second, t1 last
|
|
639
|
+
const toolExecutor = async (_name: string, input: Record<string, unknown>) => {
|
|
640
|
+
const path = (input as { path: string }).path;
|
|
641
|
+
const delays: Record<string, number> = { '/slow.txt': 80, '/fast.txt': 10, '/medium.txt': 40 };
|
|
642
|
+
await new Promise(resolve => setTimeout(resolve, delays[path] ?? 10));
|
|
643
|
+
return { content: `contents of ${path}`, isError: false };
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
const loop = new AgentLoop(provider, 'system', {}, dummyTools, toolExecutor);
|
|
647
|
+
const events: AgentEvent[] = [];
|
|
648
|
+
await loop.run([userMessage], collectEvents(events));
|
|
649
|
+
|
|
650
|
+
// Collect tool_result events in order
|
|
651
|
+
const toolResultEvents = events.filter(
|
|
652
|
+
(e): e is Extract<AgentEvent, { type: 'tool_result' }> => e.type === 'tool_result',
|
|
653
|
+
);
|
|
654
|
+
expect(toolResultEvents).toHaveLength(3);
|
|
655
|
+
|
|
656
|
+
// Results must be in tool_use order (t1, t2, t3), NOT completion order (t2, t3, t1)
|
|
657
|
+
expect(toolResultEvents[0].toolUseId).toBe('t1');
|
|
658
|
+
expect(toolResultEvents[1].toolUseId).toBe('t2');
|
|
659
|
+
expect(toolResultEvents[2].toolUseId).toBe('t3');
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
// ---------------------------------------------------------------------------
|
|
663
|
+
// Checkpoint callback tests
|
|
664
|
+
// ---------------------------------------------------------------------------
|
|
665
|
+
|
|
666
|
+
// 16. Checkpoint callback is called after tool results with correct info
|
|
667
|
+
test('checkpoint callback is called after tool results with correct info', async () => {
|
|
668
|
+
const { provider } = createMockProvider([
|
|
669
|
+
toolUseResponse('t1', 'read_file', { path: '/test.txt' }),
|
|
670
|
+
textResponse('Done'),
|
|
671
|
+
]);
|
|
672
|
+
|
|
673
|
+
const toolExecutor = async () => ({ content: 'file data', isError: false });
|
|
674
|
+
const loop = new AgentLoop(provider, 'system', {}, dummyTools, toolExecutor);
|
|
675
|
+
|
|
676
|
+
const checkpoints: CheckpointInfo[] = [];
|
|
677
|
+
const onCheckpoint = (checkpoint: CheckpointInfo): CheckpointDecision => {
|
|
678
|
+
checkpoints.push(checkpoint);
|
|
679
|
+
return 'continue';
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
await loop.run([userMessage], () => {}, undefined, undefined, onCheckpoint);
|
|
683
|
+
|
|
684
|
+
expect(checkpoints).toHaveLength(1);
|
|
685
|
+
expect(checkpoints[0]).toEqual({
|
|
686
|
+
turnIndex: 0,
|
|
687
|
+
toolCount: 1,
|
|
688
|
+
hasToolUse: true,
|
|
689
|
+
});
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
// 17. Returning 'continue' lets the loop proceed normally
|
|
693
|
+
test('checkpoint returning continue lets the loop proceed normally', async () => {
|
|
694
|
+
const { provider, calls } = createMockProvider([
|
|
695
|
+
toolUseResponse('t1', 'read_file', { path: '/a.txt' }),
|
|
696
|
+
toolUseResponse('t2', 'read_file', { path: '/b.txt' }),
|
|
697
|
+
textResponse('All done'),
|
|
698
|
+
]);
|
|
699
|
+
|
|
700
|
+
const toolExecutor = async () => ({ content: 'data', isError: false });
|
|
701
|
+
const loop = new AgentLoop(provider, 'system', {}, dummyTools, toolExecutor);
|
|
702
|
+
|
|
703
|
+
const onCheckpoint = (): CheckpointDecision => 'continue';
|
|
704
|
+
|
|
705
|
+
const history = await loop.run([userMessage], () => {}, undefined, undefined, onCheckpoint);
|
|
706
|
+
|
|
707
|
+
// All 3 provider calls should happen (2 tool turns + final text)
|
|
708
|
+
expect(calls).toHaveLength(3);
|
|
709
|
+
// Full history: user, assistant(t1), user(result1), assistant(t2), user(result2), assistant(text)
|
|
710
|
+
expect(history).toHaveLength(6);
|
|
711
|
+
expect(history[5].content).toEqual([{ type: 'text', text: 'All done' }]);
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
// 18. Returning 'yield' causes the loop to stop after that turn
|
|
715
|
+
test('checkpoint returning yield causes the loop to stop', async () => {
|
|
716
|
+
const { provider, calls } = createMockProvider([
|
|
717
|
+
toolUseResponse('t1', 'read_file', { path: '/a.txt' }),
|
|
718
|
+
toolUseResponse('t2', 'read_file', { path: '/b.txt' }),
|
|
719
|
+
textResponse('Should not reach'),
|
|
720
|
+
]);
|
|
721
|
+
|
|
722
|
+
const toolExecutor = async () => ({ content: 'data', isError: false });
|
|
723
|
+
const loop = new AgentLoop(provider, 'system', {}, dummyTools, toolExecutor);
|
|
724
|
+
|
|
725
|
+
const onCheckpoint = (): CheckpointDecision => 'yield';
|
|
726
|
+
|
|
727
|
+
const history = await loop.run([userMessage], () => {}, undefined, undefined, onCheckpoint);
|
|
728
|
+
|
|
729
|
+
// Only 1 provider call should happen — loop yields after first tool turn
|
|
730
|
+
expect(calls).toHaveLength(1);
|
|
731
|
+
// History: user, assistant(t1), user(result1)
|
|
732
|
+
expect(history).toHaveLength(3);
|
|
733
|
+
expect(history[1].role).toBe('assistant');
|
|
734
|
+
expect(history[2].role).toBe('user');
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
// 19. Without a checkpoint callback, behavior is unchanged
|
|
738
|
+
test('without checkpoint callback behavior is unchanged', async () => {
|
|
739
|
+
const { provider, calls } = createMockProvider([
|
|
740
|
+
toolUseResponse('t1', 'read_file', { path: '/a.txt' }),
|
|
741
|
+
textResponse('Done'),
|
|
742
|
+
]);
|
|
743
|
+
|
|
744
|
+
const toolExecutor = async () => ({ content: 'data', isError: false });
|
|
745
|
+
const loop = new AgentLoop(provider, 'system', {}, dummyTools, toolExecutor);
|
|
746
|
+
|
|
747
|
+
const history = await loop.run([userMessage], () => {});
|
|
748
|
+
|
|
749
|
+
// Normal behavior: 2 provider calls, full history
|
|
750
|
+
expect(calls).toHaveLength(2);
|
|
751
|
+
expect(history).toHaveLength(4);
|
|
752
|
+
expect(history[3].content).toEqual([{ type: 'text', text: 'Done' }]);
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
// 20. turnIndex increments correctly across turns
|
|
756
|
+
test('turnIndex increments correctly across multiple turns', async () => {
|
|
757
|
+
const { provider } = createMockProvider([
|
|
758
|
+
toolUseResponse('t1', 'read_file', { path: '/a.txt' }),
|
|
759
|
+
toolUseResponse('t2', 'read_file', { path: '/b.txt' }),
|
|
760
|
+
toolUseResponse('t3', 'read_file', { path: '/c.txt' }),
|
|
761
|
+
textResponse('Done'),
|
|
762
|
+
]);
|
|
763
|
+
|
|
764
|
+
const toolExecutor = async () => ({ content: 'data', isError: false });
|
|
765
|
+
const loop = new AgentLoop(provider, 'system', {}, dummyTools, toolExecutor);
|
|
766
|
+
|
|
767
|
+
const checkpoints: CheckpointInfo[] = [];
|
|
768
|
+
const onCheckpoint = (checkpoint: CheckpointInfo): CheckpointDecision => {
|
|
769
|
+
checkpoints.push(checkpoint);
|
|
770
|
+
return 'continue';
|
|
771
|
+
};
|
|
772
|
+
|
|
773
|
+
await loop.run([userMessage], () => {}, undefined, undefined, onCheckpoint);
|
|
774
|
+
|
|
775
|
+
expect(checkpoints).toHaveLength(3);
|
|
776
|
+
expect(checkpoints[0].turnIndex).toBe(0);
|
|
777
|
+
expect(checkpoints[1].turnIndex).toBe(1);
|
|
778
|
+
expect(checkpoints[2].turnIndex).toBe(2);
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
// 21. Checkpoint is NOT called when there's no tool use
|
|
782
|
+
test('checkpoint is not called when assistant responds with text only', async () => {
|
|
783
|
+
const { provider } = createMockProvider([textResponse('Just a text response')]);
|
|
784
|
+
const loop = new AgentLoop(provider, 'system', {}, dummyTools);
|
|
785
|
+
|
|
786
|
+
const checkpoints: CheckpointInfo[] = [];
|
|
787
|
+
const onCheckpoint = (checkpoint: CheckpointInfo): CheckpointDecision => {
|
|
788
|
+
checkpoints.push(checkpoint);
|
|
789
|
+
return 'continue';
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
const history = await loop.run([userMessage], () => {}, undefined, undefined, onCheckpoint);
|
|
793
|
+
|
|
794
|
+
// Checkpoint should never be called for a text-only response
|
|
795
|
+
expect(checkpoints).toHaveLength(0);
|
|
796
|
+
// Normal response
|
|
797
|
+
expect(history).toHaveLength(2);
|
|
798
|
+
expect(history[1].content).toEqual([{ type: 'text', text: 'Just a text response' }]);
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
// 22. Checkpoint reports correct toolCount for parallel tool execution
|
|
802
|
+
test('checkpoint reports correct toolCount for parallel tools', async () => {
|
|
803
|
+
const { provider } = createMockProvider([
|
|
804
|
+
{
|
|
805
|
+
content: [
|
|
806
|
+
{ type: 'tool_use' as const, id: 't1', name: 'read_file', input: { path: '/a.txt' } },
|
|
807
|
+
{ type: 'tool_use' as const, id: 't2', name: 'read_file', input: { path: '/b.txt' } },
|
|
808
|
+
{ type: 'tool_use' as const, id: 't3', name: 'read_file', input: { path: '/c.txt' } },
|
|
809
|
+
],
|
|
810
|
+
model: 'mock-model',
|
|
811
|
+
usage: { inputTokens: 10, outputTokens: 5 },
|
|
812
|
+
stopReason: 'tool_use' as const,
|
|
813
|
+
},
|
|
814
|
+
textResponse('Got all three'),
|
|
815
|
+
]);
|
|
816
|
+
|
|
817
|
+
const toolExecutor = async () => ({ content: 'data', isError: false });
|
|
818
|
+
const loop = new AgentLoop(provider, 'system', {}, dummyTools, toolExecutor);
|
|
819
|
+
|
|
820
|
+
const checkpoints: CheckpointInfo[] = [];
|
|
821
|
+
const onCheckpoint = (checkpoint: CheckpointInfo): CheckpointDecision => {
|
|
822
|
+
checkpoints.push(checkpoint);
|
|
823
|
+
return 'continue';
|
|
824
|
+
};
|
|
825
|
+
|
|
826
|
+
await loop.run([userMessage], () => {}, undefined, undefined, onCheckpoint);
|
|
827
|
+
|
|
828
|
+
expect(checkpoints).toHaveLength(1);
|
|
829
|
+
expect(checkpoints[0].toolCount).toBe(3);
|
|
830
|
+
expect(checkpoints[0].hasToolUse).toBe(true);
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
// 23. Multiple checkpoints across a multi-turn run with selective yield on turn 3
|
|
834
|
+
test('multiple checkpoints with selective yield — executes turns 0-2, yields at turn 3, never runs 4+', async () => {
|
|
835
|
+
// Mock provider to return tool_use for 5 turns, then text
|
|
836
|
+
const responses: ProviderResponse[] = [];
|
|
837
|
+
for (let i = 0; i < 5; i++) {
|
|
838
|
+
responses.push(toolUseResponse(`t${i}`, 'read_file', { path: `/file${i}.txt` }));
|
|
839
|
+
}
|
|
840
|
+
responses.push(textResponse('Should never reach this'));
|
|
841
|
+
|
|
842
|
+
const { provider, calls } = createMockProvider(responses);
|
|
843
|
+
const toolExecutor = async () => ({ content: 'data', isError: false });
|
|
844
|
+
const loop = new AgentLoop(provider, 'system', {}, dummyTools, toolExecutor);
|
|
845
|
+
|
|
846
|
+
const checkpoints: CheckpointInfo[] = [];
|
|
847
|
+
const onCheckpoint = (checkpoint: CheckpointInfo): CheckpointDecision => {
|
|
848
|
+
checkpoints.push(checkpoint);
|
|
849
|
+
// Yield on turn 3 (0-indexed)
|
|
850
|
+
return checkpoint.turnIndex === 3 ? 'yield' : 'continue';
|
|
851
|
+
};
|
|
852
|
+
|
|
853
|
+
const events: AgentEvent[] = [];
|
|
854
|
+
const history = await loop.run([userMessage], collectEvents(events), undefined, undefined, onCheckpoint);
|
|
855
|
+
|
|
856
|
+
// Turns 0, 1, 2, 3 execute (4 provider calls). Turn 3 yields, so turns 4+ never execute.
|
|
857
|
+
expect(calls).toHaveLength(4);
|
|
858
|
+
|
|
859
|
+
// Checkpoints should have been called for turns 0 through 3
|
|
860
|
+
expect(checkpoints).toHaveLength(4);
|
|
861
|
+
expect(checkpoints[0].turnIndex).toBe(0);
|
|
862
|
+
expect(checkpoints[1].turnIndex).toBe(1);
|
|
863
|
+
expect(checkpoints[2].turnIndex).toBe(2);
|
|
864
|
+
expect(checkpoints[3].turnIndex).toBe(3);
|
|
865
|
+
|
|
866
|
+
// History should contain results from turns 0-3:
|
|
867
|
+
// user, assistant(t0), user(result0), assistant(t1), user(result1),
|
|
868
|
+
// assistant(t2), user(result2), assistant(t3), user(result3)
|
|
869
|
+
// = 1 original + 4*(assistant + user) = 9
|
|
870
|
+
expect(history).toHaveLength(9);
|
|
871
|
+
|
|
872
|
+
// Verify the last two messages are from turn 3
|
|
873
|
+
expect(history[7].role).toBe('assistant');
|
|
874
|
+
const lastAssistantToolUse = history[7].content.find((b) => b.type === 'tool_use');
|
|
875
|
+
expect(lastAssistantToolUse).toBeDefined();
|
|
876
|
+
if (lastAssistantToolUse && lastAssistantToolUse.type === 'tool_use') {
|
|
877
|
+
expect(lastAssistantToolUse.id).toBe('t3');
|
|
878
|
+
}
|
|
879
|
+
expect(history[8].role).toBe('user');
|
|
880
|
+
const lastToolResult = history[8].content.find(
|
|
881
|
+
(b): b is Extract<ContentBlock, { type: 'tool_result' }> => b.type === 'tool_result',
|
|
882
|
+
);
|
|
883
|
+
expect(lastToolResult).toBeDefined();
|
|
884
|
+
expect(lastToolResult!.tool_use_id).toBe('t3');
|
|
885
|
+
|
|
886
|
+
// Verify turns 4+ never executed — no tool_use event for t4
|
|
887
|
+
const toolUseEvents = events.filter(
|
|
888
|
+
(e): e is Extract<AgentEvent, { type: 'tool_use' }> => e.type === 'tool_use',
|
|
889
|
+
);
|
|
890
|
+
const toolUseNames = toolUseEvents.map((e) => e.id);
|
|
891
|
+
expect(toolUseNames).toEqual(['t0', 't1', 't2', 't3']);
|
|
892
|
+
expect(toolUseNames).not.toContain('t4');
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
// 24. Yield on second turn — first turn proceeds, second stops
|
|
896
|
+
test('yield on second turn lets first turn proceed and stops on second', async () => {
|
|
897
|
+
const { provider, calls } = createMockProvider([
|
|
898
|
+
toolUseResponse('t1', 'read_file', { path: '/a.txt' }),
|
|
899
|
+
toolUseResponse('t2', 'read_file', { path: '/b.txt' }),
|
|
900
|
+
textResponse('Should not reach'),
|
|
901
|
+
]);
|
|
902
|
+
|
|
903
|
+
const toolExecutor = async () => ({ content: 'data', isError: false });
|
|
904
|
+
const loop = new AgentLoop(provider, 'system', {}, dummyTools, toolExecutor);
|
|
905
|
+
|
|
906
|
+
const onCheckpoint = (checkpoint: CheckpointInfo): CheckpointDecision => {
|
|
907
|
+
// Yield on the second turn (turnIndex 1)
|
|
908
|
+
return checkpoint.turnIndex === 1 ? 'yield' : 'continue';
|
|
909
|
+
};
|
|
910
|
+
|
|
911
|
+
const history = await loop.run([userMessage], () => {}, undefined, undefined, onCheckpoint);
|
|
912
|
+
|
|
913
|
+
// 2 provider calls: first tool turn + second tool turn (yield after second)
|
|
914
|
+
expect(calls).toHaveLength(2);
|
|
915
|
+
// History: user, assistant(t1), user(result1), assistant(t2), user(result2)
|
|
916
|
+
expect(history).toHaveLength(5);
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
// ---------------------------------------------------------------------------
|
|
920
|
+
// Dynamic tool resolver (resolveTools) tests
|
|
921
|
+
// ---------------------------------------------------------------------------
|
|
922
|
+
|
|
923
|
+
// 25. Without resolveTools, static tools are used (backward compatible)
|
|
924
|
+
test('without resolveTools, static tools are passed to provider', async () => {
|
|
925
|
+
const { provider, calls } = createMockProvider([textResponse('Hi')]);
|
|
926
|
+
const loop = new AgentLoop(provider, 'system', {}, dummyTools);
|
|
927
|
+
|
|
928
|
+
await loop.run([userMessage], () => {});
|
|
929
|
+
|
|
930
|
+
expect(calls[0].tools).toEqual(dummyTools);
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
// 26. resolveTools callback is invoked before each provider call
|
|
934
|
+
test('resolveTools is invoked before each provider call', async () => {
|
|
935
|
+
const resolverCalls: Message[][] = [];
|
|
936
|
+
const resolvedTools: ToolDefinition[] = [
|
|
937
|
+
{ name: 'search', description: 'Search files', input_schema: { type: 'object', properties: { query: { type: 'string' } } } },
|
|
938
|
+
];
|
|
939
|
+
|
|
940
|
+
const { provider } = createMockProvider([
|
|
941
|
+
toolUseResponse('t1', 'search', { query: 'foo' }),
|
|
942
|
+
textResponse('Found it'),
|
|
943
|
+
]);
|
|
944
|
+
|
|
945
|
+
const toolExecutor = async () => ({ content: 'result', isError: false });
|
|
946
|
+
|
|
947
|
+
const resolveTools = (history: Message[]): ToolDefinition[] => {
|
|
948
|
+
resolverCalls.push([...history]);
|
|
949
|
+
return resolvedTools;
|
|
950
|
+
};
|
|
951
|
+
|
|
952
|
+
const loop = new AgentLoop(provider, 'system', {}, [], toolExecutor, resolveTools);
|
|
953
|
+
await loop.run([userMessage], () => {});
|
|
954
|
+
|
|
955
|
+
// resolveTools should be called once per provider turn (2 turns total)
|
|
956
|
+
expect(resolverCalls).toHaveLength(2);
|
|
957
|
+
|
|
958
|
+
// First call receives just the initial user message
|
|
959
|
+
expect(resolverCalls[0]).toHaveLength(1);
|
|
960
|
+
expect(resolverCalls[0][0]).toEqual(userMessage);
|
|
961
|
+
|
|
962
|
+
// Second call receives the accumulated history (user + assistant + tool_result)
|
|
963
|
+
expect(resolverCalls[1].length).toBeGreaterThan(1);
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
// 27. Resolved tool list is passed to the provider
|
|
967
|
+
test('resolved tools are passed to the provider instead of static tools', async () => {
|
|
968
|
+
const dynamicTools: ToolDefinition[] = [
|
|
969
|
+
{ name: 'dynamic_tool', description: 'Dynamic', input_schema: { type: 'object' } },
|
|
970
|
+
];
|
|
971
|
+
|
|
972
|
+
const { provider, calls } = createMockProvider([textResponse('Hi')]);
|
|
973
|
+
|
|
974
|
+
const resolveTools = (): ToolDefinition[] => dynamicTools;
|
|
975
|
+
|
|
976
|
+
// Pass different static tools to verify they are overridden
|
|
977
|
+
const loop = new AgentLoop(provider, 'system', {}, dummyTools, undefined, resolveTools);
|
|
978
|
+
await loop.run([userMessage], () => {});
|
|
979
|
+
|
|
980
|
+
// Provider should receive the dynamically resolved tools, not the static ones
|
|
981
|
+
expect(calls[0].tools).toEqual(dynamicTools);
|
|
982
|
+
expect(calls[0].tools).not.toEqual(dummyTools);
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
// 28. Tool list can change between turns
|
|
986
|
+
test('resolveTools can return different tools on each turn', async () => {
|
|
987
|
+
const toolsPerTurn: ToolDefinition[][] = [
|
|
988
|
+
[{ name: 'tool_a', description: 'Tool A', input_schema: { type: 'object' } }],
|
|
989
|
+
[
|
|
990
|
+
{ name: 'tool_a', description: 'Tool A', input_schema: { type: 'object' } },
|
|
991
|
+
{ name: 'tool_b', description: 'Tool B', input_schema: { type: 'object' } },
|
|
992
|
+
],
|
|
993
|
+
[{ name: 'tool_c', description: 'Tool C', input_schema: { type: 'object' } }],
|
|
994
|
+
];
|
|
995
|
+
|
|
996
|
+
let turnIndex = 0;
|
|
997
|
+
const resolveTools = (): ToolDefinition[] => {
|
|
998
|
+
const tools = toolsPerTurn[turnIndex] ?? toolsPerTurn[toolsPerTurn.length - 1];
|
|
999
|
+
turnIndex++;
|
|
1000
|
+
return tools;
|
|
1001
|
+
};
|
|
1002
|
+
|
|
1003
|
+
const { provider, calls } = createMockProvider([
|
|
1004
|
+
toolUseResponse('t1', 'tool_a', {}),
|
|
1005
|
+
toolUseResponse('t2', 'tool_a', {}),
|
|
1006
|
+
textResponse('Done'),
|
|
1007
|
+
]);
|
|
1008
|
+
|
|
1009
|
+
const toolExecutor = async () => ({ content: 'ok', isError: false });
|
|
1010
|
+
const loop = new AgentLoop(provider, 'system', {}, [], toolExecutor, resolveTools);
|
|
1011
|
+
await loop.run([userMessage], () => {});
|
|
1012
|
+
|
|
1013
|
+
// Provider should have been called 3 times
|
|
1014
|
+
expect(calls).toHaveLength(3);
|
|
1015
|
+
|
|
1016
|
+
// Each call should have received different tools
|
|
1017
|
+
expect(calls[0].tools).toEqual(toolsPerTurn[0]);
|
|
1018
|
+
expect(calls[1].tools).toEqual(toolsPerTurn[1]);
|
|
1019
|
+
expect(calls[2].tools).toEqual(toolsPerTurn[2]);
|
|
1020
|
+
});
|
|
1021
|
+
|
|
1022
|
+
// 29. resolveTools returning empty array means no tools passed to provider
|
|
1023
|
+
test('resolveTools returning empty array sends no tools to provider', async () => {
|
|
1024
|
+
const resolveTools = (): ToolDefinition[] => [];
|
|
1025
|
+
|
|
1026
|
+
const { provider, calls } = createMockProvider([textResponse('No tools available')]);
|
|
1027
|
+
|
|
1028
|
+
const loop = new AgentLoop(provider, 'system', {}, dummyTools, undefined, resolveTools);
|
|
1029
|
+
await loop.run([userMessage], () => {});
|
|
1030
|
+
|
|
1031
|
+
// Empty array should result in undefined tools (same as no-tools behavior)
|
|
1032
|
+
expect(calls[0].tools).toBeUndefined();
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
// ---------------------------------------------------------------------------
|
|
1036
|
+
// Tool result truncation tests
|
|
1037
|
+
// ---------------------------------------------------------------------------
|
|
1038
|
+
|
|
1039
|
+
// 30. Oversized tool results are truncated before entering history
|
|
1040
|
+
test('truncates oversized tool results before adding to history', async () => {
|
|
1041
|
+
const toolCallId = 'tool-large';
|
|
1042
|
+
const largeContent = 'x'.repeat(500_000);
|
|
1043
|
+
|
|
1044
|
+
const { provider, calls } = createMockProvider([
|
|
1045
|
+
toolUseResponse(toolCallId, 'read_file', { path: '/huge.txt' }),
|
|
1046
|
+
textResponse('Got it.'),
|
|
1047
|
+
]);
|
|
1048
|
+
|
|
1049
|
+
const toolExecutor = async () => {
|
|
1050
|
+
return { content: largeContent, isError: false };
|
|
1051
|
+
};
|
|
1052
|
+
|
|
1053
|
+
const loop = new AgentLoop(
|
|
1054
|
+
provider,
|
|
1055
|
+
'system',
|
|
1056
|
+
{ maxInputTokens: 180_000 },
|
|
1057
|
+
dummyTools,
|
|
1058
|
+
toolExecutor,
|
|
1059
|
+
);
|
|
1060
|
+
const events: AgentEvent[] = [];
|
|
1061
|
+
const history = await loop.run([userMessage], collectEvents(events));
|
|
1062
|
+
|
|
1063
|
+
// The tool result user message is at index 2 in history
|
|
1064
|
+
const toolResultMsg = history[2];
|
|
1065
|
+
expect(toolResultMsg.role).toBe('user');
|
|
1066
|
+
|
|
1067
|
+
const toolResultBlock = toolResultMsg.content.find(
|
|
1068
|
+
(b): b is Extract<ContentBlock, { type: 'tool_result' }> => b.type === 'tool_result',
|
|
1069
|
+
);
|
|
1070
|
+
expect(toolResultBlock).toBeDefined();
|
|
1071
|
+
|
|
1072
|
+
// Content should have been truncated (much shorter than the original 500K)
|
|
1073
|
+
expect(toolResultBlock!.content.length).toBeLessThan(500_000);
|
|
1074
|
+
|
|
1075
|
+
// Content should end with the truncation suffix
|
|
1076
|
+
expect(toolResultBlock!.content).toContain(
|
|
1077
|
+
'[Content truncated',
|
|
1078
|
+
);
|
|
1079
|
+
|
|
1080
|
+
// The second provider call should also have the truncated content in messages
|
|
1081
|
+
const secondCallMessages = calls[1].messages;
|
|
1082
|
+
const lastMsg = secondCallMessages[secondCallMessages.length - 1];
|
|
1083
|
+
const sentBlock = lastMsg.content.find(
|
|
1084
|
+
(b): b is Extract<ContentBlock, { type: 'tool_result' }> => b.type === 'tool_result',
|
|
1085
|
+
);
|
|
1086
|
+
expect(sentBlock).toBeDefined();
|
|
1087
|
+
expect(sentBlock!.content.length).toBeLessThan(500_000);
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
// 31. Non-oversized tool results pass through unchanged
|
|
1091
|
+
test('non-oversized tool results pass through unchanged', async () => {
|
|
1092
|
+
const toolCallId = 'tool-small';
|
|
1093
|
+
const smallContent = 'small content';
|
|
1094
|
+
|
|
1095
|
+
const { provider, calls } = createMockProvider([
|
|
1096
|
+
toolUseResponse(toolCallId, 'read_file', { path: '/small.txt' }),
|
|
1097
|
+
textResponse('Got it.'),
|
|
1098
|
+
]);
|
|
1099
|
+
|
|
1100
|
+
const toolExecutor = async () => {
|
|
1101
|
+
return { content: smallContent, isError: false };
|
|
1102
|
+
};
|
|
1103
|
+
|
|
1104
|
+
const loop = new AgentLoop(
|
|
1105
|
+
provider,
|
|
1106
|
+
'system',
|
|
1107
|
+
{ maxInputTokens: 180_000 },
|
|
1108
|
+
dummyTools,
|
|
1109
|
+
toolExecutor,
|
|
1110
|
+
);
|
|
1111
|
+
const events: AgentEvent[] = [];
|
|
1112
|
+
const history = await loop.run([userMessage], collectEvents(events));
|
|
1113
|
+
|
|
1114
|
+
// The tool result user message is at index 2 in history
|
|
1115
|
+
const toolResultMsg = history[2];
|
|
1116
|
+
expect(toolResultMsg.role).toBe('user');
|
|
1117
|
+
|
|
1118
|
+
const toolResultBlock = toolResultMsg.content.find(
|
|
1119
|
+
(b): b is Extract<ContentBlock, { type: 'tool_result' }> => b.type === 'tool_result',
|
|
1120
|
+
);
|
|
1121
|
+
expect(toolResultBlock).toBeDefined();
|
|
1122
|
+
|
|
1123
|
+
// Content should be exactly the original small content — no truncation
|
|
1124
|
+
expect(toolResultBlock!.content).toBe(smallContent);
|
|
1125
|
+
|
|
1126
|
+
// The second provider call should also have the unchanged content
|
|
1127
|
+
const secondCallMessages = calls[1].messages;
|
|
1128
|
+
const lastMsg = secondCallMessages[secondCallMessages.length - 1];
|
|
1129
|
+
const sentBlock = lastMsg.content.find(
|
|
1130
|
+
(b): b is Extract<ContentBlock, { type: 'tool_result' }> => b.type === 'tool_result',
|
|
1131
|
+
);
|
|
1132
|
+
expect(sentBlock).toBeDefined();
|
|
1133
|
+
expect(sentBlock!.content).toBe(smallContent);
|
|
1134
|
+
});
|
|
1135
|
+
});
|