@openclaw/lobster 2026.9.4 → 2026.9.6
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/assets/activity.svg +4 -0
- package/assets/icon.png +0 -0
- package/dist/index.js +39 -10
- package/node_modules/@clawdbot/lobster/README.md +129 -131
- package/node_modules/@clawdbot/lobster/VISION.md +15 -238
- package/node_modules/@clawdbot/lobster/bin/clawd.invoke.js +2 -15
- package/node_modules/@clawdbot/lobster/bin/invoke.js +19 -0
- package/node_modules/@clawdbot/lobster/bin/lobster.js +2 -21
- package/node_modules/@clawdbot/lobster/bin/openclaw.invoke.js +2 -18
- package/node_modules/@clawdbot/lobster/dist/src/abortable_process.js +297 -0
- package/node_modules/@clawdbot/lobster/dist/src/abortable_process.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/src/cli.js +128 -296
- package/node_modules/@clawdbot/lobster/dist/src/cli.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/command_tokens.js +81 -0
- package/node_modules/@clawdbot/lobster/dist/src/command_tokens.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/src/commands/commands_list.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/registry.js +2 -0
- package/node_modules/@clawdbot/lobster/dist/src/commands/registry.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/approve.js +2 -0
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/approve.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/ask.js +3 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/ask.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/dedupe.js +1 -12
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/dedupe.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/diff_last.js +6 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/diff_last.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/email_triage.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/exec.js +30 -34
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/exec.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/gog_gmail_search.js +12 -25
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/gog_gmail_search.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/gog_gmail_send.js +19 -25
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/gog_gmail_send.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/group_by.js +2 -14
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/group_by.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/head.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/json.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/llm_invoke.js +260 -42
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/llm_invoke.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/map.js +2 -13
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/map.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/openclaw_agent.js +139 -0
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/openclaw_agent.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/openclaw_invoke.js +20 -2
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/openclaw_invoke.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/pick.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/sort.js +4 -21
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/sort.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/state.js +48 -20
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/state.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/table.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/template.js +2 -13
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/template.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/stdlib/where.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/workflows/workflows_list.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/commands/workflows/workflows_run.js +0 -18
- package/node_modules/@clawdbot/lobster/dist/src/commands/workflows/workflows_run.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/core/cost_tracker.js +45 -3
- package/node_modules/@clawdbot/lobster/dist/src/core/cost_tracker.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/core/filters.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/core/llm_accounting.js +304 -0
- package/node_modules/@clawdbot/lobster/dist/src/core/llm_accounting.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/src/core/retry.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/core/tool_runtime.js +147 -21
- package/node_modules/@clawdbot/lobster/dist/src/core/tool_runtime.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/core/value_path.js +11 -0
- package/node_modules/@clawdbot/lobster/dist/src/core/value_path.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/src/input_request.js +4 -2
- package/node_modules/@clawdbot/lobster/dist/src/input_request.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/parser.js +5 -69
- package/node_modules/@clawdbot/lobster/dist/src/parser.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/pipeline_resume_state.js +241 -74
- package/node_modules/@clawdbot/lobster/dist/src/pipeline_resume_state.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/read_line.js +52 -7
- package/node_modules/@clawdbot/lobster/dist/src/read_line.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/read_response_text.js +86 -0
- package/node_modules/@clawdbot/lobster/dist/src/read_response_text.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/src/recipes/github/index.js +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/recipes/github/pr-monitor.js +9 -66
- package/node_modules/@clawdbot/lobster/dist/src/recipes/github/pr-monitor.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/recipes/github/read_pr.js +45 -0
- package/node_modules/@clawdbot/lobster/dist/src/recipes/github/read_pr.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/src/recipes/github/snapshot.js +45 -0
- package/node_modules/@clawdbot/lobster/dist/src/recipes/github/snapshot.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/src/recipes/github/stages/pr-view.js +13 -65
- package/node_modules/@clawdbot/lobster/dist/src/recipes/github/stages/pr-view.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/recipes/registry.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/renderers/json.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/resume.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/runtime.js +187 -17
- package/node_modules/@clawdbot/lobster/dist/src/runtime.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/sdk/Lobster.js +25 -134
- package/node_modules/@clawdbot/lobster/dist/src/sdk/Lobster.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/sdk/index.js +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/sdk/primitives/approve.js +1 -2
- package/node_modules/@clawdbot/lobster/dist/src/sdk/primitives/approve.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/sdk/primitives/diff.js +11 -86
- package/node_modules/@clawdbot/lobster/dist/src/sdk/primitives/diff.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/sdk/primitives/exec.js +20 -96
- package/node_modules/@clawdbot/lobster/dist/src/sdk/primitives/exec.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/sdk/primitives/state.js +8 -106
- package/node_modules/@clawdbot/lobster/dist/src/sdk/primitives/state.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/sdk/runtime.js +2 -13
- package/node_modules/@clawdbot/lobster/dist/src/sdk/runtime.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/sdk/state_env.js +6 -0
- package/node_modules/@clawdbot/lobster/dist/src/sdk/state_env.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/src/shell.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/state/atomic_file.js +192 -0
- package/node_modules/@clawdbot/lobster/dist/src/state/atomic_file.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/src/state/store.js +550 -136
- package/node_modules/@clawdbot/lobster/dist/src/state/store.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/token.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/validation.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/workflows/dry_run.js +262 -0
- package/node_modules/@clawdbot/lobster/dist/src/workflows/dry_run.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/src/workflows/expressions.js +345 -0
- package/node_modules/@clawdbot/lobster/dist/src/workflows/expressions.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/src/workflows/file.js +950 -1285
- package/node_modules/@clawdbot/lobster/dist/src/workflows/file.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/workflows/github_pr_monitor.js +20 -90
- package/node_modules/@clawdbot/lobster/dist/src/workflows/github_pr_monitor.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/workflows/graph.js +9 -8
- package/node_modules/@clawdbot/lobster/dist/src/workflows/graph.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/workflows/load.js +347 -0
- package/node_modules/@clawdbot/lobster/dist/src/workflows/load.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/src/workflows/registry.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/workflows/types.js +2 -0
- package/node_modules/@clawdbot/lobster/dist/src/workflows/types.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/src/workflows/values.js +93 -0
- package/node_modules/@clawdbot/lobster/dist/src/workflows/values.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/test/approval_id.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/approve_preview.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/bin_smoke.test.js +25 -0
- package/node_modules/@clawdbot/lobster/dist/test/bin_smoke.test.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/test/cancellation.test.js +5171 -0
- package/node_modules/@clawdbot/lobster/dist/test/cancellation.test.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/test/clawd_invoke.test.js +22 -0
- package/node_modules/@clawdbot/lobster/dist/test/clawd_invoke.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/clawd_invoke_legacy.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/cli_run_errors.test.js +53 -0
- package/node_modules/@clawdbot/lobster/dist/test/cli_run_errors.test.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/test/cli_run_file_args_json.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/command_arguments.test.js +82 -0
- package/node_modules/@clawdbot/lobster/dist/test/command_arguments.test.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/test/commands_list.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/condition_comparison.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/core_tool_runtime.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/cost_tracker.test.js +1774 -12
- package/node_modules/@clawdbot/lobster/dist/test/cost_tracker.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/dedupe.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/diff_last.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/doctor.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/dry_run.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/email_triage.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/exec_stdin.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/for_each.test.js +254 -0
- package/node_modules/@clawdbot/lobster/dist/test/for_each.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/github_pr_notify_format.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/github_pr_summary.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/github_pr_view.test.js +176 -0
- package/node_modules/@clawdbot/lobster/dist/test/github_pr_view.test.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/test/gog_gmail_send.test.js +100 -0
- package/node_modules/@clawdbot/lobster/dist/test/gog_gmail_send.test.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/test/group_by.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/helpers/node_shell_fixture.js +17 -0
- package/node_modules/@clawdbot/lobster/dist/test/helpers/node_shell_fixture.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/test/helpers/wait_for_pid.js +19 -0
- package/node_modules/@clawdbot/lobster/dist/test/helpers/wait_for_pid.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/test/http_response_body_cap.test.js +318 -0
- package/node_modules/@clawdbot/lobster/dist/test/http_response_body_cap.test.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/test/llm_invoke.test.js +948 -1
- package/node_modules/@clawdbot/lobster/dist/test/llm_invoke.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/llm_task_invoke.test.js +103 -0
- package/node_modules/@clawdbot/lobster/dist/test/llm_task_invoke.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/map.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/multi_approval_resume.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/on_error.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/openclaw_agent.test.js +169 -0
- package/node_modules/@clawdbot/lobster/dist/test/openclaw_agent.test.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/test/openclaw_dispatch_retry.test.js +96 -0
- package/node_modules/@clawdbot/lobster/dist/test/openclaw_dispatch_retry.test.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/test/openclaw_invoke_alias.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/parallel.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/parser.test.js +57 -0
- package/node_modules/@clawdbot/lobster/dist/test/parser.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/pipeline_resume_state.test.js +65 -0
- package/node_modules/@clawdbot/lobster/dist/test/pipeline_resume_state.test.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/test/process_output_limit.test.js +175 -0
- package/node_modules/@clawdbot/lobster/dist/test/process_output_limit.test.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/test/read_line.test.js +54 -0
- package/node_modules/@clawdbot/lobster/dist/test/read_line.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/release_changelog.test.js +39 -0
- package/node_modules/@clawdbot/lobster/dist/test/release_changelog.test.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/test/request_input.test.js +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/request_input.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/resume.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/sdk_exec.test.js +248 -0
- package/node_modules/@clawdbot/lobster/dist/test/sdk_exec.test.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/test/sdk_lobster.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/shell.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/sort.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/state.test.js +584 -1
- package/node_modules/@clawdbot/lobster/dist/test/state.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/step_retry.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/step_stdin_epipe.test.js +44 -0
- package/node_modules/@clawdbot/lobster/dist/test/step_stdin_epipe.test.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/test/step_timeout.test.js +67 -1
- package/node_modules/@clawdbot/lobster/dist/test/step_timeout.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/template.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/template_filters.test.js +37 -0
- package/node_modules/@clawdbot/lobster/dist/test/template_filters.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/tool_envelope_version.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/tool_mode.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/validation.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/where.test.js +94 -0
- package/node_modules/@clawdbot/lobster/dist/test/where.test.js.map +1 -0
- package/node_modules/@clawdbot/lobster/dist/test/workflow_args_env.test.js +5 -3
- package/node_modules/@clawdbot/lobster/dist/test/workflow_args_env.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/workflow_composition.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/workflow_file.test.js +647 -1
- package/node_modules/@clawdbot/lobster/dist/test/workflow_file.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/workflow_graph.test.js +13 -3
- package/node_modules/@clawdbot/lobster/dist/test/workflow_graph.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/test/workflows.test.js.map +1 -1
- package/node_modules/@clawdbot/lobster/package.json +72 -73
- package/node_modules/typebox/build/compile/validator.d.mts +1 -1
- package/node_modules/typebox/build/compile/validator.mjs +1 -1
- package/node_modules/typebox/build/format/idna/idn-hostname.mjs +7 -5
- package/node_modules/typebox/build/format/iri.mjs +5 -1
- package/node_modules/typebox/build/guard/emit.mjs +2 -4
- package/node_modules/typebox/build/guard/guard.mjs +6 -18
- package/node_modules/typebox/build/guard/string.mjs +16 -24
- package/node_modules/typebox/build/schema/build.mjs +2 -2
- package/node_modules/typebox/build/schema/check.mjs +1 -1
- package/node_modules/typebox/build/schema/compile.d.mts +3 -2
- package/node_modules/typebox/build/schema/compile.mjs +8 -8
- package/node_modules/typebox/build/schema/engine/_context.d.mts +6 -7
- package/node_modules/typebox/build/schema/engine/_context.mjs +23 -15
- package/node_modules/typebox/build/schema/engine/_functions.d.mts +2 -2
- package/node_modules/typebox/build/schema/engine/_functions.mjs +1 -1
- package/node_modules/typebox/build/schema/engine/_reducer.d.mts +3 -3
- package/node_modules/typebox/build/schema/engine/_refine.d.mts +5 -5
- package/node_modules/typebox/build/schema/engine/_refine.mjs +4 -7
- package/node_modules/typebox/build/schema/engine/_stack.d.mts +34 -16
- package/node_modules/typebox/build/schema/engine/_stack.mjs +148 -104
- package/node_modules/typebox/build/schema/engine/additionalItems.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/additionalProperties.d.mts +8 -8
- package/node_modules/typebox/build/schema/engine/additionalProperties.mjs +26 -18
- package/node_modules/typebox/build/schema/engine/allOf.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/allOf.mjs +3 -3
- package/node_modules/typebox/build/schema/engine/anyOf.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/anyOf.mjs +5 -10
- package/node_modules/typebox/build/schema/engine/boolean.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/boolean.mjs +2 -6
- package/node_modules/typebox/build/schema/engine/const.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/const.mjs +2 -6
- package/node_modules/typebox/build/schema/engine/contains.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/contains.mjs +2 -6
- package/node_modules/typebox/build/schema/engine/dependencies.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/dependencies.mjs +3 -6
- package/node_modules/typebox/build/schema/engine/dependentRequired.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/dependentRequired.mjs +2 -6
- package/node_modules/typebox/build/schema/engine/dependentSchemas.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/dynamicRef.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/dynamicRef.mjs +11 -7
- package/node_modules/typebox/build/schema/engine/enum.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/enum.mjs +2 -6
- package/node_modules/typebox/build/schema/engine/exclusiveMaximum.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/exclusiveMaximum.mjs +2 -6
- package/node_modules/typebox/build/schema/engine/exclusiveMinimum.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/exclusiveMinimum.mjs +2 -6
- package/node_modules/typebox/build/schema/engine/format.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/format.mjs +2 -6
- package/node_modules/typebox/build/schema/engine/if.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/if.mjs +7 -15
- package/node_modules/typebox/build/schema/engine/items.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/items.mjs +1 -1
- package/node_modules/typebox/build/schema/engine/maxContains.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/maxContains.mjs +2 -6
- package/node_modules/typebox/build/schema/engine/maxItems.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/maxItems.mjs +2 -6
- package/node_modules/typebox/build/schema/engine/maxLength.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/maxLength.mjs +2 -6
- package/node_modules/typebox/build/schema/engine/maxProperties.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/maxProperties.mjs +2 -6
- package/node_modules/typebox/build/schema/engine/maximum.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/maximum.mjs +2 -6
- package/node_modules/typebox/build/schema/engine/minContains.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/minContains.mjs +2 -6
- package/node_modules/typebox/build/schema/engine/minItems.d.mts +5 -5
- package/node_modules/typebox/build/schema/engine/minItems.mjs +2 -6
- package/node_modules/typebox/build/schema/engine/minLength.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/minLength.mjs +2 -6
- package/node_modules/typebox/build/schema/engine/minProperties.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/minProperties.mjs +2 -6
- package/node_modules/typebox/build/schema/engine/minimum.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/minimum.mjs +2 -6
- package/node_modules/typebox/build/schema/engine/multipleOf.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/multipleOf.mjs +2 -6
- package/node_modules/typebox/build/schema/engine/not.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/not.mjs +4 -8
- package/node_modules/typebox/build/schema/engine/oneOf.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/oneOf.mjs +6 -11
- package/node_modules/typebox/build/schema/engine/pattern.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/pattern.mjs +3 -7
- package/node_modules/typebox/build/schema/engine/patternProperties.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/patternProperties.mjs +1 -1
- package/node_modules/typebox/build/schema/engine/prefixItems.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/prefixItems.mjs +1 -1
- package/node_modules/typebox/build/schema/engine/properties.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/properties.mjs +1 -1
- package/node_modules/typebox/build/schema/engine/propertyNames.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/propertyNames.mjs +4 -10
- package/node_modules/typebox/build/schema/engine/recursiveRef.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/recursiveRef.mjs +11 -7
- package/node_modules/typebox/build/schema/engine/ref.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/ref.mjs +19 -15
- package/node_modules/typebox/build/schema/engine/required.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/required.mjs +2 -6
- package/node_modules/typebox/build/schema/engine/schema.d.mts +7 -7
- package/node_modules/typebox/build/schema/engine/schema.mjs +137 -133
- package/node_modules/typebox/build/schema/engine/type.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/type.mjs +2 -6
- package/node_modules/typebox/build/schema/engine/unevaluatedItems.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/unevaluatedItems.mjs +5 -9
- package/node_modules/typebox/build/schema/engine/unevaluatedProperties.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/unevaluatedProperties.mjs +5 -9
- package/node_modules/typebox/build/schema/engine/uniqueItems.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/uniqueItems.mjs +2 -6
- package/node_modules/typebox/build/schema/errors.d.mts +2 -2
- package/node_modules/typebox/build/schema/errors.mjs +8 -14
- package/node_modules/typebox/build/schema/intern/index.d.mts +1 -0
- package/node_modules/typebox/build/schema/intern/index.mjs +1 -0
- package/node_modules/typebox/build/schema/intern/intern.d.mts +11 -0
- package/node_modules/typebox/build/schema/intern/intern.mjs +293 -0
- package/node_modules/typebox/build/schema/meta/draft_2019_09.d.mts +2 -0
- package/node_modules/typebox/build/schema/meta/draft_2019_09.mjs +288 -0
- package/node_modules/typebox/build/schema/meta/draft_2020_12.d.mts +2 -0
- package/node_modules/typebox/build/schema/meta/draft_2020_12.mjs +300 -0
- package/node_modules/typebox/build/schema/meta/draft_3.d.mts +2 -0
- package/node_modules/typebox/build/schema/meta/draft_3.mjs +143 -0
- package/node_modules/typebox/build/schema/meta/draft_4.d.mts +2 -0
- package/node_modules/typebox/build/schema/meta/draft_4.mjs +149 -0
- package/node_modules/typebox/build/schema/meta/draft_6.d.mts +2 -0
- package/node_modules/typebox/build/schema/meta/draft_6.mjs +155 -0
- package/node_modules/typebox/build/schema/meta/draft_7.d.mts +2 -0
- package/node_modules/typebox/build/schema/meta/draft_7.mjs +172 -0
- package/node_modules/typebox/build/schema/meta/index.d.mts +2 -0
- package/node_modules/typebox/build/schema/meta/index.mjs +2 -0
- package/node_modules/typebox/build/schema/meta/meta.d.mts +9 -0
- package/node_modules/typebox/build/schema/meta/meta.mjs +15 -0
- package/node_modules/typebox/build/schema/meta/schema.d.mts +6 -0
- package/node_modules/typebox/build/schema/meta/schema.mjs +1 -0
- package/node_modules/typebox/build/schema/parse.d.mts +1 -0
- package/node_modules/typebox/build/schema/parse.mjs +10 -5
- package/node_modules/typebox/build/schema/resolve/resolve.d.mts +8 -3
- package/node_modules/typebox/build/schema/resolve/resolve.mjs +268 -139
- package/node_modules/typebox/build/schema/schema.d.mts +2 -0
- package/node_modules/typebox/build/schema/schema.mjs +2 -0
- package/node_modules/typebox/build/value/errors/errors.d.mts +2 -12
- package/node_modules/typebox/build/value/errors/errors.mjs +2 -7
- package/node_modules/typebox/package.json +1 -1
- package/node_modules/typebox/readme.md +59 -89
- package/openclaw.plugin.json +1 -1
- package/package.json +10 -7
- package/node_modules/@clawdbot/lobster/dist/src/sdk/token.js +0 -9
- package/node_modules/@clawdbot/lobster/dist/src/sdk/token.js.map +0 -1
|
@@ -1,19 +1,50 @@
|
|
|
1
|
+
import { dryRunWorkflow } from "./dry_run.js";
|
|
2
|
+
import { createRecord, mergeEnv, resolveCwd, resolveInputValue, resolveShellStdin, resolveWorkflowStepArgs, } from "./values.js";
|
|
3
|
+
import { loadWorkflowFile, getStepExecution, isApprovalStep, isInputStep } from "./load.js";
|
|
4
|
+
import { evaluateCondition, resolveTemplate } from "./expressions.js";
|
|
1
5
|
import { promises as fsp } from "node:fs";
|
|
2
6
|
import path from "node:path";
|
|
3
|
-
import { parse as parseYaml } from "yaml";
|
|
4
7
|
import { randomUUID } from "node:crypto";
|
|
5
8
|
import { isDeepStrictEqual } from "node:util";
|
|
6
9
|
import { PassThrough } from "node:stream";
|
|
7
10
|
import { parsePipeline } from "../parser.js";
|
|
8
11
|
import { runPipeline } from "../runtime.js";
|
|
9
12
|
import { encodeToken, decodeToken } from "../token.js";
|
|
10
|
-
import { createApprovalIndex, deleteStateJson,
|
|
13
|
+
import { cleanupApprovalIndexByStateKey, consumeResumeState, createApprovalIndex, deleteStateJsonWithBoundedResumeCleanup, deleteResumeStateWithRollback, deleteStateJson, deleteUnconsumedResumeState, isConsumedResumeState, readStateJsonWithLock, restoreConsumedResumeState, writeStateJson, stableStringify, } from "../state/store.js";
|
|
11
14
|
import { readLineFromStream } from "../read_line.js";
|
|
12
|
-
import { resolveInlineShellCommand } from "../shell.js";
|
|
13
15
|
import { compileCached } from "../validation.js";
|
|
14
|
-
import {
|
|
16
|
+
import { createLlmSpendLedger, carryLlmProvenance, markLlmProvenanceReplayed, llmProvenanceOf, restoreLlmProvenance, } from "../core/llm_accounting.js";
|
|
17
|
+
import { CostLimitExceededError, CostTracker } from "../core/cost_tracker.js";
|
|
15
18
|
import { withRetry, resolveRetryConfig } from "../core/retry.js";
|
|
16
19
|
import { RequestInputResumeError, validateCommandInputState, } from "../input_request.js";
|
|
20
|
+
import { runAbortableProcess } from "../abortable_process.js";
|
|
21
|
+
export { loadWorkflowFile } from "./load.js";
|
|
22
|
+
const NON_RETRYABLE_STEP_ERROR = Symbol("lobster.nonRetryableStepError");
|
|
23
|
+
function markStepErrorNonRetryable(error) {
|
|
24
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
25
|
+
Object.defineProperty(normalized, NON_RETRYABLE_STEP_ERROR, { value: true });
|
|
26
|
+
return normalized;
|
|
27
|
+
}
|
|
28
|
+
function isStepErrorNonRetryable(error) {
|
|
29
|
+
return Boolean(error instanceof Error &&
|
|
30
|
+
error[NON_RETRYABLE_STEP_ERROR]);
|
|
31
|
+
}
|
|
32
|
+
const PARALLEL_BRANCH_ABORT_SETTLE_TIMEOUT_MS = 500;
|
|
33
|
+
async function waitForCancelledParallelBranches(branchesSettled) {
|
|
34
|
+
let timeoutId;
|
|
35
|
+
try {
|
|
36
|
+
await Promise.race([
|
|
37
|
+
branchesSettled,
|
|
38
|
+
new Promise((_resolve, reject) => {
|
|
39
|
+
timeoutId = setTimeout(() => reject(new Error("Parallel branches did not settle after cancellation")), PARALLEL_BRANCH_ABORT_SETTLE_TIMEOUT_MS);
|
|
40
|
+
}),
|
|
41
|
+
]);
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
if (timeoutId !== undefined)
|
|
45
|
+
clearTimeout(timeoutId);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
17
48
|
export class WorkflowResumeArgumentError extends Error {
|
|
18
49
|
constructor(message) {
|
|
19
50
|
super(message);
|
|
@@ -32,322 +63,8 @@ class WorkflowPipelineInputSuspension extends Error {
|
|
|
32
63
|
this.pipelineInput = pipelineInput;
|
|
33
64
|
}
|
|
34
65
|
}
|
|
35
|
-
export async function loadWorkflowFile(filePath) {
|
|
36
|
-
const text = await fsp.readFile(filePath, "utf8");
|
|
37
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
38
|
-
const parsed = ext === ".json" ? JSON.parse(text) : parseYaml(text);
|
|
39
|
-
if (!parsed || typeof parsed !== "object") {
|
|
40
|
-
throw new Error("Workflow file must be a JSON/YAML object");
|
|
41
|
-
}
|
|
42
|
-
const steps = parsed.steps;
|
|
43
|
-
if (!Array.isArray(steps) || steps.length === 0) {
|
|
44
|
-
throw new Error("Workflow file requires a non-empty steps array");
|
|
45
|
-
}
|
|
46
|
-
const costLimit = parsed.cost_limit;
|
|
47
|
-
if (costLimit !== undefined) {
|
|
48
|
-
if (!costLimit || typeof costLimit !== "object" || Array.isArray(costLimit)) {
|
|
49
|
-
throw new Error("Workflow cost_limit must be an object");
|
|
50
|
-
}
|
|
51
|
-
if (!Number.isFinite(Number(costLimit.max_usd)) || Number(costLimit.max_usd) < 0) {
|
|
52
|
-
throw new Error("Workflow cost_limit.max_usd must be a non-negative number");
|
|
53
|
-
}
|
|
54
|
-
if (costLimit.action !== undefined &&
|
|
55
|
-
costLimit.action !== "warn" &&
|
|
56
|
-
costLimit.action !== "stop") {
|
|
57
|
-
throw new Error('Workflow cost_limit.action must be "warn" or "stop"');
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
const seen = new Set();
|
|
61
|
-
for (const step of steps) {
|
|
62
|
-
if (!step || typeof step !== "object") {
|
|
63
|
-
throw new Error("Workflow step must be an object");
|
|
64
|
-
}
|
|
65
|
-
if (!step.id || typeof step.id !== "string") {
|
|
66
|
-
throw new Error("Workflow step requires an id");
|
|
67
|
-
}
|
|
68
|
-
if (step.workflow !== undefined && typeof step.workflow !== "string") {
|
|
69
|
-
throw new Error(`Workflow step ${step.id} workflow must be a string (file path)`);
|
|
70
|
-
}
|
|
71
|
-
if (typeof step.workflow === "string" && !step.workflow.trim()) {
|
|
72
|
-
throw new Error(`Workflow step ${step.id} workflow path cannot be blank`);
|
|
73
|
-
}
|
|
74
|
-
if (step.workflow_args !== undefined) {
|
|
75
|
-
if (!step.workflow_args ||
|
|
76
|
-
typeof step.workflow_args !== "object" ||
|
|
77
|
-
Array.isArray(step.workflow_args)) {
|
|
78
|
-
throw new Error(`Workflow step ${step.id} workflow_args must be a plain object`);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
if (step.parallel !== undefined &&
|
|
82
|
-
(!step.parallel || typeof step.parallel !== "object" || Array.isArray(step.parallel))) {
|
|
83
|
-
throw new Error(`Workflow step ${step.id} parallel must be an object`);
|
|
84
|
-
}
|
|
85
|
-
const isParallel = Boolean(step.parallel && typeof step.parallel === "object" && !Array.isArray(step.parallel));
|
|
86
|
-
if (isParallel) {
|
|
87
|
-
const parallel = step.parallel;
|
|
88
|
-
if (!Array.isArray(parallel.branches) || parallel.branches.length === 0) {
|
|
89
|
-
throw new Error(`Workflow step ${step.id} parallel requires a non-empty branches array`);
|
|
90
|
-
}
|
|
91
|
-
if (parallel.wait !== undefined && parallel.wait !== "all" && parallel.wait !== "any") {
|
|
92
|
-
throw new Error(`Workflow step ${step.id} parallel wait must be "all" or "any"`);
|
|
93
|
-
}
|
|
94
|
-
if (parallel.timeout_ms !== undefined &&
|
|
95
|
-
(typeof parallel.timeout_ms !== "number" ||
|
|
96
|
-
!Number.isFinite(parallel.timeout_ms) ||
|
|
97
|
-
!Number.isInteger(parallel.timeout_ms) ||
|
|
98
|
-
parallel.timeout_ms < 1 ||
|
|
99
|
-
parallel.timeout_ms > 2_147_483_647)) {
|
|
100
|
-
throw new Error(`Workflow step ${step.id} parallel timeout_ms must be a positive integer between 1 and 2147483647`);
|
|
101
|
-
}
|
|
102
|
-
const branchIds = new Set();
|
|
103
|
-
for (const branch of parallel.branches) {
|
|
104
|
-
if (!branch || typeof branch !== "object") {
|
|
105
|
-
throw new Error(`Workflow step ${step.id} parallel branches must be objects`);
|
|
106
|
-
}
|
|
107
|
-
if (!branch.id || typeof branch.id !== "string") {
|
|
108
|
-
throw new Error(`Workflow step ${step.id} parallel branch requires an id`);
|
|
109
|
-
}
|
|
110
|
-
if (branch.id === step.id) {
|
|
111
|
-
throw new Error(`Workflow step ${step.id} parallel branch id cannot match the step id`);
|
|
112
|
-
}
|
|
113
|
-
if (branchIds.has(branch.id)) {
|
|
114
|
-
throw new Error(`Workflow step ${step.id} duplicate parallel branch id: ${branch.id}`);
|
|
115
|
-
}
|
|
116
|
-
if (seen.has(branch.id)) {
|
|
117
|
-
throw new Error(`Duplicate workflow id across steps/parallel branches: ${branch.id}`);
|
|
118
|
-
}
|
|
119
|
-
branchIds.add(branch.id);
|
|
120
|
-
const branchShell = typeof branch.run === "string" ? branch.run : branch.command;
|
|
121
|
-
const branchPipeline = typeof branch.pipeline === "string" ? branch.pipeline : undefined;
|
|
122
|
-
const branchExecCount = Number(Boolean(branchShell)) + Number(Boolean(branchPipeline));
|
|
123
|
-
if (branchExecCount === 0) {
|
|
124
|
-
throw new Error(`Workflow step ${step.id} parallel branch ${branch.id} requires run, command, or pipeline`);
|
|
125
|
-
}
|
|
126
|
-
if (branchExecCount > 1) {
|
|
127
|
-
throw new Error(`Workflow step ${step.id} parallel branch ${branch.id} can only define one of run, command, or pipeline`);
|
|
128
|
-
}
|
|
129
|
-
if (branch.run !== undefined && typeof branch.run !== "string") {
|
|
130
|
-
throw new Error(`Workflow step ${step.id} parallel branch ${branch.id} run must be a string`);
|
|
131
|
-
}
|
|
132
|
-
if (branch.command !== undefined && typeof branch.command !== "string") {
|
|
133
|
-
throw new Error(`Workflow step ${step.id} parallel branch ${branch.id} command must be a string`);
|
|
134
|
-
}
|
|
135
|
-
if (branch.pipeline !== undefined && typeof branch.pipeline !== "string") {
|
|
136
|
-
throw new Error(`Workflow step ${step.id} parallel branch ${branch.id} pipeline must be a string`);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
if (step.for_each !== undefined && typeof step.for_each !== "string") {
|
|
141
|
-
throw new Error(`Workflow step ${step.id} for_each must be a string (step reference expression)`);
|
|
142
|
-
}
|
|
143
|
-
const isForEach = typeof step.for_each === "string";
|
|
144
|
-
if (isForEach) {
|
|
145
|
-
if (!Array.isArray(step.steps) || step.steps.length === 0) {
|
|
146
|
-
throw new Error(`Workflow step ${step.id} for_each requires a non-empty steps array`);
|
|
147
|
-
}
|
|
148
|
-
if (step.batch_size !== undefined &&
|
|
149
|
-
(typeof step.batch_size !== "number" ||
|
|
150
|
-
!Number.isInteger(step.batch_size) ||
|
|
151
|
-
step.batch_size < 1)) {
|
|
152
|
-
throw new Error(`Workflow step ${step.id} batch_size must be a positive integer`);
|
|
153
|
-
}
|
|
154
|
-
if (step.pause_ms !== undefined &&
|
|
155
|
-
(typeof step.pause_ms !== "number" || !Number.isFinite(step.pause_ms) || step.pause_ms < 0)) {
|
|
156
|
-
throw new Error(`Workflow step ${step.id} pause_ms must be a finite non-negative number`);
|
|
157
|
-
}
|
|
158
|
-
if (isApprovalStep(step.approval)) {
|
|
159
|
-
throw new Error(`Workflow step ${step.id} for_each steps cannot define approval (use a separate step after the loop)`);
|
|
160
|
-
}
|
|
161
|
-
if (isInputStep(step.input)) {
|
|
162
|
-
throw new Error(`Workflow step ${step.id} for_each steps cannot define input (use a separate step after the loop)`);
|
|
163
|
-
}
|
|
164
|
-
if (step.stdin !== undefined && step.stdin !== null) {
|
|
165
|
-
throw new Error(`Workflow step ${step.id} for_each steps cannot define stdin (loop input comes from the for_each expression)`);
|
|
166
|
-
}
|
|
167
|
-
const loopShell = typeof step.run === "string" ? step.run : step.command;
|
|
168
|
-
const loopPipeline = typeof step.pipeline === "string" ? step.pipeline : undefined;
|
|
169
|
-
if (loopShell || loopPipeline || step.workflow || step.parallel) {
|
|
170
|
-
throw new Error(`Workflow step ${step.id} for_each cannot also define run, command, pipeline, workflow, or parallel`);
|
|
171
|
-
}
|
|
172
|
-
if (step.item_var !== undefined && typeof step.item_var !== "string") {
|
|
173
|
-
throw new Error(`Workflow step ${step.id} item_var must be a string`);
|
|
174
|
-
}
|
|
175
|
-
if (step.index_var !== undefined && typeof step.index_var !== "string") {
|
|
176
|
-
throw new Error(`Workflow step ${step.id} index_var must be a string`);
|
|
177
|
-
}
|
|
178
|
-
const loopItemVar = step.item_var ?? "item";
|
|
179
|
-
const loopIndexVar = step.index_var ?? "index";
|
|
180
|
-
if (loopItemVar === loopIndexVar) {
|
|
181
|
-
throw new Error(`Workflow step ${step.id} item_var and index_var cannot be the same`);
|
|
182
|
-
}
|
|
183
|
-
const subStepIds = new Set();
|
|
184
|
-
for (const sub of step.steps) {
|
|
185
|
-
if (!sub || typeof sub !== "object" || !sub.id || typeof sub.id !== "string") {
|
|
186
|
-
throw new Error(`Workflow step ${step.id} for_each sub-step requires an id`);
|
|
187
|
-
}
|
|
188
|
-
if (sub.id === loopItemVar || sub.id === loopIndexVar) {
|
|
189
|
-
throw new Error(`Workflow step ${step.id} for_each sub-step id '${sub.id}' conflicts with loop variable`);
|
|
190
|
-
}
|
|
191
|
-
if (subStepIds.has(sub.id)) {
|
|
192
|
-
throw new Error(`Workflow step ${step.id} duplicate for_each sub-step id: ${sub.id}`);
|
|
193
|
-
}
|
|
194
|
-
subStepIds.add(sub.id);
|
|
195
|
-
if (isApprovalStep(sub.approval) || isInputStep(sub.input)) {
|
|
196
|
-
throw new Error(`Workflow step ${step.id} for_each sub-steps cannot contain approval or input steps`);
|
|
197
|
-
}
|
|
198
|
-
if (sub.run !== undefined && typeof sub.run !== "string") {
|
|
199
|
-
throw new Error(`Workflow step ${step.id} for_each sub-step ${sub.id} run must be a string`);
|
|
200
|
-
}
|
|
201
|
-
if (sub.command !== undefined && typeof sub.command !== "string") {
|
|
202
|
-
throw new Error(`Workflow step ${step.id} for_each sub-step ${sub.id} command must be a string`);
|
|
203
|
-
}
|
|
204
|
-
if (sub.pipeline !== undefined && typeof sub.pipeline !== "string") {
|
|
205
|
-
throw new Error(`Workflow step ${step.id} for_each sub-step ${sub.id} pipeline must be a string`);
|
|
206
|
-
}
|
|
207
|
-
if (sub.workflow || sub.parallel || sub.for_each) {
|
|
208
|
-
throw new Error(`Workflow step ${step.id} for_each sub-step ${sub.id} cannot define workflow, parallel, or for_each`);
|
|
209
|
-
}
|
|
210
|
-
const subShell = typeof sub.run === "string" && sub.run.trim()
|
|
211
|
-
? sub.run
|
|
212
|
-
: typeof sub.command === "string" && sub.command.trim()
|
|
213
|
-
? sub.command
|
|
214
|
-
: undefined;
|
|
215
|
-
const subPipeline = typeof sub.pipeline === "string" && sub.pipeline.trim() ? sub.pipeline : undefined;
|
|
216
|
-
if (!subShell && !subPipeline) {
|
|
217
|
-
throw new Error(`Workflow step ${step.id} for_each sub-step ${sub.id} requires run, command, or pipeline`);
|
|
218
|
-
}
|
|
219
|
-
if (Number(Boolean(subShell)) + Number(Boolean(subPipeline)) > 1) {
|
|
220
|
-
throw new Error(`Workflow step ${step.id} for_each sub-step ${sub.id} can only define one of run, command, or pipeline`);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
const shellCommand = typeof step.run === "string" ? step.run : step.command;
|
|
225
|
-
const pipeline = typeof step.pipeline === "string" ? step.pipeline : undefined;
|
|
226
|
-
const workflowRef = typeof step.workflow === "string" && step.workflow.trim() ? step.workflow : undefined;
|
|
227
|
-
const executionCount = Number(Boolean(shellCommand)) +
|
|
228
|
-
Number(Boolean(pipeline)) +
|
|
229
|
-
Number(Boolean(workflowRef)) +
|
|
230
|
-
Number(isParallel) +
|
|
231
|
-
Number(isForEach);
|
|
232
|
-
if (executionCount === 0 && !isApprovalStep(step.approval) && !isInputStep(step.input)) {
|
|
233
|
-
throw new Error(`Workflow step ${step.id} requires run, command, pipeline, workflow, parallel, for_each, approval, or input`);
|
|
234
|
-
}
|
|
235
|
-
if (executionCount > 1) {
|
|
236
|
-
throw new Error(`Workflow step ${step.id} can only define one of run, command, pipeline, workflow, parallel, or for_each`);
|
|
237
|
-
}
|
|
238
|
-
if (executionCount > 0 && isInputStep(step.input)) {
|
|
239
|
-
throw new Error(`Workflow step ${step.id} input steps cannot define run, command, pipeline, workflow, parallel, or for_each`);
|
|
240
|
-
}
|
|
241
|
-
if (isApprovalStep(step.approval) && isInputStep(step.input)) {
|
|
242
|
-
throw new Error(`Workflow step ${step.id} cannot define both approval and input`);
|
|
243
|
-
}
|
|
244
|
-
if (step.run !== undefined && typeof step.run !== "string") {
|
|
245
|
-
throw new Error(`Workflow step ${step.id} run must be a string`);
|
|
246
|
-
}
|
|
247
|
-
if (step.command !== undefined && typeof step.command !== "string") {
|
|
248
|
-
throw new Error(`Workflow step ${step.id} command must be a string`);
|
|
249
|
-
}
|
|
250
|
-
if (step.pipeline !== undefined && typeof step.pipeline !== "string") {
|
|
251
|
-
throw new Error(`Workflow step ${step.id} pipeline must be a string`);
|
|
252
|
-
}
|
|
253
|
-
if (step.input !== undefined && !isInputStep(step.input)) {
|
|
254
|
-
throw new Error(`Workflow step ${step.id} input must be an object`);
|
|
255
|
-
}
|
|
256
|
-
if (step.input && typeof step.input.prompt !== "string") {
|
|
257
|
-
throw new Error(`Workflow step ${step.id} input.prompt must be a string`);
|
|
258
|
-
}
|
|
259
|
-
if (step.input &&
|
|
260
|
-
(step.input.responseSchema === undefined || typeof step.input.responseSchema !== "object")) {
|
|
261
|
-
throw new Error(`Workflow step ${step.id} input.responseSchema must be an object`);
|
|
262
|
-
}
|
|
263
|
-
if (step.input) {
|
|
264
|
-
try {
|
|
265
|
-
compileCached(step.input.responseSchema);
|
|
266
|
-
}
|
|
267
|
-
catch (err) {
|
|
268
|
-
throw new Error(`Workflow step ${step.id} input.responseSchema is invalid: ${err?.message ?? String(err)}`);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
if (step.approval && typeof step.approval === "object" && !Array.isArray(step.approval)) {
|
|
272
|
-
const approval = step.approval;
|
|
273
|
-
if (approval.initiated_by !== undefined && typeof approval.initiated_by !== "string") {
|
|
274
|
-
throw new Error(`Workflow step ${step.id} approval.initiated_by must be a string`);
|
|
275
|
-
}
|
|
276
|
-
if (approval.initiatedBy !== undefined && typeof approval.initiatedBy !== "string") {
|
|
277
|
-
throw new Error(`Workflow step ${step.id} approval.initiatedBy must be a string`);
|
|
278
|
-
}
|
|
279
|
-
if (approval.required_approver !== undefined &&
|
|
280
|
-
typeof approval.required_approver !== "string") {
|
|
281
|
-
throw new Error(`Workflow step ${step.id} approval.required_approver must be a string`);
|
|
282
|
-
}
|
|
283
|
-
if (approval.requiredApprover !== undefined &&
|
|
284
|
-
typeof approval.requiredApprover !== "string") {
|
|
285
|
-
throw new Error(`Workflow step ${step.id} approval.requiredApprover must be a string`);
|
|
286
|
-
}
|
|
287
|
-
if (approval.require_different_approver !== undefined &&
|
|
288
|
-
typeof approval.require_different_approver !== "boolean") {
|
|
289
|
-
throw new Error(`Workflow step ${step.id} approval.require_different_approver must be a boolean`);
|
|
290
|
-
}
|
|
291
|
-
if (approval.requireDifferentApprover !== undefined &&
|
|
292
|
-
typeof approval.requireDifferentApprover !== "boolean") {
|
|
293
|
-
throw new Error(`Workflow step ${step.id} approval.requireDifferentApprover must be a boolean`);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
if (step.timeout_ms !== undefined &&
|
|
297
|
-
(typeof step.timeout_ms !== "number" ||
|
|
298
|
-
!Number.isFinite(step.timeout_ms) ||
|
|
299
|
-
!Number.isInteger(step.timeout_ms) ||
|
|
300
|
-
step.timeout_ms < 1 ||
|
|
301
|
-
step.timeout_ms > 2_147_483_647)) {
|
|
302
|
-
throw new Error(`Workflow step ${step.id} timeout_ms must be a positive integer between 1 and 2147483647`);
|
|
303
|
-
}
|
|
304
|
-
if (step.on_error !== undefined &&
|
|
305
|
-
step.on_error !== "stop" &&
|
|
306
|
-
step.on_error !== "continue" &&
|
|
307
|
-
step.on_error !== "skip_rest") {
|
|
308
|
-
throw new Error(`Workflow step ${step.id} on_error must be "stop", "continue", or "skip_rest"`);
|
|
309
|
-
}
|
|
310
|
-
if (step.retry !== undefined) {
|
|
311
|
-
if (!step.retry || typeof step.retry !== "object" || Array.isArray(step.retry)) {
|
|
312
|
-
throw new Error(`Workflow step ${step.id} retry must be an object`);
|
|
313
|
-
}
|
|
314
|
-
const r = step.retry;
|
|
315
|
-
if (r.max !== undefined &&
|
|
316
|
-
(typeof r.max !== "number" || !Number.isInteger(r.max) || r.max < 1)) {
|
|
317
|
-
throw new Error(`Workflow step ${step.id} retry.max must be a positive integer`);
|
|
318
|
-
}
|
|
319
|
-
if (r.backoff !== undefined && r.backoff !== "fixed" && r.backoff !== "exponential") {
|
|
320
|
-
throw new Error(`Workflow step ${step.id} retry.backoff must be "fixed" or "exponential"`);
|
|
321
|
-
}
|
|
322
|
-
if (r.delay_ms !== undefined &&
|
|
323
|
-
(typeof r.delay_ms !== "number" || !Number.isFinite(r.delay_ms) || r.delay_ms < 0)) {
|
|
324
|
-
throw new Error(`Workflow step ${step.id} retry.delay_ms must be a finite non-negative number`);
|
|
325
|
-
}
|
|
326
|
-
if (r.max_delay_ms !== undefined &&
|
|
327
|
-
(typeof r.max_delay_ms !== "number" ||
|
|
328
|
-
!Number.isFinite(r.max_delay_ms) ||
|
|
329
|
-
r.max_delay_ms < 0)) {
|
|
330
|
-
throw new Error(`Workflow step ${step.id} retry.max_delay_ms must be a finite non-negative number`);
|
|
331
|
-
}
|
|
332
|
-
if (r.jitter !== undefined && typeof r.jitter !== "boolean") {
|
|
333
|
-
throw new Error(`Workflow step ${step.id} retry.jitter must be a boolean`);
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
if (seen.has(step.id)) {
|
|
337
|
-
throw new Error(`Duplicate workflow step id: ${step.id}`);
|
|
338
|
-
}
|
|
339
|
-
if (isParallel) {
|
|
340
|
-
const parallel = step.parallel;
|
|
341
|
-
for (const branch of parallel.branches) {
|
|
342
|
-
seen.add(branch.id);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
seen.add(step.id);
|
|
346
|
-
}
|
|
347
|
-
return parsed;
|
|
348
|
-
}
|
|
349
66
|
export function resolveWorkflowArgs(argDefs, provided) {
|
|
350
|
-
const resolved =
|
|
67
|
+
const resolved = createRecord();
|
|
351
68
|
if (argDefs) {
|
|
352
69
|
for (const [key, def] of Object.entries(argDefs)) {
|
|
353
70
|
if (def && typeof def === "object" && "default" in def) {
|
|
@@ -364,11 +81,74 @@ export function resolveWorkflowArgs(argDefs, provided) {
|
|
|
364
81
|
}
|
|
365
82
|
export async function runWorkflowFile({ filePath, args, ctx, resume, approved, response, cancel, }) {
|
|
366
83
|
const consumedResumeStateKey = resume?.stateKey && typeof resume.stateKey === "string"
|
|
367
|
-
? await resolveWorkflowResumeStateKey(ctx.env, resume.stateKey)
|
|
84
|
+
? await resolveWorkflowResumeStateKey(ctx.env, resume.stateKey, ctx.signal)
|
|
368
85
|
: null;
|
|
86
|
+
if (consumedResumeStateKey)
|
|
87
|
+
ctx._onResumeStateResolved?.(consumedResumeStateKey);
|
|
369
88
|
const resumeState = resume?.stateKey
|
|
370
|
-
? await loadWorkflowResumeState(ctx.env, consumedResumeStateKey ?? resume.stateKey)
|
|
89
|
+
? await loadWorkflowResumeState(ctx.env, consumedResumeStateKey ?? resume.stateKey, ctx.signal)
|
|
371
90
|
: (resume ?? null);
|
|
91
|
+
let resumedExecutionStarted = false;
|
|
92
|
+
let resumeStateConsumed = false;
|
|
93
|
+
let executionBoundaryStarted = false;
|
|
94
|
+
const executionSignalListeners = [];
|
|
95
|
+
let executionStart;
|
|
96
|
+
const markExecutionStarted = async (signal) => {
|
|
97
|
+
if (!consumedResumeStateKey && !ctx._onExecutionStarted && !ctx._onExecutionInterrupted)
|
|
98
|
+
return;
|
|
99
|
+
if (executionStart)
|
|
100
|
+
return executionStart;
|
|
101
|
+
executionStart = (async () => {
|
|
102
|
+
signal?.throwIfAborted();
|
|
103
|
+
if (consumedResumeStateKey) {
|
|
104
|
+
const consumption = await consumeResumeState({
|
|
105
|
+
env: ctx.env,
|
|
106
|
+
key: consumedResumeStateKey,
|
|
107
|
+
expectedState: resumeState,
|
|
108
|
+
signal,
|
|
109
|
+
});
|
|
110
|
+
if (!consumption.consumed) {
|
|
111
|
+
throw new Error("Workflow resume state not found");
|
|
112
|
+
}
|
|
113
|
+
if (consumption.signalAbortedAfterCommit) {
|
|
114
|
+
await restoreConsumedResumeState({
|
|
115
|
+
env: ctx.env,
|
|
116
|
+
key: consumedResumeStateKey,
|
|
117
|
+
expectedState: resumeState,
|
|
118
|
+
claimId: consumption.claimId,
|
|
119
|
+
});
|
|
120
|
+
signal?.throwIfAborted();
|
|
121
|
+
}
|
|
122
|
+
signal?.throwIfAborted();
|
|
123
|
+
resumeStateConsumed = true;
|
|
124
|
+
resumedExecutionStarted = true;
|
|
125
|
+
}
|
|
126
|
+
await ctx._onExecutionStarted?.();
|
|
127
|
+
executionBoundaryStarted = true;
|
|
128
|
+
if (!signal)
|
|
129
|
+
return;
|
|
130
|
+
const onAbort = () => {
|
|
131
|
+
ctx._onExecutionInterrupted?.();
|
|
132
|
+
};
|
|
133
|
+
if (signal.aborted) {
|
|
134
|
+
onAbort();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
138
|
+
executionSignalListeners.push({ signal, onAbort });
|
|
139
|
+
})();
|
|
140
|
+
try {
|
|
141
|
+
await executionStart;
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
// A per-attempt timeout can abort while this invocation is only waiting
|
|
145
|
+
// to claim its resume state. Keep the successful boundary memoized, but
|
|
146
|
+
// let the workflow retry make a fresh claim after that pre-dispatch wait.
|
|
147
|
+
if (!resumeStateConsumed && !executionBoundaryStarted)
|
|
148
|
+
executionStart = undefined;
|
|
149
|
+
throw err;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
372
152
|
if (resumeState?.approvalStepId && resumeState?.inputStepId) {
|
|
373
153
|
throw new Error("Invalid workflow resume state");
|
|
374
154
|
}
|
|
@@ -381,14 +161,26 @@ export async function runWorkflowFile({ filePath, args, ctx, resume, approved, r
|
|
|
381
161
|
}
|
|
382
162
|
if (cancel === true || approved === false) {
|
|
383
163
|
if (consumedResumeStateKey) {
|
|
384
|
-
await
|
|
164
|
+
const deletion = await deleteUnconsumedResumeState({
|
|
165
|
+
env: ctx.env,
|
|
166
|
+
key: consumedResumeStateKey,
|
|
167
|
+
signal: ctx.signal,
|
|
168
|
+
});
|
|
169
|
+
if (deletion !== "deleted")
|
|
170
|
+
throw new Error("Workflow resume state not found");
|
|
385
171
|
}
|
|
386
172
|
return { status: "cancelled", output: [] };
|
|
387
173
|
}
|
|
388
174
|
}
|
|
389
175
|
if (resumeState?.inputStepId && cancel === true) {
|
|
390
176
|
if (consumedResumeStateKey) {
|
|
391
|
-
await
|
|
177
|
+
const deletion = await deleteUnconsumedResumeState({
|
|
178
|
+
env: ctx.env,
|
|
179
|
+
key: consumedResumeStateKey,
|
|
180
|
+
signal: ctx.signal,
|
|
181
|
+
});
|
|
182
|
+
if (deletion !== "deleted")
|
|
183
|
+
throw new Error("Workflow resume state not found");
|
|
392
184
|
}
|
|
393
185
|
return { status: "cancelled", output: [] };
|
|
394
186
|
}
|
|
@@ -408,7 +200,7 @@ export async function runWorkflowFile({ filePath, args, ctx, resume, approved, r
|
|
|
408
200
|
const stepIndexById = new Map(steps.map((step, idx) => [step.id, idx]));
|
|
409
201
|
const results = resumeState?.steps
|
|
410
202
|
? cloneResults(resumeState.steps)
|
|
411
|
-
:
|
|
203
|
+
: createRecord();
|
|
412
204
|
const startIndex = resumeState?.resumeAtIndex ?? 0;
|
|
413
205
|
if (resumeState?.approvalStepId && typeof approved === "boolean") {
|
|
414
206
|
const previous = results[resumeState.approvalStepId] ?? { id: resumeState.approvalStepId };
|
|
@@ -459,11 +251,6 @@ export async function runWorkflowFile({ filePath, args, ctx, resume, approved, r
|
|
|
459
251
|
stepId: resumeState.inputStepId,
|
|
460
252
|
response,
|
|
461
253
|
pipelineInput: resumeState.pipelineInput,
|
|
462
|
-
onConsumed: consumedResumeStateKey
|
|
463
|
-
? async () => {
|
|
464
|
-
await deleteStateJson({ env: ctx.env, key: consumedResumeStateKey });
|
|
465
|
-
}
|
|
466
|
-
: undefined,
|
|
467
254
|
};
|
|
468
255
|
}
|
|
469
256
|
else {
|
|
@@ -492,8 +279,30 @@ export async function runWorkflowFile({ filePath, args, ctx, resume, approved, r
|
|
|
492
279
|
return dryRunWorkflow({ steps, resolvedArgs, results, startIndex, ctx });
|
|
493
280
|
}
|
|
494
281
|
const costTracker = new CostTracker(CostTracker.parsePricingFromEnv(ctx.env, ctx.stderr), ctx.stderr);
|
|
282
|
+
// A gate pauses a run, it does not reset its spend: seed this run with what the paused
|
|
283
|
+
// run recorded, so `_meta.cost` covers the whole workflow and `cost_limit` cannot be
|
|
284
|
+
// walked past one gate at a time. A replay of a call billed before the pause then stays
|
|
285
|
+
// exempt, which is what an empty ledger already means for a charge someone else settled.
|
|
286
|
+
const pausedCost = resumeState && "cost" in resumeState ? resumeState.cost : undefined;
|
|
287
|
+
costTracker.restore(pausedCost?.byStep);
|
|
288
|
+
// One ledger per run: a live call this run paid for can be recovered from the replay a
|
|
289
|
+
// retry produced, and a cache entry written by any other run has nothing to claim here.
|
|
290
|
+
const llmSpendLedger = createLlmSpendLedger(composingSpendLedger(ctx));
|
|
291
|
+
// A pipeline that paused at an `ask` gate had already paid for its LLM call without
|
|
292
|
+
// billing it, so the charge travels in the same resume state and reopens here. It is the
|
|
293
|
+
// same run continuing, which is why this cannot hand a charge to an unrelated one.
|
|
294
|
+
llmSpendLedger.restore(resumeState && "llmSpend" in resumeState ? resumeState.llmSpend : undefined);
|
|
295
|
+
// The completed steps come back as JSON, so an LLM output among them has lost everything
|
|
296
|
+
// this process attached to it. A later step that re-emits one hands on a copy that still
|
|
297
|
+
// names its call, and the restored record is what lets that copy be recognized as one the
|
|
298
|
+
// paused run already paid for rather than billed a second time on top of `cost`.
|
|
299
|
+
llmSpendLedger.restoreSettled(resumeState && "llmBilled" in resumeState ? resumeState.llmBilled : undefined);
|
|
300
|
+
// Completed results lost their symbol keys when the pause was persisted. Restore those
|
|
301
|
+
// private marks only at this trusted resume boundary; ordinary command JSON stays billable.
|
|
302
|
+
restoreLlmProvenance(results, resumeState && "llmBilled" in resumeState ? resumeState.llmBilled : undefined);
|
|
495
303
|
let lastStepId = resumeState?.inputStepId ?? findLastCompletedStepId(steps, results);
|
|
496
304
|
for (let idx = startIndex; idx < steps.length; idx++) {
|
|
305
|
+
ctx.signal?.throwIfAborted();
|
|
497
306
|
const step = steps[idx];
|
|
498
307
|
if (!evaluateCondition(step.when ?? step.condition, results)) {
|
|
499
308
|
results[step.id] = { id: step.id, skipped: true };
|
|
@@ -515,40 +324,65 @@ export async function runWorkflowFile({ filePath, args, ctx, resume, approved, r
|
|
|
515
324
|
subject,
|
|
516
325
|
maxEnvelopeBytes: resolveToolEnvelopeMaxBytes(ctx.env),
|
|
517
326
|
});
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
327
|
+
let stateKey;
|
|
328
|
+
try {
|
|
329
|
+
stateKey = await saveWorkflowResumeState(ctx.env, {
|
|
330
|
+
filePath: resolvedFilePath,
|
|
331
|
+
resumeAtIndex: idx + 1,
|
|
332
|
+
steps: results,
|
|
333
|
+
args: resolvedArgs,
|
|
334
|
+
inputStepId: step.id,
|
|
335
|
+
inputSchema: step.input.responseSchema,
|
|
336
|
+
// Preserve the full resolved subject for resume semantics; the tool
|
|
337
|
+
// envelope may contain a truncated preview to stay within size limits.
|
|
338
|
+
inputSubject: subject,
|
|
339
|
+
...(costTracker.hasUsage() ? { cost: costTracker.getSummary() } : null),
|
|
340
|
+
...(llmSpendLedger.outstanding().length
|
|
341
|
+
? { llmSpend: llmSpendLedger.outstanding() }
|
|
342
|
+
: null),
|
|
343
|
+
...(llmSpendLedger.settled().length
|
|
344
|
+
? { llmBilled: llmSpendLedger.settled() }
|
|
345
|
+
: null),
|
|
346
|
+
supersededResumeStateKeys: collectSupersededWorkflowResumeStateKeys(consumedResumeStateKey, resumeState),
|
|
347
|
+
createdAt: new Date().toISOString(),
|
|
348
|
+
}, ctx.signal);
|
|
349
|
+
const replaced = await replaceWorkflowResumeState({
|
|
350
|
+
env: ctx.env,
|
|
351
|
+
previousStateKey: consumedResumeStateKey,
|
|
352
|
+
expectedPreviousState: resumeState,
|
|
353
|
+
previousStateConsumed: resumeStateConsumed,
|
|
354
|
+
replacementStateKey: stateKey,
|
|
355
|
+
signal: ctx.signal,
|
|
356
|
+
});
|
|
357
|
+
if (!replaced) {
|
|
358
|
+
throw new Error("Workflow resume state not found");
|
|
359
|
+
}
|
|
360
|
+
const resumeToken = encodeToken({
|
|
361
|
+
protocolVersion: 1,
|
|
362
|
+
v: 1,
|
|
363
|
+
kind: "workflow-file",
|
|
364
|
+
stateKey,
|
|
365
|
+
});
|
|
366
|
+
return {
|
|
367
|
+
status: "needs_input",
|
|
368
|
+
output: [],
|
|
369
|
+
requiresInput: {
|
|
370
|
+
...inputRequest,
|
|
371
|
+
resumeToken,
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
catch (err) {
|
|
376
|
+
if (stateKey)
|
|
377
|
+
await discardWorkflowResumeState(ctx.env, stateKey);
|
|
378
|
+
throw err;
|
|
532
379
|
}
|
|
533
|
-
const resumeToken = encodeToken({
|
|
534
|
-
protocolVersion: 1,
|
|
535
|
-
v: 1,
|
|
536
|
-
kind: "workflow-file",
|
|
537
|
-
stateKey,
|
|
538
|
-
});
|
|
539
|
-
return {
|
|
540
|
-
status: "needs_input",
|
|
541
|
-
output: [],
|
|
542
|
-
requiresInput: {
|
|
543
|
-
...inputRequest,
|
|
544
|
-
resumeToken,
|
|
545
|
-
},
|
|
546
|
-
};
|
|
547
380
|
}
|
|
548
381
|
ctx.stdout.write(`${step.input.prompt}\n`);
|
|
549
382
|
ctx.stdout.write("Enter JSON response: ");
|
|
550
383
|
const raw = await readLineFromStream(ctx.stdin, {
|
|
551
384
|
timeoutMs: parseApprovalTimeoutMs(ctx.env),
|
|
385
|
+
signal: ctx.signal,
|
|
552
386
|
});
|
|
553
387
|
const parsed = parseResponseJson(String(raw ?? "").trim());
|
|
554
388
|
validateInputResponse({
|
|
@@ -564,132 +398,170 @@ export async function runWorkflowFile({ filePath, args, ctx, resume, approved, r
|
|
|
564
398
|
lastStepId = step.id;
|
|
565
399
|
continue;
|
|
566
400
|
}
|
|
567
|
-
if (typeof step.for_each === "string" && Array.isArray(step.steps)) {
|
|
568
|
-
const itemsRef = resolveInputValue(step.for_each, resolvedArgs, results);
|
|
569
|
-
if (!Array.isArray(itemsRef)) {
|
|
570
|
-
throw new Error(`Workflow step ${step.id} for_each: expected array, got ${typeof itemsRef}`);
|
|
571
|
-
}
|
|
572
|
-
const itemVar = step.item_var ?? "item";
|
|
573
|
-
const indexVar = step.index_var ?? "index";
|
|
574
|
-
const batchSize = step.batch_size ?? 1;
|
|
575
|
-
const iterationResults = [];
|
|
576
|
-
for (let itemIdx = 0; itemIdx < itemsRef.length; itemIdx++) {
|
|
577
|
-
if (step.pause_ms && itemIdx > 0 && itemIdx % batchSize === 0) {
|
|
578
|
-
await abortableSleep(step.pause_ms, ctx.signal);
|
|
579
|
-
}
|
|
580
|
-
const item = itemsRef[itemIdx];
|
|
581
|
-
const scopedResults = { ...results };
|
|
582
|
-
scopedResults[itemVar] = {
|
|
583
|
-
id: itemVar,
|
|
584
|
-
json: item,
|
|
585
|
-
stdout: typeof item === "string" ? item : JSON.stringify(item),
|
|
586
|
-
};
|
|
587
|
-
scopedResults[indexVar] = {
|
|
588
|
-
id: indexVar,
|
|
589
|
-
json: itemIdx,
|
|
590
|
-
stdout: String(itemIdx),
|
|
591
|
-
};
|
|
592
|
-
for (const subStep of step.steps) {
|
|
593
|
-
if (!evaluateCondition(subStep.when ?? subStep.condition, scopedResults)) {
|
|
594
|
-
scopedResults[subStep.id] = { id: subStep.id, skipped: true };
|
|
595
|
-
continue;
|
|
596
|
-
}
|
|
597
|
-
const loopEnvBase = mergeEnv(ctx.env, workflow.env, step.env, resolvedArgs, scopedResults);
|
|
598
|
-
const subEnv = subStep.env
|
|
599
|
-
? mergeEnv(loopEnvBase, undefined, subStep.env, resolvedArgs, scopedResults)
|
|
600
|
-
: loopEnvBase;
|
|
601
|
-
const subCwd = resolveCwd(subStep.cwd ?? step.cwd ?? workflow.cwd, resolvedArgs) ?? ctx.cwd;
|
|
602
|
-
const subExecution = getStepExecution(subStep);
|
|
603
|
-
let subResult;
|
|
604
|
-
if (subExecution.kind === "shell") {
|
|
605
|
-
const command = resolveTemplate(subExecution.value, resolvedArgs, scopedResults);
|
|
606
|
-
const stdinValue = resolveShellStdin(subStep.stdin, resolvedArgs, scopedResults);
|
|
607
|
-
const { stdout } = await runShellCommand({
|
|
608
|
-
command,
|
|
609
|
-
stdin: stdinValue,
|
|
610
|
-
env: subEnv,
|
|
611
|
-
cwd: subCwd,
|
|
612
|
-
signal: ctx.signal,
|
|
613
|
-
});
|
|
614
|
-
subResult = { id: subStep.id, stdout, json: parseJson(stdout) };
|
|
615
|
-
}
|
|
616
|
-
else if (subExecution.kind === "pipeline") {
|
|
617
|
-
if (!ctx.registry) {
|
|
618
|
-
throw new Error(`Workflow step ${step.id} for_each sub-step ${subStep.id} requires a command registry for pipeline execution`);
|
|
619
|
-
}
|
|
620
|
-
const pipelineText = resolveTemplate(subExecution.value, resolvedArgs, scopedResults);
|
|
621
|
-
const inputValue = resolveInputValue(subStep.stdin, resolvedArgs, scopedResults);
|
|
622
|
-
subResult = await runPipelineStep({
|
|
623
|
-
stepId: subStep.id,
|
|
624
|
-
pipelineText,
|
|
625
|
-
inputValue,
|
|
626
|
-
ctx,
|
|
627
|
-
env: subEnv,
|
|
628
|
-
cwd: subCwd,
|
|
629
|
-
requestInputEnabled: false,
|
|
630
|
-
});
|
|
631
|
-
}
|
|
632
|
-
else {
|
|
633
|
-
const inputValue = resolveInputValue(subStep.stdin, resolvedArgs, scopedResults);
|
|
634
|
-
subResult = createSyntheticStepResult(subStep.id, inputValue);
|
|
635
|
-
}
|
|
636
|
-
scopedResults[subStep.id] = subResult;
|
|
637
|
-
trackStepCost(costTracker, `${step.id}.${subStep.id}`, subResult);
|
|
638
|
-
if (workflow.cost_limit) {
|
|
639
|
-
costTracker.checkLimit(workflow.cost_limit, ctx.stderr);
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
const iterResult = { [itemVar]: item, [indexVar]: itemIdx };
|
|
643
|
-
for (const subStep of step.steps) {
|
|
644
|
-
const subResult = scopedResults[subStep.id];
|
|
645
|
-
if (subResult && !subResult.skipped) {
|
|
646
|
-
iterResult[subStep.id] =
|
|
647
|
-
subResult.json !== undefined ? subResult.json : subResult.stdout;
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
iterationResults.push(iterResult);
|
|
651
|
-
}
|
|
652
|
-
const loopResult = {
|
|
653
|
-
id: step.id,
|
|
654
|
-
json: iterationResults,
|
|
655
|
-
stdout: JSON.stringify(iterationResults),
|
|
656
|
-
};
|
|
657
|
-
results[step.id] = loopResult;
|
|
658
|
-
lastStepId = step.id;
|
|
659
|
-
trackStepCost(costTracker, step.id, loopResult);
|
|
660
|
-
if (workflow.cost_limit) {
|
|
661
|
-
costTracker.checkLimit(workflow.cost_limit, ctx.stderr);
|
|
662
|
-
}
|
|
663
|
-
continue;
|
|
664
|
-
}
|
|
665
401
|
const env = mergeEnv(ctx.env, workflow.env, step.env, resolvedArgs, results);
|
|
666
402
|
const cwd = resolveCwd(step.cwd ?? workflow.cwd, resolvedArgs) ?? ctx.cwd;
|
|
667
403
|
const execution = getStepExecution(step);
|
|
668
404
|
const retryConfig = resolveRetryConfig(step.retry);
|
|
669
405
|
const executeStepAttempt = async () => {
|
|
406
|
+
let attemptHadNonRetryableSideEffect = false;
|
|
407
|
+
const markNonRetryableSideEffect = () => {
|
|
408
|
+
attemptHadNonRetryableSideEffect = true;
|
|
409
|
+
ctx._onNonRetryableSideEffect?.();
|
|
410
|
+
};
|
|
411
|
+
ctx.signal?.throwIfAborted();
|
|
670
412
|
// Combine external cancellation and optional per-step timeout into one signal.
|
|
671
413
|
let stepSignal = ctx.signal;
|
|
672
414
|
let timeoutId;
|
|
415
|
+
let stepTimeoutController;
|
|
673
416
|
if (step.timeout_ms) {
|
|
674
|
-
|
|
675
|
-
timeoutId = setTimeout(() =>
|
|
417
|
+
stepTimeoutController = new AbortController();
|
|
418
|
+
timeoutId = setTimeout(() => stepTimeoutController.abort(new Error(`Step '${step.id}' timed out after ${step.timeout_ms}ms`)), step.timeout_ms);
|
|
676
419
|
stepSignal = ctx.signal
|
|
677
|
-
? AbortSignal.any([ctx.signal,
|
|
678
|
-
:
|
|
420
|
+
? AbortSignal.any([ctx.signal, stepTimeoutController.signal])
|
|
421
|
+
: stepTimeoutController.signal;
|
|
679
422
|
}
|
|
680
423
|
let result;
|
|
681
424
|
let parallelBranchResults = null;
|
|
682
425
|
try {
|
|
683
|
-
if (
|
|
426
|
+
if (typeof step.for_each === "string" && Array.isArray(step.steps)) {
|
|
427
|
+
const itemsRef = resolveInputValue(step.for_each, resolvedArgs, results);
|
|
428
|
+
if (!Array.isArray(itemsRef)) {
|
|
429
|
+
throw new Error(`Workflow step ${step.id} for_each: expected array, got ${typeof itemsRef}`);
|
|
430
|
+
}
|
|
431
|
+
const itemVar = step.item_var ?? "item";
|
|
432
|
+
const indexVar = step.index_var ?? "index";
|
|
433
|
+
const batchSize = step.batch_size ?? 1;
|
|
434
|
+
const iterationResults = [];
|
|
435
|
+
for (let itemIdx = 0; itemIdx < itemsRef.length; itemIdx++) {
|
|
436
|
+
stepSignal?.throwIfAborted();
|
|
437
|
+
if (step.pause_ms && itemIdx > 0 && itemIdx % batchSize === 0) {
|
|
438
|
+
await abortableSleep(step.pause_ms, stepSignal);
|
|
439
|
+
}
|
|
440
|
+
const item = itemsRef[itemIdx];
|
|
441
|
+
const scopedResults = createRecord(results);
|
|
442
|
+
scopedResults[itemVar] = {
|
|
443
|
+
id: itemVar,
|
|
444
|
+
json: item,
|
|
445
|
+
stdout: typeof item === "string" ? item : JSON.stringify(item),
|
|
446
|
+
};
|
|
447
|
+
scopedResults[indexVar] = {
|
|
448
|
+
id: indexVar,
|
|
449
|
+
json: itemIdx,
|
|
450
|
+
stdout: String(itemIdx),
|
|
451
|
+
};
|
|
452
|
+
for (const subStep of step.steps) {
|
|
453
|
+
stepSignal?.throwIfAborted();
|
|
454
|
+
if (!evaluateCondition(subStep.when ?? subStep.condition, scopedResults)) {
|
|
455
|
+
scopedResults[subStep.id] = { id: subStep.id, skipped: true };
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
const loopEnvBase = mergeEnv(ctx.env, workflow.env, step.env, resolvedArgs, scopedResults);
|
|
459
|
+
const subEnv = subStep.env
|
|
460
|
+
? mergeEnv(loopEnvBase, undefined, subStep.env, resolvedArgs, scopedResults)
|
|
461
|
+
: loopEnvBase;
|
|
462
|
+
const subCwd = resolveCwd(subStep.cwd ?? step.cwd ?? workflow.cwd, resolvedArgs) ?? ctx.cwd;
|
|
463
|
+
const subExecution = getStepExecution(subStep);
|
|
464
|
+
let subResult;
|
|
465
|
+
if (subExecution.kind === "shell") {
|
|
466
|
+
const command = resolveTemplate(subExecution.value, resolvedArgs, scopedResults);
|
|
467
|
+
const stdinValue = resolveShellStdin(subStep.stdin, resolvedArgs, scopedResults);
|
|
468
|
+
await markExecutionStarted(stepSignal);
|
|
469
|
+
const { stdout } = await runShellCommand({
|
|
470
|
+
command,
|
|
471
|
+
stdin: stdinValue,
|
|
472
|
+
env: subEnv,
|
|
473
|
+
cwd: subCwd,
|
|
474
|
+
signal: stepSignal,
|
|
475
|
+
forceTerminationSignal: ctx.forceTerminationSignal,
|
|
476
|
+
killSignal: () => stepTimeoutController?.signal.aborted
|
|
477
|
+
? "SIGKILL"
|
|
478
|
+
: undefined,
|
|
479
|
+
});
|
|
480
|
+
subResult = { id: subStep.id, stdout, json: parseJson(stdout) };
|
|
481
|
+
}
|
|
482
|
+
else if (subExecution.kind === "pipeline") {
|
|
483
|
+
if (!ctx.registry) {
|
|
484
|
+
throw new Error(`Workflow step ${step.id} for_each sub-step ${subStep.id} requires a command registry for pipeline execution`);
|
|
485
|
+
}
|
|
486
|
+
const pipelineText = resolveTemplate(subExecution.value, resolvedArgs, scopedResults);
|
|
487
|
+
const inputValue = resolveInputValue(subStep.stdin, resolvedArgs, scopedResults);
|
|
488
|
+
subResult = await runPipelineStep({
|
|
489
|
+
stepId: subStep.id,
|
|
490
|
+
pipelineText,
|
|
491
|
+
inputValue,
|
|
492
|
+
ctx: {
|
|
493
|
+
...ctx,
|
|
494
|
+
signal: stepSignal,
|
|
495
|
+
_onNonRetryableSideEffect: markNonRetryableSideEffect,
|
|
496
|
+
},
|
|
497
|
+
llmSpendLedger,
|
|
498
|
+
env: subEnv,
|
|
499
|
+
cwd: subCwd,
|
|
500
|
+
requestInputEnabled: false,
|
|
501
|
+
onExecutionStart: () => markExecutionStarted(stepSignal),
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
const inputValue = resolveInputValue(subStep.stdin, resolvedArgs, scopedResults);
|
|
506
|
+
subResult = createSyntheticStepResult(subStep.id, inputValue);
|
|
507
|
+
}
|
|
508
|
+
scopedResults[subStep.id] = subResult;
|
|
509
|
+
trackStepCost(costTracker, `${step.id}.${subStep.id}`, subResult, llmSpendLedger);
|
|
510
|
+
// Before the next step runs: a charge no item carried is spend the step really made,
|
|
511
|
+
// and leaving it for the end would let `cost_limit` wave through every step after the
|
|
512
|
+
// one that blew the budget.
|
|
513
|
+
settleUnbilledCharges(costTracker, llmSpendLedger);
|
|
514
|
+
if (workflow.cost_limit) {
|
|
515
|
+
costTracker.checkLimit(workflow.cost_limit, ctx.stderr);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
const iterResult = createRecord();
|
|
519
|
+
iterResult[itemVar] = item;
|
|
520
|
+
iterResult[indexVar] = itemIdx;
|
|
521
|
+
for (const subStep of step.steps) {
|
|
522
|
+
const subResult = scopedResults[subStep.id];
|
|
523
|
+
if (subResult && !subResult.skipped) {
|
|
524
|
+
iterResult[subStep.id] =
|
|
525
|
+
subResult.json !== undefined ? subResult.json : subResult.stdout;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
iterationResults.push(Object.fromEntries(Object.entries(iterResult)));
|
|
529
|
+
}
|
|
530
|
+
result = {
|
|
531
|
+
id: step.id,
|
|
532
|
+
json: iterationResults,
|
|
533
|
+
stdout: JSON.stringify(iterationResults),
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
else if (execution.kind === "parallel") {
|
|
684
537
|
const parallel = execution.value;
|
|
685
538
|
const wait = parallel.wait ?? "all";
|
|
686
539
|
const branchAbortController = new AbortController();
|
|
687
|
-
const
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
540
|
+
const parallelTimeoutController = new AbortController();
|
|
541
|
+
const parallelSignal = stepSignal
|
|
542
|
+
? AbortSignal.any([stepSignal, parallelTimeoutController.signal])
|
|
543
|
+
: parallelTimeoutController.signal;
|
|
544
|
+
const branchSignal = AbortSignal.any([parallelSignal, branchAbortController.signal]);
|
|
545
|
+
const timeoutKillSignal = () => stepTimeoutController?.signal.aborted || parallelTimeoutController.signal.aborted
|
|
546
|
+
? "SIGKILL"
|
|
547
|
+
: undefined;
|
|
548
|
+
// `wait: "any"` keeps the first branch to answer and abandons the rest, so a
|
|
549
|
+
// losing branch spends against a buffer of its own. Only the winner's charges
|
|
550
|
+
// reach the run. `wait: "all"` keeps every branch, so those bill straight
|
|
551
|
+
// through as before.
|
|
552
|
+
const branchLedgers = new Map();
|
|
553
|
+
const ledgerFor = (branchId) => {
|
|
554
|
+
if (wait !== "any")
|
|
555
|
+
return llmSpendLedger;
|
|
556
|
+
let buffered = branchLedgers.get(branchId);
|
|
557
|
+
if (!buffered) {
|
|
558
|
+
buffered = branchSpendBuffer(llmSpendLedger);
|
|
559
|
+
branchLedgers.set(branchId, buffered);
|
|
560
|
+
}
|
|
561
|
+
return buffered;
|
|
562
|
+
};
|
|
691
563
|
const runBranch = async (branch) => {
|
|
692
|
-
const mergedBranchEnv = { ...
|
|
564
|
+
const mergedBranchEnv = { ...step.env, ...branch.env };
|
|
693
565
|
const branchEnv = mergeEnv(ctx.env, workflow.env, mergedBranchEnv, resolvedArgs, results);
|
|
694
566
|
const branchCwd = resolveCwd(branch.cwd ?? step.cwd ?? workflow.cwd, resolvedArgs) ?? ctx.cwd;
|
|
695
567
|
const branchShell = typeof branch.run === "string" ? branch.run : branch.command;
|
|
@@ -701,13 +573,15 @@ export async function runWorkflowFile({ filePath, args, ctx, resume, approved, r
|
|
|
701
573
|
if (branchExec.kind === "shell") {
|
|
702
574
|
const command = resolveTemplate(branchExec.value, resolvedArgs, results);
|
|
703
575
|
const stdinValue = resolveShellStdin(branch.stdin, resolvedArgs, results);
|
|
576
|
+
await markExecutionStarted(branchSignal);
|
|
704
577
|
const { stdout } = await runShellCommand({
|
|
705
578
|
command,
|
|
706
579
|
stdin: stdinValue,
|
|
707
580
|
env: branchEnv,
|
|
708
581
|
cwd: branchCwd,
|
|
709
582
|
signal: branchSignal,
|
|
710
|
-
|
|
583
|
+
forceTerminationSignal: ctx.forceTerminationSignal,
|
|
584
|
+
killSignal: timeoutKillSignal,
|
|
711
585
|
});
|
|
712
586
|
return {
|
|
713
587
|
branchId: branch.id,
|
|
@@ -724,10 +598,16 @@ export async function runWorkflowFile({ filePath, args, ctx, resume, approved, r
|
|
|
724
598
|
stepId: branch.id,
|
|
725
599
|
pipelineText,
|
|
726
600
|
inputValue,
|
|
727
|
-
ctx: {
|
|
601
|
+
ctx: {
|
|
602
|
+
...ctx,
|
|
603
|
+
signal: branchSignal,
|
|
604
|
+
_onNonRetryableSideEffect: markNonRetryableSideEffect,
|
|
605
|
+
},
|
|
606
|
+
llmSpendLedger: ledgerFor(branch.id),
|
|
728
607
|
env: branchEnv,
|
|
729
608
|
cwd: branchCwd,
|
|
730
609
|
requestInputEnabled: false,
|
|
610
|
+
onExecutionStart: () => markExecutionStarted(branchSignal),
|
|
731
611
|
});
|
|
732
612
|
return { branchId: branch.id, result: branchResult };
|
|
733
613
|
}
|
|
@@ -737,26 +617,27 @@ export async function runWorkflowFile({ filePath, args, ctx, resume, approved, r
|
|
|
737
617
|
const timeoutPromise = parallel.timeout_ms
|
|
738
618
|
? new Promise((_resolve, reject) => {
|
|
739
619
|
parallelTimeoutId = setTimeout(() => {
|
|
740
|
-
|
|
620
|
+
parallelTimeoutController.abort(new Error(`Parallel step ${step.id} timed out after ${parallel.timeout_ms}ms`));
|
|
741
621
|
reject(new Error(`Parallel step ${step.id} timed out after ${parallel.timeout_ms}ms`));
|
|
742
622
|
}, parallel.timeout_ms);
|
|
743
623
|
})
|
|
744
624
|
: null;
|
|
625
|
+
const branchPromises = parallel.branches.map((branch) => runBranch(branch));
|
|
626
|
+
const branchesSettled = Promise.allSettled(branchPromises);
|
|
745
627
|
try {
|
|
746
628
|
if (wait === "any") {
|
|
747
|
-
const branchPromises = parallel.branches.map((branch) => runBranch(branch));
|
|
748
629
|
const winner = (await (timeoutPromise
|
|
749
630
|
? Promise.race([...branchPromises, timeoutPromise])
|
|
750
631
|
: Promise.race(branchPromises)));
|
|
751
|
-
parallelBranchResults =
|
|
752
|
-
|
|
632
|
+
parallelBranchResults = createRecord();
|
|
633
|
+
parallelBranchResults[winner.branchId] = winner.result;
|
|
634
|
+
branchLedgers.get(winner.branchId)?.release();
|
|
753
635
|
}
|
|
754
636
|
else {
|
|
755
|
-
const branchPromises = parallel.branches.map((branch) => runBranch(branch));
|
|
756
637
|
const settled = (await (timeoutPromise
|
|
757
638
|
? Promise.race([Promise.allSettled(branchPromises), timeoutPromise])
|
|
758
|
-
:
|
|
759
|
-
parallelBranchResults =
|
|
639
|
+
: branchesSettled));
|
|
640
|
+
parallelBranchResults = createRecord();
|
|
760
641
|
for (const entry of settled) {
|
|
761
642
|
if (entry.status === "rejected") {
|
|
762
643
|
throw new Error(`Parallel branch failed: ${entry.reason?.message ?? String(entry.reason)}`);
|
|
@@ -768,11 +649,18 @@ export async function runWorkflowFile({ filePath, args, ctx, resume, approved, r
|
|
|
768
649
|
finally {
|
|
769
650
|
if (parallelTimeoutId !== undefined)
|
|
770
651
|
clearTimeout(parallelTimeoutId);
|
|
652
|
+
const branchesCancelled = wait === "any" || parallelSignal.aborted;
|
|
653
|
+
if (branchesCancelled)
|
|
654
|
+
branchAbortController.abort();
|
|
655
|
+
if (branchesCancelled)
|
|
656
|
+
await waitForCancelledParallelBranches(branchesSettled);
|
|
657
|
+
else
|
|
658
|
+
await branchesSettled;
|
|
771
659
|
}
|
|
772
|
-
const merged = {}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
660
|
+
const merged = Object.fromEntries(Object.entries(parallelBranchResults ?? {}).map(([branchId, branchResult]) => [
|
|
661
|
+
branchId,
|
|
662
|
+
branchResult.json,
|
|
663
|
+
]));
|
|
776
664
|
result = {
|
|
777
665
|
id: step.id,
|
|
778
666
|
json: merged,
|
|
@@ -800,10 +688,25 @@ export async function runWorkflowFile({ filePath, args, ctx, resume, approved, r
|
|
|
800
688
|
const childActive = new Set(activeWorkflows);
|
|
801
689
|
childActive.add(canonicalWorkflowPath);
|
|
802
690
|
const subArgs = resolveWorkflowStepArgs(step.workflow_args, resolvedArgs, results);
|
|
691
|
+
stepSignal?.throwIfAborted();
|
|
803
692
|
const subResult = await runWorkflowFile({
|
|
804
693
|
filePath: resolvedWorkflowPath,
|
|
805
694
|
args: subArgs,
|
|
806
|
-
ctx: {
|
|
695
|
+
ctx: {
|
|
696
|
+
...ctx,
|
|
697
|
+
env,
|
|
698
|
+
cwd,
|
|
699
|
+
signal: stepSignal,
|
|
700
|
+
_activeWorkflows: childActive,
|
|
701
|
+
_llmSpendLedger: stepScopedLedger(llmSpendLedger, step.id),
|
|
702
|
+
_onExecutionStarted: async () => {
|
|
703
|
+
await markExecutionStarted(stepSignal);
|
|
704
|
+
},
|
|
705
|
+
_onExecutionInterrupted: () => {
|
|
706
|
+
ctx._onExecutionInterrupted?.();
|
|
707
|
+
},
|
|
708
|
+
_onNonRetryableSideEffect: markNonRetryableSideEffect,
|
|
709
|
+
},
|
|
807
710
|
});
|
|
808
711
|
if (subResult.status === "needs_approval" || subResult.status === "needs_input") {
|
|
809
712
|
const resumeToken = subResult.requiresApproval?.resumeToken ?? subResult.requiresInput?.resumeToken;
|
|
@@ -823,17 +726,26 @@ export async function runWorkflowFile({ filePath, args, ctx, resume, approved, r
|
|
|
823
726
|
const json = subResult.output.length === 1 ? subResult.output[0] : subResult.output;
|
|
824
727
|
const stdout = subResult.output.length ? serializeValueForStdout(json) : "";
|
|
825
728
|
result = { id: step.id, stdout, json };
|
|
729
|
+
// A child ending in a rendered pipeline (`llm.invoke | json`) prints its items
|
|
730
|
+
// and returns JSON parsed back out of that print, which no symbol survives.
|
|
731
|
+
// Carry the child's own items across the composition boundary so this run
|
|
732
|
+
// applies to them the same replay rule it applies to its own pipelines.
|
|
733
|
+
const composedItems = pipelineSourceItems(subResult);
|
|
734
|
+
if (composedItems)
|
|
735
|
+
attachPipelineSourceItems(result, composedItems);
|
|
826
736
|
}
|
|
827
737
|
else if (execution.kind === "shell") {
|
|
828
738
|
const command = resolveTemplate(execution.value, resolvedArgs, results);
|
|
829
739
|
const stdinValue = resolveShellStdin(step.stdin, resolvedArgs, results);
|
|
740
|
+
await markExecutionStarted(stepSignal);
|
|
830
741
|
const { stdout } = await runShellCommand({
|
|
831
742
|
command,
|
|
832
743
|
stdin: stdinValue,
|
|
833
744
|
env,
|
|
834
745
|
cwd,
|
|
835
746
|
signal: stepSignal,
|
|
836
|
-
|
|
747
|
+
forceTerminationSignal: ctx.forceTerminationSignal,
|
|
748
|
+
killSignal: () => stepTimeoutController?.signal.aborted ? "SIGKILL" : undefined,
|
|
837
749
|
});
|
|
838
750
|
result = { id: step.id, stdout, json: parseJson(stdout) };
|
|
839
751
|
}
|
|
@@ -847,7 +759,12 @@ export async function runWorkflowFile({ filePath, args, ctx, resume, approved, r
|
|
|
847
759
|
stepId: step.id,
|
|
848
760
|
pipelineText,
|
|
849
761
|
inputValue,
|
|
850
|
-
ctx: {
|
|
762
|
+
ctx: {
|
|
763
|
+
...ctx,
|
|
764
|
+
signal: stepSignal,
|
|
765
|
+
_onNonRetryableSideEffect: markNonRetryableSideEffect,
|
|
766
|
+
},
|
|
767
|
+
llmSpendLedger,
|
|
851
768
|
env,
|
|
852
769
|
cwd,
|
|
853
770
|
resume: resumedPipelineInput?.stepId === step.id
|
|
@@ -857,14 +774,23 @@ export async function runWorkflowFile({ filePath, args, ctx, resume, approved, r
|
|
|
857
774
|
onConsumed: resumedPipelineInput.onConsumed,
|
|
858
775
|
}
|
|
859
776
|
: undefined,
|
|
777
|
+
onExecutionStart: () => markExecutionStarted(stepSignal),
|
|
860
778
|
});
|
|
861
779
|
}
|
|
862
780
|
else {
|
|
781
|
+
if (execution.kind !== "none")
|
|
782
|
+
await markExecutionStarted(stepSignal);
|
|
863
783
|
const inputValue = resolveInputValue(step.stdin, resolvedArgs, results);
|
|
864
784
|
result = createSyntheticStepResult(step.id, inputValue);
|
|
865
785
|
}
|
|
866
786
|
return { result, parallelBranchResults };
|
|
867
787
|
}
|
|
788
|
+
catch (error) {
|
|
789
|
+
if (attemptHadNonRetryableSideEffect) {
|
|
790
|
+
throw markStepErrorNonRetryable(error);
|
|
791
|
+
}
|
|
792
|
+
throw error;
|
|
793
|
+
}
|
|
868
794
|
finally {
|
|
869
795
|
if (timeoutId !== undefined)
|
|
870
796
|
clearTimeout(timeoutId);
|
|
@@ -878,9 +804,20 @@ export async function runWorkflowFile({ filePath, args, ctx, resume, approved, r
|
|
|
878
804
|
signal: ctx.signal,
|
|
879
805
|
shouldRetry: (error) => {
|
|
880
806
|
if (error instanceof WorkflowPipelineInputSuspension ||
|
|
881
|
-
error instanceof RequestInputResumeError
|
|
807
|
+
error instanceof RequestInputResumeError ||
|
|
808
|
+
error instanceof CostLimitExceededError) {
|
|
809
|
+
return false;
|
|
810
|
+
}
|
|
811
|
+
if (isStepErrorNonRetryable(error)) {
|
|
882
812
|
return false;
|
|
883
813
|
}
|
|
814
|
+
if (ctx.signal?.aborted)
|
|
815
|
+
return false;
|
|
816
|
+
if (workflow.cost_limit?.action === "stop") {
|
|
817
|
+
// Failed pipelines can discard output after the provider was paid.
|
|
818
|
+
settleUnbilledCharges(costTracker, llmSpendLedger);
|
|
819
|
+
costTracker.checkLimit(workflow.cost_limit, ctx.stderr);
|
|
820
|
+
}
|
|
884
821
|
const message = error?.message ?? String(error);
|
|
885
822
|
return !/halted (for approval inside|before completion at) pipeline/.test(message);
|
|
886
823
|
},
|
|
@@ -902,40 +839,67 @@ export async function runWorkflowFile({ filePath, args, ctx, resume, approved, r
|
|
|
902
839
|
subject: err.request.subject,
|
|
903
840
|
maxEnvelopeBytes: resolveToolEnvelopeMaxBytes(ctx.env),
|
|
904
841
|
});
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
842
|
+
let stateKey;
|
|
843
|
+
try {
|
|
844
|
+
stateKey = await saveWorkflowResumeState(ctx.env, {
|
|
845
|
+
filePath: resolvedFilePath,
|
|
846
|
+
resumeAtIndex: idx,
|
|
847
|
+
steps: results,
|
|
848
|
+
args: resolvedArgs,
|
|
849
|
+
inputStepId: err.stepId,
|
|
850
|
+
inputKind: "pipeline_command",
|
|
851
|
+
inputSchema: err.request.responseSchema,
|
|
852
|
+
inputSubject: err.request.subject,
|
|
853
|
+
pipelineInput: err.pipelineInput,
|
|
854
|
+
...(costTracker.hasUsage() ? { cost: costTracker.getSummary() } : null),
|
|
855
|
+
...(llmSpendLedger.outstanding().length
|
|
856
|
+
? { llmSpend: llmSpendLedger.outstanding() }
|
|
857
|
+
: null),
|
|
858
|
+
...(llmSpendLedger.settled().length
|
|
859
|
+
? { llmBilled: llmSpendLedger.settled() }
|
|
860
|
+
: null),
|
|
861
|
+
supersededResumeStateKeys: collectSupersededWorkflowResumeStateKeys(consumedResumeStateKey, resumeState),
|
|
862
|
+
createdAt: new Date().toISOString(),
|
|
863
|
+
}, ctx.signal);
|
|
864
|
+
const replaced = await replaceWorkflowResumeState({
|
|
865
|
+
env: ctx.env,
|
|
866
|
+
previousStateKey: consumedResumeStateKey,
|
|
867
|
+
expectedPreviousState: resumeState,
|
|
868
|
+
previousStateConsumed: resumeStateConsumed,
|
|
869
|
+
replacementStateKey: stateKey,
|
|
870
|
+
signal: ctx.signal,
|
|
871
|
+
});
|
|
872
|
+
if (!replaced) {
|
|
873
|
+
throw new Error("Workflow resume state not found");
|
|
874
|
+
}
|
|
875
|
+
const resumeToken = encodeToken({
|
|
876
|
+
protocolVersion: 1,
|
|
877
|
+
v: 1,
|
|
878
|
+
kind: "workflow-file",
|
|
879
|
+
stateKey,
|
|
880
|
+
});
|
|
881
|
+
return {
|
|
882
|
+
status: "needs_input",
|
|
883
|
+
output: [],
|
|
884
|
+
requiresInput: {
|
|
885
|
+
...inputRequest,
|
|
886
|
+
resumeToken,
|
|
887
|
+
},
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
catch (stateError) {
|
|
891
|
+
if (stateKey)
|
|
892
|
+
await discardWorkflowResumeState(ctx.env, stateKey);
|
|
893
|
+
throw stateError;
|
|
919
894
|
}
|
|
920
|
-
const resumeToken = encodeToken({
|
|
921
|
-
protocolVersion: 1,
|
|
922
|
-
v: 1,
|
|
923
|
-
kind: "workflow-file",
|
|
924
|
-
stateKey,
|
|
925
|
-
});
|
|
926
|
-
return {
|
|
927
|
-
status: "needs_input",
|
|
928
|
-
output: [],
|
|
929
|
-
requiresInput: {
|
|
930
|
-
...inputRequest,
|
|
931
|
-
resumeToken,
|
|
932
|
-
},
|
|
933
|
-
};
|
|
934
895
|
}
|
|
935
896
|
if (err instanceof RequestInputResumeError) {
|
|
936
897
|
throw err;
|
|
937
898
|
}
|
|
938
|
-
if (
|
|
899
|
+
if (err instanceof CostLimitExceededError) {
|
|
900
|
+
throw err;
|
|
901
|
+
}
|
|
902
|
+
if (ctx.signal?.aborted) {
|
|
939
903
|
throw err;
|
|
940
904
|
}
|
|
941
905
|
if (err?.message &&
|
|
@@ -956,20 +920,36 @@ export async function runWorkflowFile({ filePath, args, ctx, resume, approved, r
|
|
|
956
920
|
error: true,
|
|
957
921
|
errorMessage,
|
|
958
922
|
};
|
|
923
|
+
// The step failed after paying a provider: a call it made before failing is spend
|
|
924
|
+
// this run really made, and its items are gone with the failure. Settling here is
|
|
925
|
+
// what stops a budget already blown from letting the next step run its side
|
|
926
|
+
// effects, which is the whole point of `action: stop`.
|
|
927
|
+
settleUnbilledCharges(costTracker, llmSpendLedger);
|
|
928
|
+
if (workflow.cost_limit) {
|
|
929
|
+
costTracker.checkLimit(workflow.cost_limit, ctx.stderr);
|
|
930
|
+
}
|
|
959
931
|
if (policy === "skip_rest") {
|
|
960
932
|
break;
|
|
961
933
|
}
|
|
962
934
|
continue;
|
|
963
935
|
}
|
|
936
|
+
// One accounting group per step: its parallel branches and its own result settle
|
|
937
|
+
// together, so no branch can be billed for a charge another branch is about to claim.
|
|
938
|
+
const deferredCharges = [];
|
|
964
939
|
if (parallelBranchResults) {
|
|
965
940
|
for (const [branchId, branchResult] of Object.entries(parallelBranchResults)) {
|
|
966
941
|
results[branchId] = branchResult;
|
|
967
|
-
trackStepCost(costTracker, branchId, branchResult);
|
|
942
|
+
trackStepCost(costTracker, branchId, branchResult, llmSpendLedger, deferredCharges);
|
|
968
943
|
}
|
|
969
944
|
}
|
|
970
945
|
results[step.id] = result;
|
|
971
946
|
lastStepId = step.id;
|
|
972
|
-
trackStepCost(costTracker, step.id, result);
|
|
947
|
+
trackStepCost(costTracker, step.id, result, llmSpendLedger, deferredCharges);
|
|
948
|
+
settleDeferredCosts(costTracker, deferredCharges, llmSpendLedger);
|
|
949
|
+
// Before the next step runs: a charge no item carried is spend the step really made,
|
|
950
|
+
// and leaving it for the end would let `cost_limit` wave through every step after the
|
|
951
|
+
// one that blew the budget.
|
|
952
|
+
settleUnbilledCharges(costTracker, llmSpendLedger);
|
|
973
953
|
if (workflow.cost_limit) {
|
|
974
954
|
costTracker.checkLimit(workflow.cost_limit, ctx.stderr);
|
|
975
955
|
}
|
|
@@ -980,45 +960,64 @@ export async function runWorkflowFile({ filePath, args, ctx, resume, approved, r
|
|
|
980
960
|
results[step.id].initiatedBy = approvalIdentity.initiatedBy;
|
|
981
961
|
}
|
|
982
962
|
if (ctx.mode === "tool" || !isInteractive(ctx.stdin)) {
|
|
983
|
-
|
|
984
|
-
filePath: resolvedFilePath,
|
|
985
|
-
resumeAtIndex: idx + 1,
|
|
986
|
-
steps: results,
|
|
987
|
-
args: resolvedArgs,
|
|
988
|
-
approvalStepId: step.id,
|
|
989
|
-
approvalIdentity,
|
|
990
|
-
createdAt: new Date().toISOString(),
|
|
991
|
-
});
|
|
992
|
-
let approvalId;
|
|
963
|
+
let stateKey;
|
|
993
964
|
try {
|
|
994
|
-
|
|
965
|
+
stateKey = await saveWorkflowResumeState(ctx.env, {
|
|
966
|
+
filePath: resolvedFilePath,
|
|
967
|
+
resumeAtIndex: idx + 1,
|
|
968
|
+
steps: results,
|
|
969
|
+
args: resolvedArgs,
|
|
970
|
+
approvalStepId: step.id,
|
|
971
|
+
approvalIdentity,
|
|
972
|
+
...(costTracker.hasUsage() ? { cost: costTracker.getSummary() } : null),
|
|
973
|
+
...(llmSpendLedger.outstanding().length
|
|
974
|
+
? { llmSpend: llmSpendLedger.outstanding() }
|
|
975
|
+
: null),
|
|
976
|
+
...(llmSpendLedger.settled().length
|
|
977
|
+
? { llmBilled: llmSpendLedger.settled() }
|
|
978
|
+
: null),
|
|
979
|
+
supersededResumeStateKeys: collectSupersededWorkflowResumeStateKeys(consumedResumeStateKey, resumeState),
|
|
980
|
+
createdAt: new Date().toISOString(),
|
|
981
|
+
}, ctx.signal);
|
|
982
|
+
const approvalId = await createApprovalIndex({ env: ctx.env, stateKey });
|
|
983
|
+
ctx.signal?.throwIfAborted();
|
|
984
|
+
const replaced = await replaceWorkflowResumeState({
|
|
985
|
+
env: ctx.env,
|
|
986
|
+
previousStateKey: consumedResumeStateKey,
|
|
987
|
+
expectedPreviousState: resumeState,
|
|
988
|
+
previousStateConsumed: resumeStateConsumed,
|
|
989
|
+
replacementStateKey: stateKey,
|
|
990
|
+
signal: ctx.signal,
|
|
991
|
+
});
|
|
992
|
+
if (!replaced) {
|
|
993
|
+
throw new Error("Workflow resume state not found");
|
|
994
|
+
}
|
|
995
|
+
const resumeToken = encodeToken({
|
|
996
|
+
protocolVersion: 1,
|
|
997
|
+
v: 1,
|
|
998
|
+
kind: "workflow-file",
|
|
999
|
+
stateKey,
|
|
1000
|
+
});
|
|
1001
|
+
return {
|
|
1002
|
+
status: "needs_approval",
|
|
1003
|
+
output: [],
|
|
1004
|
+
requiresApproval: {
|
|
1005
|
+
...approval,
|
|
1006
|
+
resumeToken,
|
|
1007
|
+
...(approvalId ? { approvalId } : null),
|
|
1008
|
+
},
|
|
1009
|
+
};
|
|
995
1010
|
}
|
|
996
1011
|
catch (err) {
|
|
997
|
-
|
|
1012
|
+
if (stateKey)
|
|
1013
|
+
await discardWorkflowResumeState(ctx.env, stateKey);
|
|
998
1014
|
throw err;
|
|
999
1015
|
}
|
|
1000
|
-
if (consumedResumeStateKey && consumedResumeStateKey !== stateKey) {
|
|
1001
|
-
await deleteStateJson({ env: ctx.env, key: consumedResumeStateKey });
|
|
1002
|
-
}
|
|
1003
|
-
const resumeToken = encodeToken({
|
|
1004
|
-
protocolVersion: 1,
|
|
1005
|
-
v: 1,
|
|
1006
|
-
kind: "workflow-file",
|
|
1007
|
-
stateKey,
|
|
1008
|
-
});
|
|
1009
|
-
return {
|
|
1010
|
-
status: "needs_approval",
|
|
1011
|
-
output: [],
|
|
1012
|
-
requiresApproval: {
|
|
1013
|
-
...approval,
|
|
1014
|
-
resumeToken,
|
|
1015
|
-
...(approvalId ? { approvalId } : null),
|
|
1016
|
-
},
|
|
1017
|
-
};
|
|
1018
1016
|
}
|
|
1019
1017
|
ctx.stdout.write(`${approval.prompt} [y/N] `);
|
|
1020
1018
|
const answer = await readLineFromStream(ctx.stdin, {
|
|
1021
1019
|
timeoutMs: parseApprovalTimeoutMs(ctx.env),
|
|
1020
|
+
signal: ctx.signal,
|
|
1022
1021
|
});
|
|
1023
1022
|
if (!/^y(es)?$/i.test(String(answer).trim())) {
|
|
1024
1023
|
throw new Error("Not approved");
|
|
@@ -1034,270 +1033,88 @@ export async function runWorkflowFile({ filePath, args, ctx, resume, approved, r
|
|
|
1034
1033
|
results[step.id].approvedBy = approvedBy;
|
|
1035
1034
|
}
|
|
1036
1035
|
}
|
|
1036
|
+
// Whatever a skipped or failed step left open, and anything from a run with no step left
|
|
1037
|
+
// to settle it against. A paused run does not reach this line -- its charges travel in
|
|
1038
|
+
// the resume state instead -- so this settles only calls with nowhere left to be billed.
|
|
1039
|
+
settleUnbilledCharges(costTracker, llmSpendLedger);
|
|
1040
|
+
if (workflow.cost_limit) {
|
|
1041
|
+
costTracker.checkLimit(workflow.cost_limit, ctx.stderr);
|
|
1042
|
+
}
|
|
1037
1043
|
const output = lastStepId ? toOutputItems(results[lastStepId]) : [];
|
|
1038
1044
|
if (consumedResumeStateKey) {
|
|
1039
|
-
|
|
1045
|
+
if (resumeStateConsumed) {
|
|
1046
|
+
ctx.signal?.throwIfAborted();
|
|
1047
|
+
await deleteStateJson({
|
|
1048
|
+
env: ctx.env,
|
|
1049
|
+
key: consumedResumeStateKey,
|
|
1050
|
+
signal: ctx.signal,
|
|
1051
|
+
});
|
|
1052
|
+
ctx.signal?.throwIfAborted();
|
|
1053
|
+
}
|
|
1054
|
+
else {
|
|
1055
|
+
const deleted = await deleteResumeStateWithRollback({
|
|
1056
|
+
env: ctx.env,
|
|
1057
|
+
key: consumedResumeStateKey,
|
|
1058
|
+
expectedState: resumeState,
|
|
1059
|
+
signal: ctx.signal,
|
|
1060
|
+
});
|
|
1061
|
+
if (!deleted) {
|
|
1062
|
+
throw new Error("Workflow resume state not found");
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
await cleanupSupersededWorkflowResumeStates(ctx.env, resumeState?.supersededResumeStateKeys);
|
|
1066
|
+
}
|
|
1067
|
+
else {
|
|
1068
|
+
ctx.signal?.throwIfAborted();
|
|
1040
1069
|
}
|
|
1041
1070
|
const runResult = { status: "ok", output };
|
|
1071
|
+
// `output` is what the last step could serialize; a run composing this one also needs the
|
|
1072
|
+
// items themselves, because that is where replay provenance lives.
|
|
1073
|
+
const lastItems = lastStepId ? pipelineSourceItems(results[lastStepId]) : undefined;
|
|
1074
|
+
if (lastItems)
|
|
1075
|
+
attachPipelineSourceItems(runResult, lastItems);
|
|
1042
1076
|
if (costTracker.hasUsage()) {
|
|
1043
1077
|
runResult._meta = { cost: costTracker.getSummary() };
|
|
1044
1078
|
}
|
|
1045
1079
|
return runResult;
|
|
1046
1080
|
}
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
|
-
// Returns a human-readable note if a step.stdin value references a prior step's
|
|
1052
|
-
// output. Because dry-run placeholders have no actual stdout/json, we surface
|
|
1053
|
-
// this so users know the value is unknown at plan time rather than silently
|
|
1054
|
-
// resolving to an empty string.
|
|
1055
|
-
function dryRunStdinNote(stdin) {
|
|
1056
|
-
if (stdin === null || stdin === undefined)
|
|
1057
|
-
return null;
|
|
1058
|
-
if (typeof stdin !== "string")
|
|
1059
|
-
return null;
|
|
1060
|
-
const trimmed = stdin.trim();
|
|
1061
|
-
// Strict step ref: '$step-id.stdout' or '$step-id.json'
|
|
1062
|
-
if (/^\$[A-Za-z0-9_-]+\.(stdout|json)$/.test(trimmed)) {
|
|
1063
|
-
return `${trimmed} [output unknown at plan time]`;
|
|
1064
|
-
}
|
|
1065
|
-
// Inline template ref: contains '$stepid.stdout' or '$stepid.json'
|
|
1066
|
-
if (/\$[A-Za-z0-9_-]+\.(stdout|json)/.test(trimmed)) {
|
|
1067
|
-
return `${trimmed} [contains step output refs — unknown at plan time]`;
|
|
1068
|
-
}
|
|
1069
|
-
return null;
|
|
1070
|
-
}
|
|
1071
|
-
function dryRunTemplateNote(input) {
|
|
1072
|
-
if (/\$[A-Za-z0-9_-]+\.(stdout|json)/.test(input)) {
|
|
1073
|
-
return "[contains step output refs — unknown at plan time]";
|
|
1074
|
-
}
|
|
1075
|
-
return null;
|
|
1076
|
-
}
|
|
1077
|
-
function hasDeferredDryRunStageName(input) {
|
|
1078
|
-
return /\$[A-Za-z0-9_-]+\.(stdout|json)/.test(input);
|
|
1079
|
-
}
|
|
1080
|
-
function resolveDryRunTemplate(input, args, results) {
|
|
1081
|
-
const withArgs = resolveArgsTemplate(input, args);
|
|
1082
|
-
return withArgs.replace(/\$([A-Za-z0-9_-]+)\.(stdout|json|approved)/g, (match, id, field) => {
|
|
1083
|
-
if (field === "approved") {
|
|
1084
|
-
const step = results[id];
|
|
1085
|
-
if (!step)
|
|
1086
|
-
return match;
|
|
1087
|
-
return step.approved === true ? "true" : "false";
|
|
1088
|
-
}
|
|
1089
|
-
return match;
|
|
1090
|
-
});
|
|
1091
|
-
}
|
|
1092
|
-
function dryRunWorkflow({ steps, resolvedArgs, results, startIndex, ctx, }) {
|
|
1093
|
-
const lines = [];
|
|
1094
|
-
const totalSteps = steps.length - startIndex;
|
|
1095
|
-
lines.push(`[DRY RUN] Would execute ${totalSteps} step${totalSteps !== 1 ? "s" : ""}:\n`);
|
|
1096
|
-
for (let idx = startIndex; idx < steps.length; idx++) {
|
|
1097
|
-
const step = steps[idx];
|
|
1098
|
-
const num = idx - startIndex + 1;
|
|
1099
|
-
if (!evaluateCondition(step.when ?? step.condition, results)) {
|
|
1100
|
-
results[step.id] = { id: step.id, skipped: true };
|
|
1101
|
-
lines.push(` ${num}. ${step.id} [skipped — condition: false]`);
|
|
1102
|
-
continue;
|
|
1103
|
-
}
|
|
1104
|
-
if (isInputStep(step.input)) {
|
|
1105
|
-
lines.push(` ${num}. ${step.id} [input]`);
|
|
1106
|
-
lines.push(` prompt: ${step.input.prompt}`);
|
|
1107
|
-
lines.push(` [input required]`);
|
|
1108
|
-
results[step.id] = { id: step.id, response: { pending: true } };
|
|
1109
|
-
continue;
|
|
1110
|
-
}
|
|
1111
|
-
if (typeof step.for_each === "string" && Array.isArray(step.steps)) {
|
|
1112
|
-
lines.push(` ${num}. ${step.id} [for_each]`);
|
|
1113
|
-
const forEachRef = step.for_each;
|
|
1114
|
-
const forEachNote = dryRunTemplateNote(forEachRef);
|
|
1115
|
-
lines.push(` for_each: ${forEachRef}${forEachNote ? ` ${forEachNote}` : ""}`);
|
|
1116
|
-
if (forEachRef.trim().startsWith("$")) {
|
|
1117
|
-
try {
|
|
1118
|
-
resolveInputValue(forEachRef, resolvedArgs, results);
|
|
1119
|
-
}
|
|
1120
|
-
catch (err) {
|
|
1121
|
-
throw new Error(`Workflow step ${step.id} for_each: ${err?.message ?? String(err)}`);
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
const dryItemVar = step.item_var ?? "item";
|
|
1125
|
-
const dryIndexVar = step.index_var ?? "index";
|
|
1126
|
-
lines.push(` item_var: ${dryItemVar}, index_var: ${dryIndexVar}`);
|
|
1127
|
-
if (step.batch_size)
|
|
1128
|
-
lines.push(` batch_size: ${step.batch_size}`);
|
|
1129
|
-
if (step.pause_ms)
|
|
1130
|
-
lines.push(` pause_ms: ${step.pause_ms}`);
|
|
1131
|
-
lines.push(` sub-steps: ${step.steps.length}`);
|
|
1132
|
-
const loopScopedResults = { ...results };
|
|
1133
|
-
loopScopedResults[dryItemVar] = { id: dryItemVar, json: { _placeholder: true } };
|
|
1134
|
-
loopScopedResults[dryIndexVar] = { id: dryIndexVar, json: 0 };
|
|
1135
|
-
for (let subIdx = 0; subIdx < step.steps.length; subIdx++) {
|
|
1136
|
-
const sub = step.steps[subIdx];
|
|
1137
|
-
if (!evaluateCondition(sub.when ?? sub.condition, loopScopedResults)) {
|
|
1138
|
-
lines.push(` ${subIdx + 1}. ${sub.id} [skipped — condition: false]`);
|
|
1139
|
-
loopScopedResults[sub.id] = { id: sub.id, skipped: true };
|
|
1140
|
-
continue;
|
|
1141
|
-
}
|
|
1142
|
-
if (sub.stdin !== undefined && sub.stdin !== null) {
|
|
1143
|
-
try {
|
|
1144
|
-
resolveInputValue(sub.stdin, resolvedArgs, loopScopedResults);
|
|
1145
|
-
}
|
|
1146
|
-
catch (err) {
|
|
1147
|
-
throw new Error(`Workflow step ${step.id} for_each sub-step ${sub.id} stdin: ${err?.message ?? String(err)}`);
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1150
|
-
const subExec = getStepExecution(sub);
|
|
1151
|
-
if (subExec.kind === "shell") {
|
|
1152
|
-
const command = resolveDryRunTemplate(subExec.value, resolvedArgs, loopScopedResults);
|
|
1153
|
-
lines.push(` ${subIdx + 1}. ${sub.id} [shell] run: ${command}`);
|
|
1154
|
-
}
|
|
1155
|
-
else if (subExec.kind === "pipeline") {
|
|
1156
|
-
if (!ctx.registry) {
|
|
1157
|
-
throw new Error(`Workflow step ${step.id} for_each sub-step ${sub.id} requires a command registry for pipeline execution`);
|
|
1158
|
-
}
|
|
1159
|
-
const pipelineText = resolveDryRunTemplate(subExec.value, resolvedArgs, loopScopedResults);
|
|
1160
|
-
const stages = parsePipeline(pipelineText);
|
|
1161
|
-
for (const stage of stages) {
|
|
1162
|
-
if (hasDeferredDryRunStageName(stage.name))
|
|
1163
|
-
continue;
|
|
1164
|
-
if (!ctx.registry.get(stage.name)) {
|
|
1165
|
-
throw new Error(`Workflow step ${step.id} for_each sub-step ${sub.id} pipeline: unknown command: ${stage.name}`);
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
lines.push(` ${subIdx + 1}. ${sub.id} [pipeline] pipeline: ${pipelineText}`);
|
|
1169
|
-
}
|
|
1170
|
-
else {
|
|
1171
|
-
lines.push(` ${subIdx + 1}. ${sub.id} [no-op]`);
|
|
1172
|
-
}
|
|
1173
|
-
loopScopedResults[sub.id] = { id: sub.id };
|
|
1174
|
-
}
|
|
1175
|
-
results[step.id] = { id: step.id };
|
|
1176
|
-
continue;
|
|
1177
|
-
}
|
|
1178
|
-
// Validate stdin refs early — throws if a strict ref like '$missing.stdout'
|
|
1179
|
-
// points to a step that doesn't exist at all (real execution would also fail).
|
|
1180
|
-
// We call resolveInputValue with the current results so refs to steps we've
|
|
1181
|
-
// already visited (placeholders) are accepted without throwing.
|
|
1182
|
-
if (step.stdin !== undefined && step.stdin !== null) {
|
|
1081
|
+
catch (err) {
|
|
1082
|
+
if (resumedExecutionStarted && consumedResumeStateKey) {
|
|
1183
1083
|
try {
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
}
|
|
1190
|
-
const execution = getStepExecution(step);
|
|
1191
|
-
// Annotate when the resolved command/pipeline references a prior step's output.
|
|
1192
|
-
// Since dry-run placeholders have no actual stdout/json, note it explicitly
|
|
1193
|
-
// rather than silently collapsing the reference to an empty string.
|
|
1194
|
-
const stdinNote = dryRunStdinNote(step.stdin);
|
|
1195
|
-
if (execution.kind === "parallel") {
|
|
1196
|
-
lines.push(` ${num}. ${step.id} [parallel]`);
|
|
1197
|
-
lines.push(` wait: ${step.parallel?.wait ?? "all"}`);
|
|
1198
|
-
if (step.parallel?.timeout_ms) {
|
|
1199
|
-
lines.push(` timeout: ${step.parallel.timeout_ms}ms`);
|
|
1200
|
-
}
|
|
1201
|
-
for (const branch of step.parallel?.branches ?? []) {
|
|
1202
|
-
const branchShell = typeof branch.run === "string" ? branch.run : branch.command;
|
|
1203
|
-
if (typeof branch.pipeline === "string" && branch.pipeline.trim()) {
|
|
1204
|
-
const pipelineText = resolveDryRunTemplate(branch.pipeline, resolvedArgs, results);
|
|
1205
|
-
const pipelineNote = dryRunTemplateNote(pipelineText);
|
|
1206
|
-
if (!ctx.registry) {
|
|
1207
|
-
throw new Error(`Parallel branch ${branch.id} requires a command registry for pipeline execution`);
|
|
1208
|
-
}
|
|
1209
|
-
const stages = parsePipeline(pipelineText);
|
|
1210
|
-
for (const stage of stages) {
|
|
1211
|
-
if (hasDeferredDryRunStageName(stage.name))
|
|
1212
|
-
continue;
|
|
1213
|
-
if (!ctx.registry.get(stage.name)) {
|
|
1214
|
-
throw new Error(`Parallel branch ${branch.id} pipeline references unknown command: ${stage.name}`);
|
|
1215
|
-
}
|
|
1216
|
-
}
|
|
1217
|
-
lines.push(` branch ${branch.id}: [pipeline] ${pipelineText}${pipelineNote ? ` ${pipelineNote}` : ""}`);
|
|
1218
|
-
}
|
|
1219
|
-
else if (typeof branchShell === "string" && branchShell.trim()) {
|
|
1220
|
-
const command = resolveDryRunTemplate(branchShell, resolvedArgs, results);
|
|
1221
|
-
const commandNote = dryRunTemplateNote(command);
|
|
1222
|
-
lines.push(` branch ${branch.id}: [shell] ${command}${commandNote ? ` ${commandNote}` : ""}`);
|
|
1084
|
+
if (ctx.signal?.aborted) {
|
|
1085
|
+
await deleteStateJsonWithBoundedResumeCleanup({
|
|
1086
|
+
env: ctx.env,
|
|
1087
|
+
key: consumedResumeStateKey,
|
|
1088
|
+
});
|
|
1223
1089
|
}
|
|
1224
1090
|
else {
|
|
1225
|
-
|
|
1091
|
+
await deleteStateJson({ env: ctx.env, key: consumedResumeStateKey });
|
|
1226
1092
|
}
|
|
1227
|
-
results[branch.id] = { id: branch.id };
|
|
1228
1093
|
}
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
const pathNote = dryRunTemplateNote(workflowPath);
|
|
1233
|
-
lines.push(` ${num}. ${step.id} [workflow]`);
|
|
1234
|
-
lines.push(` workflow: ${workflowPath}${pathNote ? ` ${pathNote}` : ""}`);
|
|
1235
|
-
if (step.workflow_args && typeof step.workflow_args === "object") {
|
|
1236
|
-
const argKeys = Object.keys(step.workflow_args);
|
|
1237
|
-
if (argKeys.length) {
|
|
1238
|
-
lines.push(` args: ${argKeys.join(", ")}`);
|
|
1239
|
-
}
|
|
1240
|
-
}
|
|
1241
|
-
}
|
|
1242
|
-
else if (execution.kind === "shell") {
|
|
1243
|
-
const command = resolveDryRunTemplate(execution.value, resolvedArgs, results);
|
|
1244
|
-
const commandNote = dryRunTemplateNote(command);
|
|
1245
|
-
lines.push(` ${num}. ${step.id} [shell]`);
|
|
1246
|
-
lines.push(` run: ${command}${commandNote ? ` ${commandNote}` : ""}`);
|
|
1247
|
-
}
|
|
1248
|
-
else if (execution.kind === "pipeline") {
|
|
1249
|
-
const pipelineText = resolveDryRunTemplate(execution.value, resolvedArgs, results);
|
|
1250
|
-
const pipelineNote = dryRunTemplateNote(pipelineText);
|
|
1251
|
-
// Validate pipeline syntax and registry even in dry-run so errors surface early.
|
|
1252
|
-
if (!ctx.registry) {
|
|
1253
|
-
throw new Error(`Workflow step ${step.id} requires a command registry for pipeline execution`);
|
|
1254
|
-
}
|
|
1255
|
-
// Validate that every stage name is a known command.
|
|
1256
|
-
const stages = parsePipeline(pipelineText);
|
|
1257
|
-
for (const stage of stages) {
|
|
1258
|
-
if (hasDeferredDryRunStageName(stage.name)) {
|
|
1259
|
-
continue;
|
|
1260
|
-
}
|
|
1261
|
-
if (!ctx.registry.get(stage.name)) {
|
|
1262
|
-
throw new Error(`Workflow step ${step.id} pipeline references unknown command: ${stage.name}`);
|
|
1263
|
-
}
|
|
1264
|
-
}
|
|
1265
|
-
lines.push(` ${num}. ${step.id} [pipeline]`);
|
|
1266
|
-
lines.push(` pipeline: ${pipelineText}${pipelineNote ? ` ${pipelineNote}` : ""}`);
|
|
1267
|
-
if (stages.some((stage) => hasDeferredDryRunStageName(stage.name))) {
|
|
1268
|
-
lines.push(" [command validation deferred — stage name depends on step output]");
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1271
|
-
else {
|
|
1272
|
-
lines.push(` ${num}. ${step.id} [no-op]`);
|
|
1273
|
-
}
|
|
1274
|
-
if (stdinNote)
|
|
1275
|
-
lines.push(` stdin: ${stdinNote}`);
|
|
1276
|
-
if (step.timeout_ms)
|
|
1277
|
-
lines.push(` timeout: ${step.timeout_ms}ms`);
|
|
1278
|
-
if (step.on_error && step.on_error !== "stop")
|
|
1279
|
-
lines.push(` on_error: ${step.on_error}`);
|
|
1280
|
-
if (step.retry && typeof step.retry === "object") {
|
|
1281
|
-
const rc = resolveRetryConfig(step.retry);
|
|
1282
|
-
if (rc.max > 1) {
|
|
1283
|
-
lines.push(` retry: up to ${rc.max} attempts, ${rc.backoff} backoff (base: ${rc.delay_ms}ms${rc.jitter ? ", jitter" : ""})`);
|
|
1094
|
+
catch {
|
|
1095
|
+
// Leave the tombstone in place if cleanup fails: it is the durable
|
|
1096
|
+
// record that prevents the consumed token from replaying an effect.
|
|
1284
1097
|
}
|
|
1098
|
+
// A consumed marker remains non-replayable even when its bounded cleanup
|
|
1099
|
+
// times out, so its short approval ID can be retired independently.
|
|
1100
|
+
await cleanupApprovalIndexByStateKey({
|
|
1101
|
+
env: ctx.env,
|
|
1102
|
+
stateKey: consumedResumeStateKey,
|
|
1103
|
+
}).catch(() => { });
|
|
1285
1104
|
}
|
|
1286
|
-
|
|
1287
|
-
|
|
1105
|
+
throw err;
|
|
1106
|
+
}
|
|
1107
|
+
finally {
|
|
1108
|
+
for (const { signal, onAbort } of executionSignalListeners) {
|
|
1109
|
+
signal.removeEventListener("abort", onAbort);
|
|
1288
1110
|
}
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
// $step.approved evaluate correctly in the plan (rather than always being false).
|
|
1292
|
-
// We intentionally omit stdout/json — dryRunStdinNote() surfaces that gap.
|
|
1293
|
-
results[step.id] = isApprovalStep(step.approval)
|
|
1294
|
-
? { id: step.id, approved: true }
|
|
1295
|
-
: { id: step.id };
|
|
1296
|
-
}
|
|
1297
|
-
lines.push("");
|
|
1298
|
-
ctx.stderr.write(lines.join("\n"));
|
|
1299
|
-
return { status: "ok", output: [] };
|
|
1111
|
+
ctx._activeWorkflows?.delete(canonicalFilePath);
|
|
1112
|
+
}
|
|
1300
1113
|
}
|
|
1114
|
+
// Returns a human-readable note if a step.stdin value references a prior step's
|
|
1115
|
+
// output. Because dry-run placeholders have no actual stdout/json, we surface
|
|
1116
|
+
// this so users know the value is unknown at plan time rather than silently
|
|
1117
|
+
// resolving to an empty string.
|
|
1301
1118
|
export function decodeWorkflowResumePayload(payload) {
|
|
1302
1119
|
if (!payload || typeof payload !== "object")
|
|
1303
1120
|
return null;
|
|
@@ -1319,12 +1136,87 @@ export function decodeWorkflowResumePayload(payload) {
|
|
|
1319
1136
|
throw new Error("Invalid workflow token");
|
|
1320
1137
|
return data;
|
|
1321
1138
|
}
|
|
1322
|
-
async function saveWorkflowResumeState(env, state) {
|
|
1139
|
+
async function saveWorkflowResumeState(env, state, signal) {
|
|
1323
1140
|
const stateKey = `workflow_resume_${randomUUID()}`;
|
|
1324
|
-
|
|
1325
|
-
|
|
1141
|
+
try {
|
|
1142
|
+
signal?.throwIfAborted();
|
|
1143
|
+
await writeStateJson({ env, key: stateKey, value: state, signal });
|
|
1144
|
+
signal?.throwIfAborted();
|
|
1145
|
+
return stateKey;
|
|
1146
|
+
}
|
|
1147
|
+
catch (err) {
|
|
1148
|
+
if (signal?.aborted)
|
|
1149
|
+
await discardWorkflowResumeState(env, stateKey);
|
|
1150
|
+
throw err;
|
|
1151
|
+
}
|
|
1326
1152
|
}
|
|
1327
|
-
function
|
|
1153
|
+
async function replaceWorkflowResumeState({ env, previousStateKey, expectedPreviousState, previousStateConsumed, replacementStateKey, signal, }) {
|
|
1154
|
+
if (!previousStateKey || previousStateKey === replacementStateKey) {
|
|
1155
|
+
signal?.throwIfAborted();
|
|
1156
|
+
return true;
|
|
1157
|
+
}
|
|
1158
|
+
if (previousStateConsumed) {
|
|
1159
|
+
signal?.throwIfAborted();
|
|
1160
|
+
// The successor is already durable. Retire the predecessor's short ID here,
|
|
1161
|
+
// rather than when the preceding unsafe execution is only being claimed.
|
|
1162
|
+
await cleanupApprovalIndexByStateKey({ env, stateKey: previousStateKey }).catch(() => { });
|
|
1163
|
+
return true;
|
|
1164
|
+
}
|
|
1165
|
+
if (!expectedPreviousState)
|
|
1166
|
+
return false;
|
|
1167
|
+
let claimId;
|
|
1168
|
+
try {
|
|
1169
|
+
const consumption = await consumeResumeState({
|
|
1170
|
+
env,
|
|
1171
|
+
key: previousStateKey,
|
|
1172
|
+
expectedState: expectedPreviousState,
|
|
1173
|
+
signal,
|
|
1174
|
+
});
|
|
1175
|
+
if (!consumption.consumed)
|
|
1176
|
+
return false;
|
|
1177
|
+
claimId = consumption.claimId;
|
|
1178
|
+
// Keep the consumed marker until terminal cleanup. It is the atomic
|
|
1179
|
+
// predecessor-to-successor handoff and prevents another resume from
|
|
1180
|
+
// creating a competing successor after this call returns.
|
|
1181
|
+
signal?.throwIfAborted();
|
|
1182
|
+
await cleanupApprovalIndexByStateKey({ env, stateKey: previousStateKey }).catch(() => { });
|
|
1183
|
+
return true;
|
|
1184
|
+
}
|
|
1185
|
+
catch (err) {
|
|
1186
|
+
if (claimId && signal?.aborted) {
|
|
1187
|
+
await restoreConsumedResumeState({
|
|
1188
|
+
env,
|
|
1189
|
+
key: previousStateKey,
|
|
1190
|
+
expectedState: expectedPreviousState,
|
|
1191
|
+
claimId,
|
|
1192
|
+
}).catch(() => { });
|
|
1193
|
+
}
|
|
1194
|
+
throw err;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
function collectSupersededWorkflowResumeStateKeys(previousStateKey, previousState) {
|
|
1198
|
+
return [
|
|
1199
|
+
...(previousState?.supersededResumeStateKeys ?? []),
|
|
1200
|
+
...(previousStateKey ? [previousStateKey] : []),
|
|
1201
|
+
].filter((stateKey, index, all) => stateKey && all.indexOf(stateKey) === index);
|
|
1202
|
+
}
|
|
1203
|
+
async function cleanupSupersededWorkflowResumeStates(env, stateKeys) {
|
|
1204
|
+
for (const stateKey of stateKeys ?? []) {
|
|
1205
|
+
try {
|
|
1206
|
+
if (isConsumedResumeState(await readStateJsonWithLock({ env, key: stateKey }))) {
|
|
1207
|
+
await deleteStateJson({ env, key: stateKey });
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
catch {
|
|
1211
|
+
// The completed successor makes retained ancestor markers safe.
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
async function discardWorkflowResumeState(env, stateKey) {
|
|
1216
|
+
await cleanupApprovalIndexByStateKey({ env, stateKey }).catch(() => { });
|
|
1217
|
+
await deleteStateJson({ env, key: stateKey }).catch(() => { });
|
|
1218
|
+
}
|
|
1219
|
+
export function alternateWorkflowResumeStateKey(stateKey) {
|
|
1328
1220
|
if (stateKey.includes("workflow-resume_")) {
|
|
1329
1221
|
return stateKey.replace("workflow-resume_", "workflow_resume_");
|
|
1330
1222
|
}
|
|
@@ -1333,8 +1225,8 @@ function alternateWorkflowResumeStateKey(stateKey) {
|
|
|
1333
1225
|
}
|
|
1334
1226
|
return null;
|
|
1335
1227
|
}
|
|
1336
|
-
async function resolveWorkflowResumeStateKey(env, stateKey) {
|
|
1337
|
-
const stored = await
|
|
1228
|
+
async function resolveWorkflowResumeStateKey(env, stateKey, signal) {
|
|
1229
|
+
const stored = await readStateJsonWithLock({ env, key: stateKey, signal });
|
|
1338
1230
|
if (stored && typeof stored === "object") {
|
|
1339
1231
|
return stateKey;
|
|
1340
1232
|
}
|
|
@@ -1342,18 +1234,24 @@ async function resolveWorkflowResumeStateKey(env, stateKey) {
|
|
|
1342
1234
|
if (!altKey) {
|
|
1343
1235
|
return stateKey;
|
|
1344
1236
|
}
|
|
1345
|
-
const altStored = await
|
|
1237
|
+
const altStored = await readStateJsonWithLock({ env, key: altKey, signal });
|
|
1346
1238
|
if (altStored && typeof altStored === "object") {
|
|
1347
1239
|
return altKey;
|
|
1348
1240
|
}
|
|
1349
1241
|
return stateKey;
|
|
1350
1242
|
}
|
|
1351
|
-
async function loadWorkflowResumeState(env, stateKey) {
|
|
1352
|
-
let stored = await
|
|
1243
|
+
async function loadWorkflowResumeState(env, stateKey, signal) {
|
|
1244
|
+
let stored = await readStateJsonWithLock({ env, key: stateKey, signal });
|
|
1245
|
+
if (isConsumedResumeState(stored)) {
|
|
1246
|
+
throw new Error("Workflow resume state not found");
|
|
1247
|
+
}
|
|
1353
1248
|
if ((!stored || typeof stored !== "object") && typeof stateKey === "string") {
|
|
1354
1249
|
const altKey = alternateWorkflowResumeStateKey(stateKey);
|
|
1355
1250
|
if (altKey) {
|
|
1356
|
-
stored = await
|
|
1251
|
+
stored = await readStateJsonWithLock({ env, key: altKey, signal });
|
|
1252
|
+
if (isConsumedResumeState(stored)) {
|
|
1253
|
+
throw new Error("Workflow resume state not found");
|
|
1254
|
+
}
|
|
1357
1255
|
}
|
|
1358
1256
|
}
|
|
1359
1257
|
if (!stored || typeof stored !== "object") {
|
|
@@ -1368,6 +1266,11 @@ async function loadWorkflowResumeState(env, stateKey) {
|
|
|
1368
1266
|
throw new Error("Invalid workflow resume state");
|
|
1369
1267
|
if (!data.args || typeof data.args !== "object")
|
|
1370
1268
|
throw new Error("Invalid workflow resume state");
|
|
1269
|
+
if (data.supersededResumeStateKeys !== undefined &&
|
|
1270
|
+
(!Array.isArray(data.supersededResumeStateKeys) ||
|
|
1271
|
+
data.supersededResumeStateKeys.some((stateKey) => typeof stateKey !== "string"))) {
|
|
1272
|
+
throw new Error("Invalid workflow resume state");
|
|
1273
|
+
}
|
|
1371
1274
|
if (data.inputKind !== undefined &&
|
|
1372
1275
|
!["workflow_step", "pipeline_command"].includes(data.inputKind)) {
|
|
1373
1276
|
throw new Error("Invalid workflow resume state");
|
|
@@ -1417,141 +1320,6 @@ function validateWorkflowPipelineShape(pipeline) {
|
|
|
1417
1320
|
throw new Error("Invalid workflow resume state");
|
|
1418
1321
|
}
|
|
1419
1322
|
}
|
|
1420
|
-
function mergeEnv(base, workflowEnv, stepEnv, args, results) {
|
|
1421
|
-
const env = { ...base };
|
|
1422
|
-
// Expose resolved args as env vars so shell commands can safely reference them
|
|
1423
|
-
// without embedding raw values into the command string.
|
|
1424
|
-
// Example: $LOBSTER_ARG_TEXT
|
|
1425
|
-
env.LOBSTER_ARGS_JSON = JSON.stringify(args ?? {});
|
|
1426
|
-
for (const [key, value] of Object.entries(args ?? {})) {
|
|
1427
|
-
const normalized = normalizeArgEnvKey(key);
|
|
1428
|
-
if (!normalized)
|
|
1429
|
-
continue;
|
|
1430
|
-
env[`LOBSTER_ARG_${normalized}`] = String(value);
|
|
1431
|
-
}
|
|
1432
|
-
const apply = (source) => {
|
|
1433
|
-
if (!source)
|
|
1434
|
-
return;
|
|
1435
|
-
for (const [key, value] of Object.entries(source)) {
|
|
1436
|
-
if (typeof value === "string") {
|
|
1437
|
-
env[key] = resolveTemplate(value, args, results);
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
1440
|
-
};
|
|
1441
|
-
// Allow explicit env blocks to override injected defaults.
|
|
1442
|
-
apply(workflowEnv);
|
|
1443
|
-
apply(stepEnv);
|
|
1444
|
-
return env;
|
|
1445
|
-
}
|
|
1446
|
-
function normalizeArgEnvKey(key) {
|
|
1447
|
-
const trimmed = String(key ?? "").trim();
|
|
1448
|
-
if (!trimmed)
|
|
1449
|
-
return null;
|
|
1450
|
-
// Keep it predictable for shells: uppercase and [A-Z0-9_]
|
|
1451
|
-
const up = trimmed.toUpperCase();
|
|
1452
|
-
const normalized = up.replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
1453
|
-
return normalized || null;
|
|
1454
|
-
}
|
|
1455
|
-
function resolveCwd(cwd, args) {
|
|
1456
|
-
if (!cwd)
|
|
1457
|
-
return undefined;
|
|
1458
|
-
return resolveArgsTemplate(cwd, args);
|
|
1459
|
-
}
|
|
1460
|
-
function resolveInputValue(stdin, args, results) {
|
|
1461
|
-
if (stdin === null || stdin === undefined)
|
|
1462
|
-
return null;
|
|
1463
|
-
if (typeof stdin === "string") {
|
|
1464
|
-
const ref = parseStepRef(stdin.trim());
|
|
1465
|
-
if (ref)
|
|
1466
|
-
return getStepRefValue(ref, results, true);
|
|
1467
|
-
return resolveTemplate(stdin, args, results);
|
|
1468
|
-
}
|
|
1469
|
-
return stdin;
|
|
1470
|
-
}
|
|
1471
|
-
function resolveShellStdin(stdin, args, results) {
|
|
1472
|
-
const value = resolveInputValue(stdin, args, results);
|
|
1473
|
-
return encodeShellInput(value);
|
|
1474
|
-
}
|
|
1475
|
-
function resolveTemplate(input, args, results) {
|
|
1476
|
-
const withArgs = resolveArgsTemplate(input, args);
|
|
1477
|
-
return resolveStepRefs(withArgs, results);
|
|
1478
|
-
}
|
|
1479
|
-
function resolveWorkflowStepArgs(workflowArgs, parentArgs, results) {
|
|
1480
|
-
if (!workflowArgs)
|
|
1481
|
-
return {};
|
|
1482
|
-
const resolved = {};
|
|
1483
|
-
for (const [key, value] of Object.entries(workflowArgs)) {
|
|
1484
|
-
if (typeof value === "string") {
|
|
1485
|
-
resolved[key] = resolveTemplate(value, parentArgs, results);
|
|
1486
|
-
}
|
|
1487
|
-
else {
|
|
1488
|
-
resolved[key] = value;
|
|
1489
|
-
}
|
|
1490
|
-
}
|
|
1491
|
-
return resolved;
|
|
1492
|
-
}
|
|
1493
|
-
function resolveArgsTemplate(input, args) {
|
|
1494
|
-
return input.replace(/\$\{([A-Za-z0-9_-]+)\}/g, (match, key) => {
|
|
1495
|
-
if (key in args)
|
|
1496
|
-
return String(args[key]);
|
|
1497
|
-
return match;
|
|
1498
|
-
});
|
|
1499
|
-
}
|
|
1500
|
-
function resolveStepRefs(input, results) {
|
|
1501
|
-
return input.replace(/\$([A-Za-z0-9_-]+)\.([A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)*)/g, (match, id, pathValue) => {
|
|
1502
|
-
if (!(id in results)) {
|
|
1503
|
-
return match;
|
|
1504
|
-
}
|
|
1505
|
-
const refValue = getStepRefValue({ id, path: pathValue }, results, false);
|
|
1506
|
-
if (refValue === undefined) {
|
|
1507
|
-
if (pathValue === "approved" || pathValue === "skipped")
|
|
1508
|
-
return "false";
|
|
1509
|
-
return "";
|
|
1510
|
-
}
|
|
1511
|
-
return renderTemplateValue(refValue);
|
|
1512
|
-
});
|
|
1513
|
-
}
|
|
1514
|
-
function parseStepRef(value) {
|
|
1515
|
-
const match = value.match(/^\$([A-Za-z0-9_-]+)\.([A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)*)$/);
|
|
1516
|
-
if (!match)
|
|
1517
|
-
return null;
|
|
1518
|
-
return { id: match[1], path: match[2] };
|
|
1519
|
-
}
|
|
1520
|
-
function getStepRefValue(ref, results, strict) {
|
|
1521
|
-
const step = results[ref.id];
|
|
1522
|
-
if (!step) {
|
|
1523
|
-
if (strict)
|
|
1524
|
-
throw new Error(`Unknown step reference: ${ref.id}.${ref.path}`);
|
|
1525
|
-
return undefined;
|
|
1526
|
-
}
|
|
1527
|
-
return getValueByPath(step, ref.path);
|
|
1528
|
-
}
|
|
1529
|
-
function evaluateCondition(condition, results) {
|
|
1530
|
-
if (condition === undefined || condition === null)
|
|
1531
|
-
return true;
|
|
1532
|
-
if (typeof condition === "boolean")
|
|
1533
|
-
return condition;
|
|
1534
|
-
if (typeof condition !== "string")
|
|
1535
|
-
throw new Error("Unsupported condition type");
|
|
1536
|
-
const trimmed = condition.trim();
|
|
1537
|
-
if (trimmed === "true")
|
|
1538
|
-
return true;
|
|
1539
|
-
if (trimmed === "false")
|
|
1540
|
-
return false;
|
|
1541
|
-
return evaluateConditionExpression(trimmed, results);
|
|
1542
|
-
}
|
|
1543
|
-
function isApprovalStep(approval) {
|
|
1544
|
-
if (approval === true)
|
|
1545
|
-
return true;
|
|
1546
|
-
if (typeof approval === "string" && approval.trim().length > 0)
|
|
1547
|
-
return true;
|
|
1548
|
-
if (approval && typeof approval === "object" && !Array.isArray(approval))
|
|
1549
|
-
return true;
|
|
1550
|
-
return false;
|
|
1551
|
-
}
|
|
1552
|
-
function isInputStep(input) {
|
|
1553
|
-
return Boolean(input && typeof input === "object" && !Array.isArray(input));
|
|
1554
|
-
}
|
|
1555
1323
|
function extractApprovalRequest(step, result, env) {
|
|
1556
1324
|
const approvalConfig = normalizeApprovalConfig(step.approval);
|
|
1557
1325
|
const configIdentity = approvalIdentityFromRaw(approvalConfig);
|
|
@@ -1671,21 +1439,226 @@ function parseBoolLike(value) {
|
|
|
1671
1439
|
return false;
|
|
1672
1440
|
return undefined;
|
|
1673
1441
|
}
|
|
1674
|
-
|
|
1675
|
-
|
|
1442
|
+
// The objects a step's pipeline produced, held apart from the serializable step result.
|
|
1443
|
+
// A rendered pipeline such as `llm.invoke | json` prints its items and returns an empty
|
|
1444
|
+
// stream, so the step's `json` is parsed back out of stdout and only carries what JSON can
|
|
1445
|
+
// express. A symbol key survives neither `JSON.stringify` nor `JSON.parse`, so this holds
|
|
1446
|
+
// exactly the items this process made, and no workflow input can put one here.
|
|
1447
|
+
const PIPELINE_SOURCE_ITEMS = Symbol("lobster.workflow.pipelineSourceItems");
|
|
1448
|
+
function attachPipelineSourceItems(target, items) {
|
|
1449
|
+
Object.defineProperty(target, PIPELINE_SOURCE_ITEMS, {
|
|
1450
|
+
value: items,
|
|
1451
|
+
enumerable: false,
|
|
1452
|
+
configurable: true,
|
|
1453
|
+
});
|
|
1454
|
+
return target;
|
|
1455
|
+
}
|
|
1456
|
+
function pipelineSourceItems(target) {
|
|
1457
|
+
if (!target)
|
|
1458
|
+
return undefined;
|
|
1459
|
+
const items = target[PIPELINE_SOURCE_ITEMS];
|
|
1460
|
+
return Array.isArray(items) ? items : undefined;
|
|
1461
|
+
}
|
|
1462
|
+
// The ledger of the run that invoked this one as a `workflow:` step. A composed run bills its
|
|
1463
|
+
// own steps and the composing run bills the output handed back to it, so a live call has to be
|
|
1464
|
+
// open in both ledgers for each of them to count it once. Read defensively: this arrives
|
|
1465
|
+
// through the same context object a caller supplies.
|
|
1466
|
+
function composingSpendLedger(ctx) {
|
|
1467
|
+
const ledger = ctx._llmSpendLedger;
|
|
1468
|
+
if (!ledger || typeof ledger !== "object")
|
|
1469
|
+
return null;
|
|
1470
|
+
return typeof ledger.record === "function" && typeof ledger.claim === "function" ? ledger : null;
|
|
1471
|
+
}
|
|
1472
|
+
/**
|
|
1473
|
+
* One `wait: "any"` branch's view of the run ledger. The race returns as soon as a branch
|
|
1474
|
+
* answers and leaves the others running, so a losing branch can pay a provider long after the
|
|
1475
|
+
* step settled. Writing that charge straight to the run ledger makes the workflow's own total
|
|
1476
|
+
* depend on how long the run happened to live afterwards: land before the run ends and the
|
|
1477
|
+
* discarded branch's spend is billed, land after and it is billed nowhere.
|
|
1478
|
+
*
|
|
1479
|
+
* So a branch opens its charges here instead, and only the winner releases them into the run.
|
|
1480
|
+
* The output of a losing branch is thrown away; its accounting goes with it. Reads pass
|
|
1481
|
+
* through, because they settle charges the run opened before the race and are made by the
|
|
1482
|
+
* workflow after a branch has returned, never from inside one.
|
|
1483
|
+
*/
|
|
1484
|
+
function branchSpendBuffer(ledger) {
|
|
1485
|
+
const held = [];
|
|
1486
|
+
let released = false;
|
|
1487
|
+
return {
|
|
1488
|
+
record: (cacheKey, charge) => {
|
|
1489
|
+
if (released)
|
|
1490
|
+
ledger.record(cacheKey, charge);
|
|
1491
|
+
else
|
|
1492
|
+
held.push({ cacheKey, charge });
|
|
1493
|
+
},
|
|
1494
|
+
claim: (cacheKey, cost) => ledger.claim(cacheKey, cost),
|
|
1495
|
+
billCopy: (cacheKey, model, usage) => ledger.billCopy(cacheKey, model, usage),
|
|
1496
|
+
outstanding: () => ledger.outstanding(),
|
|
1497
|
+
restore: (charges) => ledger.restore(charges),
|
|
1498
|
+
settled: () => ledger.settled(),
|
|
1499
|
+
restoreSettled: (charges) => ledger.restoreSettled(charges),
|
|
1500
|
+
release: () => {
|
|
1501
|
+
if (released)
|
|
1502
|
+
return;
|
|
1503
|
+
released = true;
|
|
1504
|
+
for (const { cacheKey, charge } of held)
|
|
1505
|
+
ledger.record(cacheKey, charge);
|
|
1506
|
+
held.length = 0;
|
|
1507
|
+
},
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
/**
|
|
1511
|
+
* The run ledger seen by one step, so a charge opened inside it knows where it came from. A
|
|
1512
|
+
* composed run's charges reach the composing run through this too, which is why the step id is
|
|
1513
|
+
* overwritten rather than kept: in the parent's totals the spend belongs to the `workflow:`
|
|
1514
|
+
* step, not to whatever the child called its own.
|
|
1515
|
+
*/
|
|
1516
|
+
function stepScopedLedger(ledger, stepId) {
|
|
1517
|
+
return {
|
|
1518
|
+
record: (cacheKey, charge) => ledger.record(cacheKey, { ...charge, stepId }),
|
|
1519
|
+
claim: (cacheKey, cost) => ledger.claim(cacheKey, cost),
|
|
1520
|
+
billCopy: (cacheKey, model, usage) => ledger.billCopy(cacheKey, model, usage),
|
|
1521
|
+
outstanding: () => ledger.outstanding(),
|
|
1522
|
+
restore: (charges) => ledger.restore(charges),
|
|
1523
|
+
settled: () => ledger.settled(),
|
|
1524
|
+
restoreSettled: (charges) => ledger.restoreSettled(charges),
|
|
1525
|
+
};
|
|
1526
|
+
}
|
|
1527
|
+
/**
|
|
1528
|
+
* Bills the calls that ended the run unclaimed. An item is the usual carrier of usage, but a
|
|
1529
|
+
* renderer can consume the pipeline before a later stage emits items of its own, an `ask` gate
|
|
1530
|
+
* can swallow the only item, and a composed run can end on a step that carries none. The
|
|
1531
|
+
* charge outlives all three, and dropping it would leave real spend out of `_meta.cost` and
|
|
1532
|
+
* out of `cost_limit`.
|
|
1533
|
+
*/
|
|
1534
|
+
function settleUnbilledCharges(costTracker, llmSpendLedger) {
|
|
1535
|
+
for (const charge of llmSpendLedger.outstanding()) {
|
|
1536
|
+
if (!charge.usage)
|
|
1537
|
+
continue;
|
|
1538
|
+
if (!llmSpendLedger.claim(charge.cacheKey))
|
|
1539
|
+
continue;
|
|
1540
|
+
costTracker.recordUsage(charge.stepId ?? "llm", charge.model ?? null, charge.usage);
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
function trackStepCost(costTracker, stepId, result, llmSpendLedger, deferredCharges) {
|
|
1544
|
+
// Bill the items the pipeline produced rather than the JSON re-read from its stdout: a
|
|
1545
|
+
// renderer serializes them, and replay provenance does not survive that round trip. Steps
|
|
1546
|
+
// without a pipeline (shell commands) have no such items and are read from `json` as before.
|
|
1547
|
+
const json = pipelineSourceItems(result) ?? result.json;
|
|
1676
1548
|
if (!json || typeof json !== "object")
|
|
1677
1549
|
return;
|
|
1550
|
+
// Replays wait for the live results accounted with them. Branches of one parallel step are
|
|
1551
|
+
// billed in declaration order, so a cached replay declared before an identical `--refresh`
|
|
1552
|
+
// branch would otherwise claim the charge that branch had just opened — and the live branch,
|
|
1553
|
+
// billed whatever its own claim returns, would be billed for it too. One provider call, two
|
|
1554
|
+
// charges, and a `cost_limit` tripped on spend that never happened.
|
|
1555
|
+
const deferred = deferredCharges ?? [];
|
|
1678
1556
|
const items = Array.isArray(json) ? json : [json];
|
|
1679
1557
|
for (const item of items) {
|
|
1680
1558
|
if (!item || typeof item !== "object")
|
|
1681
1559
|
continue;
|
|
1682
|
-
const
|
|
1560
|
+
const record = item;
|
|
1561
|
+
const usage = record.usage;
|
|
1683
1562
|
if (!usage || typeof usage !== "object")
|
|
1684
1563
|
continue;
|
|
1685
|
-
const modelValue =
|
|
1564
|
+
const modelValue = record.model;
|
|
1686
1565
|
const model = typeof modelValue === "string" ? modelValue : null;
|
|
1566
|
+
// Provenance the LLM commands attach in-process, never a field in the JSON: a step that
|
|
1567
|
+
// prints a replay-shaped object stays billed, so `cost_limit` cannot be evaded. A
|
|
1568
|
+
// projection can drop the item's own mark while carrying its usage record across, so the
|
|
1569
|
+
// record is read too — both marks are symbol keys built in this process.
|
|
1570
|
+
const provenance = llmProvenanceOf(record) ?? llmProvenanceOf(usage);
|
|
1571
|
+
if (provenance) {
|
|
1572
|
+
if (provenance.replayed) {
|
|
1573
|
+
deferred.push({
|
|
1574
|
+
kind: "replay",
|
|
1575
|
+
stepId,
|
|
1576
|
+
cacheKey: provenance.cacheKey,
|
|
1577
|
+
model,
|
|
1578
|
+
usage: usage,
|
|
1579
|
+
});
|
|
1580
|
+
continue;
|
|
1581
|
+
}
|
|
1582
|
+
// Settles the charge this live call opened -- the one that cost what this item cost,
|
|
1583
|
+
// not merely the first under the key. A step retried after an attempt that failed
|
|
1584
|
+
// after paying leaves more than one charge open, and they did not cost the same.
|
|
1585
|
+
const settled = llmSpendLedger.claim(provenance.cacheKey, {
|
|
1586
|
+
model,
|
|
1587
|
+
usage: usage,
|
|
1588
|
+
});
|
|
1589
|
+
// Bill what the provider was really asked for, not what the item says by the time it
|
|
1590
|
+
// gets here. A pipeline can drop the model on the way -- `| pick usage` hands on the
|
|
1591
|
+
// marked usage record without it -- or rewrite it, and the step is then priced at the
|
|
1592
|
+
// wrong rate or at nothing at all while `cost_limit` reads the difference as room left.
|
|
1593
|
+
// The charge the call opened is the same witness a replay is already billed from. A
|
|
1594
|
+
// charge restored from resume state written before it carried a cost falls back.
|
|
1595
|
+
if (settled?.usage) {
|
|
1596
|
+
costTracker.recordUsage(stepId, settled.model ?? null, settled.usage);
|
|
1597
|
+
markLlmProvenanceReplayed(record);
|
|
1598
|
+
markLlmProvenanceReplayed(usage);
|
|
1599
|
+
continue;
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
else {
|
|
1603
|
+
// No mark, but numbers: a stage that JSON round-trips the stream hands on a copy that
|
|
1604
|
+
// kept `model` and `usage` and lost everything this process attached -- sometimes the
|
|
1605
|
+
// cache key with it. Held back like a replay, and for the same reason: a copy that
|
|
1606
|
+
// lost its key is read by cost alone, and until the marked results have settled their
|
|
1607
|
+
// own charges an identical parallel call's charge is still open to be mistaken for
|
|
1608
|
+
// the one this copy came from.
|
|
1609
|
+
deferred.push({
|
|
1610
|
+
kind: "copy",
|
|
1611
|
+
stepId,
|
|
1612
|
+
cacheKey: typeof record.cacheKey === "string" && record.cacheKey ? record.cacheKey : null,
|
|
1613
|
+
model,
|
|
1614
|
+
usage: usage,
|
|
1615
|
+
});
|
|
1616
|
+
continue;
|
|
1617
|
+
}
|
|
1687
1618
|
costTracker.recordUsage(stepId, model, usage);
|
|
1619
|
+
markLlmProvenanceReplayed(record);
|
|
1620
|
+
markLlmProvenanceReplayed(usage);
|
|
1621
|
+
}
|
|
1622
|
+
if (!deferredCharges)
|
|
1623
|
+
settleDeferredCosts(costTracker, deferred, llmSpendLedger);
|
|
1624
|
+
}
|
|
1625
|
+
/**
|
|
1626
|
+
* Bills what a step held back, now that every marked live result accounted alongside them has
|
|
1627
|
+
* settled its own charge. Replays go first: one carries the mark of the call it stands in for,
|
|
1628
|
+
* which is better evidence than the cost a copy carries on its own.
|
|
1629
|
+
*
|
|
1630
|
+
* A replay carries the usage of the call that produced it, so counting it again charges a run
|
|
1631
|
+
* for tokens no provider was asked to spend. It is billed only when the call it replays is
|
|
1632
|
+
* still unpaid for: a step is costed once it succeeds, so a step that failed after its LLM
|
|
1633
|
+
* call and was retried leaves its real spend riding on the replay the retry produced.
|
|
1634
|
+
*
|
|
1635
|
+
* A copy is billed either way — it is a record of a call, and nothing in a JSON payload can be
|
|
1636
|
+
* trusted to say otherwise — but the ledger still decides whether it settles a charge, so the
|
|
1637
|
+
* step settlement does not add a second record of the same call.
|
|
1638
|
+
*/
|
|
1639
|
+
function settleDeferredCosts(costTracker, deferred, llmSpendLedger) {
|
|
1640
|
+
for (const replay of deferred) {
|
|
1641
|
+
if (replay.kind !== "replay" || replay.cacheKey === null)
|
|
1642
|
+
continue;
|
|
1643
|
+
const settled = llmSpendLedger.claim(replay.cacheKey);
|
|
1644
|
+
if (!settled)
|
|
1645
|
+
continue;
|
|
1646
|
+
// Bill what the call this settles actually cost, not what the replay carries. Identical
|
|
1647
|
+
// calls that raced on a cold cache are separate charges under one key, and every replay
|
|
1648
|
+
// of them repeats whichever single answer was stored -- so costing each replay by its
|
|
1649
|
+
// own copy reports all of those calls at the price of the one that won the cache write.
|
|
1650
|
+
// A charge restored from resume state written before it carried a cost falls back.
|
|
1651
|
+
const cost = typeof settled === "object" && settled.usage ? settled : null;
|
|
1652
|
+
costTracker.recordUsage(replay.stepId, cost ? (cost.model ?? null) : replay.model, cost?.usage ?? replay.usage);
|
|
1653
|
+
}
|
|
1654
|
+
for (const copy of deferred) {
|
|
1655
|
+
if (copy.kind !== "copy")
|
|
1656
|
+
continue;
|
|
1657
|
+
if (llmSpendLedger.billCopy(copy.cacheKey, copy.model, copy.usage)) {
|
|
1658
|
+
costTracker.recordUsage(copy.stepId, copy.model, copy.usage);
|
|
1659
|
+
}
|
|
1688
1660
|
}
|
|
1661
|
+
deferred.length = 0;
|
|
1689
1662
|
}
|
|
1690
1663
|
function parseJson(stdout) {
|
|
1691
1664
|
const trimmed = stdout.trim();
|
|
@@ -1713,7 +1686,7 @@ function toOutputItems(result) {
|
|
|
1713
1686
|
return [];
|
|
1714
1687
|
}
|
|
1715
1688
|
function cloneResults(results) {
|
|
1716
|
-
const out =
|
|
1689
|
+
const out = createRecord();
|
|
1717
1690
|
for (const [key, value] of Object.entries(results)) {
|
|
1718
1691
|
out[key] = { ...value };
|
|
1719
1692
|
}
|
|
@@ -1852,346 +1825,24 @@ function estimateSerializedBytes(value) {
|
|
|
1852
1825
|
return Number.POSITIVE_INFINITY;
|
|
1853
1826
|
}
|
|
1854
1827
|
}
|
|
1855
|
-
function
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
return String(value);
|
|
1868
|
-
}
|
|
1869
|
-
}
|
|
1870
|
-
function getValueByPath(value, pathValue) {
|
|
1871
|
-
const fields = pathValue.split(".");
|
|
1872
|
-
let current = value;
|
|
1873
|
-
for (const field of fields) {
|
|
1874
|
-
if (current === null || current === undefined)
|
|
1875
|
-
return undefined;
|
|
1876
|
-
if (Array.isArray(current)) {
|
|
1877
|
-
const idx = Number(field);
|
|
1878
|
-
if (!Number.isInteger(idx) || idx < 0 || idx >= current.length)
|
|
1879
|
-
return undefined;
|
|
1880
|
-
current = current[idx];
|
|
1881
|
-
continue;
|
|
1882
|
-
}
|
|
1883
|
-
if (typeof current !== "object")
|
|
1884
|
-
return undefined;
|
|
1885
|
-
current = current[field];
|
|
1886
|
-
}
|
|
1887
|
-
return current;
|
|
1888
|
-
}
|
|
1889
|
-
function evaluateConditionExpression(expression, results) {
|
|
1890
|
-
const tokens = tokenizeCondition(expression);
|
|
1891
|
-
if (tokens.length === 0) {
|
|
1892
|
-
throw new Error(`Unsupported condition: ${expression}`);
|
|
1893
|
-
}
|
|
1894
|
-
let index = 0;
|
|
1895
|
-
function parseOr() {
|
|
1896
|
-
let left = parseAnd();
|
|
1897
|
-
while (match("or")) {
|
|
1898
|
-
const right = parseAnd();
|
|
1899
|
-
left = Boolean(left) || Boolean(right);
|
|
1900
|
-
}
|
|
1901
|
-
return left;
|
|
1902
|
-
}
|
|
1903
|
-
function parseAnd() {
|
|
1904
|
-
let left = parseEquality();
|
|
1905
|
-
while (match("and")) {
|
|
1906
|
-
const right = parseEquality();
|
|
1907
|
-
left = Boolean(left) && Boolean(right);
|
|
1908
|
-
}
|
|
1909
|
-
return left;
|
|
1910
|
-
}
|
|
1911
|
-
function parseEquality() {
|
|
1912
|
-
const left = parseUnary(false);
|
|
1913
|
-
if (match("eq")) {
|
|
1914
|
-
return compareConditionValues(left, parseUnary(true));
|
|
1915
|
-
}
|
|
1916
|
-
if (match("neq")) {
|
|
1917
|
-
return !compareConditionValues(left, parseUnary(true));
|
|
1918
|
-
}
|
|
1919
|
-
if (match("lt")) {
|
|
1920
|
-
return numericCompare(left, parseUnary(true), (a, b) => a < b);
|
|
1921
|
-
}
|
|
1922
|
-
if (match("lte")) {
|
|
1923
|
-
return numericCompare(left, parseUnary(true), (a, b) => a <= b);
|
|
1924
|
-
}
|
|
1925
|
-
if (match("gt")) {
|
|
1926
|
-
return numericCompare(left, parseUnary(true), (a, b) => a > b);
|
|
1927
|
-
}
|
|
1928
|
-
if (match("gte")) {
|
|
1929
|
-
return numericCompare(left, parseUnary(true), (a, b) => a >= b);
|
|
1930
|
-
}
|
|
1931
|
-
return left;
|
|
1932
|
-
}
|
|
1933
|
-
function parseUnary(allowBareIdentifier) {
|
|
1934
|
-
if (match("not")) {
|
|
1935
|
-
return !Boolean(parseUnary(allowBareIdentifier));
|
|
1936
|
-
}
|
|
1937
|
-
return parsePrimary(allowBareIdentifier);
|
|
1938
|
-
}
|
|
1939
|
-
function parsePrimary(allowBareIdentifier) {
|
|
1940
|
-
const token = tokens[index];
|
|
1941
|
-
if (!token) {
|
|
1942
|
-
throw new Error(`Unsupported condition: ${expression}`);
|
|
1943
|
-
}
|
|
1944
|
-
index += 1;
|
|
1945
|
-
if (token.type === "lparen") {
|
|
1946
|
-
const value = parseOr();
|
|
1947
|
-
expect("rparen");
|
|
1948
|
-
return value;
|
|
1949
|
-
}
|
|
1950
|
-
if (token.type === "step_ref") {
|
|
1951
|
-
return getStepRefValue(token.value, results, true);
|
|
1952
|
-
}
|
|
1953
|
-
if (token.type === "string" ||
|
|
1954
|
-
token.type === "number" ||
|
|
1955
|
-
token.type === "boolean" ||
|
|
1956
|
-
token.type === "null") {
|
|
1957
|
-
return token.value;
|
|
1958
|
-
}
|
|
1959
|
-
if (token.type === "identifier" && allowBareIdentifier) {
|
|
1960
|
-
return token.value;
|
|
1961
|
-
}
|
|
1962
|
-
throw new Error(`Unsupported condition: ${expression}`);
|
|
1963
|
-
}
|
|
1964
|
-
function match(type) {
|
|
1965
|
-
if (tokens[index]?.type !== type)
|
|
1966
|
-
return false;
|
|
1967
|
-
index += 1;
|
|
1968
|
-
return true;
|
|
1969
|
-
}
|
|
1970
|
-
function expect(type) {
|
|
1971
|
-
if (!match(type)) {
|
|
1972
|
-
throw new Error(`Unsupported condition: ${expression}`);
|
|
1973
|
-
}
|
|
1974
|
-
}
|
|
1975
|
-
const value = parseOr();
|
|
1976
|
-
if (index !== tokens.length) {
|
|
1977
|
-
throw new Error(`Unsupported condition: ${expression}`);
|
|
1978
|
-
}
|
|
1979
|
-
return Boolean(value);
|
|
1980
|
-
}
|
|
1981
|
-
function compareConditionValues(left, right) {
|
|
1982
|
-
if (Array.isArray(left) ||
|
|
1983
|
-
Array.isArray(right) ||
|
|
1984
|
-
isPlainConditionObject(left) ||
|
|
1985
|
-
isPlainConditionObject(right)) {
|
|
1986
|
-
return isDeepStrictEqual(left, right);
|
|
1987
|
-
}
|
|
1988
|
-
return Object.is(left, right);
|
|
1989
|
-
}
|
|
1990
|
-
function isStrictlyNumeric(value) {
|
|
1991
|
-
if (typeof value === "number")
|
|
1992
|
-
return !Number.isNaN(value);
|
|
1993
|
-
if (typeof value === "string")
|
|
1994
|
-
return value.trim() !== "" && !Number.isNaN(Number(value));
|
|
1995
|
-
return false;
|
|
1996
|
-
}
|
|
1997
|
-
function numericCompare(left, right, cmp) {
|
|
1998
|
-
if (!isStrictlyNumeric(left) || !isStrictlyNumeric(right))
|
|
1999
|
-
return false;
|
|
2000
|
-
return cmp(Number(left), Number(right));
|
|
2001
|
-
}
|
|
2002
|
-
function isPlainConditionObject(value) {
|
|
2003
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
2004
|
-
}
|
|
2005
|
-
function tokenizeCondition(expression) {
|
|
2006
|
-
const tokens = [];
|
|
2007
|
-
let index = 0;
|
|
2008
|
-
while (index < expression.length) {
|
|
2009
|
-
const ch = expression[index];
|
|
2010
|
-
if (/\s/.test(ch)) {
|
|
2011
|
-
index += 1;
|
|
2012
|
-
continue;
|
|
2013
|
-
}
|
|
2014
|
-
if (ch === "(") {
|
|
2015
|
-
tokens.push({ type: "lparen" });
|
|
2016
|
-
index += 1;
|
|
2017
|
-
continue;
|
|
2018
|
-
}
|
|
2019
|
-
if (ch === ")") {
|
|
2020
|
-
tokens.push({ type: "rparen" });
|
|
2021
|
-
index += 1;
|
|
2022
|
-
continue;
|
|
2023
|
-
}
|
|
2024
|
-
if (expression.startsWith("&&", index)) {
|
|
2025
|
-
tokens.push({ type: "and" });
|
|
2026
|
-
index += 2;
|
|
2027
|
-
continue;
|
|
2028
|
-
}
|
|
2029
|
-
if (expression.startsWith("||", index)) {
|
|
2030
|
-
tokens.push({ type: "or" });
|
|
2031
|
-
index += 2;
|
|
2032
|
-
continue;
|
|
2033
|
-
}
|
|
2034
|
-
if (expression.startsWith("==", index)) {
|
|
2035
|
-
tokens.push({ type: "eq" });
|
|
2036
|
-
index += 2;
|
|
2037
|
-
continue;
|
|
2038
|
-
}
|
|
2039
|
-
if (expression.startsWith("!=", index)) {
|
|
2040
|
-
tokens.push({ type: "neq" });
|
|
2041
|
-
index += 2;
|
|
2042
|
-
continue;
|
|
2043
|
-
}
|
|
2044
|
-
if (expression.startsWith("<=", index)) {
|
|
2045
|
-
tokens.push({ type: "lte" });
|
|
2046
|
-
index += 2;
|
|
2047
|
-
continue;
|
|
2048
|
-
}
|
|
2049
|
-
if (expression.startsWith(">=", index)) {
|
|
2050
|
-
tokens.push({ type: "gte" });
|
|
2051
|
-
index += 2;
|
|
2052
|
-
continue;
|
|
2053
|
-
}
|
|
2054
|
-
if (ch === "<") {
|
|
2055
|
-
tokens.push({ type: "lt" });
|
|
2056
|
-
index += 1;
|
|
2057
|
-
continue;
|
|
2058
|
-
}
|
|
2059
|
-
if (ch === ">") {
|
|
2060
|
-
tokens.push({ type: "gt" });
|
|
2061
|
-
index += 1;
|
|
2062
|
-
continue;
|
|
2063
|
-
}
|
|
2064
|
-
if (ch === "!") {
|
|
2065
|
-
tokens.push({ type: "not" });
|
|
2066
|
-
index += 1;
|
|
2067
|
-
continue;
|
|
2068
|
-
}
|
|
2069
|
-
if (ch === "$") {
|
|
2070
|
-
const matched = matchConditionStepRef(expression, index);
|
|
2071
|
-
if (!matched) {
|
|
2072
|
-
throw new Error(`Unsupported condition: ${expression}`);
|
|
2073
|
-
}
|
|
2074
|
-
tokens.push({ type: "step_ref", value: matched.ref });
|
|
2075
|
-
index = matched.nextIndex;
|
|
2076
|
-
continue;
|
|
2077
|
-
}
|
|
2078
|
-
if (ch === '"' || ch === "'") {
|
|
2079
|
-
const parsed = parseQuotedConditionString(expression, index, ch);
|
|
2080
|
-
tokens.push({ type: "string", value: parsed.value });
|
|
2081
|
-
index = parsed.nextIndex;
|
|
2082
|
-
continue;
|
|
2083
|
-
}
|
|
2084
|
-
const numberMatch = expression.slice(index).match(/^-?\d+(?:\.\d+)?/);
|
|
2085
|
-
if (numberMatch) {
|
|
2086
|
-
tokens.push({ type: "number", value: Number(numberMatch[0]) });
|
|
2087
|
-
index += numberMatch[0].length;
|
|
2088
|
-
continue;
|
|
2089
|
-
}
|
|
2090
|
-
const identMatch = expression.slice(index).match(/^[A-Za-z_][A-Za-z0-9_-]*/);
|
|
2091
|
-
if (identMatch) {
|
|
2092
|
-
const raw = identMatch[0];
|
|
2093
|
-
if (raw === "true") {
|
|
2094
|
-
tokens.push({ type: "boolean", value: true });
|
|
2095
|
-
}
|
|
2096
|
-
else if (raw === "false") {
|
|
2097
|
-
tokens.push({ type: "boolean", value: false });
|
|
2098
|
-
}
|
|
2099
|
-
else if (raw === "null") {
|
|
2100
|
-
tokens.push({ type: "null", value: null });
|
|
2101
|
-
}
|
|
2102
|
-
else {
|
|
2103
|
-
tokens.push({ type: "identifier", value: raw });
|
|
2104
|
-
}
|
|
2105
|
-
index += raw.length;
|
|
2106
|
-
continue;
|
|
2107
|
-
}
|
|
2108
|
-
throw new Error(`Unsupported condition: ${expression}`);
|
|
2109
|
-
}
|
|
2110
|
-
return tokens;
|
|
2111
|
-
}
|
|
2112
|
-
function matchConditionStepRef(expression, startIndex) {
|
|
2113
|
-
const match = expression
|
|
2114
|
-
.slice(startIndex)
|
|
2115
|
-
.match(/^\$([A-Za-z0-9_-]+)\.([A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)*)/);
|
|
2116
|
-
if (!match)
|
|
2117
|
-
return null;
|
|
2118
|
-
return {
|
|
2119
|
-
ref: { id: match[1], path: match[2] },
|
|
2120
|
-
nextIndex: startIndex + match[0].length,
|
|
2121
|
-
};
|
|
2122
|
-
}
|
|
2123
|
-
function parseQuotedConditionString(expression, startIndex, quoteChar) {
|
|
2124
|
-
let value = "";
|
|
2125
|
-
let index = startIndex + 1;
|
|
2126
|
-
while (index < expression.length) {
|
|
2127
|
-
const ch = expression[index];
|
|
2128
|
-
if (ch === "\\") {
|
|
2129
|
-
const next = expression[index + 1];
|
|
2130
|
-
if (next === undefined)
|
|
2131
|
-
break;
|
|
2132
|
-
value += next;
|
|
2133
|
-
index += 2;
|
|
2134
|
-
continue;
|
|
2135
|
-
}
|
|
2136
|
-
if (ch === quoteChar) {
|
|
2137
|
-
return { value, nextIndex: index + 1 };
|
|
2138
|
-
}
|
|
2139
|
-
value += ch;
|
|
2140
|
-
index += 1;
|
|
2141
|
-
}
|
|
2142
|
-
throw new Error(`Unsupported condition: ${expression}`);
|
|
2143
|
-
}
|
|
2144
|
-
async function runShellCommand({ command, stdin, env, cwd, signal, killSignal, }) {
|
|
2145
|
-
const { spawn } = await import("node:child_process");
|
|
2146
|
-
return await new Promise((resolve, reject) => {
|
|
2147
|
-
const shell = resolveInlineShellCommand({ command, env });
|
|
2148
|
-
const child = spawn(shell.command, shell.argv, {
|
|
2149
|
-
env,
|
|
2150
|
-
cwd,
|
|
2151
|
-
signal,
|
|
2152
|
-
killSignal,
|
|
2153
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
2154
|
-
});
|
|
2155
|
-
let stdout = "";
|
|
2156
|
-
let stderr = "";
|
|
2157
|
-
child.stdout.setEncoding("utf8");
|
|
2158
|
-
child.stderr.setEncoding("utf8");
|
|
2159
|
-
child.stdout.on("data", (d) => {
|
|
2160
|
-
stdout += d;
|
|
2161
|
-
});
|
|
2162
|
-
child.stderr.on("data", (d) => {
|
|
2163
|
-
stderr += d;
|
|
2164
|
-
});
|
|
2165
|
-
if (typeof stdin === "string") {
|
|
2166
|
-
child.stdin.setDefaultEncoding("utf8");
|
|
2167
|
-
child.stdin.write(stdin);
|
|
2168
|
-
}
|
|
2169
|
-
child.stdin.end();
|
|
2170
|
-
child.on("error", reject);
|
|
2171
|
-
child.on("close", (code) => {
|
|
2172
|
-
if (code === 0)
|
|
2173
|
-
return resolve({ stdout, stderr });
|
|
2174
|
-
reject(new Error(`workflow command failed (${code}): ${stderr.trim() || stdout.trim() || command}`));
|
|
2175
|
-
});
|
|
1828
|
+
async function runShellCommand({ command, stdin, env, cwd, signal, forceTerminationSignal, killSignal, }) {
|
|
1829
|
+
signal?.throwIfAborted();
|
|
1830
|
+
signal?.throwIfAborted();
|
|
1831
|
+
const { stdout, stderr, code } = await runAbortableProcess({
|
|
1832
|
+
shellCommand: command,
|
|
1833
|
+
env,
|
|
1834
|
+
cwd,
|
|
1835
|
+
stdin,
|
|
1836
|
+
signal,
|
|
1837
|
+
forceTerminationSignal,
|
|
1838
|
+
killSignal,
|
|
1839
|
+
notFoundMessage: "workflow shell not found; check LOBSTER_SHELL or ComSpec",
|
|
2176
1840
|
});
|
|
1841
|
+
if (code === 0)
|
|
1842
|
+
return { stdout, stderr };
|
|
1843
|
+
throw new Error(`workflow command failed (${code}): ${stderr.trim() || stdout.trim() || command}`);
|
|
2177
1844
|
}
|
|
2178
|
-
function
|
|
2179
|
-
if (step.parallel && typeof step.parallel === "object" && !Array.isArray(step.parallel)) {
|
|
2180
|
-
return { kind: "parallel", value: step.parallel };
|
|
2181
|
-
}
|
|
2182
|
-
if (typeof step.workflow === "string" && step.workflow.trim()) {
|
|
2183
|
-
return { kind: "workflow", value: step.workflow };
|
|
2184
|
-
}
|
|
2185
|
-
if (typeof step.pipeline === "string" && step.pipeline.trim()) {
|
|
2186
|
-
return { kind: "pipeline", value: step.pipeline };
|
|
2187
|
-
}
|
|
2188
|
-
const shellCommand = typeof step.run === "string" ? step.run : step.command;
|
|
2189
|
-
if (typeof shellCommand === "string" && shellCommand.trim()) {
|
|
2190
|
-
return { kind: "shell", value: shellCommand };
|
|
2191
|
-
}
|
|
2192
|
-
return { kind: "none" };
|
|
2193
|
-
}
|
|
2194
|
-
async function runPipelineStep({ stepId, pipelineText, inputValue, ctx, env, cwd, resume, requestInputEnabled = true, }) {
|
|
1845
|
+
async function runPipelineStep({ stepId, pipelineText, inputValue, ctx, llmSpendLedger, env, cwd, resume, requestInputEnabled = true, onExecutionStart, }) {
|
|
2195
1846
|
let pipeline;
|
|
2196
1847
|
try {
|
|
2197
1848
|
const currentPipeline = parsePipeline(pipelineText);
|
|
@@ -2228,7 +1879,11 @@ async function runPipelineStep({ stepId, pipelineText, inputValue, ctx, env, cwd
|
|
|
2228
1879
|
mode: ctx.mode,
|
|
2229
1880
|
cwd,
|
|
2230
1881
|
signal: ctx.signal,
|
|
1882
|
+
forceTerminationSignal: ctx.forceTerminationSignal,
|
|
1883
|
+
haltAfterStageOnAbort: true,
|
|
2231
1884
|
llmAdapters: ctx.llmAdapters,
|
|
1885
|
+
// Scoped so a charge nothing bills can still name the step that opened it.
|
|
1886
|
+
llmSpendLedger: llmSpendLedger ? stepScopedLedger(llmSpendLedger, stepId) : undefined,
|
|
2232
1887
|
input: resume ? resume.pipelineInput.items : inputValueToPipelineItems(inputValue),
|
|
2233
1888
|
requestInputEnabled,
|
|
2234
1889
|
requestInputResume: resume
|
|
@@ -2238,8 +1893,11 @@ async function runPipelineStep({ stepId, pipelineText, inputValue, ctx, env, cwd
|
|
|
2238
1893
|
onConsumed: resume.onConsumed,
|
|
2239
1894
|
}
|
|
2240
1895
|
: undefined,
|
|
1896
|
+
onExecutionStart,
|
|
1897
|
+
onNonRetryableSideEffect: ctx._onNonRetryableSideEffect,
|
|
2241
1898
|
});
|
|
2242
1899
|
stdout.end();
|
|
1900
|
+
ctx.signal?.throwIfAborted();
|
|
2243
1901
|
if (result.halted) {
|
|
2244
1902
|
const haltedName = result.haltedAt?.stage?.name ?? "unknown";
|
|
2245
1903
|
if (result.items.length === 1 && result.items[0]?.type === "approval_request") {
|
|
@@ -2273,11 +1931,25 @@ async function runPipelineStep({ stepId, pipelineText, inputValue, ctx, env, cwd
|
|
|
2273
1931
|
? result.items[0]
|
|
2274
1932
|
: result.items
|
|
2275
1933
|
: parseJson(renderedStdout);
|
|
2276
|
-
|
|
1934
|
+
const stepResult = {
|
|
2277
1935
|
id: stepId,
|
|
2278
1936
|
stdout: normalizedStdout,
|
|
2279
1937
|
json,
|
|
2280
1938
|
};
|
|
1939
|
+
// Only when a renderer consumed the pipeline: otherwise `json` already is those items.
|
|
1940
|
+
if (!result.items.length && result.renderedItems.length) {
|
|
1941
|
+
// Only restore marks from the renderer's own matching objects, never from public JSON fields.
|
|
1942
|
+
const source = Array.isArray(json)
|
|
1943
|
+
? result.renderedItems
|
|
1944
|
+
: result.renderedItems.length === 1
|
|
1945
|
+
? result.renderedItems[0]
|
|
1946
|
+
: undefined;
|
|
1947
|
+
if (source !== undefined && stableStringify(source) === stableStringify(json)) {
|
|
1948
|
+
carryLlmProvenance(source, json);
|
|
1949
|
+
}
|
|
1950
|
+
attachPipelineSourceItems(stepResult, result.renderedItems);
|
|
1951
|
+
}
|
|
1952
|
+
return stepResult;
|
|
2281
1953
|
}
|
|
2282
1954
|
function createSyntheticStepResult(stepId, value) {
|
|
2283
1955
|
if (value === null || value === undefined) {
|
|
@@ -2317,13 +1989,6 @@ function abortableSleep(ms, signal) {
|
|
|
2317
1989
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
2318
1990
|
});
|
|
2319
1991
|
}
|
|
2320
|
-
function encodeShellInput(value) {
|
|
2321
|
-
if (value === null || value === undefined)
|
|
2322
|
-
return null;
|
|
2323
|
-
if (typeof value === "string")
|
|
2324
|
-
return value;
|
|
2325
|
-
return JSON.stringify(value);
|
|
2326
|
-
}
|
|
2327
1992
|
function* inputValueToItems(value) {
|
|
2328
1993
|
if (value === null || value === undefined)
|
|
2329
1994
|
return;
|