@selesai/code 0.13.0 → 0.13.1
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/CHANGELOG.md +12 -0
- package/dist/core/agent-session.d.ts +1 -1
- package/dist/core/agent-session.js +1 -1
- package/dist/core/sdk.d.ts +2 -2
- package/dist/core/sdk.js +1 -1
- package/dist/core/system-prompt.d.ts +1 -1
- package/dist/core/system-prompt.js +1 -1
- package/dist/extensions/ascii.txt +9 -0
- package/dist/extensions/package.json +1 -2
- package/dist/extensions/pi-hermes-memory/CHANGELOG.md +371 -0
- package/dist/extensions/pi-hermes-memory/LICENSE +21 -0
- package/dist/extensions/pi-hermes-memory/README.md +643 -0
- package/dist/extensions/pi-hermes-memory/docs/0.1/TASKS.md +197 -0
- package/dist/extensions/pi-hermes-memory/docs/0.2/PLAN.md +290 -0
- package/dist/extensions/pi-hermes-memory/docs/0.2/TASKS.md +134 -0
- package/dist/extensions/pi-hermes-memory/docs/0.2/TEST-PLAN.md +216 -0
- package/dist/extensions/pi-hermes-memory/docs/0.3/PLAN.md +330 -0
- package/dist/extensions/pi-hermes-memory/docs/0.3/TASKS.md +125 -0
- package/dist/extensions/pi-hermes-memory/docs/0.4/PLAN.md +160 -0
- package/dist/extensions/pi-hermes-memory/docs/0.4/TASKS.md +113 -0
- package/dist/extensions/pi-hermes-memory/docs/0.6/CHANGELOG.md +57 -0
- package/dist/extensions/pi-hermes-memory/docs/0.6/PLAN.md +202 -0
- package/dist/extensions/pi-hermes-memory/docs/0.6/TASKS.md +144 -0
- package/dist/extensions/pi-hermes-memory/docs/0.7/PLAN.md +349 -0
- package/dist/extensions/pi-hermes-memory/docs/0.7/TASKS.md +110 -0
- package/dist/extensions/pi-hermes-memory/docs/PUBLISHING.md +149 -0
- package/dist/extensions/pi-hermes-memory/docs/ROADMAP.md +468 -0
- package/dist/extensions/pi-hermes-memory/docs/images/memory-architecture.svg +1 -0
- package/dist/extensions/pi-hermes-memory/docs/images/pi-logo.svg +22 -0
- package/dist/extensions/pi-hermes-memory/docs/images/pi_memory.png +0 -0
- package/dist/extensions/pi-hermes-memory/docs/images/pi_memory.svg +19 -0
- package/dist/extensions/pi-hermes-memory/docs/images/security-flow.svg +1 -0
- package/dist/extensions/pi-hermes-memory/docs/images/session-lifecycle.svg +1 -0
- package/dist/extensions/pi-hermes-memory/docs/images/source-architecture.svg +1 -0
- package/dist/extensions/pi-hermes-memory/docs/mermaid/memory-architecture.mmd +49 -0
- package/dist/extensions/pi-hermes-memory/docs/mermaid/security-flow.mmd +40 -0
- package/dist/extensions/pi-hermes-memory/docs/mermaid/session-lifecycle.mmd +56 -0
- package/dist/extensions/pi-hermes-memory/docs/mermaid/source-architecture.mmd +54 -0
- package/dist/extensions/pi-hermes-memory/package-lock.json +3799 -0
- package/dist/extensions/pi-hermes-memory/package.json +66 -0
- package/dist/extensions/pi-hermes-memory/scripts/check-min-sdk.mjs +99 -0
- package/dist/extensions/pi-hermes-memory/scripts/ensure-dev.mjs +57 -0
- package/dist/extensions/pi-hermes-memory/src/auto-consolidation-warning.ts +6 -0
- package/dist/extensions/pi-hermes-memory/src/config.ts +178 -0
- package/dist/extensions/pi-hermes-memory/src/constants.ts +424 -0
- package/dist/extensions/pi-hermes-memory/src/extension-root-migration.ts +639 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/auto-consolidate.ts +401 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/background-review.ts +297 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/child-process-watchdog.mjs +90 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/correction-detector.ts +300 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/index-sessions.ts +86 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/insights.ts +78 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/interview.ts +37 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/learn-memory.ts +186 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/message-parts.ts +27 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/pi-child-process.ts +485 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/preview-context.ts +105 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/review-memory-ops.ts +506 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/session-backfill.ts +145 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/session-flush.ts +151 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/session-live-index.ts +94 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/skills-command.ts +1334 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/standing-pin.ts +111 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/switch-project.ts +75 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/sync-markdown-memories.ts +266 -0
- package/dist/extensions/pi-hermes-memory/src/index.ts +378 -0
- package/dist/extensions/pi-hermes-memory/src/lifecycle-timing.ts +54 -0
- package/dist/extensions/pi-hermes-memory/src/paths.ts +61 -0
- package/dist/extensions/pi-hermes-memory/src/project-context.ts +13 -0
- package/dist/extensions/pi-hermes-memory/src/project-memory-migration.ts +93 -0
- package/dist/extensions/pi-hermes-memory/src/project.ts +157 -0
- package/dist/extensions/pi-hermes-memory/src/prompt-context.ts +55 -0
- package/dist/extensions/pi-hermes-memory/src/store/atomic-lock-coordinator.ts +375 -0
- package/dist/extensions/pi-hermes-memory/src/store/canonical-storage-path.ts +54 -0
- package/dist/extensions/pi-hermes-memory/src/store/content-scanner.ts +101 -0
- package/dist/extensions/pi-hermes-memory/src/store/db.ts +1180 -0
- package/dist/extensions/pi-hermes-memory/src/store/fts-query.ts +101 -0
- package/dist/extensions/pi-hermes-memory/src/store/markdown-mutation-lock.ts +38 -0
- package/dist/extensions/pi-hermes-memory/src/store/memory-lookup.ts +24 -0
- package/dist/extensions/pi-hermes-memory/src/store/memory-store.ts +1191 -0
- package/dist/extensions/pi-hermes-memory/src/store/recovery-maintenance.ts +66 -0
- package/dist/extensions/pi-hermes-memory/src/store/schema.ts +118 -0
- package/dist/extensions/pi-hermes-memory/src/store/session-anchor-search.ts +472 -0
- package/dist/extensions/pi-hermes-memory/src/store/session-indexer.ts +511 -0
- package/dist/extensions/pi-hermes-memory/src/store/session-parser.ts +213 -0
- package/dist/extensions/pi-hermes-memory/src/store/session-search.ts +234 -0
- package/dist/extensions/pi-hermes-memory/src/store/skill-store.ts +950 -0
- package/dist/extensions/pi-hermes-memory/src/store/skill-utils.ts +133 -0
- package/dist/extensions/pi-hermes-memory/src/store/sqlite-memory-store.ts +1002 -0
- package/dist/extensions/pi-hermes-memory/src/store/sqlite-native.ts +241 -0
- package/dist/extensions/pi-hermes-memory/src/store/standing-instructions.ts +247 -0
- package/dist/extensions/pi-hermes-memory/src/tools/memory-search-tool.ts +95 -0
- package/dist/extensions/pi-hermes-memory/src/tools/memory-tool.ts +469 -0
- package/dist/extensions/pi-hermes-memory/src/tools/session-search-tool.ts +246 -0
- package/dist/extensions/pi-hermes-memory/src/tools/shared-output-view.ts +170 -0
- package/dist/extensions/pi-hermes-memory/src/tools/skill-tool.ts +333 -0
- package/dist/extensions/pi-hermes-memory/src/tools/tool-result-views.ts +105 -0
- package/dist/extensions/pi-hermes-memory/src/types.ts +214 -0
- package/dist/extensions/pi-hermes-memory/tests/config.test.ts +434 -0
- package/dist/extensions/pi-hermes-memory/tests/extension-root-migration.test.ts +368 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/auto-consolidate.test.ts +984 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/background-review.test.ts +950 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/correction-detector.test.ts +671 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/insights.test.ts +157 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/interview.test.ts +127 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/pi-child-process.test.ts +905 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/preview-context.test.ts +152 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/prompt-context.test.ts +166 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/resources-discover.test.ts +71 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/review-memory-ops.test.ts +614 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/session-backfill.test.ts +192 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/session-flush.test.ts +677 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/session-live-index.test.ts +225 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/skills-command.test.ts +827 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/standing-pin.test.ts +134 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/sync-markdown-memories.test.ts +491 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/system-prompt.test.ts +199 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/thinking-max.test.ts +64 -0
- package/dist/extensions/pi-hermes-memory/tests/index.test.ts +30 -0
- package/dist/extensions/pi-hermes-memory/tests/integration/flow.test.ts +126 -0
- package/dist/extensions/pi-hermes-memory/tests/lifecycle-timing.test.ts +76 -0
- package/dist/extensions/pi-hermes-memory/tests/paths.test.ts +23 -0
- package/dist/extensions/pi-hermes-memory/tests/project-memory-migration.test.ts +80 -0
- package/dist/extensions/pi-hermes-memory/tests/project-rebinding.test.ts +115 -0
- package/dist/extensions/pi-hermes-memory/tests/project.test.ts +120 -0
- package/dist/extensions/pi-hermes-memory/tests/run-all-timeout.test.ts +107 -0
- package/dist/extensions/pi-hermes-memory/tests/run-all.sh +45 -0
- package/dist/extensions/pi-hermes-memory/tests/store/atomic-lock-coordinator.test.ts +381 -0
- package/dist/extensions/pi-hermes-memory/tests/store/content-scanner.test.ts +388 -0
- package/dist/extensions/pi-hermes-memory/tests/store/db.test.ts +1046 -0
- package/dist/extensions/pi-hermes-memory/tests/store/markdown-mutation-lock.test.ts +34 -0
- package/dist/extensions/pi-hermes-memory/tests/store/memory-store.test.ts +2150 -0
- package/dist/extensions/pi-hermes-memory/tests/store/recovery-maintenance.test.ts +170 -0
- package/dist/extensions/pi-hermes-memory/tests/store/session-anchor-search.test.ts +210 -0
- package/dist/extensions/pi-hermes-memory/tests/store/session-indexer.test.ts +643 -0
- package/dist/extensions/pi-hermes-memory/tests/store/session-parser.test.ts +282 -0
- package/dist/extensions/pi-hermes-memory/tests/store/session-search.test.ts +287 -0
- package/dist/extensions/pi-hermes-memory/tests/store/skill-store.test.ts +678 -0
- package/dist/extensions/pi-hermes-memory/tests/store/skill-utils.test.ts +21 -0
- package/dist/extensions/pi-hermes-memory/tests/store/sqlite-lazy-load.test.ts +78 -0
- package/dist/extensions/pi-hermes-memory/tests/store/sqlite-memory-store.test.ts +543 -0
- package/dist/extensions/pi-hermes-memory/tests/store/sqlite-native.test.ts +164 -0
- package/dist/extensions/pi-hermes-memory/tests/store/standing-instructions.test.ts +171 -0
- package/dist/extensions/pi-hermes-memory/tests/tools/memory-search-tool.test.ts +84 -0
- package/dist/extensions/pi-hermes-memory/tests/tools/memory-tool.test.ts +709 -0
- package/dist/extensions/pi-hermes-memory/tests/tools/session-search-tool.test.ts +345 -0
- package/dist/extensions/pi-hermes-memory/tests/tools/shared-output-view.test.ts +362 -0
- package/dist/extensions/pi-hermes-memory/tests/tools/skill-tool.test.ts +478 -0
- package/dist/extensions/pi-hermes-memory/tests/tools/tool-result-renderer-wiring.test.ts +40 -0
- package/dist/extensions/pi-hermes-memory/tsconfig.json +19 -0
- package/dist/extensions/pi-subagents/.oxlintrc.json +43 -0
- package/dist/extensions/pi-subagents/AGENTS.md +5 -0
- package/dist/extensions/pi-subagents/CHANGELOG.md +157 -0
- package/dist/extensions/pi-subagents/README.md +12 -6
- package/dist/extensions/pi-subagents/VISION.md +81 -0
- package/dist/extensions/pi-subagents/agents/claude-code-writer.md +15 -0
- package/dist/extensions/pi-subagents/agents/claude-code.md +15 -0
- package/dist/extensions/pi-subagents/agents/codex-exec-writer.md +15 -0
- package/dist/extensions/pi-subagents/agents/codex-exec.md +15 -0
- package/dist/extensions/pi-subagents/agents/cursor-agent-writer.md +14 -0
- package/dist/extensions/pi-subagents/agents/cursor-agent.md +14 -0
- package/dist/extensions/pi-subagents/agents/reviewer.md +1 -3
- package/dist/extensions/pi-subagents/docs/agents.md +488 -0
- package/dist/extensions/pi-subagents/docs/configuration.md +46 -9
- package/dist/extensions/pi-subagents/docs/extension-api.md +42 -3
- package/dist/extensions/pi-subagents/docs/models.md +27 -4
- package/dist/extensions/pi-subagents/docs/observability.md +10 -9
- package/dist/extensions/pi-subagents/docs/tool-reference.md +102 -6
- package/dist/extensions/pi-subagents/docs/workflows.md +135 -0
- package/dist/extensions/pi-subagents/package-lock.json +389 -2
- package/dist/extensions/pi-subagents/package.json +3 -1
- package/dist/extensions/pi-subagents/prompts/review-loop.md +2 -2
- package/dist/extensions/pi-subagents/skills/council-mode/SKILL.md +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +3 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +7 -3
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +26 -5
- package/dist/extensions/pi-subagents/src/agents/agent-management.ts +41 -22
- package/dist/extensions/pi-subagents/src/agents/agent-refinements.ts +4 -4
- package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +9 -2
- package/dist/extensions/pi-subagents/src/agents/agents.ts +322 -75
- package/dist/extensions/pi-subagents/src/agents/builtin-names.ts +6 -0
- package/dist/extensions/pi-subagents/src/agents/runtime-agent-events.ts +70 -0
- package/dist/extensions/pi-subagents/src/agents/runtime-agent-registry.ts +27 -20
- package/dist/extensions/pi-subagents/src/api/agents.ts +10 -5
- package/dist/extensions/pi-subagents/src/api/background-work.ts +5 -1
- package/dist/extensions/pi-subagents/src/api/delegation.ts +0 -7
- package/dist/extensions/pi-subagents/src/api/preflight.ts +34 -12
- package/dist/extensions/pi-subagents/src/extension/config.ts +70 -0
- package/dist/extensions/pi-subagents/src/extension/doctor.ts +3 -3
- package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +5 -3
- package/dist/extensions/pi-subagents/src/extension/index.ts +83 -30
- package/dist/extensions/pi-subagents/src/extension/public-execution.ts +44 -15
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +55 -19
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +41 -15
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +28 -31
- package/dist/extensions/pi-subagents/src/inspectors/herdr/actions.ts +2 -1
- package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +2 -10
- package/dist/extensions/pi-subagents/src/inspectors/herdr/session-roots-codec.ts +42 -0
- package/dist/extensions/pi-subagents/src/integrations/herdr-status.ts +51 -3
- package/dist/extensions/pi-subagents/src/intercom/result-intercom.ts +2 -0
- package/dist/extensions/pi-subagents/src/profiles/profiles.ts +5 -6
- package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +77 -10
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +176 -53
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +19 -12
- package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +29 -27
- package/dist/extensions/pi-subagents/src/runs/background/async-retention.ts +20 -3
- package/dist/extensions/pi-subagents/src/runs/background/async-status-snapshot.ts +23 -261
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +73 -7
- package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +8 -3
- package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +74 -8
- package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +37 -21
- package/dist/extensions/pi-subagents/src/runs/background/inspect-rpc.ts +8 -8
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +184 -9
- package/dist/extensions/pi-subagents/src/runs/background/result-delivery-ownership.ts +45 -0
- package/dist/extensions/pi-subagents/src/runs/background/result-files.ts +28 -14
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +38 -15
- package/dist/extensions/pi-subagents/src/runs/background/resume-guidance.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +50 -13
- package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +93 -8
- package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +10 -4
- package/dist/extensions/pi-subagents/src/runs/background/steering.ts +4 -14
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +549 -377
- package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +58 -10
- package/dist/extensions/pi-subagents/src/runs/background/terminal-run-index.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +26 -1
- package/dist/extensions/pi-subagents/src/runs/background/wait-config.ts +23 -9
- package/dist/extensions/pi-subagents/src/runs/background/wait-tool.ts +9 -2
- package/dist/extensions/pi-subagents/src/runs/foreground/async-steering-action.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +204 -129
- package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +9 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/foreground-history.ts +23 -1
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +773 -311
- package/dist/extensions/pi-subagents/src/runs/foreground/workflow-detach-reconcile.ts +140 -115
- package/dist/extensions/pi-subagents/src/runs/shared/abort-recovery.ts +119 -0
- package/dist/extensions/pi-subagents/src/runs/shared/async-status-projection.ts +463 -0
- package/dist/extensions/pi-subagents/src/runs/shared/child-identity.ts +19 -4
- package/dist/extensions/pi-subagents/src/runs/shared/child-launch-plan.ts +151 -0
- package/dist/extensions/pi-subagents/src/runs/shared/child-protocol.ts +21 -7
- package/dist/extensions/pi-subagents/src/runs/shared/claude-code-adapter.ts +129 -0
- package/dist/extensions/pi-subagents/src/runs/shared/codex-exec-adapter.ts +129 -0
- package/dist/extensions/pi-subagents/src/runs/shared/completion-evidence.ts +89 -0
- package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +5 -4
- package/dist/extensions/pi-subagents/src/runs/shared/cursor-agent-adapter.ts +114 -0
- package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +3 -3
- package/dist/extensions/pi-subagents/src/runs/shared/external-cli-contract.ts +167 -0
- package/dist/extensions/pi-subagents/src/runs/shared/external-cli-preflight.ts +122 -0
- package/dist/extensions/pi-subagents/src/runs/shared/external-cli-runner.ts +348 -55
- package/dist/extensions/pi-subagents/src/runs/shared/fast-mode-extension.ts +5 -5
- package/dist/extensions/pi-subagents/src/runs/shared/host-step-status.ts +230 -0
- package/dist/extensions/pi-subagents/src/runs/shared/lane-metadata.ts +105 -0
- package/dist/extensions/pi-subagents/src/runs/shared/launch-cwd.ts +16 -0
- package/dist/extensions/pi-subagents/src/runs/shared/long-running-guard.ts +2 -1
- package/dist/extensions/pi-subagents/src/runs/shared/mcp-config-sources.ts +422 -0
- package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +221 -158
- package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-grant.ts +197 -0
- package/dist/extensions/pi-subagents/src/runs/shared/model-exclusions.ts +69 -7
- package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +47 -6
- package/dist/extensions/pi-subagents/src/runs/shared/mutation-evidence.ts +7 -2
- package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +9 -3
- package/dist/extensions/pi-subagents/src/runs/shared/nested-render.ts +9 -5
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +419 -7
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +15 -1
- package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +81 -8
- package/dist/extensions/pi-subagents/src/runs/shared/process-signal.ts +13 -0
- package/dist/extensions/pi-subagents/src/runs/shared/run-history.ts +21 -1
- package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +44 -4
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +98 -13
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/shared/worktree-cleanup-plan.ts +847 -0
- package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +18 -0
- package/dist/extensions/pi-subagents/src/shared/child-session-name.ts +46 -0
- package/dist/extensions/pi-subagents/src/shared/extension-context.ts +24 -0
- package/dist/extensions/pi-subagents/src/shared/fork-context.ts +21 -0
- package/dist/extensions/pi-subagents/src/shared/formatters.ts +18 -3
- package/dist/extensions/pi-subagents/src/shared/launch-contract.ts +5 -1
- package/dist/extensions/pi-subagents/src/shared/pruned-fork.ts +450 -0
- package/dist/extensions/pi-subagents/src/shared/session-file-trust.ts +19 -0
- package/dist/extensions/pi-subagents/src/shared/session-tokens.ts +14 -3
- package/dist/extensions/pi-subagents/src/shared/settings.ts +19 -105
- package/dist/extensions/pi-subagents/src/shared/shortcuts.ts +17 -0
- package/dist/extensions/pi-subagents/src/shared/types.ts +359 -40
- package/dist/extensions/pi-subagents/src/shared/utils.ts +41 -84
- package/dist/extensions/pi-subagents/src/shared/workflow-child-permit.ts +116 -0
- package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +1 -9
- package/dist/extensions/pi-subagents/src/slash/delegation-request.ts +0 -4
- package/dist/extensions/pi-subagents/src/slash/slash-bridge.ts +1 -2
- package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +378 -88
- package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +22 -11
- package/dist/extensions/pi-subagents/src/slash/subagents-admin.ts +3 -0
- package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +147 -79
- package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +11 -5
- package/dist/extensions/pi-subagents/src/tui/fleet.ts +39 -18
- package/dist/extensions/pi-subagents/src/tui/render.ts +369 -46
- package/dist/extensions/pi-subagents/src/watchdog/turn-delta.ts +1 -1
- package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +6 -3
- package/dist/extensions/pi-subagents/src/workflows/host-command.ts +230 -0
- package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +740 -58
- package/dist/extensions/pi-subagents/src/workflows/workflow-child-summary.ts +121 -0
- package/dist/extensions/pi-subagents/src/workflows/workflow-preflight.ts +270 -0
- package/dist/extensions/pi-subagents/src/workflows/workflow-receipt.ts +195 -6
- package/dist/extensions/pi-subagents/src/workflows/workflow-settlement.ts +246 -0
- package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +1114 -260
- package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +34 -0
- package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +150 -3
- package/dist/extensions/pi-subagents/test/integration/claude-code-smoke.test.ts +52 -0
- package/dist/extensions/pi-subagents/test/integration/claude-code-writer-smoke.test.ts +55 -0
- package/dist/extensions/pi-subagents/test/integration/codex-exec-smoke.test.ts +53 -0
- package/dist/extensions/pi-subagents/test/integration/codex-exec-writer-smoke.test.ts +57 -0
- package/dist/extensions/pi-subagents/test/integration/cursor-agent-smoke.test.ts +59 -0
- package/dist/extensions/pi-subagents/test/integration/cursor-agent-writer-smoke.test.ts +62 -0
- package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +57 -1
- package/dist/extensions/pi-subagents/test/integration/external-cli-runner.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +20 -0
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +4 -3
- package/dist/extensions/pi-subagents/test/integration/orca-progress-tabs.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +54 -4
- package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +482 -4
- package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +157 -9
- package/dist/extensions/pi-subagents/test/integration/session-tokens.test.ts +5 -5
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1508 -291
- package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +423 -1
- package/dist/extensions/pi-subagents/test/integration/template-resolution.test.ts +8 -0
- package/dist/extensions/pi-subagents/test/support/cursor-smoke-inputs.ts +51 -0
- package/dist/extensions/pi-subagents/test/support/helpers.ts +2 -0
- package/dist/extensions/pi-subagents/test/support/mock-pi.ts +5 -5
- package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +6 -6
- package/dist/extensions/pi-subagents/test/unit/abort-recovery.test.ts +152 -0
- package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +48 -0
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +184 -6
- package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +66 -6
- package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +77 -0
- package/dist/extensions/pi-subagents/test/unit/agent-refinements.test.ts +18 -0
- package/dist/extensions/pi-subagents/test/unit/anti-slop-oxlint.test.ts +86 -0
- package/dist/extensions/pi-subagents/test/unit/artifacts.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +101 -1
- package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +121 -0
- package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +28 -0
- package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +14 -7
- package/dist/extensions/pi-subagents/test/unit/async-retention.test.ts +53 -0
- package/dist/extensions/pi-subagents/test/unit/async-status-projection.test.ts +190 -0
- package/dist/extensions/pi-subagents/test/unit/async-status-snapshot.test.ts +42 -0
- package/dist/extensions/pi-subagents/test/unit/auto-drain.test.ts +8 -4
- package/dist/extensions/pi-subagents/test/unit/background-work.test.ts +23 -0
- package/dist/extensions/pi-subagents/test/unit/chain-root-attachment.test.ts +78 -1
- package/dist/extensions/pi-subagents/test/unit/child-launch-plan.test.ts +94 -0
- package/dist/extensions/pi-subagents/test/unit/child-protocol.test.ts +21 -0
- package/dist/extensions/pi-subagents/test/unit/child-session-name.test.ts +50 -0
- package/dist/extensions/pi-subagents/test/unit/claude-code-adapter.test.ts +245 -0
- package/dist/extensions/pi-subagents/test/unit/codex-exec-adapter.test.ts +192 -0
- package/dist/extensions/pi-subagents/test/unit/compaction-resume.test.ts +27 -0
- package/dist/extensions/pi-subagents/test/unit/completion-evidence.test.ts +150 -0
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +21 -0
- package/dist/extensions/pi-subagents/test/unit/cursor-agent-adapter.test.ts +259 -0
- package/dist/extensions/pi-subagents/test/unit/cursor-smoke-inputs.test.ts +77 -0
- package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +1 -4
- package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/extension-context.test.ts +27 -0
- package/dist/extensions/pi-subagents/test/unit/external-cli-runner.test.ts +223 -0
- package/dist/extensions/pi-subagents/test/unit/fast-mode-extension.test.ts +27 -0
- package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +208 -2
- package/dist/extensions/pi-subagents/test/unit/fleet-transcript.test.ts +29 -1
- package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +110 -1
- package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +10 -0
- package/dist/extensions/pi-subagents/test/unit/formatters.test.ts +14 -0
- package/dist/extensions/pi-subagents/test/unit/herdr-inspector.test.ts +18 -9
- package/dist/extensions/pi-subagents/test/unit/herdr-session-roots-codec.test.ts +63 -0
- package/dist/extensions/pi-subagents/test/unit/herdr-shell-command.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/unit/herdr-status-bridge.test.ts +132 -0
- package/dist/extensions/pi-subagents/test/unit/host-command.test.ts +156 -0
- package/dist/extensions/pi-subagents/test/unit/host-step-status.test.ts +98 -0
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +178 -26
- package/dist/extensions/pi-subagents/test/unit/index-segment.test.ts +2 -1
- package/dist/extensions/pi-subagents/test/unit/inspect-rpc.test.ts +21 -0
- package/dist/extensions/pi-subagents/test/unit/mcp-direct-tool-grant.test.ts +138 -0
- package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/model-exclusions.test.ts +91 -0
- package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +72 -7
- package/dist/extensions/pi-subagents/test/unit/mutation-evidence.test.ts +21 -0
- package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +20 -2
- package/dist/extensions/pi-subagents/test/unit/notify.test.ts +129 -1
- package/dist/extensions/pi-subagents/test/unit/orca-progress-tabs.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +4 -0
- package/dist/extensions/pi-subagents/test/unit/parallel-handoff.test.ts +358 -0
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +574 -21
- package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +109 -1
- package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +71 -1
- package/dist/extensions/pi-subagents/test/unit/profiles.test.ts +7 -7
- package/dist/extensions/pi-subagents/test/unit/pruned-fork.test.ts +214 -0
- package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +29 -8
- package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +53 -0
- package/dist/extensions/pi-subagents/test/unit/result-delivery-ownership.test.ts +34 -0
- package/dist/extensions/pi-subagents/test/unit/result-files.test.ts +101 -3
- package/dist/extensions/pi-subagents/test/unit/retained-children.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +10 -8
- package/dist/extensions/pi-subagents/test/unit/run-fanout-budget.test.ts +71 -1
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +41 -7
- package/dist/extensions/pi-subagents/test/unit/runtime-agent-registration.test.ts +136 -2
- package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +137 -0
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +33 -12
- package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +504 -1
- package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +8 -0
- package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +12 -1
- package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +3 -5
- package/dist/extensions/pi-subagents/test/unit/steering.test.ts +5 -6
- package/dist/extensions/pi-subagents/test/unit/subagent-action-recovery.test.ts +0 -8
- package/dist/extensions/pi-subagents/test/unit/subagent-guide.test.ts +5 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +157 -2
- package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +63 -4
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +27 -23
- package/dist/extensions/pi-subagents/test/unit/wait-completions.test.ts +44 -0
- package/dist/extensions/pi-subagents/test/unit/wait-subscriptions.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +7 -5
- package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +106 -0
- package/dist/extensions/pi-subagents/test/unit/workflow-detach-reconcile.test.ts +484 -9
- package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +38 -4
- package/dist/extensions/pi-subagents/test/unit/workflow-preflight.test.ts +155 -0
- package/dist/extensions/pi-subagents/test/unit/workflow-receipt.test.ts +318 -0
- package/dist/extensions/pi-subagents/test/unit/workflow-resume-hint.test.ts +365 -0
- package/dist/extensions/pi-subagents/test/unit/worktree-cleanup-plan.test.ts +349 -0
- package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +21 -0
- package/dist/extensions/pi-subagents/test/unit/writer-budget-guidance.test.ts +2 -7
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/effect/index.ts +13 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/effect/rules/no-service-constructor-imports.ts +52 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/index.ts +41 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-chained-type-assertions.ts +77 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-conditional-empty-object-spread.ts +49 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-known-value-widening.ts +247 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-module-mocking.ts +91 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-object-parameters.ts +126 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-reflect-apply.ts +28 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-reflect-get.ts +28 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-runtime-typeof.ts +67 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-shape-in-symbol-names.ts +39 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-unknown-parameters.ts +83 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-unknown-returns.ts +115 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-unknown-type-aliases.ts +72 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-unsafe-dictionary-type.ts +134 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-widen-then-assert.ts +366 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/require-safety-comment-for-type-assertion.ts +62 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/shared/dictionary-types.ts +502 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/shared/lexical-type-parameters.ts +60 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/shared/reflect-method.ts +35 -0
- package/dist/extensions/pi-tool-display/tests/capabilities.test.ts +1 -1
- package/dist/extensions/pi-tool-display/tests/settings-inspector.test.ts +2 -2
- package/dist/extensions/pi-tool-display/tests/tool-overrides-mcp.test.ts +1 -1
- package/dist/modes/interactive/interactive-mode.js +7 -7
- package/dist/skills/workflow/SKILL.md +65 -50
- package/package.json +112 -110
- package/dist/extensions/enable-readonly-tools.test.ts +0 -63
- package/dist/extensions/enable-readonly-tools.ts +0 -24
- package/dist/extensions/model-prompt-injector/config.json +0 -16
- package/dist/extensions/model-prompt-injector/index.test.ts +0 -102
- package/dist/extensions/model-prompt-injector/index.ts +0 -148
- package/dist/extensions/pi-subagents/src/runs/shared/turn-budget.ts +0 -98
- package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +0 -250
|
@@ -2,10 +2,25 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { dirname, resolve as resolvePath } from "node:path";
|
|
4
4
|
import { Worker } from "node:worker_threads";
|
|
5
|
+
import { DEFAULT_GLOBAL_CONCURRENCY_LIMIT, Semaphore } from "../runs/shared/parallel-utils.ts";
|
|
6
|
+
import { HOST_STEP_MAX_COUNT } from "../runs/shared/host-step-status.ts";
|
|
7
|
+
import type { HostStepNodeV1, SingleResult } from "../shared/types.ts";
|
|
8
|
+
import { normalizeWorkflowHostCommandParams, type WorkflowHostCommandParams, type WorkflowHostCommandResult } from "./host-command.ts";
|
|
5
9
|
|
|
6
10
|
const KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
7
11
|
const requireFromPackage = createRequire(import.meta.url);
|
|
8
12
|
|
|
13
|
+
export interface WorkflowScriptValidationError {
|
|
14
|
+
message: string;
|
|
15
|
+
line?: number;
|
|
16
|
+
column?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface WorkflowScriptValidationResult {
|
|
20
|
+
ok: boolean;
|
|
21
|
+
errors: WorkflowScriptValidationError[];
|
|
22
|
+
}
|
|
23
|
+
|
|
9
24
|
const WORKER_SOURCE = String.raw`
|
|
10
25
|
const { parentPort, workerData } = require("node:worker_threads");
|
|
11
26
|
const vm = require("node:vm");
|
|
@@ -241,11 +256,11 @@ function hostCall(method, args, observation) {
|
|
|
241
256
|
: promise;
|
|
242
257
|
}
|
|
243
258
|
|
|
244
|
-
function runHostCall(key, params, collectFailure, batch) {
|
|
259
|
+
function runHostCall(key, params, collectFailure, batch, generatedLaneKey) {
|
|
245
260
|
const callId = ++nextCallId;
|
|
246
261
|
const promise = new Promise((resolve, reject) => {
|
|
247
262
|
pending.set(callId, { resolve, reject });
|
|
248
|
-
parentPort.postMessage({ type: "call", callId, method: "run", args: { key, params, ...(collectFailure ? { collectFailure: true } : {}), ...(batch ? { batch } : {}) } });
|
|
263
|
+
parentPort.postMessage({ type: "call", callId, method: "run", args: { key, params, ...(collectFailure ? { collectFailure: true } : {}), ...(batch ? { batch } : {}), ...(generatedLaneKey ? { generatedLaneKey } : {}) } });
|
|
249
264
|
});
|
|
250
265
|
return { key, callId, promise };
|
|
251
266
|
}
|
|
@@ -258,6 +273,15 @@ function isArrayIndexProperty(prop) {
|
|
|
258
273
|
|
|
259
274
|
const runsAllResultTargets = new WeakMap();
|
|
260
275
|
|
|
276
|
+
const MAX_LANES = 32;
|
|
277
|
+
const MAX_LANE_STAGES = 16;
|
|
278
|
+
const MAX_LANE_STAGE_COUNT = 64;
|
|
279
|
+
const MAX_LANE_SPEC_BYTES = 64 * 1024;
|
|
280
|
+
const MAX_LANE_TASK_BYTES = 1024 * 1024;
|
|
281
|
+
const MAX_LANE_PATH_BYTES = 32 * 1024;
|
|
282
|
+
const MAX_LANE_BOARD_TEXT_BYTES = 256;
|
|
283
|
+
const LANE_PATH_FIELDS = new Set(["cwd", "output", "sessionDir"]);
|
|
284
|
+
|
|
261
285
|
function runsAllKeyAccessError(prop) {
|
|
262
286
|
return new Error("Cannot read runs.all result property '" + prop + "'. runs.all resolves to an ordered array, not a key map. Use results[0], array destructuring, or results.map((result) => result.output), not results." + prop + ".");
|
|
263
287
|
}
|
|
@@ -277,6 +301,224 @@ function wrapRunsAllResults(results, keys) {
|
|
|
277
301
|
return proxy;
|
|
278
302
|
}
|
|
279
303
|
|
|
304
|
+
function laneByteLength(value) {
|
|
305
|
+
return new TextEncoder().encode(value).byteLength;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function laneBoardText(value) {
|
|
309
|
+
if (typeof value !== "string" || !value) return undefined;
|
|
310
|
+
if (laneByteLength(value) <= MAX_LANE_BOARD_TEXT_BYTES) return value;
|
|
311
|
+
const bytes = new TextEncoder().encode(value).subarray(0, MAX_LANE_BOARD_TEXT_BYTES - 3);
|
|
312
|
+
return new TextDecoder().decode(bytes) + "...";
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function laneBoardPath(value) {
|
|
316
|
+
if (typeof value !== "string" || !value || laneByteLength(value) > MAX_LANE_BOARD_TEXT_BYTES) return undefined;
|
|
317
|
+
return value;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function validateLaneStageBounds(params, label) {
|
|
321
|
+
if (typeof params.task === "string" && laneByteLength(params.task) > MAX_LANE_TASK_BYTES) throw new Error(label + " task exceeds 1 MiB when UTF-8 encoded.");
|
|
322
|
+
for (const field of LANE_PATH_FIELDS) if (typeof params[field] === "string" && laneByteLength(params[field]) > MAX_LANE_PATH_BYTES) throw new Error(label + " " + field + " exceeds 32 KiB when UTF-8 encoded.");
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function validateLaneKey(value, owner) {
|
|
326
|
+
if (typeof value !== "string" || !runKeyPattern.test(value)) throw new Error(owner + " key must be 1-128 characters using letters, numbers, '.', '_' or '-', and start with a letter or number.");
|
|
327
|
+
return value;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function laneStageParams(stage, previous) {
|
|
331
|
+
const resume = stage.resume;
|
|
332
|
+
const params = { ...stage.params };
|
|
333
|
+
if (resume !== "previous") return params;
|
|
334
|
+
if (!previous || typeof previous.runId !== "string" || !previous.runId.trim()) return undefined;
|
|
335
|
+
return { ...params, resume: previous.runId.trim() };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function laneStageVerdict(result) {
|
|
339
|
+
const structured = result && typeof result === "object" && !Array.isArray(result) ? result.structuredOutput : undefined;
|
|
340
|
+
const verdict = structured && typeof structured === "object" && !Array.isArray(structured) ? structured.verdict : undefined;
|
|
341
|
+
return typeof verdict === "string" && verdict.trim() ? laneBoardText(verdict.trim()) : undefined;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function laneStageIsBlocked(result) {
|
|
345
|
+
const structured = result && typeof result === "object" && !Array.isArray(result) ? result.structuredOutput : undefined;
|
|
346
|
+
return structured && typeof structured === "object" && !Array.isArray(structured) && structured.verdict === "blocked";
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function laneStageRecord(stageKey, child, forcedState) {
|
|
350
|
+
const result = child && typeof child === "object" && !Array.isArray(child) ? child : undefined;
|
|
351
|
+
const ok = result?.ok === true;
|
|
352
|
+
const verdict = laneStageVerdict(result);
|
|
353
|
+
const blocked = laneStageIsBlocked(result);
|
|
354
|
+
const state = forcedState ?? (result?.stopped ? "stopped" : result?.detached ? "detached" : ok ? blocked ? "blocked" : "completed" : "failed");
|
|
355
|
+
const error = state !== "completed"
|
|
356
|
+
? state === "blocked" && blocked
|
|
357
|
+
? "Stage returned a blocked verdict."
|
|
358
|
+
: typeof result?.error === "string"
|
|
359
|
+
? result.error
|
|
360
|
+
: typeof result?.output === "string"
|
|
361
|
+
? result.output
|
|
362
|
+
: "Stage did not complete successfully."
|
|
363
|
+
: undefined;
|
|
364
|
+
return {
|
|
365
|
+
key: stageKey,
|
|
366
|
+
...(typeof result?.runId === "string" && result.runId.trim() ? { runId: laneBoardText(result.runId.trim()) } : {}),
|
|
367
|
+
...(result ? { ok } : {}),
|
|
368
|
+
state,
|
|
369
|
+
...(typeof result?.outputReference === "string" ? (laneBoardPath(result.outputReference) ? { outputReference: laneBoardPath(result.outputReference) } : {}) : {}),
|
|
370
|
+
...(verdict ? { verdict } : {}),
|
|
371
|
+
...(error ? { error: laneBoardText(error) } : {}),
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function laneFailure(stageKey, error) {
|
|
376
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
377
|
+
return { key: stageKey, ok: false, output: text, error: text, artifactPaths: [] };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function runCollected(key, params, observe, generatedLaneKey) {
|
|
381
|
+
validateRunCall(key, params, "runs.lanes stage", runFingerprints);
|
|
382
|
+
const launched = runHostCall(key, params, true, undefined, generatedLaneKey);
|
|
383
|
+
observe([{ key, operation: "run", callId: launched.callId }]);
|
|
384
|
+
return launched.promise.then(decorateWorkflowChildResult);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function validateLaneSpecs(laneSpecs) {
|
|
388
|
+
if (!Array.isArray(laneSpecs)) throw new Error("runs.lanes(lanes) requires an array.");
|
|
389
|
+
if (laneSpecs.length === 0) throw new Error("runs.lanes(lanes) requires at least one lane.");
|
|
390
|
+
if (laneSpecs.length > MAX_LANES) throw new Error("runs.lanes supports at most " + MAX_LANES + " lanes.");
|
|
391
|
+
assertJsonValue(laneSpecs, "runs.lanes lanes");
|
|
392
|
+
if (laneByteLength(stableRunJson(laneSpecs)) > MAX_LANE_SPEC_BYTES) throw new Error("runs.lanes canonical JSON exceeds 64 KiB.");
|
|
393
|
+
const generatedKeys = new Set();
|
|
394
|
+
const validationFingerprints = new Map();
|
|
395
|
+
let stageCount = 0;
|
|
396
|
+
const normalized = [];
|
|
397
|
+
for (let laneIndex = 0; laneIndex < laneSpecs.length; laneIndex++) {
|
|
398
|
+
const lane = laneSpecs[laneIndex];
|
|
399
|
+
const laneLabel = "runs.lanes lane " + laneIndex;
|
|
400
|
+
if (!lane || typeof lane !== "object" || Array.isArray(lane)) throw new Error(laneLabel + " must be an object.");
|
|
401
|
+
const laneFields = Object.keys(lane);
|
|
402
|
+
if (laneFields.some((field) => field !== "key" && field !== "stages")) throw new Error(laneLabel + " contains unsupported fields.");
|
|
403
|
+
const laneKey = validateLaneKey(lane.key, laneLabel);
|
|
404
|
+
if (!Array.isArray(lane.stages) || lane.stages.length === 0) throw new Error(laneLabel + " stages must contain at least one stage.");
|
|
405
|
+
if (lane.stages.length > MAX_LANE_STAGES) throw new Error(laneLabel + " supports at most " + MAX_LANE_STAGES + " stages.");
|
|
406
|
+
const stageKeys = new Set();
|
|
407
|
+
const stages = [];
|
|
408
|
+
for (let stageIndex = 0; stageIndex < lane.stages.length; stageIndex++) {
|
|
409
|
+
stageCount++;
|
|
410
|
+
if (stageCount > MAX_LANE_STAGE_COUNT) throw new Error("runs.lanes supports at most " + MAX_LANE_STAGE_COUNT + " total stages.");
|
|
411
|
+
const stage = lane.stages[stageIndex];
|
|
412
|
+
const stageLabel = laneLabel + " stage " + stageIndex;
|
|
413
|
+
if (!stage || typeof stage !== "object" || Array.isArray(stage)) throw new Error(stageLabel + " must be an object.");
|
|
414
|
+
const stageKey = validateLaneKey(stage.key, stageLabel);
|
|
415
|
+
if (stageKeys.has(stageKey)) throw new Error(laneLabel + " contains duplicate stage key '" + stageKey + "'.");
|
|
416
|
+
stageKeys.add(stageKey);
|
|
417
|
+
const generatedKey = laneKey + "." + stageKey;
|
|
418
|
+
validateLaneKey(generatedKey, stageLabel + " generated");
|
|
419
|
+
if (generatedKeys.has(generatedKey)) throw new Error("runs.lanes generated child key '" + generatedKey + "' is duplicated.");
|
|
420
|
+
generatedKeys.add(generatedKey);
|
|
421
|
+
const resume = stage.resume;
|
|
422
|
+
if (resume !== undefined && resume !== "previous") {
|
|
423
|
+
if (stageIndex === 0 && typeof resume === "string") {
|
|
424
|
+
throw new Error(stageLabel + " cannot resume a retained run id in runs.lanes; use runs.run(key, { resume: id }) outside lanes, or start the lane with an agent stage and use resume: \"previous\" later.");
|
|
425
|
+
}
|
|
426
|
+
throw new Error(stageLabel + " resume must be 'previous'.");
|
|
427
|
+
}
|
|
428
|
+
if (stageIndex === 0 && resume === "previous") throw new Error(stageLabel + " cannot resume previous without a predecessor stage.");
|
|
429
|
+
const { key: _stageKey, resume: _resume, ...params } = stage;
|
|
430
|
+
const validationParams = resume === "previous" ? { ...params, resume: "retained-run-placeholder" } : params;
|
|
431
|
+
validateLaneStageBounds(validationParams, stageLabel);
|
|
432
|
+
validateRunCall(generatedKey, validationParams, stageLabel, validationFingerprints);
|
|
433
|
+
const existingFingerprint = runFingerprints.get(generatedKey);
|
|
434
|
+
if (existingFingerprint !== undefined && (resume === "previous" || existingFingerprint !== stableRunJson(params))) {
|
|
435
|
+
throw new Error("runs.lanes generated child key '" + generatedKey + "' is already used with incompatible launch params.");
|
|
436
|
+
}
|
|
437
|
+
stages.push({ key: stageKey, generatedKey, resume, params });
|
|
438
|
+
}
|
|
439
|
+
normalized.push({ key: laneKey, stages });
|
|
440
|
+
}
|
|
441
|
+
return normalized;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function runLane(lane, firstResult, observe) {
|
|
445
|
+
const records = [];
|
|
446
|
+
const appendSkipped = (start) => {
|
|
447
|
+
for (let index = start; index < lane.stages.length; index++) records.push({ key: lane.stages[index].key, state: "skipped" });
|
|
448
|
+
};
|
|
449
|
+
const finish = (state, failedStage) => ({ key: lane.key, state, ...(failedStage ? { failedStage } : {}), stages: records });
|
|
450
|
+
const visit = (index, previous) => {
|
|
451
|
+
if (index >= lane.stages.length) return Promise.resolve(finish("complete"));
|
|
452
|
+
const stage = lane.stages[index];
|
|
453
|
+
if (index === 0) {
|
|
454
|
+
const record = laneStageRecord(stage.key, previous);
|
|
455
|
+
records.push(record);
|
|
456
|
+
if (record.state !== "completed") {
|
|
457
|
+
appendSkipped(index + 1);
|
|
458
|
+
return Promise.resolve(finish("blocked", stage.key));
|
|
459
|
+
}
|
|
460
|
+
return visit(index + 1, previous);
|
|
461
|
+
}
|
|
462
|
+
if (stage.resume === "previous" && (!previous || typeof previous.runId !== "string" || !previous.runId.trim())) {
|
|
463
|
+
records.push({ key: stage.key, state: "blocked", error: "Previous stage did not return a retained run id." });
|
|
464
|
+
appendSkipped(index + 1);
|
|
465
|
+
return Promise.resolve(finish("blocked", stage.key));
|
|
466
|
+
}
|
|
467
|
+
if (!previous || previous.ok !== true || laneStageIsBlocked(previous)) {
|
|
468
|
+
records.push({ key: stage.key, state: "blocked", error: "Previous stage did not complete successfully." });
|
|
469
|
+
appendSkipped(index + 1);
|
|
470
|
+
return Promise.resolve(finish("blocked", stage.key));
|
|
471
|
+
}
|
|
472
|
+
const params = laneStageParams(stage, previous);
|
|
473
|
+
if (!params) {
|
|
474
|
+
records.push({ key: stage.key, state: "blocked", error: "Previous stage did not return a retained run id." });
|
|
475
|
+
appendSkipped(index + 1);
|
|
476
|
+
return Promise.resolve(finish("blocked", stage.key));
|
|
477
|
+
}
|
|
478
|
+
let launched;
|
|
479
|
+
try {
|
|
480
|
+
launched = runCollected(stage.generatedKey, params, observe, lane.key);
|
|
481
|
+
} catch (error) {
|
|
482
|
+
const failed = laneFailure(stage.generatedKey, error);
|
|
483
|
+
records.push(laneStageRecord(stage.key, failed));
|
|
484
|
+
appendSkipped(index + 1);
|
|
485
|
+
return Promise.resolve(finish("blocked", stage.key));
|
|
486
|
+
}
|
|
487
|
+
return launched.then((result) => {
|
|
488
|
+
const record = laneStageRecord(stage.key, result);
|
|
489
|
+
records.push(record);
|
|
490
|
+
if (record.state === "completed") return visit(index + 1, result);
|
|
491
|
+
appendSkipped(index + 1);
|
|
492
|
+
return finish("blocked", stage.key);
|
|
493
|
+
}, (error) => {
|
|
494
|
+
const failed = laneFailure(stage.generatedKey, error);
|
|
495
|
+
records.push(laneStageRecord(stage.key, failed));
|
|
496
|
+
appendSkipped(index + 1);
|
|
497
|
+
return finish("blocked", stage.key);
|
|
498
|
+
});
|
|
499
|
+
};
|
|
500
|
+
return visit(0, firstResult);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function runLanes(laneSpecs) {
|
|
504
|
+
const lanes = validateLaneSpecs(laneSpecs);
|
|
505
|
+
const firstItems = lanes.map((lane) => {
|
|
506
|
+
const first = lane.stages[0];
|
|
507
|
+
return { key: first.generatedKey, ...first.params };
|
|
508
|
+
});
|
|
509
|
+
// Share the runs.all batch launcher while allowing each lane to advance independently.
|
|
510
|
+
const firstBatch = launchRunsAll(firstItems, lanes.map((lane) => lane.key));
|
|
511
|
+
const firstResults = firstBatch.launched.map(({ promise }) => promise.then(decorateWorkflowChildResult));
|
|
512
|
+
let trackedAggregate;
|
|
513
|
+
const observe = (observations) => trackRunObservation(observations, trackedAggregate);
|
|
514
|
+
const laneAggregate = Promise.all(lanes.map((lane, index) => firstResults[index].then(
|
|
515
|
+
(result) => runLane(lane, result, observe),
|
|
516
|
+
(error) => runLane(lane, laneFailure(lane.stages[0].generatedKey, error), observe),
|
|
517
|
+
)));
|
|
518
|
+
trackedAggregate = trackRunObservation(firstBatch.launched.map(({ key, callId }) => ({ key, operation: "run", callId })), laneAggregate);
|
|
519
|
+
return trackedAggregate;
|
|
520
|
+
}
|
|
521
|
+
|
|
280
522
|
function formatRef(result) {
|
|
281
523
|
if (!result || typeof result !== "object") throw new Error("runs.ref(result) requires a run result object.");
|
|
282
524
|
const parts = ["run " + (result.key || "unknown")];
|
|
@@ -321,8 +563,28 @@ function validateExtensionBindings(value, label) {
|
|
|
321
563
|
if (new TextEncoder().encode(stableRunJson(value)).byteLength > 16384) throw new Error(label + " extensionBindings canonical JSON exceeds 16384 bytes.");
|
|
322
564
|
}
|
|
323
565
|
|
|
566
|
+
function validateLaneMetadata(value, label, workflowKey) {
|
|
567
|
+
if (value === undefined) return;
|
|
568
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(label + " must be a plain JSON object.");
|
|
569
|
+
const fields = Object.keys(value);
|
|
570
|
+
const allowed = ["version", "key", "mode", "sourceRef", "claims", "outputPaths"];
|
|
571
|
+
const unknown = fields.filter((field) => !allowed.includes(field));
|
|
572
|
+
if (unknown.length > 0) throw new Error(label + " has unsupported fields: " + unknown.join(", ") + ".");
|
|
573
|
+
if (value.version !== 1) throw new Error(label + ".version must be 1.");
|
|
574
|
+
if (typeof value.key !== "string" || !value.key.trim() || !runKeyPattern.test(value.key.trim())) throw new Error(label + ".key is invalid.");
|
|
575
|
+
if (workflowKey !== undefined && value.key.trim() !== workflowKey) throw new Error(label + ".key must match workflow key '" + workflowKey + "'.");
|
|
576
|
+
if (value.mode !== undefined && !["mutation", "review", "scout", "gate"].includes(value.mode)) throw new Error(label + ".mode is invalid.");
|
|
577
|
+
const bounded = (entry, maxBytes) => typeof entry === "string" && entry.trim() && new TextEncoder().encode(entry.trim()).byteLength <= maxBytes && !/[\r\n\u0000]/.test(entry);
|
|
578
|
+
if (value.sourceRef !== undefined && !bounded(value.sourceRef, 128)) throw new Error(label + ".sourceRef is invalid.");
|
|
579
|
+
for (const [name, maxItems, maxLength] of [["claims", 20, 160], ["outputPaths", 10, 256]]) {
|
|
580
|
+
if (value[name] === undefined) continue;
|
|
581
|
+
if (!Array.isArray(value[name]) || value[name].length > maxItems || value[name].some((entry) => !bounded(entry, maxLength))) throw new Error(label + "." + name + " is invalid.");
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
324
585
|
function validateRunCall(key, params, label, fingerprints) {
|
|
325
586
|
if (typeof key !== "string" || !runKeyPattern.test(key)) throw new Error(label + " has an invalid key.");
|
|
587
|
+
if (hostKeys.has(key)) throw new Error("Workflow key '" + key + "' is already used by runs.host.");
|
|
326
588
|
if (!params || typeof params !== "object" || Array.isArray(params)) throw new Error(label + " requires a params object.");
|
|
327
589
|
if (Object.prototype.hasOwnProperty.call(params, "action") || Object.prototype.hasOwnProperty.call(params, "workflowScript") || Object.prototype.hasOwnProperty.call(params, "tasks") || Object.prototype.hasOwnProperty.call(params, "chain") || Object.prototype.hasOwnProperty.call(params, "parallel") || Object.prototype.hasOwnProperty.call(params, "concurrency") || Object.prototype.hasOwnProperty.call(params, "chainDir")) {
|
|
328
590
|
const hint = label === "runs.run" ? "; use runs.all(...) and JavaScript control flow for orchestration." : ".";
|
|
@@ -330,6 +592,7 @@ function validateRunCall(key, params, label, fingerprints) {
|
|
|
330
592
|
}
|
|
331
593
|
if (Object.prototype.hasOwnProperty.call(params, "clarify")) throw new Error(label + " does not support clarify UI.");
|
|
332
594
|
if (params.worktree !== undefined && typeof params.worktree !== "boolean") throw new Error(label + " worktree must be true or false.");
|
|
595
|
+
validateLaneMetadata(params.lane, label + " lane", key);
|
|
333
596
|
if (params.gate !== undefined && (typeof params.gate !== "string" || !params.gate.trim())) throw new Error(label + " gate must be a non-empty command string.");
|
|
334
597
|
if (params.gate !== undefined && params.acceptance !== undefined) throw new Error(label + " gate cannot be combined with acceptance; use one gate command or acceptance.verify.");
|
|
335
598
|
if (params.gate !== undefined && params.resume !== undefined) throw new Error(label + " gate is not supported with retained resume.");
|
|
@@ -354,6 +617,44 @@ function validateRunCall(key, params, label, fingerprints) {
|
|
|
354
617
|
fingerprints.set(key, fingerprint);
|
|
355
618
|
}
|
|
356
619
|
|
|
620
|
+
const hostKeys = new Set();
|
|
621
|
+
|
|
622
|
+
function validateHostCommand(key, params) {
|
|
623
|
+
validateLaneKey(key, "runs.host");
|
|
624
|
+
if (!params || typeof params !== "object" || Array.isArray(params)) throw new Error("runs.host('" + key + "') params must be an object.");
|
|
625
|
+
const allowed = new Set(["kind", "command", "timeoutMs", "output", "role", "provider"]);
|
|
626
|
+
const unknown = Object.keys(params).filter((field) => !allowed.has(field));
|
|
627
|
+
if (unknown.length) throw new Error("runs.host('" + key + "') params have unsupported fields: " + unknown.join(", ") + ".");
|
|
628
|
+
if (params.kind !== "command") throw new Error("runs.host('" + key + "') kind must be 'command'.");
|
|
629
|
+
if (typeof params.command !== "string" || !params.command.trim() || params.command.includes("\u0000") || new TextEncoder().encode(params.command.trim()).byteLength > 16384) throw new Error("runs.host('" + key + "') command must be a non-empty string of at most 16384 bytes without NUL.");
|
|
630
|
+
if (!Number.isInteger(params.timeoutMs) || params.timeoutMs < 1 || params.timeoutMs > 86400000) throw new Error("runs.host('" + key + "') timeoutMs must be an integer from 1 to 86400000.");
|
|
631
|
+
if (params.output !== undefined && (typeof params.output !== "string" || !params.output.trim() || /^[/\\]|(?:^|[/\\])\.\.(?:[/\\]|$)/.test(params.output) || /[\u0000-\u001f\u007f]/.test(params.output) || new TextEncoder().encode(params.output.trim()).byteLength > 240)) throw new Error("runs.host('" + key + "') output must be a bounded relative path without traversal or control characters.");
|
|
632
|
+
if (params.role !== undefined && params.role !== "ci" && params.role !== "gate") throw new Error("runs.host('" + key + "') role must be 'ci' or 'gate'.");
|
|
633
|
+
if (params.provider !== undefined && (typeof params.provider !== "string" || !params.provider.trim() || /[\r\n\u0000]/.test(params.provider) || new TextEncoder().encode(params.provider.trim()).byteLength > 64)) throw new Error("runs.host('" + key + "') provider must be a non-empty single-line string of at most 64 bytes.");
|
|
634
|
+
assertJsonValue(params, "runs.host('" + key + "') params");
|
|
635
|
+
if (hostKeys.has(key) || runFingerprints.has(key)) throw new Error("Workflow key '" + key + "' is already in use.");
|
|
636
|
+
if (hostKeys.size >= 32) throw new Error("workflowScript supports at most 32 runs.host calls.");
|
|
637
|
+
hostKeys.add(key);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function launchRunsAll(items, generatedLaneKeys) {
|
|
641
|
+
if (!Array.isArray(items)) throw new Error("runs.all(items) requires an array.");
|
|
642
|
+
const fingerprints = new Map(runFingerprints);
|
|
643
|
+
const calls = [];
|
|
644
|
+
for (let index = 0; index < items.length; index++) {
|
|
645
|
+
if (!Object.prototype.hasOwnProperty.call(items, index)) throw new Error("runs.all items must not contain sparse entries.");
|
|
646
|
+
const item = items[index];
|
|
647
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) throw new Error("runs.all item " + index + " must be an object.");
|
|
648
|
+
const { key, ...params } = item;
|
|
649
|
+
validateRunCall(key, params, "runs.all item " + index, fingerprints);
|
|
650
|
+
calls.push({ key, params });
|
|
651
|
+
}
|
|
652
|
+
runFingerprints = fingerprints;
|
|
653
|
+
const batch = { id: "batch-" + (++nextCallId), calls };
|
|
654
|
+
const launched = calls.map(({ key, params }, index) => runHostCall(key, params, true, batch, generatedLaneKeys?.[index]));
|
|
655
|
+
return { calls, launched };
|
|
656
|
+
}
|
|
657
|
+
|
|
357
658
|
const runs = Object.freeze({
|
|
358
659
|
run(key, params) {
|
|
359
660
|
validateRunCall(key, params, "runs.run", runFingerprints);
|
|
@@ -361,22 +662,16 @@ const runs = Object.freeze({
|
|
|
361
662
|
return trackRunObservation([{ key, operation: "run", callId: launched.callId }], launched.promise.then(decorateWorkflowChildResult));
|
|
362
663
|
},
|
|
363
664
|
all(items) {
|
|
364
|
-
|
|
365
|
-
const fingerprints = new Map(runFingerprints);
|
|
366
|
-
const calls = [];
|
|
367
|
-
for (let index = 0; index < items.length; index++) {
|
|
368
|
-
if (!Object.prototype.hasOwnProperty.call(items, index)) throw new Error("runs.all items must not contain sparse entries.");
|
|
369
|
-
const item = items[index];
|
|
370
|
-
if (!item || typeof item !== "object" || Array.isArray(item)) throw new Error("runs.all item " + index + " must be an object.");
|
|
371
|
-
const { key, ...params } = item;
|
|
372
|
-
validateRunCall(key, params, "runs.all item " + index, fingerprints);
|
|
373
|
-
calls.push({ key, params });
|
|
374
|
-
}
|
|
375
|
-
runFingerprints = fingerprints;
|
|
376
|
-
const batch = { id: "batch-" + (++nextCallId), calls };
|
|
377
|
-
const launched = calls.map(({ key, params }) => runHostCall(key, params, true, batch));
|
|
665
|
+
const { calls, launched } = launchRunsAll(items);
|
|
378
666
|
return trackRunObservation(launched.map(({ key, callId }) => ({ key, operation: "run", callId })), Promise.all(launched.map(({ promise }) => promise)).then((results) => wrapRunsAllResults(results.map(decorateWorkflowChildResult), calls.map(({ key }) => key))));
|
|
379
667
|
},
|
|
668
|
+
lanes(laneSpecs) {
|
|
669
|
+
return runLanes(laneSpecs);
|
|
670
|
+
},
|
|
671
|
+
host(key, params) {
|
|
672
|
+
validateHostCommand(key, params);
|
|
673
|
+
return hostCall("host", { key, params }, { key, operation: "host" });
|
|
674
|
+
},
|
|
380
675
|
steer(key, message, options = {}) {
|
|
381
676
|
if (typeof key !== "string" || !runKeyPattern.test(key)) throw new Error("runs.steer has an invalid key.");
|
|
382
677
|
if (typeof message !== "string" || !message.trim()) throw new Error("runs.steer message must be a non-empty string.");
|
|
@@ -440,7 +735,7 @@ function isSyntaxError(error) {
|
|
|
440
735
|
return error instanceof SyntaxError || error?.name === "SyntaxError";
|
|
441
736
|
}
|
|
442
737
|
|
|
443
|
-
const NESTED_ASYNC_WORKFLOW_ERROR = "workflowScript does not support nested async functions. Use top-level await, plain helper functions that return runs.run(...), or explicit Promise chains so workflows stay portable across Node and Bun.";
|
|
738
|
+
const NESTED_ASYNC_WORKFLOW_ERROR = "workflowScript validation failed before child launch; no children launched. workflowScript does not support nested async functions. Use top-level await, plain helper functions that return runs.run(...), or explicit Promise chains so workflows stay portable across Node and Bun. Parallel plus sequential rewrite: const a = runs.run(\"a\", { agent: \"worker\", task: \"A\" }); const writer = await runs.run(\"writer\", { agent: \"worker\", task: \"Write\" }); const review = await runs.run(\"review\", { agent: \"reviewer\", task: writer.output }); const [aResult] = await Promise.all([a]); return { a: aResult.output, issue: { writerRunId: writer.runId, reviewRunId: review.runId } };";
|
|
444
739
|
const AST_SCALAR_KEYS = new Set(["type", "start", "end"]);
|
|
445
740
|
|
|
446
741
|
function assertPortableWorkflowScript(source) {
|
|
@@ -627,7 +922,12 @@ parentPort.on("message", async (message) => {
|
|
|
627
922
|
}
|
|
628
923
|
}
|
|
629
924
|
const persistedValue = value === undefined ? null : omitUndefinedWorkflowValues(value);
|
|
630
|
-
|
|
925
|
+
try {
|
|
926
|
+
assertJsonValue(persistedValue, "return");
|
|
927
|
+
} catch (error) {
|
|
928
|
+
parentPort.postMessage({ type: "error", errorPhase: "return-serialization", error: formatWorkflowScriptError(error) });
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
631
931
|
parentPort.postMessage({ type: "complete", value: persistedValue });
|
|
632
932
|
} catch (error) {
|
|
633
933
|
parentPort.postMessage({ type: "error", error: isSyntaxError(error) ? formatWorkflowScriptSyntaxError(error) : formatWorkflowScriptError(error), ...(error && error.workflowErrorKind === "detached-child" ? { errorKind: "detached-child" } : {}) });
|
|
@@ -638,6 +938,8 @@ parentPort.on("message", async (message) => {
|
|
|
638
938
|
export interface WorkflowScriptChildResult {
|
|
639
939
|
key: string;
|
|
640
940
|
ok: boolean;
|
|
941
|
+
lane?: import("../shared/types.ts").WorkflowLaneMetadata;
|
|
942
|
+
terminalOutcome?: import("../shared/types.ts").WorkflowTerminalOutcome;
|
|
641
943
|
stopped?: boolean;
|
|
642
944
|
/** Canonical child agent name when launch resolution produced one. */
|
|
643
945
|
agent?: string;
|
|
@@ -650,14 +952,16 @@ export interface WorkflowScriptChildResult {
|
|
|
650
952
|
requestedContext?: "fresh" | "fork";
|
|
651
953
|
resolvedContext?: "fresh" | "fork" | "mixed";
|
|
652
954
|
outputReference?: string;
|
|
955
|
+
outputPathMapping?: { requestedPath: string; savedPath: string };
|
|
956
|
+
externalAdapter?: import("../shared/types.ts").ExternalCliReceiptMetadata;
|
|
653
957
|
resumability?: { state: "resumable" } | { state: "not-resumable"; reason: string };
|
|
654
958
|
continuation?: { runIds: string[] };
|
|
655
959
|
artifactPaths: string[];
|
|
656
|
-
results?:
|
|
960
|
+
results?: SingleResult[];
|
|
657
961
|
}
|
|
658
962
|
|
|
659
963
|
export interface WorkflowScriptTraceEntry {
|
|
660
|
-
operation: "run" | "status" | "steer";
|
|
964
|
+
operation: "run" | "status" | "steer" | "host";
|
|
661
965
|
key: string;
|
|
662
966
|
state: "started" | "completed" | "failed" | "detached" | "stopped" | "reused" | "queued" | "delivered" | "missed";
|
|
663
967
|
/** Canonical child agent name when resolved launch or result data is available. */
|
|
@@ -667,6 +971,10 @@ export interface WorkflowScriptTraceEntry {
|
|
|
667
971
|
phase?: string;
|
|
668
972
|
label?: string;
|
|
669
973
|
error?: string;
|
|
974
|
+
/** Internal provenance for a generated runs.lanes child key. */
|
|
975
|
+
generatedLaneKey?: string;
|
|
976
|
+
lane?: import("../shared/types.ts").WorkflowLaneMetadata;
|
|
977
|
+
warning?: string;
|
|
670
978
|
}
|
|
671
979
|
|
|
672
980
|
export interface WorkflowSteerOptions {
|
|
@@ -705,9 +1013,9 @@ export interface WorkflowScriptResult {
|
|
|
705
1013
|
|
|
706
1014
|
export class WorkflowScriptError extends Error {
|
|
707
1015
|
readonly partial: Omit<WorkflowScriptResult, "value">;
|
|
708
|
-
readonly errorKind?: "detached-child";
|
|
1016
|
+
readonly errorKind?: "detached-child" | "timeout";
|
|
709
1017
|
|
|
710
|
-
constructor(message: string, partial: Omit<WorkflowScriptResult, "value">, errorKind?: "detached-child") {
|
|
1018
|
+
constructor(message: string, partial: Omit<WorkflowScriptResult, "value">, errorKind?: "detached-child" | "timeout") {
|
|
711
1019
|
super(message);
|
|
712
1020
|
this.name = "WorkflowScriptError";
|
|
713
1021
|
this.partial = partial;
|
|
@@ -717,13 +1025,19 @@ export class WorkflowScriptError extends Error {
|
|
|
717
1025
|
|
|
718
1026
|
export interface RunWorkflowScriptOptions {
|
|
719
1027
|
script: string;
|
|
1028
|
+
/** Host-only first-slice admission context. It is never sent to the workflow worker. */
|
|
1029
|
+
oneUsePermit?: { claim: (key: string) => string | undefined };
|
|
720
1030
|
timeoutMs?: number;
|
|
721
1031
|
signal?: AbortSignal;
|
|
1032
|
+
/** Maximum children executing concurrently within this workflow. Defaults to 20. */
|
|
1033
|
+
globalConcurrencyLimit?: number;
|
|
722
1034
|
admit?: (calls: Array<{ key: string; params: Record<string, unknown> }>) => void | Promise<void>;
|
|
723
|
-
launch: (key: string, params: Record<string, unknown>, signal: AbortSignal, admission: { admitted: boolean }) => Promise<WorkflowScriptChildResult>;
|
|
1035
|
+
launch: (key: string, params: Record<string, unknown>, signal: AbortSignal, admission: { admitted: boolean; batch: boolean }) => Promise<WorkflowScriptChildResult>;
|
|
724
1036
|
resolveResume?: (reference: WorkflowReceiptResumeReference, signal: AbortSignal) => string | WorkflowResolvedResumeReference | Promise<string | WorkflowResolvedResumeReference>;
|
|
725
1037
|
status: (keyOrRunId: string, signal: AbortSignal) => Promise<WorkflowScriptChildResult>;
|
|
726
1038
|
steer?: (key: string, message: string, options: WorkflowSteerOptions, signal: AbortSignal) => Promise<WorkflowSteerResult>;
|
|
1039
|
+
host?: (key: string, params: WorkflowHostCommandParams, signal: AbortSignal) => Promise<WorkflowHostCommandResult>;
|
|
1040
|
+
onHostStep?: (hostStep: HostStepNodeV1) => void;
|
|
727
1041
|
state?: {
|
|
728
1042
|
get: (key: string) => unknown | Promise<unknown>;
|
|
729
1043
|
set: (key: string, value: unknown) => void | Promise<void>;
|
|
@@ -827,6 +1141,15 @@ export function formatWorkflowJsonPreview(value: unknown, maxLength: number): st
|
|
|
827
1141
|
}
|
|
828
1142
|
}
|
|
829
1143
|
|
|
1144
|
+
function workflowReturnRecoveryHint(children: WorkflowScriptChildResult[]): string {
|
|
1145
|
+
if (children.length === 0) return " Return only plain JSON data. For a child result, select fields such as { runId: child.runId, ok: child.ok, outputReference: child.outputReference }.";
|
|
1146
|
+
const references = children.slice(0, 10).map((child) => {
|
|
1147
|
+
const fields = [child.runId ? `runId=${child.runId.slice(0, 500)}` : undefined, child.outputReference ? `outputReference=${child.outputReference.slice(0, 500)}` : undefined, child.artifactPaths[0] ? `artifact=${child.artifactPaths[0].slice(0, 500)}` : undefined].filter((field): field is string => field !== undefined);
|
|
1148
|
+
return `'${child.key}'${fields.length > 0 ? ` (${fields.join(", ")})` : ""}`;
|
|
1149
|
+
});
|
|
1150
|
+
return ` Child work completed before return serialization failed. Recover outputs from: ${references.join(", ")}${children.length > references.length ? `, and ${children.length - references.length} more` : ""}. Return a plain projection such as { runId: child.runId, ok: child.ok, outputReference: child.outputReference }.`;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
830
1153
|
export interface SimpleWorkflowRunPreview {
|
|
831
1154
|
agent?: string;
|
|
832
1155
|
task?: string;
|
|
@@ -864,6 +1187,230 @@ function validateKey(value: unknown, owner = "runs.run"): string {
|
|
|
864
1187
|
return value;
|
|
865
1188
|
}
|
|
866
1189
|
|
|
1190
|
+
type AstNode = {
|
|
1191
|
+
type: string;
|
|
1192
|
+
loc?: { start: { line: number; column: number } };
|
|
1193
|
+
[key: string]: unknown;
|
|
1194
|
+
};
|
|
1195
|
+
|
|
1196
|
+
const AST_LOCATION_KEYS = new Set(["type", "start", "end", "loc", "range"]);
|
|
1197
|
+
|
|
1198
|
+
function astNode(value: unknown): value is AstNode {
|
|
1199
|
+
return Boolean(value) && typeof value === "object" && typeof (value as { type?: unknown }).type === "string";
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
function literalString(node: unknown): string | undefined {
|
|
1203
|
+
if (!astNode(node)) return undefined;
|
|
1204
|
+
if (node.type === "Literal" && typeof node.value === "string") return node.value;
|
|
1205
|
+
if (node.type === "TemplateLiteral" && Array.isArray(node.expressions) && node.expressions.length === 0 && Array.isArray(node.quasis)) {
|
|
1206
|
+
const first = node.quasis[0] as { value?: { cooked?: unknown } } | undefined;
|
|
1207
|
+
if (typeof first?.value?.cooked === "string") return first.value.cooked;
|
|
1208
|
+
}
|
|
1209
|
+
return undefined;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
function directRunsCall(node: unknown, method: "run" | "all" | "host"): node is AstNode {
|
|
1213
|
+
if (!astNode(node) || node.type !== "CallExpression" || !astNode(node.callee) || node.callee.type !== "MemberExpression") return false;
|
|
1214
|
+
const property = node.callee.computed === true ? literalString(node.callee.property) : astNode(node.callee.property) && node.callee.property.type === "Identifier" ? node.callee.property.name : undefined;
|
|
1215
|
+
return property === method && astNode(node.callee.object) && node.callee.object.type === "Identifier" && node.callee.object.name === "runs";
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
function validateStaticHostCall(call: AstNode): WorkflowScriptValidationError[] {
|
|
1219
|
+
const args = Array.isArray(call.arguments) ? call.arguments : [];
|
|
1220
|
+
const keyNode = astNode(args[0]) ? args[0] : undefined;
|
|
1221
|
+
const params = astNode(args[1]) ? args[1] : undefined;
|
|
1222
|
+
const errors: WorkflowScriptValidationError[] = [];
|
|
1223
|
+
const key = literalString(keyNode);
|
|
1224
|
+
if (keyNode && key !== undefined && !KEY_PATTERN.test(key)) errors.push({ message: "runs.host key must be 1-128 characters using letters, numbers, '.', '_' or '-', and start with a letter or number.", ...nodeLocation(keyNode) });
|
|
1225
|
+
if (!params || params.type !== "ObjectExpression" || !Array.isArray(params.properties) || params.properties.some((property) => !astNode(property) || property.type !== "Property")) return errors;
|
|
1226
|
+
const allowed = new Set(["kind", "command", "timeoutMs", "output", "role", "provider"]);
|
|
1227
|
+
for (const property of params.properties) {
|
|
1228
|
+
if (!astNode(property)) continue;
|
|
1229
|
+
const name = staticPropertyKey(property);
|
|
1230
|
+
if (name !== undefined && !allowed.has(name)) errors.push({ message: `runs.host params contain unsupported field '${name}'.`, ...nodeLocation(property) });
|
|
1231
|
+
}
|
|
1232
|
+
const kindNode = directObjectPropertyValue(params, "kind");
|
|
1233
|
+
const commandNode = directObjectPropertyValue(params, "command");
|
|
1234
|
+
const timeoutNode = directObjectPropertyValue(params, "timeoutMs");
|
|
1235
|
+
const kind = literalString(kindNode);
|
|
1236
|
+
const command = literalString(commandNode);
|
|
1237
|
+
const timeout = astNode(timeoutNode) && timeoutNode.type === "Literal" ? timeoutNode.value : undefined;
|
|
1238
|
+
if (!kindNode) errors.push({ message: "runs.host params require kind: 'command'.", ...nodeLocation(params) });
|
|
1239
|
+
else if (kind !== undefined && kind !== "command") errors.push({ message: "runs.host params kind must be 'command'.", ...nodeLocation(kindNode) });
|
|
1240
|
+
if (!commandNode) errors.push({ message: "runs.host params require command.", ...nodeLocation(params) });
|
|
1241
|
+
else if (command !== undefined && !command.trim()) errors.push({ message: "runs.host params command must be non-empty.", ...nodeLocation(commandNode) });
|
|
1242
|
+
if (!timeoutNode) errors.push({ message: "runs.host params require timeoutMs.", ...nodeLocation(params) });
|
|
1243
|
+
else if (typeof timeout === "number" && (!Number.isInteger(timeout) || timeout < 1)) errors.push({ message: "runs.host params timeoutMs must be a positive integer.", ...nodeLocation(timeoutNode) });
|
|
1244
|
+
const outputNode = directObjectPropertyValue(params, "output");
|
|
1245
|
+
const output = literalString(outputNode);
|
|
1246
|
+
if (outputNode && output !== undefined && (!output.trim() || /^[/\\]|(?:^|[/\\])\.\.(?:[/\\]|$)/.test(output))) errors.push({ message: "runs.host params output must be a non-empty relative path without traversal.", ...nodeLocation(outputNode) });
|
|
1247
|
+
const roleNode = directObjectPropertyValue(params, "role");
|
|
1248
|
+
const role = literalString(roleNode);
|
|
1249
|
+
if (roleNode && role !== undefined && role !== "ci" && role !== "gate") errors.push({ message: "runs.host params role must be 'ci' or 'gate'.", ...nodeLocation(roleNode) });
|
|
1250
|
+
return errors;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
function nodeLocation(node: AstNode): Pick<WorkflowScriptValidationError, "line" | "column"> {
|
|
1254
|
+
return node.loc ? { line: Math.max(1, node.loc.start.line - 1), column: node.loc.start.column + 1 } : {};
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
function walkAst(node: unknown, visit: (node: AstNode) => void, includeNestedFunctions = true): void {
|
|
1258
|
+
if (Array.isArray(node)) {
|
|
1259
|
+
for (const item of node) walkAst(item, visit, includeNestedFunctions);
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
if (!astNode(node)) return;
|
|
1263
|
+
visit(node);
|
|
1264
|
+
if (!includeNestedFunctions && (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression")) return;
|
|
1265
|
+
for (const [key, child] of Object.entries(node)) {
|
|
1266
|
+
if (!AST_LOCATION_KEYS.has(key)) walkAst(child, visit, includeNestedFunctions);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
function definitelyNonJson(node: AstNode, normalizeUndefined = false): string | undefined {
|
|
1271
|
+
if (node.type === "Literal") {
|
|
1272
|
+
if (typeof node.bigint === "string") return "BigInt values are not JSON-representable";
|
|
1273
|
+
if (node.regex !== undefined) return "regular expressions are not JSON-representable";
|
|
1274
|
+
return undefined;
|
|
1275
|
+
}
|
|
1276
|
+
if (node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") return "functions are not JSON-representable";
|
|
1277
|
+
if (node.type === "Identifier" && node.name === "undefined") return normalizeUndefined ? undefined : "undefined is not JSON-representable";
|
|
1278
|
+
if (node.type === "UnaryExpression" && node.operator === "void") return normalizeUndefined ? undefined : "undefined is not JSON-representable";
|
|
1279
|
+
if (node.type === "ArrayExpression" && Array.isArray(node.elements)) {
|
|
1280
|
+
if (node.elements.some((entry) => entry === null)) return "sparse arrays are not JSON-representable";
|
|
1281
|
+
for (const entry of node.elements) if (astNode(entry)) {
|
|
1282
|
+
const error = definitelyNonJson(entry, normalizeUndefined);
|
|
1283
|
+
if (error) return error;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
if (node.type === "ObjectExpression" && Array.isArray(node.properties)) {
|
|
1287
|
+
const values = new Map<string, AstNode>();
|
|
1288
|
+
for (const property of node.properties) {
|
|
1289
|
+
if (!astNode(property) || property.type !== "Property" || !astNode(property.value)) return undefined;
|
|
1290
|
+
const key = staticPropertyKey(property);
|
|
1291
|
+
if (key === undefined) return undefined;
|
|
1292
|
+
values.set(key, property.value);
|
|
1293
|
+
}
|
|
1294
|
+
for (const value of values.values()) {
|
|
1295
|
+
const error = definitelyNonJson(value, normalizeUndefined);
|
|
1296
|
+
if (error) return error;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
return undefined;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
function staticPropertyKey(property: AstNode): string | undefined {
|
|
1303
|
+
return property.computed === true
|
|
1304
|
+
? literalString(property.key)
|
|
1305
|
+
: literalString(property.key) ?? (astNode(property.key) && property.key.type === "Identifier" ? property.key.name as string : undefined);
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
function directObjectPropertyValue(node: AstNode, name: string): AstNode | undefined {
|
|
1309
|
+
if (node.type !== "ObjectExpression" || !Array.isArray(node.properties)) return undefined;
|
|
1310
|
+
let value: AstNode | undefined;
|
|
1311
|
+
for (const property of node.properties) {
|
|
1312
|
+
if (!astNode(property) || property.type !== "Property" || !astNode(property.value)) continue;
|
|
1313
|
+
if (staticPropertyKey(property) === name) value = property.value;
|
|
1314
|
+
}
|
|
1315
|
+
return value;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
function directRunsAllKeys(call: AstNode): Array<{ key: string; node: AstNode }> {
|
|
1319
|
+
const args = Array.isArray(call.arguments) ? call.arguments : [];
|
|
1320
|
+
const items = astNode(args[0]) && args[0].type === "ArrayExpression" && Array.isArray(args[0].elements) ? args[0].elements : [];
|
|
1321
|
+
return items.flatMap((item) => {
|
|
1322
|
+
if (!astNode(item)) return [];
|
|
1323
|
+
const keyNode = directObjectPropertyValue(item, "key");
|
|
1324
|
+
const key = literalString(keyNode);
|
|
1325
|
+
return keyNode && key !== undefined ? [{ key, node: keyNode }] : [];
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
/** Parse a workflowScript and apply only rules that are decidable from its local syntax. */
|
|
1330
|
+
export function validateWorkflowScript(script: string): WorkflowScriptValidationResult {
|
|
1331
|
+
const errors: WorkflowScriptValidationError[] = [];
|
|
1332
|
+
if (!script.trim()) return { ok: false, errors: [{ message: "workflowScript must not be empty." }] };
|
|
1333
|
+
let root: AstNode;
|
|
1334
|
+
try {
|
|
1335
|
+
const parser = requireFromPackage(resolveWorkflowParserEntry()) as { parse(source: string, options: Record<string, unknown>): unknown };
|
|
1336
|
+
root = parser.parse(`(async () => {\n${script}\n})()`, { ecmaVersion: "latest", sourceType: "script", locations: true }) as AstNode;
|
|
1337
|
+
} catch (error) {
|
|
1338
|
+
const location = error && typeof error === "object" && "loc" in error && error.loc && typeof error.loc === "object"
|
|
1339
|
+
? error.loc as { line?: unknown; column?: unknown }
|
|
1340
|
+
: undefined;
|
|
1341
|
+
const message = (error instanceof Error ? error.message : String(error)).replace(/\s+\(\d+:\d+\)$/, "");
|
|
1342
|
+
return { ok: false, errors: [{ message, ...(typeof location?.line === "number" ? { line: Math.max(1, location.line - 1) } : {}), ...(typeof location?.column === "number" ? { column: location.column + 1 } : {}) }] };
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
const wrapper = astNode(root.body) ? undefined : Array.isArray(root.body) && astNode(root.body[0]) && astNode(root.body[0].expression) && astNode(root.body[0].expression.callee)
|
|
1346
|
+
? root.body[0].expression.callee
|
|
1347
|
+
: undefined;
|
|
1348
|
+
const workflowBody = wrapper && astNode(wrapper.body) ? wrapper.body : root;
|
|
1349
|
+
walkAst(workflowBody, (node) => {
|
|
1350
|
+
if (node !== wrapper && node.async === true && (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression")) {
|
|
1351
|
+
errors.push({ message: "workflowScript does not support nested async functions. Use top-level await, plain helper functions that return runs.run(...), or explicit Promise chains.", ...nodeLocation(node) });
|
|
1352
|
+
}
|
|
1353
|
+
if (directRunsCall(node, "run")) {
|
|
1354
|
+
const args = Array.isArray(node.arguments) ? node.arguments : [];
|
|
1355
|
+
const keyNode = astNode(args[0]) ? args[0] : undefined;
|
|
1356
|
+
const key = literalString(keyNode);
|
|
1357
|
+
if (keyNode && key !== undefined && !KEY_PATTERN.test(key)) errors.push({ message: "runs.run key must be 1-128 characters using letters, numbers, '.', '_' or '-', and start with a letter or number.", ...nodeLocation(keyNode) });
|
|
1358
|
+
if (astNode(args[1])) {
|
|
1359
|
+
const message = definitelyNonJson(args[1]);
|
|
1360
|
+
if (message) errors.push({ message: `runs.run params are invalid: ${message}.`, ...nodeLocation(args[1]) });
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
if (directRunsCall(node, "all")) {
|
|
1364
|
+
for (const entry of directRunsAllKeys(node)) if (!KEY_PATTERN.test(entry.key)) errors.push({ message: "runs.all item key must be 1-128 characters using letters, numbers, '.', '_' or '-', and start with a letter or number.", ...nodeLocation(entry.node) });
|
|
1365
|
+
const args = Array.isArray(node.arguments) ? node.arguments : [];
|
|
1366
|
+
if (astNode(args[0]) && args[0].type === "ArrayExpression" && Array.isArray(args[0].elements)) {
|
|
1367
|
+
for (const item of args[0].elements) if (astNode(item)) {
|
|
1368
|
+
const message = definitelyNonJson(item);
|
|
1369
|
+
if (message) errors.push({ message: `runs.all item params are invalid: ${message}.`, ...nodeLocation(item) });
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
if (directRunsCall(node, "host")) errors.push(...validateStaticHostCall(node));
|
|
1374
|
+
const boundaryValue = node.type === "CallExpression" && astNode(node.callee) && node.callee.type === "Identifier" && node.callee.name === "emit" && Array.isArray(node.arguments) && astNode(node.arguments[0])
|
|
1375
|
+
? node.arguments[0]
|
|
1376
|
+
: node.type === "CallExpression" && astNode(node.callee) && node.callee.type === "MemberExpression" && astNode(node.callee.object) && node.callee.object.type === "Identifier" && node.callee.object.name === "state" && astNode(node.callee.property) && node.callee.property.type === "Identifier" && node.callee.property.name === "set" && Array.isArray(node.arguments) && astNode(node.arguments[1])
|
|
1377
|
+
? node.arguments[1]
|
|
1378
|
+
: undefined;
|
|
1379
|
+
if (boundaryValue) {
|
|
1380
|
+
const message = definitelyNonJson(boundaryValue);
|
|
1381
|
+
if (message) errors.push({ message: `workflowScript boundary value is invalid: ${message}.`, ...nodeLocation(boundaryValue) });
|
|
1382
|
+
}
|
|
1383
|
+
});
|
|
1384
|
+
walkAst(workflowBody, (node) => {
|
|
1385
|
+
if (node.type !== "ReturnStatement" || !astNode(node.argument)) return;
|
|
1386
|
+
const message = definitelyNonJson(node.argument, true);
|
|
1387
|
+
if (message) errors.push({ message: `workflowScript boundary value is invalid: ${message}.`, ...nodeLocation(node.argument) });
|
|
1388
|
+
}, false);
|
|
1389
|
+
|
|
1390
|
+
if (workflowBody.type === "BlockStatement" && Array.isArray(workflowBody.body)) {
|
|
1391
|
+
for (let statementIndex = 0; statementIndex < workflowBody.body.length; statementIndex++) {
|
|
1392
|
+
const statement = workflowBody.body[statementIndex];
|
|
1393
|
+
if (!astNode(statement) || statement.type !== "VariableDeclaration" || !Array.isArray(statement.declarations)) continue;
|
|
1394
|
+
for (const declaration of statement.declarations) {
|
|
1395
|
+
if (!astNode(declaration) || !astNode(declaration.id) || declaration.id.type !== "Identifier" || !astNode(declaration.init) || declaration.init.type !== "AwaitExpression" || !directRunsCall(declaration.init.argument, "all")) continue;
|
|
1396
|
+
const name = declaration.id.name as string;
|
|
1397
|
+
const keys = new Set(directRunsAllKeys(declaration.init.argument).map((entry) => entry.key));
|
|
1398
|
+
if (keys.size === 0) continue;
|
|
1399
|
+
const args = Array.isArray(declaration.init.argument.arguments) ? declaration.init.argument.arguments : [];
|
|
1400
|
+
const itemCount = astNode(args[0]) && args[0].type === "ArrayExpression" && Array.isArray(args[0].elements) ? args[0].elements.length : 0;
|
|
1401
|
+
const arrayResultShape = Array.from({ length: itemCount });
|
|
1402
|
+
for (const later of workflowBody.body.slice(statementIndex + 1)) walkAst(later, (node) => {
|
|
1403
|
+
if (node.type !== "MemberExpression" || !astNode(node.object) || node.object.type !== "Identifier" || node.object.name !== name) return;
|
|
1404
|
+
const property = node.computed === true ? literalString(node.property) : astNode(node.property) && node.property.type === "Identifier" ? node.property.name as string : undefined;
|
|
1405
|
+
if (property && keys.has(property) && !(property in arrayResultShape)) errors.push({ message: `runs.all returns an ordered array; '${name}.${property}' is keyed access. Use an index, destructuring, or map(...).`, ...nodeLocation(node) });
|
|
1406
|
+
}, false);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
const unique = errors.filter((error, index) => errors.findIndex((candidate) => candidate.message === error.message && candidate.line === error.line && candidate.column === error.column) === index);
|
|
1412
|
+
return { ok: unique.length === 0, errors: unique };
|
|
1413
|
+
}
|
|
867
1414
|
function workflowStringMetadata(params: Record<string, unknown>): Pick<WorkflowScriptTraceEntry, "phase" | "label" | "agent"> {
|
|
868
1415
|
return {
|
|
869
1416
|
...(typeof params.phase === "string" && params.phase.trim() ? { phase: params.phase.trim() } : {}),
|
|
@@ -893,9 +1440,48 @@ function resolveWorkflowParserEntry(): string {
|
|
|
893
1440
|
}
|
|
894
1441
|
}
|
|
895
1442
|
|
|
1443
|
+
const AUTO_RESUME_PARAM_KEYS = ["acceptance", "agentContract", "index", "intercomBridge", "label", "lane", "maxRuntimeMs", "output", "outputMode", "outputSchema", "phase", "skill", "skills", "task", "timeoutMs", "toolBudget", "worktree"] as const;
|
|
1444
|
+
|
|
1445
|
+
function isZeroUsage(usage: unknown): boolean {
|
|
1446
|
+
if (!isRecord(usage)) return false;
|
|
1447
|
+
const cost = usage.cost;
|
|
1448
|
+
return (usage.input ?? 0) === 0
|
|
1449
|
+
&& (usage.output ?? 0) === 0
|
|
1450
|
+
&& (usage.cacheRead ?? 0) === 0
|
|
1451
|
+
&& (usage.cacheWrite ?? 0) === 0
|
|
1452
|
+
&& (!isRecord(cost) || (cost.total ?? 0) === 0);
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
function setupAbortResumeParams(params: Record<string, unknown>, result: WorkflowScriptChildResult, signal: AbortSignal): Record<string, unknown> | undefined {
|
|
1456
|
+
if (signal.aborted || result.ok || result.stopped || result.interrupted || !result.runId) return undefined;
|
|
1457
|
+
const childResult = Array.isArray(result.results) && result.results.length === 1 && isRecord(result.results[0]) ? result.results[0] : undefined;
|
|
1458
|
+
const error = typeof childResult?.error === "string" ? childResult.error : result.error;
|
|
1459
|
+
if (error !== "This operation was aborted" || !isZeroUsage(childResult?.usage)) return undefined;
|
|
1460
|
+
const messages = Array.isArray(childResult?.messages) ? childResult.messages : [];
|
|
1461
|
+
const message = messages.findLast((entry) => isRecord(entry) && entry.role === "assistant");
|
|
1462
|
+
if (message !== undefined) {
|
|
1463
|
+
if (!isRecord(message)) return undefined;
|
|
1464
|
+
if (message.stopReason !== "error" || message.errorMessage !== error) return undefined;
|
|
1465
|
+
if (!Array.isArray(message.content) || message.content.length > 0 || !isZeroUsage(message.usage)) return undefined;
|
|
1466
|
+
if (Object.hasOwn(message, "diagnostics") || Object.hasOwn(message, "responseId")) return undefined;
|
|
1467
|
+
}
|
|
1468
|
+
const task = typeof params.task === "string" && params.task.trim() ? params.task.trim() : "Continue after the setup abort.";
|
|
1469
|
+
const resumeParams: Record<string, unknown> = { resume: result.runId, task };
|
|
1470
|
+
for (const key of AUTO_RESUME_PARAM_KEYS) {
|
|
1471
|
+
if (Object.hasOwn(params, key)) resumeParams[key] = params[key];
|
|
1472
|
+
}
|
|
1473
|
+
resumeParams.resume = result.runId;
|
|
1474
|
+
resumeParams.task = task;
|
|
1475
|
+
return resumeParams;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
896
1478
|
export async function runWorkflowScript(options: RunWorkflowScriptOptions): Promise<WorkflowScriptResult> {
|
|
897
1479
|
if (!options.script.trim()) throw new Error("workflowScript must not be empty.");
|
|
898
1480
|
if (options.timeoutMs !== undefined && (!Number.isInteger(options.timeoutMs) || options.timeoutMs < 1)) throw new Error("workflow script timeout must be a positive integer.");
|
|
1481
|
+
if (options.globalConcurrencyLimit !== undefined && (!Number.isInteger(options.globalConcurrencyLimit) || options.globalConcurrencyLimit < 1)) {
|
|
1482
|
+
throw new Error("workflow script global concurrency limit must be a positive integer.");
|
|
1483
|
+
}
|
|
1484
|
+
const launchSemaphore = new Semaphore(options.globalConcurrencyLimit ?? DEFAULT_GLOBAL_CONCURRENCY_LIMIT);
|
|
899
1485
|
|
|
900
1486
|
let acornPath: string;
|
|
901
1487
|
try {
|
|
@@ -909,13 +1495,15 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
909
1495
|
const trace: WorkflowScriptTraceEntry[] = [];
|
|
910
1496
|
const children = new Map<string, WorkflowScriptChildResult>();
|
|
911
1497
|
const childOrder: string[] = [];
|
|
912
|
-
const launches = new Map<string, { fingerprint: string; promise: Promise<WorkflowScriptChildResult>; observed: boolean }>();
|
|
1498
|
+
const launches = new Map<string, { fingerprint: string; promise: Promise<WorkflowScriptChildResult>; observed: boolean; generatedLaneKey?: string }>();
|
|
913
1499
|
const steers = new Map<number, { key: string; promise: Promise<WorkflowSteerResult>; observed: boolean }>();
|
|
1500
|
+
const hostCalls = new Map<number, { key: string; promise: Promise<WorkflowHostCommandResult>; observed: boolean }>();
|
|
914
1501
|
const stoppedLaunches = new Set<string>();
|
|
915
1502
|
const childStopControllers = new Map<string, AbortController>();
|
|
916
1503
|
const batchAdmissions = new Map<string, Promise<void>>();
|
|
917
1504
|
const observedRunCalls = new Set<number>();
|
|
918
1505
|
const observedSteerCalls = new Set<number>();
|
|
1506
|
+
const observedHostCalls = new Set<number>();
|
|
919
1507
|
const childController = new AbortController();
|
|
920
1508
|
let settled = false;
|
|
921
1509
|
let finishing = false;
|
|
@@ -936,7 +1524,18 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
936
1524
|
console.error("Workflow onTrace callback failed:", error);
|
|
937
1525
|
}
|
|
938
1526
|
};
|
|
1527
|
+
const hostStepChanged = (hostStep: HostStepNodeV1) => {
|
|
1528
|
+
try {
|
|
1529
|
+
options.onHostStep?.(hostStep);
|
|
1530
|
+
} catch (error) {
|
|
1531
|
+
console.error("Workflow onHostStep callback failed:", error);
|
|
1532
|
+
}
|
|
1533
|
+
};
|
|
939
1534
|
const stoppedChildResult = (key: string, message: string): WorkflowScriptChildResult => ({ key, ok: false, stopped: true, output: message, error: message, artifactPaths: [] });
|
|
1535
|
+
const responseBoundaryFailure = (key: string, error: unknown): WorkflowScriptChildResult => {
|
|
1536
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
1537
|
+
return { key, ok: false, output: text, error: text, artifactPaths: [] };
|
|
1538
|
+
};
|
|
940
1539
|
const stopChild = (key: string, message = `Workflow child '${key}' stopped by user.`): boolean => {
|
|
941
1540
|
if (!launches.has(key) || children.has(key)) return false;
|
|
942
1541
|
stoppedLaunches.add(key);
|
|
@@ -950,6 +1549,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
950
1549
|
...(started?.agent ? { agent: started.agent } : {}),
|
|
951
1550
|
...(started?.phase ? { phase: started.phase } : {}),
|
|
952
1551
|
...(started?.label ? { label: started.label } : {}),
|
|
1552
|
+
...(started?.generatedLaneKey ? { generatedLaneKey: started.generatedLaneKey } : {}),
|
|
953
1553
|
error: message,
|
|
954
1554
|
});
|
|
955
1555
|
traceChanged();
|
|
@@ -962,7 +1562,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
962
1562
|
if (settled || finishing) return;
|
|
963
1563
|
finishing = true;
|
|
964
1564
|
childController.abort("error" in outcome ? outcome.error : new Error("Workflow script completed."));
|
|
965
|
-
void Promise.allSettled([...steers.values()].map(({ promise }) => promise)).then(() => {
|
|
1565
|
+
void Promise.allSettled([...steers.values(), ...hostCalls.values()].map(({ promise }) => promise)).then(() => {
|
|
966
1566
|
if (settled) return;
|
|
967
1567
|
settled = true;
|
|
968
1568
|
options.registerStopChild?.(undefined);
|
|
@@ -975,10 +1575,12 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
975
1575
|
: "value" in outcome
|
|
976
1576
|
? (() => {
|
|
977
1577
|
const unobservedSteers = [...steers.values()].filter((steer) => !steer.observed).map((steer) => steer.key);
|
|
978
|
-
|
|
1578
|
+
if (unobservedSteers.length > 0) return new Error(`workflowScript completed with unawaited runs.steer call(s): ${unobservedSteers.map((key) => `'${key}'`).join(", ")}. Await or return each call.`);
|
|
1579
|
+
const unobservedHosts = [...hostCalls.values()].filter((host) => !host.observed).map((host) => host.key);
|
|
1580
|
+
return unobservedHosts.length > 0 ? new Error(`workflowScript completed with unawaited runs.host call(s): ${unobservedHosts.map((key) => `'${key}'`).join(", ")}. Await or return each call.`) : undefined;
|
|
979
1581
|
})()
|
|
980
1582
|
: undefined;
|
|
981
|
-
if ("error" in outcome) reject(new WorkflowScriptError(outcome.error.message, partial(), outcome.error.workflowErrorKind === "detached-child"
|
|
1583
|
+
if ("error" in outcome) reject(new WorkflowScriptError(outcome.error.message, partial(), outcome.error.workflowErrorKind === "detached-child" || outcome.error.workflowErrorKind === "timeout" ? outcome.error.workflowErrorKind : undefined));
|
|
982
1584
|
else if (completionError) reject(new WorkflowScriptError(completionError.message, partial()));
|
|
983
1585
|
else resolve({ value: outcome.value, ...partial() });
|
|
984
1586
|
});
|
|
@@ -1001,6 +1603,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1001
1603
|
...(started?.agent ? { agent: started.agent } : {}),
|
|
1002
1604
|
...(started?.phase ? { phase: started.phase } : {}),
|
|
1003
1605
|
...(started?.label ? { label: started.label } : {}),
|
|
1606
|
+
...(started?.generatedLaneKey ? { generatedLaneKey: started.generatedLaneKey } : {}),
|
|
1004
1607
|
error: error.message,
|
|
1005
1608
|
});
|
|
1006
1609
|
}
|
|
@@ -1009,7 +1612,11 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1009
1612
|
};
|
|
1010
1613
|
const timer = options.timeoutMs === undefined
|
|
1011
1614
|
? undefined
|
|
1012
|
-
: setTimeout(() =>
|
|
1615
|
+
: setTimeout(() => {
|
|
1616
|
+
const error = new Error(`Workflow script timed out after ${options.timeoutMs}ms.`) as Error & { workflowErrorKind: "timeout" };
|
|
1617
|
+
error.workflowErrorKind = "timeout";
|
|
1618
|
+
finish({ error });
|
|
1619
|
+
}, options.timeoutMs);
|
|
1013
1620
|
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
1014
1621
|
if (options.signal?.aborted) return onAbort();
|
|
1015
1622
|
|
|
@@ -1049,7 +1656,9 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1049
1656
|
return finish({ value: message.value });
|
|
1050
1657
|
}
|
|
1051
1658
|
if (message.type === "error") {
|
|
1052
|
-
const
|
|
1659
|
+
const rawError = typeof message.error === "string" ? message.error : "Workflow script failed.";
|
|
1660
|
+
const text = message.errorPhase === "return-serialization" ? `${rawError}${workflowReturnRecoveryHint(partial().children)}` : rawError;
|
|
1661
|
+
const workflowError = new Error(text) as Error & { workflowErrorKind?: "detached-child" };
|
|
1053
1662
|
if (message.errorKind === "detached-child") workflowError.workflowErrorKind = "detached-child";
|
|
1054
1663
|
return finish({ error: workflowError });
|
|
1055
1664
|
}
|
|
@@ -1063,12 +1672,16 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1063
1672
|
const steer = steers.get(message.callId);
|
|
1064
1673
|
if (steer) steer.observed = true;
|
|
1065
1674
|
else observedSteerCalls.add(message.callId);
|
|
1675
|
+
} else if (message.operation === "host") {
|
|
1676
|
+
const host = hostCalls.get(message.callId);
|
|
1677
|
+
if (host) host.observed = true;
|
|
1678
|
+
else observedHostCalls.add(message.callId);
|
|
1066
1679
|
}
|
|
1067
1680
|
return;
|
|
1068
1681
|
}
|
|
1069
1682
|
if (message.type !== "call" || typeof message.callId !== "number" || typeof message.method !== "string" || !isRecord(message.args)) return;
|
|
1070
1683
|
|
|
1071
|
-
const respond = (promise: Promise<unknown>, responsePath?: string) => {
|
|
1684
|
+
const respond = (promise: Promise<unknown>, responsePath?: string, onBoundaryError?: (error: unknown) => void) => {
|
|
1072
1685
|
void promise.then(
|
|
1073
1686
|
(value) => {
|
|
1074
1687
|
if (settled) return;
|
|
@@ -1081,6 +1694,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1081
1694
|
assertWorkflowJsonValue(normalized, responsePath);
|
|
1082
1695
|
worker.postMessage({ type: "response", callId: message.callId, ok: true, value: normalized });
|
|
1083
1696
|
} catch (error) {
|
|
1697
|
+
onBoundaryError?.(error);
|
|
1084
1698
|
worker.postMessage({ type: "response", callId: message.callId, ok: false, error: `${responsePath} must contain only JSON data before it can be returned from workflowScript. Return a plain projection such as { runId, ok, output }. ${error instanceof Error ? error.message : String(error)}` });
|
|
1085
1699
|
}
|
|
1086
1700
|
},
|
|
@@ -1155,6 +1769,52 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1155
1769
|
respond(promise);
|
|
1156
1770
|
return;
|
|
1157
1771
|
}
|
|
1772
|
+
if (message.method === "host") {
|
|
1773
|
+
let key: string;
|
|
1774
|
+
let params: WorkflowHostCommandParams;
|
|
1775
|
+
try {
|
|
1776
|
+
key = validateKey(message.args.key, "runs.host");
|
|
1777
|
+
params = normalizeWorkflowHostCommandParams(message.args.params, `runs.host('${key}') params`);
|
|
1778
|
+
} catch (error) {
|
|
1779
|
+
return respond(Promise.reject(error));
|
|
1780
|
+
}
|
|
1781
|
+
if (!options.host) return respond(Promise.reject(new Error("runs.host is unavailable in this host context.")));
|
|
1782
|
+
if (hostCalls.size >= HOST_STEP_MAX_COUNT) return respond(Promise.reject(new Error(`workflowScript supports at most ${HOST_STEP_MAX_COUNT} runs.host calls.`)));
|
|
1783
|
+
const startedAt = Date.now();
|
|
1784
|
+
const startedStep: HostStepNodeV1 = {
|
|
1785
|
+
version: 1,
|
|
1786
|
+
kind: "host-step",
|
|
1787
|
+
monitorKind: "command",
|
|
1788
|
+
id: key,
|
|
1789
|
+
label: key,
|
|
1790
|
+
...(params.role ? { role: params.role } : {}),
|
|
1791
|
+
...(params.provider ? { provider: params.provider } : {}),
|
|
1792
|
+
state: "running",
|
|
1793
|
+
updatedAt: startedAt,
|
|
1794
|
+
deadlineAt: startedAt + params.timeoutMs,
|
|
1795
|
+
};
|
|
1796
|
+
hostStepChanged(startedStep);
|
|
1797
|
+
trace.push({ operation: "host", key, state: "started" });
|
|
1798
|
+
traceChanged();
|
|
1799
|
+
const promise = Promise.resolve().then(() => options.host!(key, params, childController.signal)).then((result) => {
|
|
1800
|
+
const state = result.state === "stopped" ? "cancelled" : result.ok ? "done" : "error";
|
|
1801
|
+
const detail = [result.error, result.stderr.trim() || result.stdout.trim()].filter(Boolean).join(" ").replace(/\s+/g, " ").trim().slice(0, 200);
|
|
1802
|
+
hostStepChanged({ ...startedStep, state, ...(state === "done" ? { verdict: "pass" as const } : {}), ...(!result.ok ? { reasonCode: result.state === "timed-out" ? "timed_out" : result.state === "stopped" ? "aborted" : "command_failed" } : {}), ...(detail ? { detail } : {}), reportPath: params.output ?? result.outputPath.split(/[\\/]/).at(-1), exitCode: result.exitCode, updatedAt: Date.now() });
|
|
1803
|
+
trace.push({ operation: "host", key, state: result.ok ? "completed" : result.state === "stopped" ? "stopped" : "failed", durationMs: result.durationMs, ...(!result.ok ? { error: result.error ?? "Host command failed." } : {}) });
|
|
1804
|
+
traceChanged();
|
|
1805
|
+
if (!result.ok) throw new Error(`Host command '${key}' failed: ${detail || result.error || `exit code ${result.exitCode ?? "unknown"}`}`);
|
|
1806
|
+
return result;
|
|
1807
|
+
}, (error: unknown) => {
|
|
1808
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
1809
|
+
hostStepChanged({ ...startedStep, state: "error", reasonCode: "execution_failed", detail: text.replace(/\s+/g, " ").slice(0, 200), updatedAt: Date.now() });
|
|
1810
|
+
trace.push({ operation: "host", key, state: "failed", durationMs: Date.now() - startedAt, error: text });
|
|
1811
|
+
traceChanged();
|
|
1812
|
+
throw error;
|
|
1813
|
+
});
|
|
1814
|
+
hostCalls.set(message.callId, { key, promise, observed: observedHostCalls.delete(message.callId) });
|
|
1815
|
+
respond(promise, `runs.host('${key}') result`);
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1158
1818
|
if (message.method !== "run") return respond(Promise.reject(new Error(`Unknown runs API method '${message.method}'.`)));
|
|
1159
1819
|
|
|
1160
1820
|
let key: string;
|
|
@@ -1165,6 +1825,34 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1165
1825
|
}
|
|
1166
1826
|
const params = message.args.params;
|
|
1167
1827
|
if (!isRecord(params)) return respond(Promise.reject(new Error(`runs.run('${key}', params) requires a params object.`)));
|
|
1828
|
+
const generatedLaneKey = typeof message.args.generatedLaneKey === "string" && KEY_PATTERN.test(message.args.generatedLaneKey) && key.startsWith(`${message.args.generatedLaneKey}.`)
|
|
1829
|
+
? message.args.generatedLaneKey
|
|
1830
|
+
: undefined;
|
|
1831
|
+
const collectFailure = message.args.collectFailure === true;
|
|
1832
|
+
const callObserved = observedRunCalls.delete(message.callId);
|
|
1833
|
+
const deliver = (promise: Promise<WorkflowScriptChildResult>) => collectFailure
|
|
1834
|
+
? promise
|
|
1835
|
+
: promise.then((result) => {
|
|
1836
|
+
if (!result.ok && !result.stopped) {
|
|
1837
|
+
const childError = new Error(result.detached ? `Run '${key}' detached: ${result.error ?? result.output}` : `Run '${key}' failed: ${result.error ?? result.output}`) as Error & { workflowErrorKind?: "detached-child" };
|
|
1838
|
+
if (result.detached) childError.workflowErrorKind = "detached-child";
|
|
1839
|
+
throw childError;
|
|
1840
|
+
}
|
|
1841
|
+
return result;
|
|
1842
|
+
});
|
|
1843
|
+
const fingerprint = stableJson(params);
|
|
1844
|
+
const existing = launches.get(key);
|
|
1845
|
+
if (existing) {
|
|
1846
|
+
if (existing.fingerprint !== fingerprint) return respond(Promise.reject(new Error(`Duplicate workflow key '${key}' used with incompatible launch params.`)));
|
|
1847
|
+
if (callObserved) existing.observed = true;
|
|
1848
|
+
trace.push({ operation: "run", key, state: "reused", ...workflowStringMetadata(params), ...(existing.generatedLaneKey ? { generatedLaneKey: existing.generatedLaneKey } : {}) });
|
|
1849
|
+
traceChanged();
|
|
1850
|
+
return respond(deliver(existing.promise), `runs.run('${key}') result`, (error) => children.set(key, responseBoundaryFailure(key, error)));
|
|
1851
|
+
}
|
|
1852
|
+
const permitError = options.oneUsePermit?.claim(key);
|
|
1853
|
+
if (permitError) return respond(Promise.reject(new Error(permitError)));
|
|
1854
|
+
if (options.oneUsePermit && message.args.batch !== undefined) return respond(Promise.reject(new Error("Workflow child permit does not support runs.all.")));
|
|
1855
|
+
if (options.oneUsePermit && params.resume !== undefined) return respond(Promise.reject(new Error("Workflow child permit does not support retained resume.")));
|
|
1168
1856
|
if (params.action !== undefined) return respond(Promise.reject(new Error(`runs.run('${key}') accepts execution params only; management action is not allowed.`)));
|
|
1169
1857
|
if (params.workflowScript !== undefined) return respond(Promise.reject(new Error(`runs.run('${key}') cannot start a nested workflow script.`)));
|
|
1170
1858
|
if (params.tasks !== undefined || params.chain !== undefined || params.parallel !== undefined || params.concurrency !== undefined || params.chainDir !== undefined) {
|
|
@@ -1195,28 +1883,6 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1195
1883
|
if (params.resume !== undefined && (typeof params.task !== "string" || !params.task.trim())) {
|
|
1196
1884
|
return respond(Promise.reject(new Error(`runs.run('${key}') resume requires a non-empty task follow-up.`)));
|
|
1197
1885
|
}
|
|
1198
|
-
const collectFailure = message.args.collectFailure === true;
|
|
1199
|
-
const callObserved = observedRunCalls.delete(message.callId);
|
|
1200
|
-
const deliver = (promise: Promise<WorkflowScriptChildResult>) => collectFailure
|
|
1201
|
-
? promise
|
|
1202
|
-
: promise.then((result) => {
|
|
1203
|
-
if (!result.ok && !result.stopped) {
|
|
1204
|
-
const childError = new Error(result.detached ? `Run '${key}' detached: ${result.error ?? result.output}` : `Run '${key}' failed: ${result.error ?? result.output}`) as Error & { workflowErrorKind?: "detached-child" };
|
|
1205
|
-
if (result.detached) childError.workflowErrorKind = "detached-child";
|
|
1206
|
-
throw childError;
|
|
1207
|
-
}
|
|
1208
|
-
return result;
|
|
1209
|
-
});
|
|
1210
|
-
const fingerprint = stableJson(params);
|
|
1211
|
-
const existing = launches.get(key);
|
|
1212
|
-
if (existing) {
|
|
1213
|
-
if (existing.fingerprint !== fingerprint) return respond(Promise.reject(new Error(`Duplicate workflow key '${key}' used with incompatible launch params.`)));
|
|
1214
|
-
if (callObserved) existing.observed = true;
|
|
1215
|
-
trace.push({ operation: "run", key, state: "reused", ...workflowStringMetadata(params) });
|
|
1216
|
-
traceChanged();
|
|
1217
|
-
return respond(deliver(existing.promise), `runs.run('${key}') result`);
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
1886
|
const startedAt = Date.now();
|
|
1221
1887
|
const batch = isRecord(message.args.batch) && typeof message.args.batch.id === "string" && Array.isArray(message.args.batch.calls)
|
|
1222
1888
|
? { id: message.args.batch.id, calls: message.args.batch.calls.filter((call): call is { key: string; params: Record<string, unknown> } => isRecord(call) && typeof call.key === "string" && isRecord(call.params)) }
|
|
@@ -1266,7 +1932,23 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1266
1932
|
if (resolvedResumeLineage.at(-1) !== resolvedResumeId) resolvedResumeLineage.push(resolvedResumeId!);
|
|
1267
1933
|
}
|
|
1268
1934
|
const launchParams = resolvedResumeId ? { ...params, resume: resolvedResumeId } : params;
|
|
1269
|
-
|
|
1935
|
+
await launchSemaphore.acquire();
|
|
1936
|
+
try {
|
|
1937
|
+
if (settled || finishing || stoppedLaunches.has(key) || childSignal.aborted) {
|
|
1938
|
+
const reason = childSignal.reason;
|
|
1939
|
+
const text = children.get(key)?.error ?? (reason instanceof Error ? reason.message : typeof reason === "string" ? reason : "Workflow script aborted.");
|
|
1940
|
+
return stoppedChildResult(key, text);
|
|
1941
|
+
}
|
|
1942
|
+
const result = await options.launch(key, launchParams, childSignal, { admitted: true, batch: batch !== undefined });
|
|
1943
|
+
const autoResumeParams = setupAbortResumeParams(params, result, childSignal);
|
|
1944
|
+
if (!autoResumeParams) return result;
|
|
1945
|
+
resolvedResumeLineage = [...new Set([...(resolvedResumeLineage ?? []), result.runId!])];
|
|
1946
|
+
trace.push({ operation: "run", key, state: "started", ...workflowStringMetadata(autoResumeParams), ...(generatedLaneKey ? { generatedLaneKey } : {}), phase: "auto-resume", runId: result.runId });
|
|
1947
|
+
traceChanged();
|
|
1948
|
+
return options.launch(key, autoResumeParams, childSignal, { admitted: true, batch: batch !== undefined });
|
|
1949
|
+
} finally {
|
|
1950
|
+
launchSemaphore.release();
|
|
1951
|
+
}
|
|
1270
1952
|
}).then((result) => {
|
|
1271
1953
|
let normalized = !result.ok && !result.error ? { ...result, error: result.output } : result;
|
|
1272
1954
|
if (resolvedResumeLineage?.length && normalized.runId) {
|
|
@@ -1276,7 +1958,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1276
1958
|
if (stoppedLaunches.has(key)) return children.get(key) ?? normalized;
|
|
1277
1959
|
children.set(key, normalized);
|
|
1278
1960
|
const state = normalized.ok ? "completed" : normalized.stopped ? "stopped" : normalized.detached ? "detached" : "failed";
|
|
1279
|
-
trace.push({ operation: "run", key, state, durationMs: Date.now() - startedAt, ...workflowStringMetadata(params), ...(normalized.agent ? { agent: normalized.agent } : {}), ...(normalized.runId ? { runId: normalized.runId } : {}), ...(!normalized.ok ? { error: normalized.error ?? normalized.output } : {}) });
|
|
1961
|
+
trace.push({ operation: "run", key, state, durationMs: Date.now() - startedAt, ...workflowStringMetadata(params), ...(generatedLaneKey ? { generatedLaneKey } : {}), ...(normalized.agent ? { agent: normalized.agent } : {}), ...(normalized.runId ? { runId: normalized.runId } : {}), ...(!normalized.ok ? { error: normalized.error ?? normalized.output } : {}) });
|
|
1280
1962
|
traceChanged();
|
|
1281
1963
|
return normalized;
|
|
1282
1964
|
}, (error: unknown) => {
|
|
@@ -1285,15 +1967,15 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1285
1967
|
childStopControllers.delete(key);
|
|
1286
1968
|
if (stoppedLaunches.has(key)) return children.get(key) ?? { ...failure, stopped: true };
|
|
1287
1969
|
children.set(key, failure);
|
|
1288
|
-
trace.push({ operation: "run", key, state: "failed", durationMs: Date.now() - startedAt, ...workflowStringMetadata(params), error: text });
|
|
1970
|
+
trace.push({ operation: "run", key, state: "failed", durationMs: Date.now() - startedAt, ...workflowStringMetadata(params), ...(generatedLaneKey ? { generatedLaneKey } : {}), error: text });
|
|
1289
1971
|
traceChanged();
|
|
1290
1972
|
return failure;
|
|
1291
1973
|
});
|
|
1292
|
-
launches.set(key, { fingerprint, promise, observed: callObserved });
|
|
1974
|
+
launches.set(key, { fingerprint, promise, observed: callObserved, ...(generatedLaneKey ? { generatedLaneKey } : {}) });
|
|
1293
1975
|
childOrder.push(key);
|
|
1294
|
-
trace.push({ operation: "run", key, state: "started", ...workflowStringMetadata(params) });
|
|
1976
|
+
trace.push({ operation: "run", key, state: "started", ...workflowStringMetadata(params), ...(generatedLaneKey ? { generatedLaneKey } : {}) });
|
|
1295
1977
|
traceChanged();
|
|
1296
|
-
respond(deliver(promise), `runs.run('${key}') result
|
|
1978
|
+
respond(deliver(promise), `runs.run('${key}') result`, (error) => children.set(key, responseBoundaryFailure(key, error)));
|
|
1297
1979
|
});
|
|
1298
1980
|
|
|
1299
1981
|
worker.postMessage({ type: "start", script: options.script, stateEnabled: options.state !== undefined });
|