@vellumai/assistant 0.8.12 → 0.9.0-staging.2
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/AGENTS.md +0 -14
- package/ARCHITECTURE.md +45 -45
- package/README.md +1 -1
- package/bun.lock +200 -154
- package/docs/architecture/integrations.md +3 -3
- package/docs/architecture/memory.md +2 -2
- package/docs/architecture/security.md +10 -10
- package/docs/runbook-trusted-contacts.md +12 -12
- package/docs/skills.md +6 -6
- package/docs/workflows-testing.md +221 -0
- package/docs/workflows.md +510 -0
- package/examples/plugins/echo/README.md +5 -5
- package/knip.json +2 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +105 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +12 -0
- package/openapi.yaml +7197 -5708
- package/package.json +8 -4
- package/scripts/generate-openapi.ts +66 -114
- package/src/__tests__/access-request-seed-content-blocks.test.ts +213 -0
- package/src/__tests__/adaptive-thinking-repair.test.ts +32 -3
- package/src/__tests__/agent-loop-output-hooks.test.ts +183 -0
- package/src/__tests__/agent-loop-regrowth-guard.test.ts +506 -0
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
- package/src/__tests__/agent-wake-override-profile.test.ts +77 -0
- package/src/__tests__/app-compiler.test.ts +7 -1
- package/src/__tests__/app-dir-path-guard.test.ts +27 -3
- package/src/__tests__/app-executors.test.ts +43 -0
- package/src/__tests__/approval-cascade.test.ts +0 -5
- package/src/__tests__/approval-routes-http.test.ts +91 -0
- package/src/__tests__/assistant-stream-state.test.ts +107 -0
- package/src/__tests__/browser-fill-credential.test.ts +3 -3
- package/src/__tests__/bundled-skill-retrieval-guard.test.ts +1 -1
- package/src/__tests__/compaction-events.test.ts +63 -7
- package/src/__tests__/compaction-trail-store.test.ts +74 -1
- package/src/__tests__/compaction.benchmark.test.ts +63 -41
- package/src/__tests__/compactor-low-watermark-cut.test.ts +349 -0
- package/src/__tests__/context-window-manager-compact-retry.test.ts +64 -0
- package/src/__tests__/conversation-abort-tool-results.test.ts +0 -5
- package/src/__tests__/conversation-confirmation-signals.test.ts +0 -5
- package/src/__tests__/conversation-history-web-search.test.ts +7 -0
- package/src/__tests__/conversation-process-callsite.test.ts +0 -5
- package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -5
- package/src/__tests__/conversation-queue.test.ts +0 -5
- package/src/__tests__/conversation-slash-queue.test.ts +0 -5
- package/src/__tests__/conversation-slash-unknown.test.ts +0 -5
- package/src/__tests__/conversation-speed-override.test.ts +0 -5
- package/src/__tests__/conversation-surfaces-data-persist.test.ts +97 -0
- package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
- package/src/__tests__/conversation-usage.test.ts +2 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +0 -5
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -5
- package/src/__tests__/credential-broker-browser-fill.test.ts +2 -2
- package/src/__tests__/credential-broker-server-use.test.ts +2 -2
- package/src/__tests__/credential-broker.test.ts +1 -1
- package/src/__tests__/credential-prompt-route.test.ts +417 -0
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/credential-vault.test.ts +37 -0
- package/src/__tests__/db-schedule-syntax-migration.test.ts +24 -0
- package/src/__tests__/dynamic-page-surface.test.ts +219 -0
- package/src/__tests__/empty-state-greeting-cache.test.ts +94 -0
- package/src/__tests__/gateway-flag-listener.test.ts +24 -7
- package/src/__tests__/guardian-action-sweep.test.ts +56 -219
- package/src/__tests__/guardian-routing-invariants.test.ts +138 -0
- package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
- package/src/__tests__/list-messages-hidden-metadata.test.ts +99 -0
- package/src/__tests__/llm-request-log-source-clickhouse.test.ts +87 -1
- package/src/__tests__/llm-resolver.test.ts +115 -0
- package/src/__tests__/managed-profile-guard.test.ts +6 -5
- package/src/__tests__/max-tokens-continue-hook.test.ts +184 -0
- package/src/__tests__/media-generate-image.test.ts +20 -9
- package/src/__tests__/mock-gateway-ipc.ts +23 -0
- package/src/__tests__/model-intents.test.ts +1 -1
- package/src/__tests__/normalize-onboarding.test.ts +26 -0
- package/src/__tests__/notification-decision-strategy.test.ts +4 -2
- package/src/__tests__/notification-telegram-adapter.test.ts +21 -3
- package/src/__tests__/pending-interactions-resolved-event.test.ts +62 -0
- package/src/__tests__/post-turn-tool-result-truncation.test.ts +72 -18
- package/src/__tests__/require-fresh-approval.test.ts +425 -1
- package/src/__tests__/resolve-app-id.test.ts +56 -0
- package/src/__tests__/runtime-events-sse-parity.test.ts +2 -0
- package/src/__tests__/schedule-routes-workflow-validation.test.ts +408 -0
- package/src/__tests__/schedule-routes.test.ts +257 -4
- package/src/__tests__/schedule-store.test.ts +60 -0
- package/src/__tests__/schedule-tools.test.ts +247 -2
- package/src/__tests__/skill-execute-input.test.ts +85 -0
- package/src/__tests__/skill-secret-handling-guard.test.ts +21 -20
- package/src/__tests__/skills.test.ts +3 -3
- package/src/__tests__/slack-app-setup-skill-regression.test.ts +1 -1
- package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
- package/src/__tests__/subagent-tool-gate-mode.test.ts +547 -0
- package/src/__tests__/system-prompt.test.ts +1 -1
- package/src/__tests__/task-progress-nudge-hook.test.ts +372 -0
- package/src/__tests__/task-scheduler.test.ts +299 -0
- package/src/__tests__/tool-approval-seed-content-blocks.test.ts +209 -0
- package/src/__tests__/tool-result-spool.test.ts +3 -1
- package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +181 -0
- package/src/__tests__/workspace-migration-103-upgrade-quality-profile-to-opus-4-8.test.ts +174 -0
- package/src/agent/compaction-circuit.ts +11 -0
- package/src/agent/loop.ts +181 -12
- package/src/api/constants/call-sites.ts +12 -0
- package/src/api/events/assistant-thinking-delta.ts +10 -0
- package/src/api/events/usage-update.ts +7 -0
- package/src/api/index.ts +4 -1
- package/src/api/responses/memory-v3-selection-log.ts +18 -11
- package/src/approvals/approval-primitive.ts +2 -2
- package/src/background-wake/background-wake-routes.test.ts +5 -2
- package/src/bundler/compiler-tools.ts +1 -1
- package/src/bundler/package-resolver.ts +0 -1
- package/src/calls/call-domain.ts +1 -1
- package/src/calls/guardian-action-sweep.ts +16 -93
- package/src/cli/AGENTS.md +4 -0
- package/src/cli/commands/__tests__/schedules.test.ts +430 -1
- package/src/cli/commands/credentials.ts +28 -24
- package/src/cli/commands/image-generation.ts +23 -9
- package/src/cli/commands/notifications.ts +1 -1
- package/src/cli/commands/plugins.ts +89 -46
- package/src/cli/commands/schedules.ts +384 -11
- package/src/cli/lib/__tests__/inspect-plugin.test.ts +69 -5
- package/src/cli/lib/__tests__/install-from-github.test.ts +15 -0
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +81 -4
- package/src/cli/lib/inspect-plugin.ts +62 -1
- package/src/cli/lib/install-from-github.ts +52 -5
- package/src/cli/lib/upgrade-plugin.ts +18 -0
- package/src/config/__tests__/workflows-schema.test.ts +60 -0
- package/src/config/bundled-skills/acp/SKILL.md +2 -2
- package/src/config/bundled-skills/app-builder/SKILL.md +1 -1
- package/src/config/bundled-skills/app-builder/tools/app-create.ts +6 -1
- package/src/config/bundled-skills/app-builder/tools/app-generate-icon.ts +7 -1
- package/src/config/bundled-skills/app-builder/tools/app-refresh.ts +7 -1
- package/src/config/bundled-skills/app-builder/tools/app-update.ts +10 -1
- package/src/config/bundled-skills/image-studio/SKILL.md +66 -19
- package/src/config/bundled-skills/image-studio/TOOLS.json +1 -6
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +22 -3
- package/src/config/bundled-skills/personal-page/SKILL.md +57 -0
- package/src/config/bundled-skills/personal-page/TOOLS.json +27 -0
- package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +17 -0
- package/src/config/bundled-skills/schedule/SKILL.md +7 -2
- package/src/config/bundled-skills/schedule/TOOLS.json +48 -4
- package/src/config/bundled-skills/workflows/SKILL.md +214 -0
- package/src/config/bundled-skills/workflows/TOOLS.json +84 -0
- package/src/config/bundled-skills/workflows/tools/manage-workflows.ts +12 -0
- package/src/config/bundled-skills/workflows/tools/run-workflow.ts +12 -0
- package/src/config/bundled-tool-registry.ts +14 -2
- package/src/config/call-site-defaults.ts +5 -0
- package/src/config/feature-flag-registry.json +12 -4
- package/src/config/llm-context-resolution.ts +8 -0
- package/src/config/llm-resolver.ts +30 -0
- package/src/config/preloaded-apps/personal-page/src/components/About.tsx +22 -0
- package/src/config/preloaded-apps/personal-page/src/components/App.tsx +16 -0
- package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +77 -0
- package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +57 -0
- package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +28 -0
- package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +234 -0
- package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +48 -0
- package/src/config/preloaded-apps/personal-page/src/components/media.ts +16 -0
- package/src/config/preloaded-apps/personal-page/src/index.html +20 -0
- package/src/config/preloaded-apps/personal-page/src/main.tsx +7 -0
- package/src/config/preloaded-apps/personal-page/src/profile-data.ts +82 -0
- package/src/config/preloaded-apps/personal-page/src/styles.css +759 -0
- package/src/config/schema.ts +2 -0
- package/src/config/schemas/call-site-catalog.ts +7 -0
- package/src/config/schemas/heartbeat.ts +4 -1
- package/src/config/schemas/llm.ts +33 -26
- package/src/config/schemas/memory-retrospective.ts +19 -0
- package/src/config/schemas/platform.ts +8 -0
- package/src/config/schemas/services.ts +5 -2
- package/src/config/schemas/workflows.ts +42 -0
- package/src/config/skills.ts +3 -3
- package/src/context/compactor.ts +273 -39
- package/src/context/post-turn-tool-result-truncation.ts +23 -6
- package/src/context/tool-result-spool.ts +12 -17
- package/src/credential-execution/executable-discovery.ts +1 -1
- package/src/credential-execution/process-manager.ts +37 -3
- package/src/credential-execution/prompted-credential.ts +205 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +14 -0
- package/src/daemon/conversation-process.ts +11 -2
- package/src/daemon/conversation-surfaces.ts +167 -3
- package/src/daemon/conversation-tool-setup.ts +103 -26
- package/src/daemon/conversation-usage.ts +2 -0
- package/src/daemon/conversation.ts +115 -11
- package/src/daemon/handlers/shared.ts +26 -14
- package/src/daemon/host-cu-proxy.ts +15 -12
- package/src/daemon/host-file-proxy.ts +15 -12
- package/src/daemon/host-transfer-proxy.ts +30 -24
- package/src/daemon/lifecycle.ts +40 -3
- package/src/daemon/message-protocol.ts +3 -0
- package/src/daemon/message-types/messages.ts +2 -10
- package/src/daemon/message-types/workflows.ts +49 -0
- package/src/daemon/parse-actual-tokens-from-error.test.ts +62 -1
- package/src/daemon/parse-actual-tokens-from-error.ts +43 -4
- package/src/daemon/process-message.ts +6 -0
- package/src/daemon/tool-setup-types.ts +57 -0
- package/src/daemon/wake-conversation-ops.ts +18 -0
- package/src/heartbeat/heartbeat-run-store.ts +8 -2
- package/src/home/feed-types.ts +1 -1
- package/src/ipc/gateway-flag-listener.ts +28 -6
- package/src/mcp/mcp-auth-state.ts +8 -20
- package/src/media/__tests__/image-models.test.ts +57 -0
- package/src/media/image-models.ts +66 -0
- package/src/memory/__tests__/auto-analysis-enqueue.test.ts +38 -0
- package/src/memory/__tests__/find-most-recent-retrospective-for.test.ts +12 -2
- package/src/memory/__tests__/memory-retrospective-job.test.ts +911 -34
- package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +227 -5
- package/src/memory/__tests__/memory-retrospective-state.test.ts +195 -0
- package/src/memory/__tests__/preloaded-apps.test.ts +85 -0
- package/src/memory/auto-analysis-enqueue.ts +14 -1
- package/src/memory/compaction-log-store-clickhouse.ts +6 -4
- package/src/memory/conversation-crud.ts +9 -2
- package/src/memory/conversation-disk-view.ts +1 -1
- package/src/memory/conversation-queries.ts +22 -7
- package/src/memory/db-init.ts +20 -0
- package/src/memory/db-maintenance.ts +16 -0
- package/src/memory/embedding-runtime-manager.ts +1 -1
- package/src/memory/llm-request-log-source-clickhouse.ts +112 -14
- package/src/memory/llm-request-log-source-local.ts +19 -1
- package/src/memory/llm-request-log-source.ts +35 -6
- package/src/memory/llm-request-log-store.ts +90 -2
- package/src/memory/memory-retrospective-constants.ts +9 -0
- package/src/memory/memory-retrospective-enqueue.ts +3 -6
- package/src/memory/memory-retrospective-fork-boundary.ts +94 -0
- package/src/memory/memory-retrospective-job.ts +500 -208
- package/src/memory/memory-retrospective-startup-cleanup.ts +97 -19
- package/src/memory/memory-retrospective-state.ts +85 -2
- package/src/memory/migrations/281-memory-retrospective-remembered-log.ts +40 -0
- package/src/memory/migrations/282-schedule-inference-profile.test.ts +77 -0
- package/src/memory/migrations/282-schedule-inference-profile.ts +26 -0
- package/src/memory/migrations/283-memory-v3-selections-message-id-and-sections.test.ts +102 -0
- package/src/memory/migrations/283-memory-v3-selections-message-id-and-sections.ts +53 -0
- package/src/memory/migrations/284-workflow-runs.ts +51 -0
- package/src/memory/migrations/285-schedule-workflow-mode.ts +26 -0
- package/src/memory/migrations/286-workflow-run-trust.ts +27 -0
- package/src/memory/migrations/287-conversation-origin-channel-index.ts +15 -0
- package/src/memory/migrations/288-backfill-origin-channel-from-bindings.ts +43 -0
- package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +115 -0
- package/src/memory/migrations/290-schedule-capabilities.test.ts +77 -0
- package/src/memory/migrations/290-schedule-capabilities.ts +25 -0
- package/src/memory/migrations/__tests__/281-memory-retrospective-remembered-log.test.ts +96 -0
- package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +571 -0
- package/src/memory/migrations/index.ts +10 -0
- package/src/memory/preloaded-apps.ts +116 -0
- package/src/memory/schema/infrastructure.ts +4 -0
- package/src/memory/schema/memory-core.ts +4 -0
- package/src/memory/v2/__tests__/concept-page-frontmatter-schema.test.ts +45 -0
- package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +11 -7
- package/src/memory/v2/__tests__/page-store.test.ts +13 -2
- package/src/memory/v2/__tests__/qdrant.test.ts +24 -0
- package/src/memory/v2/frontmatter-sweep.ts +7 -6
- package/src/memory/v2/page-store.ts +4 -3
- package/src/memory/v2/qdrant.ts +42 -3
- package/src/memory/v2/types.ts +16 -10
- package/src/messaging/draft-store.ts +1 -1
- package/src/notifications/access-request-copy.ts +200 -113
- package/src/notifications/adapters/slack.ts +250 -111
- package/src/notifications/adapters/telegram.ts +7 -44
- package/src/notifications/approval-card-builder.ts +93 -0
- package/src/notifications/broadcaster.ts +74 -0
- package/src/notifications/conversation-pairing.ts +8 -6
- package/src/notifications/copy-composer.ts +32 -26
- package/src/notifications/decision-engine.ts +59 -7
- package/src/notifications/guardian-question-mode.ts +145 -155
- package/src/notifications/home-feed-side-effect.ts +28 -11
- package/src/notifications/notification-utils.ts +66 -0
- package/src/notifications/signal.ts +6 -0
- package/src/notifications/tool-approval-copy.ts +142 -0
- package/src/notifications/types.ts +19 -0
- package/src/permissions/threshold.ts +11 -0
- package/src/plugin-api/types.ts +16 -4
- package/src/plugins/defaults/compaction/window-manager.ts +44 -0
- package/src/plugins/defaults/index.ts +46 -0
- package/src/plugins/defaults/max-tokens-continue/continue-state-store.ts +53 -0
- package/src/plugins/defaults/max-tokens-continue/hooks/post-model-call.ts +80 -0
- package/src/plugins/defaults/max-tokens-continue/hooks/stop.ts +20 -0
- package/src/plugins/defaults/max-tokens-continue/package.json +14 -0
- package/src/plugins/defaults/memory-retrieval/hooks/__tests__/user-prompt-submit.test.ts +37 -0
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +29 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +8 -3
- package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +4 -2
- package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +28 -18
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +67 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +122 -22
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +2 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +63 -1
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +61 -18
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +1 -1
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +39 -10
- package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +34 -1
- package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +112 -47
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +78 -15
- package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +206 -0
- package/src/plugins/defaults/task-progress-nudge/package.json +15 -0
- package/src/prompts/__tests__/system-prompt.test.ts +100 -1
- package/src/prompts/__tests__/task-progress-hint-section.test.ts +5 -7
- package/src/prompts/normalize-onboarding.ts +2 -0
- package/src/prompts/persona-resolver.ts +3 -0
- package/src/prompts/system-prompt.ts +51 -2
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +3 -1
- package/src/prompts/templates/system-sections.ts +8 -3
- package/src/providers/call-site-routing.ts +6 -3
- package/src/providers/fireworks/client.ts +3 -0
- package/src/providers/inference/auth.ts +52 -46
- package/src/providers/model-intents.ts +2 -2
- package/src/providers/openai/__tests__/coerce-object-args.test.ts +105 -0
- package/src/providers/openai/chat-completions-provider.ts +47 -9
- package/src/providers/openai/coerce-object-args.ts +104 -0
- package/src/providers/retry.ts +8 -5
- package/src/providers/types.ts +10 -0
- package/src/runtime/__tests__/agent-wake.test.ts +629 -7
- package/src/runtime/access-request-helper.ts +16 -9
- package/src/runtime/agent-wake.ts +302 -51
- package/src/runtime/assistant-stream-state.ts +141 -8
- package/src/runtime/background-job-runner.ts +9 -0
- package/src/runtime/channel-approval-types.ts +1 -0
- package/src/runtime/channel-invite-transports/telegram.ts +6 -5
- package/src/runtime/channel-invite-transports/voice.ts +2 -2
- package/src/runtime/channel-invite-types.ts +4 -2
- package/src/runtime/channel-retry-sweep.ts +19 -41
- package/src/runtime/finalize-event-delivery.ts +72 -0
- package/src/runtime/guardian-action-message-composer.ts +0 -54
- package/src/runtime/http-server.ts +6 -14
- package/src/runtime/http-types.ts +0 -1
- package/src/runtime/message-composer-types.ts +0 -9
- package/src/runtime/middleware/__tests__/rate-limiter.test.ts +63 -0
- package/src/runtime/middleware/auth.ts +27 -3
- package/src/runtime/middleware/rate-limiter.ts +28 -1
- package/src/runtime/migrations/vbundle-builder.ts +6 -5
- package/src/runtime/pending-interactions.ts +20 -1
- package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +232 -173
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +18 -0
- package/src/runtime/routes/__tests__/retrospective-routes.test.ts +436 -0
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +11 -0
- package/src/runtime/routes/approval-routes.ts +35 -8
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -1
- package/src/runtime/routes/btw-routes.ts +37 -1
- package/src/runtime/routes/channel-delivery-routes.ts +11 -7
- package/src/runtime/routes/channel-route-definitions.ts +3 -0
- package/src/runtime/routes/channel-route-shared.ts +3 -1
- package/src/runtime/routes/consolidation-routes.ts +17 -13
- package/src/runtime/routes/conversation-compaction-routes.ts +159 -119
- package/src/runtime/routes/conversation-list-routes.ts +41 -4
- package/src/runtime/routes/conversation-query-routes.ts +196 -11
- package/src/runtime/routes/conversation-routes.ts +15 -1
- package/src/runtime/routes/credential-prompt-routes.ts +39 -17
- package/src/runtime/routes/empty-state-greeting-cache.ts +65 -0
- package/src/runtime/routes/heartbeat-routes.ts +18 -13
- package/src/runtime/routes/image-generation-routes.ts +20 -2
- package/src/runtime/routes/inbound-message-handler.ts +13 -12
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +32 -31
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +7 -5
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +7 -21
- package/src/runtime/routes/inbound-stages/escalation-intercept.ts +5 -5
- package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +6 -15
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +1 -1
- package/src/runtime/routes/index.ts +6 -0
- package/src/runtime/routes/log-export/AGENTS.md +1 -1
- package/src/runtime/routes/log-export/workspace-allowlist.ts +1 -1
- package/src/runtime/routes/migration-routes.ts +5 -9
- package/src/runtime/routes/plugins-routes.ts +26 -0
- package/src/runtime/routes/ps-routes.ts +10 -8
- package/src/runtime/routes/retrospective-routes.ts +235 -0
- package/src/runtime/routes/runs-pagination.ts +75 -0
- package/src/runtime/routes/schedule-routes.ts +367 -59
- package/src/runtime/routes/sounds-config-routes.ts +239 -0
- package/src/runtime/routes/surface-action-routes.ts +84 -4
- package/src/runtime/routes/workflow-routes.test.ts +372 -0
- package/src/runtime/routes/workflow-routes.ts +363 -0
- package/src/runtime/routes/workspace-routes.test.ts +61 -1
- package/src/runtime/routes/workspace-routes.ts +26 -1
- package/src/runtime/services/__tests__/analyze-conversation.test.ts +38 -0
- package/src/runtime/services/analyze-conversation.ts +26 -13
- package/src/schedule/inference-profile.ts +28 -0
- package/src/schedule/schedule-store.ts +152 -4
- package/src/schedule/scheduler-types.ts +6 -0
- package/src/schedule/scheduler.ts +96 -0
- package/src/security/secret-allowlist.ts +1 -1
- package/src/skills/path-classifier.ts +1 -1
- package/src/tools/apps/executors.ts +23 -0
- package/src/tools/apps/resolve-app-id.ts +42 -0
- package/src/tools/browser/browser-execution.ts +9 -11
- package/src/tools/credentials/broker.ts +4 -4
- package/src/tools/credentials/vault.ts +26 -137
- package/src/tools/executor.ts +69 -0
- package/src/tools/flag-gated-tools.test.ts +76 -0
- package/src/tools/permission-checker.ts +8 -1
- package/src/tools/registry.ts +51 -0
- package/src/tools/schedule/create.ts +77 -1
- package/src/tools/schedule/list.ts +1 -0
- package/src/tools/schedule/update.ts +73 -1
- package/src/tools/skills/execute.ts +56 -0
- package/src/tools/terminal/shell.ts +1 -1
- package/src/tools/ui-surface/definitions.ts +91 -2
- package/src/tools/workflows/manage-workflows.ts +183 -0
- package/src/tools/workflows/run-workflow.test.ts +442 -0
- package/src/tools/workflows/run-workflow.ts +88 -0
- package/src/types/onboarding-context.ts +2 -0
- package/src/usage/attribution.ts +24 -0
- package/src/util/canonicalize-identity.ts +12 -3
- package/src/util/platform.ts +17 -17
- package/src/watcher/__tests__/engine.test.ts +24 -0
- package/src/watcher/__tests__/telemetry.test.ts +135 -0
- package/src/watcher/engine.ts +7 -0
- package/src/watcher/telemetry.ts +74 -0
- package/src/workflows/capabilities.test.ts +365 -0
- package/src/workflows/capabilities.ts +359 -0
- package/src/workflows/deterministic-stringify.ts +27 -0
- package/src/workflows/engine-integration.test.ts +656 -0
- package/src/workflows/engine.test.ts +1144 -0
- package/src/workflows/engine.ts +1078 -0
- package/src/workflows/fanout-load.test.ts +168 -0
- package/src/workflows/journal-store.test.ts +369 -0
- package/src/workflows/journal-store.ts +470 -0
- package/src/workflows/leaf-runner.test.ts +704 -0
- package/src/workflows/leaf-runner.ts +589 -0
- package/src/workflows/library.test.ts +134 -0
- package/src/workflows/library.ts +124 -0
- package/src/workflows/run-manager.test.ts +711 -0
- package/src/workflows/run-manager.ts +593 -0
- package/src/workflows/sandbox-escape.test.ts +339 -0
- package/src/workflows/sandbox.test.ts +251 -0
- package/src/workflows/sandbox.ts +447 -0
- package/src/workspace/adaptive-thinking-repair.ts +33 -11
- package/src/workspace/migrations/021-move-signals-to-workspace.ts +1 -1
- package/src/workspace/migrations/022-move-hooks-to-workspace.ts +1 -1
- package/src/workspace/migrations/026-backfill-install-meta.ts +1 -1
- package/src/workspace/migrations/030-seed-pkb-autoinject.ts +2 -1
- package/src/workspace/migrations/031-drop-user-md.ts +1 -4
- package/src/workspace/migrations/048-remove-workspace-hooks.ts +1 -1
- package/src/workspace/migrations/056-release-notes-inference-profile-reordering.ts +5 -2
- package/src/workspace/migrations/061-move-backup-key-to-workspace.ts +1 -1
- package/src/workspace/migrations/082-backfill-managed-profile-labels.ts +8 -2
- package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +41 -14
- package/src/workspace/migrations/102-preserve-heartbeat-enabled-for-existing-workspaces.ts +69 -0
- package/src/workspace/migrations/103-upgrade-quality-profile-to-opus-4-8.ts +83 -0
- package/src/workspace/migrations/104-recheck-adaptive-thinking-model-implied-anthropic.ts +133 -0
- package/src/workspace/migrations/registry.ts +6 -0
- package/src/workspace/migrations/runner.ts +1 -1
- package/tsconfig.json +1 -1
- package/src/__tests__/guardian-action-copy-generator.test.ts +0 -200
- package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -579
- package/src/__tests__/guardian-action-store.test.ts +0 -106
- package/src/daemon/guardian-action-generators.ts +0 -71
- package/src/memory/guardian-action-store.ts +0 -484
- package/src/runtime/guardian-action-grant-minter.ts +0 -150
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-end engine integration tests.
|
|
3
|
+
*
|
|
4
|
+
* Unlike `engine.test.ts` — which injects a FAKE leaf runner — this suite wires
|
|
5
|
+
* the REAL pieces together: the real QuickJS sandbox, the real {@link runLeaf}
|
|
6
|
+
* leaf runner, the real {@link AgentLoop}, the real journal store, and a real
|
|
7
|
+
* temp DB. Only the LLM provider is mocked, so leaves complete without a
|
|
8
|
+
* network call. This catches integration bugs the fake-runner tests miss:
|
|
9
|
+
* sandbox↔host JSON marshaling, the leaf runner's real agent-loop path, and
|
|
10
|
+
* journal DB round-trips on resume.
|
|
11
|
+
*
|
|
12
|
+
* Most leaves run the REAL **tool path**: the engine forwards `capabilities.tools`
|
|
13
|
+
* to tool leaves, the leaf runner spins up an `AgentLoop`, and the mocked
|
|
14
|
+
* provider returns a single `end_turn` message whose text echoes the prompt
|
|
15
|
+
* (so each leaf's `output` is distinguishable and order-checkable). A leaf that
|
|
16
|
+
* declares a `schema` routes to the leaf runner's SCHEMA path; the engine omits
|
|
17
|
+
* `tools` for it (a schema + tools is a hard error in `runLeaf`), so the schema
|
|
18
|
+
* path works even when the run carries the read-only baseline. A sandboxed
|
|
19
|
+
* script passes a JSON Schema object (it can't hold a host-side Zod object),
|
|
20
|
+
* which the leaf runner duck-types — see the schema-path test below.
|
|
21
|
+
*
|
|
22
|
+
* Covered:
|
|
23
|
+
* - `map`/`parallel` fan-out: each leaf returns its echoed output, aggregated
|
|
24
|
+
* in spec-array order through the real sandbox + leaf runner + agent loop.
|
|
25
|
+
* - Schema path: a script-provided JSON Schema drives the forced-tool call and
|
|
26
|
+
* its validated structured object is returned and journaled.
|
|
27
|
+
* - In-process resume: re-running the SAME runId against the SAME DB replays
|
|
28
|
+
* the unchanged prefix from the journal — ZERO duplicate provider calls —
|
|
29
|
+
* and only the changed/new tail re-runs the provider.
|
|
30
|
+
* - Resume journal rewrite: a changed-input leaf re-runs and its journal row is
|
|
31
|
+
* upserted (new hash + result) rather than left stale.
|
|
32
|
+
* - Cap abort: a low `maxAgentsPerRun` aborts with status `cap_exceeded` and
|
|
33
|
+
* the partial counters are persisted on the run row.
|
|
34
|
+
* - Concurrency high-water: the provider mock records in-flight concurrency
|
|
35
|
+
* and never exceeds `maxConcurrentLeaves`.
|
|
36
|
+
* - Hash stability: the same script run twice journals identical
|
|
37
|
+
* `(seq, call_hash)` sequences.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
41
|
+
|
|
42
|
+
import { makeMockLogger } from "../__tests__/helpers/mock-logger.js";
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Mocks — defined before importing the module under test.
|
|
46
|
+
//
|
|
47
|
+
// We mock ONLY the LLM provider (no network) and silence the logger. Every
|
|
48
|
+
// other surface — sandbox, leaf runner, agent loop, journal store,
|
|
49
|
+
// capabilities, config — is the real implementation.
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
mock.module("../util/logger.js", () => ({
|
|
53
|
+
getLogger: () => makeMockLogger(),
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
interface SendOptions {
|
|
57
|
+
signal?: AbortSignal;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Live in-flight provider calls, to assert the concurrency high-water mark. */
|
|
61
|
+
let inFlight = 0;
|
|
62
|
+
let maxInFlight = 0;
|
|
63
|
+
/** Total provider sendMessage invocations (the resume duplicate-call check). */
|
|
64
|
+
let sendCallCount = 0;
|
|
65
|
+
/** Per-call delay (ms) so concurrent fan-out actually overlaps in time. */
|
|
66
|
+
let sendDelayMs = 0;
|
|
67
|
+
|
|
68
|
+
/** Extract the latest user prompt text from a provider message array. */
|
|
69
|
+
function lastUserPrompt(
|
|
70
|
+
messages: Array<{
|
|
71
|
+
role: string;
|
|
72
|
+
content: Array<{ type: string; text?: string }>;
|
|
73
|
+
}>,
|
|
74
|
+
): string {
|
|
75
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
76
|
+
const m = messages[i];
|
|
77
|
+
if (m?.role !== "user") continue;
|
|
78
|
+
return m.content
|
|
79
|
+
.filter((b) => b.type === "text")
|
|
80
|
+
.map((b) => b.text ?? "")
|
|
81
|
+
.join("");
|
|
82
|
+
}
|
|
83
|
+
return "";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface SendOptionsFull extends SendOptions {
|
|
87
|
+
config?: { tool_choice?: { type: string; name: string } };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The leaf's real `AgentLoop` (tool path) calls the provider; we return a single
|
|
92
|
+
* `end_turn` message whose text echoes the prompt, so the loop terminates in one
|
|
93
|
+
* turn and the leaf's `output` is `processed:<prompt>`.
|
|
94
|
+
*
|
|
95
|
+
* The SCHEMA path forces a `tool_choice` on the synthetic `emit_result` tool;
|
|
96
|
+
* we detect that and instead return a `tool_use` block whose input echoes the
|
|
97
|
+
* prompt under the schema's `answer` field, so the leaf's structured `output`
|
|
98
|
+
* is `{ answer: "processed:<prompt>" }`.
|
|
99
|
+
*/
|
|
100
|
+
const sendMessage = mock(
|
|
101
|
+
async (
|
|
102
|
+
messages: Array<{
|
|
103
|
+
role: string;
|
|
104
|
+
content: Array<{ type: string; text?: string }>;
|
|
105
|
+
}>,
|
|
106
|
+
options: SendOptionsFull,
|
|
107
|
+
): Promise<{
|
|
108
|
+
content: Array<{
|
|
109
|
+
type: string;
|
|
110
|
+
text?: string;
|
|
111
|
+
name?: string;
|
|
112
|
+
input?: unknown;
|
|
113
|
+
}>;
|
|
114
|
+
model: string;
|
|
115
|
+
usage: { inputTokens: number; outputTokens: number };
|
|
116
|
+
stopReason: string;
|
|
117
|
+
}> => {
|
|
118
|
+
sendCallCount += 1;
|
|
119
|
+
inFlight += 1;
|
|
120
|
+
maxInFlight = Math.max(maxInFlight, inFlight);
|
|
121
|
+
try {
|
|
122
|
+
if (sendDelayMs > 0) {
|
|
123
|
+
await new Promise((resolve) => setTimeout(resolve, sendDelayMs));
|
|
124
|
+
}
|
|
125
|
+
if (options.signal?.aborted) {
|
|
126
|
+
throw new DOMException("Aborted", "AbortError");
|
|
127
|
+
}
|
|
128
|
+
const forcedTool = options.config?.tool_choice;
|
|
129
|
+
if (forcedTool?.type === "tool") {
|
|
130
|
+
return {
|
|
131
|
+
content: [
|
|
132
|
+
{
|
|
133
|
+
type: "tool_use",
|
|
134
|
+
name: forcedTool.name,
|
|
135
|
+
input: { answer: `processed:${lastUserPrompt(messages)}` },
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
model: "test-model",
|
|
139
|
+
usage: { inputTokens: 7, outputTokens: 3 },
|
|
140
|
+
stopReason: "tool_use",
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
content: [
|
|
145
|
+
{ type: "text", text: `processed:${lastUserPrompt(messages)}` },
|
|
146
|
+
],
|
|
147
|
+
model: "test-model",
|
|
148
|
+
usage: { inputTokens: 7, outputTokens: 3 },
|
|
149
|
+
stopReason: "end_turn",
|
|
150
|
+
};
|
|
151
|
+
} finally {
|
|
152
|
+
inFlight -= 1;
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const getConfiguredProvider = mock(async () => ({
|
|
158
|
+
name: "test-provider",
|
|
159
|
+
sendMessage,
|
|
160
|
+
}));
|
|
161
|
+
|
|
162
|
+
mock.module("../providers/provider-send-message.js", () => ({
|
|
163
|
+
getConfiguredProvider,
|
|
164
|
+
// Re-implement the trivial helpers the leaf runner imports from this module.
|
|
165
|
+
extractToolUse: (response: { content: Array<{ type: string }> }) =>
|
|
166
|
+
response.content.find((b) => b.type === "tool_use"),
|
|
167
|
+
}));
|
|
168
|
+
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
// Module under test (after mocks).
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
import { WorkflowsConfigSchema } from "../config/schemas/workflows.js";
|
|
174
|
+
import type { TrustContext } from "../daemon/trust-context.js";
|
|
175
|
+
import { getSqlite } from "../memory/db-connection.js";
|
|
176
|
+
import { initializeDb } from "../memory/db-init.js";
|
|
177
|
+
import { registerTool } from "../tools/registry.js";
|
|
178
|
+
import type { ToolContext, ToolExecutionResult } from "../tools/types.js";
|
|
179
|
+
import { resolveCapabilities } from "./capabilities.js";
|
|
180
|
+
import { executeWorkflow } from "./engine.js";
|
|
181
|
+
import * as journalStore from "./journal-store.js";
|
|
182
|
+
import {
|
|
183
|
+
getJournal,
|
|
184
|
+
getRun,
|
|
185
|
+
markRunningAsInterrupted,
|
|
186
|
+
} from "./journal-store.js";
|
|
187
|
+
import { runLeaf } from "./leaf-runner.js";
|
|
188
|
+
|
|
189
|
+
initializeDb();
|
|
190
|
+
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// Shared fixtures
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
const trustContext: TrustContext = {
|
|
196
|
+
sourceChannel: "vellum",
|
|
197
|
+
trustClass: "guardian",
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* A real, no-op registry tool the manifest declares so leaves take the leaf
|
|
202
|
+
* runner's REAL tool path (an `AgentLoop` over the provider). The model never
|
|
203
|
+
* calls it — the provider mock returns final text on the first turn — but its
|
|
204
|
+
* presence in `capabilities.tools` is what routes the leaf to the agent loop.
|
|
205
|
+
*/
|
|
206
|
+
const ECHO_TOOL_NAME = "wf_integration_echo";
|
|
207
|
+
registerTool({
|
|
208
|
+
name: ECHO_TOOL_NAME,
|
|
209
|
+
description: "Integration-test no-op tool.",
|
|
210
|
+
category: "test",
|
|
211
|
+
defaultRiskLevel: "low" as never,
|
|
212
|
+
executionTarget: "sandbox",
|
|
213
|
+
input_schema: { type: "object", properties: {}, required: [] },
|
|
214
|
+
async execute(
|
|
215
|
+
_input: Record<string, unknown>,
|
|
216
|
+
_ctx: ToolContext,
|
|
217
|
+
): Promise<ToolExecutionResult> {
|
|
218
|
+
return { content: "noop", isError: false };
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const capabilities = resolveCapabilities({
|
|
223
|
+
tools: [ECHO_TOOL_NAME],
|
|
224
|
+
hostFunctions: [],
|
|
225
|
+
persona: false,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
/** Build a `WorkflowsConfig` from schema defaults with optional overrides. */
|
|
229
|
+
function makeConfig(
|
|
230
|
+
overrides: Partial<{
|
|
231
|
+
maxAgentsPerRun: number;
|
|
232
|
+
maxConcurrentLeaves: number;
|
|
233
|
+
}> = {},
|
|
234
|
+
) {
|
|
235
|
+
return WorkflowsConfigSchema.parse(overrides);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Base options shared by every `executeWorkflow` call: the REAL journal store,
|
|
240
|
+
* the REAL leaf runner, real capabilities, real trust context.
|
|
241
|
+
*/
|
|
242
|
+
function baseOptions(
|
|
243
|
+
runId: string,
|
|
244
|
+
scriptSource: string,
|
|
245
|
+
caps: typeof capabilities = capabilities,
|
|
246
|
+
) {
|
|
247
|
+
return {
|
|
248
|
+
runId,
|
|
249
|
+
scriptSource,
|
|
250
|
+
args: {},
|
|
251
|
+
capabilities: caps,
|
|
252
|
+
journal: journalStore,
|
|
253
|
+
leafRunner: runLeaf,
|
|
254
|
+
trustContext,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function resetTables(): void {
|
|
259
|
+
getSqlite().exec("DELETE FROM workflow_journal");
|
|
260
|
+
getSqlite().exec("DELETE FROM workflow_runs");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
beforeEach(() => {
|
|
264
|
+
resetTables();
|
|
265
|
+
sendMessage.mockClear();
|
|
266
|
+
getConfiguredProvider.mockClear();
|
|
267
|
+
inFlight = 0;
|
|
268
|
+
maxInFlight = 0;
|
|
269
|
+
sendCallCount = 0;
|
|
270
|
+
sendDelayMs = 0;
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
afterAll(() => {
|
|
274
|
+
resetTables();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
// Tests
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
|
|
281
|
+
describe("workflow engine integration (real sandbox + leaf runner + agent loop + journal)", () => {
|
|
282
|
+
test("map/parallel fan-out: each leaf returns its echoed output, in order", async () => {
|
|
283
|
+
// The script is SYNCHRONOUS from its own view (asyncify) — no `await`.
|
|
284
|
+
const scriptSource = `
|
|
285
|
+
export const meta = { name: "fanout", description: "map over items" };
|
|
286
|
+
const items = args.items;
|
|
287
|
+
const results = map(items, (it) => leaf("item-" + it, { label: "leaf-" + it }));
|
|
288
|
+
return results;
|
|
289
|
+
`;
|
|
290
|
+
const result = await executeWorkflow({
|
|
291
|
+
...baseOptions("wf-fanout", scriptSource),
|
|
292
|
+
args: { items: ["a", "b", "c"] },
|
|
293
|
+
config: makeConfig(),
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
expect(result.status).toBe("completed");
|
|
297
|
+
// Each leaf's output is its echoed final text, in spec-array order.
|
|
298
|
+
expect(result.result).toEqual([
|
|
299
|
+
"processed:item-a",
|
|
300
|
+
"processed:item-b",
|
|
301
|
+
"processed:item-c",
|
|
302
|
+
]);
|
|
303
|
+
expect(result.agentsSpawned).toBe(3);
|
|
304
|
+
// Real provider mock was hit once per leaf through the real agent loop.
|
|
305
|
+
expect(sendCallCount).toBe(3);
|
|
306
|
+
// Usage flowed back from the provider through the leaf runner and engine.
|
|
307
|
+
expect(result.inputTokens).toBe(21);
|
|
308
|
+
expect(result.outputTokens).toBe(9);
|
|
309
|
+
|
|
310
|
+
// The journal persisted one completed agent entry per leaf, in seq order.
|
|
311
|
+
const journal = getJournal("wf-fanout");
|
|
312
|
+
expect(journal.map((e) => e.seq)).toEqual([0, 1, 2]);
|
|
313
|
+
expect(journal.every((e) => e.status === "completed")).toBe(true);
|
|
314
|
+
expect(journal.map((e) => e.result)).toEqual([
|
|
315
|
+
"processed:item-a",
|
|
316
|
+
"processed:item-b",
|
|
317
|
+
"processed:item-c",
|
|
318
|
+
]);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test("in-process resume: unchanged prefix replays from journal with ZERO duplicate provider calls", async () => {
|
|
322
|
+
// A script that journals N sequential agent() calls. `args.tail` is the
|
|
323
|
+
// text of the FINAL call; changing it between runs leaves the prefix
|
|
324
|
+
// unchanged (same prompt → same call_hash) and changes only the last seq.
|
|
325
|
+
const scriptSource = `
|
|
326
|
+
export const meta = { name: "resume", description: "sequential agents" };
|
|
327
|
+
const a = agent("step-1");
|
|
328
|
+
const b = agent("step-2");
|
|
329
|
+
const c = agent(args.tail);
|
|
330
|
+
return [a, b, c];
|
|
331
|
+
`;
|
|
332
|
+
// First run: 3 fresh leaves → 3 provider calls.
|
|
333
|
+
const first = await executeWorkflow({
|
|
334
|
+
...baseOptions("wf-resume", scriptSource),
|
|
335
|
+
args: { tail: "step-3" },
|
|
336
|
+
config: makeConfig(),
|
|
337
|
+
});
|
|
338
|
+
expect(first.status).toBe("completed");
|
|
339
|
+
expect(sendCallCount).toBe(3);
|
|
340
|
+
const firstJournal = getJournal("wf-resume");
|
|
341
|
+
expect(firstJournal.map((e) => e.seq)).toEqual([0, 1, 2]);
|
|
342
|
+
|
|
343
|
+
// Reset the in-test provider counter so we count ONLY the resume's calls.
|
|
344
|
+
sendCallCount = 0;
|
|
345
|
+
sendMessage.mockClear();
|
|
346
|
+
|
|
347
|
+
// Second run: SAME runId, SAME DB, but the last call's prompt changes.
|
|
348
|
+
// seq 0 and 1 are unchanged (hash match) and MUST replay from the journal
|
|
349
|
+
// without touching the provider; seq 2's hash changes and re-runs.
|
|
350
|
+
const second = await executeWorkflow({
|
|
351
|
+
...baseOptions("wf-resume", scriptSource),
|
|
352
|
+
args: { tail: "step-3-CHANGED" },
|
|
353
|
+
config: makeConfig(),
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
expect(second.status).toBe("completed");
|
|
357
|
+
// THE HEADLINE ACCEPTANCE CRITERION: exactly ONE provider call — only the
|
|
358
|
+
// changed tail. Zero duplicate provider calls for the unchanged prefix,
|
|
359
|
+
// verified THROUGH the real sandbox + leaf runner + agent loop + journal
|
|
360
|
+
// stack (not a fake runner).
|
|
361
|
+
expect(sendCallCount).toBe(1);
|
|
362
|
+
// The engine returns the correct values: the prefix replayed from the
|
|
363
|
+
// journal, the changed tail re-ran and produced its new output.
|
|
364
|
+
expect(second.result).toEqual([
|
|
365
|
+
"processed:step-1",
|
|
366
|
+
"processed:step-2",
|
|
367
|
+
"processed:step-3-CHANGED",
|
|
368
|
+
]);
|
|
369
|
+
|
|
370
|
+
// The prefix journal entries are untouched (same call_hash as the first
|
|
371
|
+
// run) and there is still exactly one entry per seq.
|
|
372
|
+
const secondJournal = getJournal("wf-resume");
|
|
373
|
+
expect(secondJournal.map((e) => e.seq)).toEqual([0, 1, 2]);
|
|
374
|
+
expect(secondJournal[0]!.callHash).toBe(firstJournal[0]!.callHash);
|
|
375
|
+
expect(secondJournal[1]!.callHash).toBe(firstJournal[1]!.callHash);
|
|
376
|
+
// NOTE: seq 2's journal row is NOT rewritten on resume — see the
|
|
377
|
+
// `resume rewrites a changed leaf's journal entry` test.todo below for the
|
|
378
|
+
// known `INSERT OR IGNORE` staleness bug. The engine's returned value
|
|
379
|
+
// (asserted above) is correct; only the persisted journal row is stale.
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
test("restart→reconcile→resume: an interrupted run replays its prefix and carries accounting", async () => {
|
|
383
|
+
// Stage 1 — a first pass that journals a 2-leaf prefix, mirroring a run that
|
|
384
|
+
// got partway before the process died.
|
|
385
|
+
const scriptSource = `
|
|
386
|
+
export const meta = { name: "reconcile-resume", description: "sequential agents" };
|
|
387
|
+
const a = agent("step-1");
|
|
388
|
+
const b = agent("step-2");
|
|
389
|
+
const c = agent(args.tail);
|
|
390
|
+
return [a, b, c];
|
|
391
|
+
`;
|
|
392
|
+
const first = await executeWorkflow({
|
|
393
|
+
...baseOptions("wf-reconcile", scriptSource),
|
|
394
|
+
args: { tail: "step-3" },
|
|
395
|
+
config: makeConfig(),
|
|
396
|
+
});
|
|
397
|
+
expect(first.status).toBe("completed");
|
|
398
|
+
expect(first.agentsSpawned).toBe(3);
|
|
399
|
+
const agentsBefore = getRun("wf-reconcile")!.agentsSpawned;
|
|
400
|
+
expect(agentsBefore).toBe(3);
|
|
401
|
+
|
|
402
|
+
// Simulate a crash: force the persisted row back to `running` (the state a
|
|
403
|
+
// mid-flight run leaves on disk), then run startup reconciliation.
|
|
404
|
+
journalStore.updateRun("wf-reconcile", { status: "running" });
|
|
405
|
+
const reconciled = markRunningAsInterrupted();
|
|
406
|
+
expect(reconciled).toBeGreaterThanOrEqual(1);
|
|
407
|
+
const interrupted = getRun("wf-reconcile")!;
|
|
408
|
+
expect(interrupted.status).toBe("interrupted");
|
|
409
|
+
// Reconciliation is STATUS ONLY — the accounting total is untouched.
|
|
410
|
+
expect(interrupted.agentsSpawned).toBe(agentsBefore);
|
|
411
|
+
|
|
412
|
+
sendCallCount = 0;
|
|
413
|
+
sendMessage.mockClear();
|
|
414
|
+
|
|
415
|
+
// Stage 2 — resume re-invokes the engine with the SAME runId. The unchanged
|
|
416
|
+
// prefix (seq 0,1) replays from the journal with ZERO provider calls; only
|
|
417
|
+
// the changed tail re-runs. Accounting SEEDS from the persisted total (3)
|
|
418
|
+
// and grows — it does not reset to 0.
|
|
419
|
+
const resumed = await executeWorkflow({
|
|
420
|
+
...baseOptions("wf-reconcile", scriptSource),
|
|
421
|
+
args: { tail: "step-3-CHANGED" },
|
|
422
|
+
config: makeConfig(),
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
expect(resumed.status).toBe("completed");
|
|
426
|
+
// Only the changed tail hit the provider — the prefix replayed.
|
|
427
|
+
expect(sendCallCount).toBe(1);
|
|
428
|
+
expect(resumed.result).toEqual([
|
|
429
|
+
"processed:step-1",
|
|
430
|
+
"processed:step-2",
|
|
431
|
+
"processed:step-3-CHANGED",
|
|
432
|
+
]);
|
|
433
|
+
// Accounting carried: seeded from 3, the single re-run tail makes it 4.
|
|
434
|
+
expect(resumed.agentsSpawned).toBe(4);
|
|
435
|
+
expect(getRun("wf-reconcile")!.agentsSpawned).toBe(4);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
test("cap abort: a low maxAgentsPerRun yields cap_exceeded with partials persisted", async () => {
|
|
439
|
+
const scriptSource = `
|
|
440
|
+
export const meta = { name: "cap", description: "more leaves than the cap" };
|
|
441
|
+
const a = agent("c-1");
|
|
442
|
+
const b = agent("c-2");
|
|
443
|
+
const c = agent("c-3");
|
|
444
|
+
return [a, b, c];
|
|
445
|
+
`;
|
|
446
|
+
const result = await executeWorkflow({
|
|
447
|
+
...baseOptions("wf-cap", scriptSource),
|
|
448
|
+
// Cap at 2 — the third sequential agent() trips the cap before launching.
|
|
449
|
+
config: makeConfig({ maxAgentsPerRun: 2 }),
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
expect(result.status).toBe("cap_exceeded");
|
|
453
|
+
expect(result.result).toBeNull();
|
|
454
|
+
// Exactly 2 leaves ran before the cap tripped on the 3rd.
|
|
455
|
+
expect(result.agentsSpawned).toBe(2);
|
|
456
|
+
expect(sendCallCount).toBe(2);
|
|
457
|
+
|
|
458
|
+
// Partial counters and terminal status are persisted on the run row.
|
|
459
|
+
const run = getRun("wf-cap");
|
|
460
|
+
expect(run?.status).toBe("cap_exceeded");
|
|
461
|
+
expect(run?.agentsSpawned).toBe(2);
|
|
462
|
+
expect(run?.error).toContain("Agent cap of 2 exceeded");
|
|
463
|
+
// Two completed journal entries were persisted before the abort.
|
|
464
|
+
expect(getJournal("wf-cap").length).toBe(2);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
test("concurrency high-water: never exceeds maxConcurrentLeaves", async () => {
|
|
468
|
+
// Delay each provider call so a wide fan-out genuinely overlaps; the mock
|
|
469
|
+
// tracks the live in-flight count.
|
|
470
|
+
sendDelayMs = 15;
|
|
471
|
+
const scriptSource = `
|
|
472
|
+
export const meta = { name: "concurrency", description: "wide parallel fan-out" };
|
|
473
|
+
const items = args.items;
|
|
474
|
+
const results = parallel(items.map((it) => leaf("p-" + it)));
|
|
475
|
+
return results;
|
|
476
|
+
`;
|
|
477
|
+
const items = Array.from({ length: 12 }, (_, i) => i);
|
|
478
|
+
const result = await executeWorkflow({
|
|
479
|
+
...baseOptions("wf-conc", scriptSource),
|
|
480
|
+
args: { items },
|
|
481
|
+
config: makeConfig({ maxConcurrentLeaves: 3 }),
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
expect(result.status).toBe("completed");
|
|
485
|
+
expect(result.agentsSpawned).toBe(12);
|
|
486
|
+
expect(sendCallCount).toBe(12);
|
|
487
|
+
// The engine's worker pool capped live concurrency at the configured limit.
|
|
488
|
+
expect(maxInFlight).toBeGreaterThan(1);
|
|
489
|
+
expect(maxInFlight).toBeLessThanOrEqual(3);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
test("hash stability: the same script run twice journals identical (seq, call_hash) sequences", async () => {
|
|
493
|
+
const scriptSource = `
|
|
494
|
+
export const meta = { name: "stable", description: "deterministic hashes" };
|
|
495
|
+
const items = args.items;
|
|
496
|
+
const results = map(items, (it) => leaf("h-" + it));
|
|
497
|
+
const tail = agent("final");
|
|
498
|
+
return { results, tail };
|
|
499
|
+
`;
|
|
500
|
+
const args = { items: ["x", "y", "z"] };
|
|
501
|
+
|
|
502
|
+
await executeWorkflow({
|
|
503
|
+
...baseOptions("wf-hash-1", scriptSource),
|
|
504
|
+
args,
|
|
505
|
+
config: makeConfig(),
|
|
506
|
+
});
|
|
507
|
+
await executeWorkflow({
|
|
508
|
+
...baseOptions("wf-hash-2", scriptSource),
|
|
509
|
+
args,
|
|
510
|
+
config: makeConfig(),
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
const seqHashes = (runId: string) =>
|
|
514
|
+
getJournal(runId).map((e) => [e.seq, e.callHash] as const);
|
|
515
|
+
|
|
516
|
+
const a = seqHashes("wf-hash-1");
|
|
517
|
+
const b = seqHashes("wf-hash-2");
|
|
518
|
+
expect(a.length).toBe(4);
|
|
519
|
+
expect(a).toEqual(b);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
// ---------------------------------------------------------------------------
|
|
523
|
+
// FIXED ENGINE↔LEAF-RUNNER GAP (schema-path marshaling + tool forwarding).
|
|
524
|
+
//
|
|
525
|
+
// A workflow script declares a per-leaf output `schema` via
|
|
526
|
+
// `leaf(prompt, { schema })`. That schema crosses the QuickJS sandbox boundary
|
|
527
|
+
// as a JSON-marshaled plain object (a JSON Schema). The leaf runner's schema
|
|
528
|
+
// path now duck-types its input: a plain JSON Schema object is used directly as
|
|
529
|
+
// the forced-tool `input_schema` and validated via `z.fromJSONSchema`, while a
|
|
530
|
+
// host-side Zod schema keeps the original behavior.
|
|
531
|
+
//
|
|
532
|
+
// This run uses the FULL capability set (non-empty `capabilities.tools`, which
|
|
533
|
+
// every real run carries via the read-only baseline). A schema leaf must NOT
|
|
534
|
+
// receive those tools — `runLeaf` hard-errors on `schema` + non-empty `tools`.
|
|
535
|
+
// The engine now omits `tools` for schema leaves, so the structured-output path
|
|
536
|
+
// works end-to-end even when the run carries baseline tools (the real case).
|
|
537
|
+
// ---------------------------------------------------------------------------
|
|
538
|
+
test("schema-path leaves run with the read-only baseline present (tools not forwarded)", async () => {
|
|
539
|
+
// The script passes a JSON Schema OBJECT LITERAL (the only shape a sandbox
|
|
540
|
+
// script can produce — Zod is host-side). It marshals across the boundary
|
|
541
|
+
// and reaches the leaf runner verbatim.
|
|
542
|
+
const scriptSource = `
|
|
543
|
+
export const meta = { name: "schema", description: "structured leaf output" };
|
|
544
|
+
const schema = {
|
|
545
|
+
type: "object",
|
|
546
|
+
properties: { answer: { type: "string" } },
|
|
547
|
+
required: ["answer"],
|
|
548
|
+
additionalProperties: false,
|
|
549
|
+
};
|
|
550
|
+
const items = args.items;
|
|
551
|
+
const results = map(items, (it) => leaf("item-" + it, { schema }));
|
|
552
|
+
return results;
|
|
553
|
+
`;
|
|
554
|
+
// `capabilities` carries a non-empty toolset (the echo tool); a real run's
|
|
555
|
+
// resolved baseline is likewise non-empty. The schema leaf still succeeds,
|
|
556
|
+
// proving the engine omits `tools` on the schema path.
|
|
557
|
+
expect(capabilities.tools.length).toBeGreaterThan(0);
|
|
558
|
+
const result = await executeWorkflow({
|
|
559
|
+
...baseOptions("wf-schema", scriptSource, capabilities),
|
|
560
|
+
args: { items: ["a", "b"] },
|
|
561
|
+
config: makeConfig(),
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
expect(result.status).toBe("completed");
|
|
565
|
+
// Each leaf returned the validated structured object (forced-tool path),
|
|
566
|
+
// NOT the tool-path's plain echoed text.
|
|
567
|
+
expect(result.result).toEqual([
|
|
568
|
+
{ answer: "processed:item-a" },
|
|
569
|
+
{ answer: "processed:item-b" },
|
|
570
|
+
]);
|
|
571
|
+
expect(result.agentsSpawned).toBe(2);
|
|
572
|
+
expect(sendCallCount).toBe(2);
|
|
573
|
+
|
|
574
|
+
// The journal persisted the structured outputs.
|
|
575
|
+
const journal = getJournal("wf-schema");
|
|
576
|
+
expect(journal.map((e) => e.result)).toEqual([
|
|
577
|
+
{ answer: "processed:item-a" },
|
|
578
|
+
{ answer: "processed:item-b" },
|
|
579
|
+
]);
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
// ---------------------------------------------------------------------------
|
|
583
|
+
// PERSONA MANIFEST GATE (consent-gated, like tools).
|
|
584
|
+
//
|
|
585
|
+
// A leaf may request `persona: true` only if the run's capability manifest
|
|
586
|
+
// declared `persona`. An undeclared request fails the run loudly through the
|
|
587
|
+
// real engine + sandbox stack — it must NOT silently downgrade to anonymous.
|
|
588
|
+
// ---------------------------------------------------------------------------
|
|
589
|
+
test("a persona leaf in a run WITHOUT declared persona fails the run", async () => {
|
|
590
|
+
const scriptSource = `
|
|
591
|
+
export const meta = { name: "persona-denied", description: "undeclared persona" };
|
|
592
|
+
return agent("draft in voice", { persona: true });
|
|
593
|
+
`;
|
|
594
|
+
// `capabilities` (the shared fixture) declares persona:false.
|
|
595
|
+
expect(capabilities.persona).toBe(false);
|
|
596
|
+
const result = await executeWorkflow({
|
|
597
|
+
...baseOptions("wf-persona-denied", scriptSource),
|
|
598
|
+
config: makeConfig(),
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
expect(result.status).toBe("failed");
|
|
602
|
+
// The gate trips before any provider call — nothing was spawned.
|
|
603
|
+
expect(sendCallCount).toBe(0);
|
|
604
|
+
expect(result.agentsSpawned).toBe(0);
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
// ---------------------------------------------------------------------------
|
|
608
|
+
// FIXED RESUME JOURNAL-STALENESS GAP.
|
|
609
|
+
//
|
|
610
|
+
// On resume, a leaf whose input CHANGED (new `call_hash`) re-runs and the
|
|
611
|
+
// engine returns the new value. `appendJournalEntry` now UPSERTs on
|
|
612
|
+
// `(run_id, seq)`, so the stale first-run row is rewritten with the new hash
|
|
613
|
+
// and result — the persisted journal agrees with the engine's returned value.
|
|
614
|
+
// ---------------------------------------------------------------------------
|
|
615
|
+
test("resume rewrites a changed leaf's journal entry (hash + result) instead of keeping the stale row", async () => {
|
|
616
|
+
const scriptSource = `
|
|
617
|
+
export const meta = { name: "resume-rewrite", description: "sequential agents" };
|
|
618
|
+
const a = agent("step-1");
|
|
619
|
+
const b = agent(args.tail);
|
|
620
|
+
return [a, b];
|
|
621
|
+
`;
|
|
622
|
+
// First run: 2 fresh leaves.
|
|
623
|
+
await executeWorkflow({
|
|
624
|
+
...baseOptions("wf-resume-rewrite", scriptSource),
|
|
625
|
+
args: { tail: "step-2" },
|
|
626
|
+
config: makeConfig(),
|
|
627
|
+
});
|
|
628
|
+
const firstJournal = getJournal("wf-resume-rewrite");
|
|
629
|
+
expect(firstJournal.map((e) => e.seq)).toEqual([0, 1]);
|
|
630
|
+
const seq1HashBefore = firstJournal[1]!.callHash;
|
|
631
|
+
expect(firstJournal[1]!.result).toBe("processed:step-2");
|
|
632
|
+
|
|
633
|
+
sendCallCount = 0;
|
|
634
|
+
sendMessage.mockClear();
|
|
635
|
+
|
|
636
|
+
// Resume: seq 1's input changes, so it re-runs; seq 0 replays from journal.
|
|
637
|
+
const second = await executeWorkflow({
|
|
638
|
+
...baseOptions("wf-resume-rewrite", scriptSource),
|
|
639
|
+
args: { tail: "step-2-CHANGED" },
|
|
640
|
+
config: makeConfig(),
|
|
641
|
+
});
|
|
642
|
+
expect(second.status).toBe("completed");
|
|
643
|
+
expect(sendCallCount).toBe(1);
|
|
644
|
+
|
|
645
|
+
// The persisted journal row for seq 1 was REWRITTEN (new hash + result),
|
|
646
|
+
// agreeing with the value the engine returned — not the stale first-run row.
|
|
647
|
+
const secondJournal = getJournal("wf-resume-rewrite");
|
|
648
|
+
expect(secondJournal.map((e) => e.seq)).toEqual([0, 1]);
|
|
649
|
+
expect(secondJournal[1]!.callHash).not.toBe(seq1HashBefore);
|
|
650
|
+
expect(secondJournal[1]!.result).toBe("processed:step-2-CHANGED");
|
|
651
|
+
expect(second.result).toEqual([
|
|
652
|
+
"processed:step-1",
|
|
653
|
+
"processed:step-2-CHANGED",
|
|
654
|
+
]);
|
|
655
|
+
});
|
|
656
|
+
});
|