@openclaw/lobster 2026.9.4 → 2026.9.5
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 +112 -1
- package/node_modules/@clawdbot/lobster/bin/clawd.invoke.js +13 -7
- package/node_modules/@clawdbot/lobster/bin/lobster.js +7 -7
- package/node_modules/@clawdbot/lobster/bin/openclaw.invoke.js +16 -10
- 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 +96 -248
- package/node_modules/@clawdbot/lobster/dist/src/cli.js.map +1 -1
- 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.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.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 +596 -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.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.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.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.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/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/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 +4 -3
- 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/pr-monitor.js +4 -2
- package/node_modules/@clawdbot/lobster/dist/src/recipes/github/pr-monitor.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/recipes/github/stages/pr-view.js +20 -34
- 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 +10 -2
- package/node_modules/@clawdbot/lobster/dist/src/sdk/Lobster.js.map +1 -1
- 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 +13 -82
- 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 +22 -32
- 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 +9 -101
- 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 -1
- package/node_modules/@clawdbot/lobster/dist/src/sdk/runtime.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/shell.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/state/store.js +619 -19
- 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/file.js +963 -283
- 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 +27 -28
- 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 -2
- package/node_modules/@clawdbot/lobster/dist/src/workflows/graph.js.map +1 -1
- package/node_modules/@clawdbot/lobster/dist/src/workflows/registry.js.map +1 -1
- 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 +5170 -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_file_args_json.test.js.map +1 -1
- 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 +1733 -1
- 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/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 +946 -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/process_output_limit.test.js +173 -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 +201 -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 +12 -2
- 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 +74 -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
|
@@ -7,13 +7,48 @@ import { PassThrough } from "node:stream";
|
|
|
7
7
|
import { parsePipeline } from "../parser.js";
|
|
8
8
|
import { runPipeline } from "../runtime.js";
|
|
9
9
|
import { encodeToken, decodeToken } from "../token.js";
|
|
10
|
-
import { createApprovalIndex, deleteStateJson,
|
|
10
|
+
import { cleanupApprovalIndexByStateKey, consumeResumeState, createApprovalIndex, deleteStateJsonWithBoundedResumeCleanup, deleteResumeStateWithRollback, deleteStateJson, deleteUnconsumedResumeState, isConsumedResumeState, readStateJsonWithLock, restoreConsumedResumeState, writeStateJson, } from "../state/store.js";
|
|
11
11
|
import { readLineFromStream } from "../read_line.js";
|
|
12
|
-
import { resolveInlineShellCommand } from "../shell.js";
|
|
13
12
|
import { compileCached } from "../validation.js";
|
|
14
|
-
import {
|
|
13
|
+
import { createLlmSpendLedger, llmProvenanceOf, restoreLlmProvenance, } from "../commands/stdlib/llm_invoke.js";
|
|
14
|
+
import { CostLimitExceededError, CostTracker } from "../core/cost_tracker.js";
|
|
15
15
|
import { withRetry, resolveRetryConfig } from "../core/retry.js";
|
|
16
16
|
import { RequestInputResumeError, validateCommandInputState, } from "../input_request.js";
|
|
17
|
+
import { runAbortableProcess } from "../abortable_process.js";
|
|
18
|
+
const NON_RETRYABLE_STEP_ERROR = Symbol("lobster.nonRetryableStepError");
|
|
19
|
+
function createRecord(source) {
|
|
20
|
+
const record = Object.create(null);
|
|
21
|
+
if (!source)
|
|
22
|
+
return record;
|
|
23
|
+
for (const [key, value] of Object.entries(source))
|
|
24
|
+
record[key] = value;
|
|
25
|
+
return record;
|
|
26
|
+
}
|
|
27
|
+
function markStepErrorNonRetryable(error) {
|
|
28
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
29
|
+
Object.defineProperty(normalized, NON_RETRYABLE_STEP_ERROR, { value: true });
|
|
30
|
+
return normalized;
|
|
31
|
+
}
|
|
32
|
+
function isStepErrorNonRetryable(error) {
|
|
33
|
+
return Boolean(error instanceof Error &&
|
|
34
|
+
error[NON_RETRYABLE_STEP_ERROR]);
|
|
35
|
+
}
|
|
36
|
+
const PARALLEL_BRANCH_ABORT_SETTLE_TIMEOUT_MS = 500;
|
|
37
|
+
async function waitForCancelledParallelBranches(branchesSettled) {
|
|
38
|
+
let timeoutId;
|
|
39
|
+
try {
|
|
40
|
+
await Promise.race([
|
|
41
|
+
branchesSettled,
|
|
42
|
+
new Promise((_resolve, reject) => {
|
|
43
|
+
timeoutId = setTimeout(() => reject(new Error("Parallel branches did not settle after cancellation")), PARALLEL_BRANCH_ABORT_SETTLE_TIMEOUT_MS);
|
|
44
|
+
}),
|
|
45
|
+
]);
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
if (timeoutId !== undefined)
|
|
49
|
+
clearTimeout(timeoutId);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
17
52
|
export class WorkflowResumeArgumentError extends Error {
|
|
18
53
|
constructor(message) {
|
|
19
54
|
super(message);
|
|
@@ -347,7 +382,7 @@ export async function loadWorkflowFile(filePath) {
|
|
|
347
382
|
return parsed;
|
|
348
383
|
}
|
|
349
384
|
export function resolveWorkflowArgs(argDefs, provided) {
|
|
350
|
-
const resolved =
|
|
385
|
+
const resolved = createRecord();
|
|
351
386
|
if (argDefs) {
|
|
352
387
|
for (const [key, def] of Object.entries(argDefs)) {
|
|
353
388
|
if (def && typeof def === "object" && "default" in def) {
|
|
@@ -364,11 +399,74 @@ export function resolveWorkflowArgs(argDefs, provided) {
|
|
|
364
399
|
}
|
|
365
400
|
export async function runWorkflowFile({ filePath, args, ctx, resume, approved, response, cancel, }) {
|
|
366
401
|
const consumedResumeStateKey = resume?.stateKey && typeof resume.stateKey === "string"
|
|
367
|
-
? await resolveWorkflowResumeStateKey(ctx.env, resume.stateKey)
|
|
402
|
+
? await resolveWorkflowResumeStateKey(ctx.env, resume.stateKey, ctx.signal)
|
|
368
403
|
: null;
|
|
404
|
+
if (consumedResumeStateKey)
|
|
405
|
+
ctx._onResumeStateResolved?.(consumedResumeStateKey);
|
|
369
406
|
const resumeState = resume?.stateKey
|
|
370
|
-
? await loadWorkflowResumeState(ctx.env, consumedResumeStateKey ?? resume.stateKey)
|
|
407
|
+
? await loadWorkflowResumeState(ctx.env, consumedResumeStateKey ?? resume.stateKey, ctx.signal)
|
|
371
408
|
: (resume ?? null);
|
|
409
|
+
let resumedExecutionStarted = false;
|
|
410
|
+
let resumeStateConsumed = false;
|
|
411
|
+
let executionBoundaryStarted = false;
|
|
412
|
+
const executionSignalListeners = [];
|
|
413
|
+
let executionStart;
|
|
414
|
+
const markExecutionStarted = async (signal) => {
|
|
415
|
+
if (!consumedResumeStateKey && !ctx._onExecutionStarted && !ctx._onExecutionInterrupted)
|
|
416
|
+
return;
|
|
417
|
+
if (executionStart)
|
|
418
|
+
return executionStart;
|
|
419
|
+
executionStart = (async () => {
|
|
420
|
+
signal?.throwIfAborted();
|
|
421
|
+
if (consumedResumeStateKey) {
|
|
422
|
+
const consumption = await consumeResumeState({
|
|
423
|
+
env: ctx.env,
|
|
424
|
+
key: consumedResumeStateKey,
|
|
425
|
+
expectedState: resumeState,
|
|
426
|
+
signal,
|
|
427
|
+
});
|
|
428
|
+
if (!consumption.consumed) {
|
|
429
|
+
throw new Error("Workflow resume state not found");
|
|
430
|
+
}
|
|
431
|
+
if (consumption.signalAbortedAfterCommit) {
|
|
432
|
+
await restoreConsumedResumeState({
|
|
433
|
+
env: ctx.env,
|
|
434
|
+
key: consumedResumeStateKey,
|
|
435
|
+
expectedState: resumeState,
|
|
436
|
+
claimId: consumption.claimId,
|
|
437
|
+
});
|
|
438
|
+
signal?.throwIfAborted();
|
|
439
|
+
}
|
|
440
|
+
signal?.throwIfAborted();
|
|
441
|
+
resumeStateConsumed = true;
|
|
442
|
+
resumedExecutionStarted = true;
|
|
443
|
+
}
|
|
444
|
+
await ctx._onExecutionStarted?.();
|
|
445
|
+
executionBoundaryStarted = true;
|
|
446
|
+
if (!signal)
|
|
447
|
+
return;
|
|
448
|
+
const onAbort = () => {
|
|
449
|
+
ctx._onExecutionInterrupted?.();
|
|
450
|
+
};
|
|
451
|
+
if (signal.aborted) {
|
|
452
|
+
onAbort();
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
456
|
+
executionSignalListeners.push({ signal, onAbort });
|
|
457
|
+
})();
|
|
458
|
+
try {
|
|
459
|
+
await executionStart;
|
|
460
|
+
}
|
|
461
|
+
catch (err) {
|
|
462
|
+
// A per-attempt timeout can abort while this invocation is only waiting
|
|
463
|
+
// to claim its resume state. Keep the successful boundary memoized, but
|
|
464
|
+
// let the workflow retry make a fresh claim after that pre-dispatch wait.
|
|
465
|
+
if (!resumeStateConsumed && !executionBoundaryStarted)
|
|
466
|
+
executionStart = undefined;
|
|
467
|
+
throw err;
|
|
468
|
+
}
|
|
469
|
+
};
|
|
372
470
|
if (resumeState?.approvalStepId && resumeState?.inputStepId) {
|
|
373
471
|
throw new Error("Invalid workflow resume state");
|
|
374
472
|
}
|
|
@@ -381,14 +479,26 @@ export async function runWorkflowFile({ filePath, args, ctx, resume, approved, r
|
|
|
381
479
|
}
|
|
382
480
|
if (cancel === true || approved === false) {
|
|
383
481
|
if (consumedResumeStateKey) {
|
|
384
|
-
await
|
|
482
|
+
const deletion = await deleteUnconsumedResumeState({
|
|
483
|
+
env: ctx.env,
|
|
484
|
+
key: consumedResumeStateKey,
|
|
485
|
+
signal: ctx.signal,
|
|
486
|
+
});
|
|
487
|
+
if (deletion !== "deleted")
|
|
488
|
+
throw new Error("Workflow resume state not found");
|
|
385
489
|
}
|
|
386
490
|
return { status: "cancelled", output: [] };
|
|
387
491
|
}
|
|
388
492
|
}
|
|
389
493
|
if (resumeState?.inputStepId && cancel === true) {
|
|
390
494
|
if (consumedResumeStateKey) {
|
|
391
|
-
await
|
|
495
|
+
const deletion = await deleteUnconsumedResumeState({
|
|
496
|
+
env: ctx.env,
|
|
497
|
+
key: consumedResumeStateKey,
|
|
498
|
+
signal: ctx.signal,
|
|
499
|
+
});
|
|
500
|
+
if (deletion !== "deleted")
|
|
501
|
+
throw new Error("Workflow resume state not found");
|
|
392
502
|
}
|
|
393
503
|
return { status: "cancelled", output: [] };
|
|
394
504
|
}
|
|
@@ -408,7 +518,7 @@ export async function runWorkflowFile({ filePath, args, ctx, resume, approved, r
|
|
|
408
518
|
const stepIndexById = new Map(steps.map((step, idx) => [step.id, idx]));
|
|
409
519
|
const results = resumeState?.steps
|
|
410
520
|
? cloneResults(resumeState.steps)
|
|
411
|
-
:
|
|
521
|
+
: createRecord();
|
|
412
522
|
const startIndex = resumeState?.resumeAtIndex ?? 0;
|
|
413
523
|
if (resumeState?.approvalStepId && typeof approved === "boolean") {
|
|
414
524
|
const previous = results[resumeState.approvalStepId] ?? { id: resumeState.approvalStepId };
|
|
@@ -459,11 +569,6 @@ export async function runWorkflowFile({ filePath, args, ctx, resume, approved, r
|
|
|
459
569
|
stepId: resumeState.inputStepId,
|
|
460
570
|
response,
|
|
461
571
|
pipelineInput: resumeState.pipelineInput,
|
|
462
|
-
onConsumed: consumedResumeStateKey
|
|
463
|
-
? async () => {
|
|
464
|
-
await deleteStateJson({ env: ctx.env, key: consumedResumeStateKey });
|
|
465
|
-
}
|
|
466
|
-
: undefined,
|
|
467
572
|
};
|
|
468
573
|
}
|
|
469
574
|
else {
|
|
@@ -492,8 +597,30 @@ export async function runWorkflowFile({ filePath, args, ctx, resume, approved, r
|
|
|
492
597
|
return dryRunWorkflow({ steps, resolvedArgs, results, startIndex, ctx });
|
|
493
598
|
}
|
|
494
599
|
const costTracker = new CostTracker(CostTracker.parsePricingFromEnv(ctx.env, ctx.stderr), ctx.stderr);
|
|
600
|
+
// A gate pauses a run, it does not reset its spend: seed this run with what the paused
|
|
601
|
+
// run recorded, so `_meta.cost` covers the whole workflow and `cost_limit` cannot be
|
|
602
|
+
// walked past one gate at a time. A replay of a call billed before the pause then stays
|
|
603
|
+
// exempt, which is what an empty ledger already means for a charge someone else settled.
|
|
604
|
+
const pausedCost = resumeState && "cost" in resumeState ? resumeState.cost : undefined;
|
|
605
|
+
costTracker.restore(pausedCost?.byStep);
|
|
606
|
+
// One ledger per run: a live call this run paid for can be recovered from the replay a
|
|
607
|
+
// retry produced, and a cache entry written by any other run has nothing to claim here.
|
|
608
|
+
const llmSpendLedger = createLlmSpendLedger(composingSpendLedger(ctx));
|
|
609
|
+
// A pipeline that paused at an `ask` gate had already paid for its LLM call without
|
|
610
|
+
// billing it, so the charge travels in the same resume state and reopens here. It is the
|
|
611
|
+
// same run continuing, which is why this cannot hand a charge to an unrelated one.
|
|
612
|
+
llmSpendLedger.restore(resumeState && "llmSpend" in resumeState ? resumeState.llmSpend : undefined);
|
|
613
|
+
// The completed steps come back as JSON, so an LLM output among them has lost everything
|
|
614
|
+
// this process attached to it. A later step that re-emits one hands on a copy that still
|
|
615
|
+
// names its call, and the restored record is what lets that copy be recognized as one the
|
|
616
|
+
// paused run already paid for rather than billed a second time on top of `cost`.
|
|
617
|
+
llmSpendLedger.restoreSettled(resumeState && "llmBilled" in resumeState ? resumeState.llmBilled : undefined);
|
|
618
|
+
// Completed results lost their symbol keys when the pause was persisted. Restore those
|
|
619
|
+
// private marks only at this trusted resume boundary; ordinary command JSON stays billable.
|
|
620
|
+
restoreLlmProvenance(results, resumeState && "llmBilled" in resumeState ? resumeState.llmBilled : undefined);
|
|
495
621
|
let lastStepId = resumeState?.inputStepId ?? findLastCompletedStepId(steps, results);
|
|
496
622
|
for (let idx = startIndex; idx < steps.length; idx++) {
|
|
623
|
+
ctx.signal?.throwIfAborted();
|
|
497
624
|
const step = steps[idx];
|
|
498
625
|
if (!evaluateCondition(step.when ?? step.condition, results)) {
|
|
499
626
|
results[step.id] = { id: step.id, skipped: true };
|
|
@@ -515,40 +642,65 @@ export async function runWorkflowFile({ filePath, args, ctx, resume, approved, r
|
|
|
515
642
|
subject,
|
|
516
643
|
maxEnvelopeBytes: resolveToolEnvelopeMaxBytes(ctx.env),
|
|
517
644
|
});
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
645
|
+
let stateKey;
|
|
646
|
+
try {
|
|
647
|
+
stateKey = await saveWorkflowResumeState(ctx.env, {
|
|
648
|
+
filePath: resolvedFilePath,
|
|
649
|
+
resumeAtIndex: idx + 1,
|
|
650
|
+
steps: results,
|
|
651
|
+
args: resolvedArgs,
|
|
652
|
+
inputStepId: step.id,
|
|
653
|
+
inputSchema: step.input.responseSchema,
|
|
654
|
+
// Preserve the full resolved subject for resume semantics; the tool
|
|
655
|
+
// envelope may contain a truncated preview to stay within size limits.
|
|
656
|
+
inputSubject: subject,
|
|
657
|
+
...(costTracker.hasUsage() ? { cost: costTracker.getSummary() } : null),
|
|
658
|
+
...(llmSpendLedger.outstanding().length
|
|
659
|
+
? { llmSpend: llmSpendLedger.outstanding() }
|
|
660
|
+
: null),
|
|
661
|
+
...(llmSpendLedger.settled().length
|
|
662
|
+
? { llmBilled: llmSpendLedger.settled() }
|
|
663
|
+
: null),
|
|
664
|
+
supersededResumeStateKeys: collectSupersededWorkflowResumeStateKeys(consumedResumeStateKey, resumeState),
|
|
665
|
+
createdAt: new Date().toISOString(),
|
|
666
|
+
}, ctx.signal);
|
|
667
|
+
const replaced = await replaceWorkflowResumeState({
|
|
668
|
+
env: ctx.env,
|
|
669
|
+
previousStateKey: consumedResumeStateKey,
|
|
670
|
+
expectedPreviousState: resumeState,
|
|
671
|
+
previousStateConsumed: resumeStateConsumed,
|
|
672
|
+
replacementStateKey: stateKey,
|
|
673
|
+
signal: ctx.signal,
|
|
674
|
+
});
|
|
675
|
+
if (!replaced) {
|
|
676
|
+
throw new Error("Workflow resume state not found");
|
|
677
|
+
}
|
|
678
|
+
const resumeToken = encodeToken({
|
|
679
|
+
protocolVersion: 1,
|
|
680
|
+
v: 1,
|
|
681
|
+
kind: "workflow-file",
|
|
682
|
+
stateKey,
|
|
683
|
+
});
|
|
684
|
+
return {
|
|
685
|
+
status: "needs_input",
|
|
686
|
+
output: [],
|
|
687
|
+
requiresInput: {
|
|
688
|
+
...inputRequest,
|
|
689
|
+
resumeToken,
|
|
690
|
+
},
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
catch (err) {
|
|
694
|
+
if (stateKey)
|
|
695
|
+
await discardWorkflowResumeState(ctx.env, stateKey);
|
|
696
|
+
throw err;
|
|
532
697
|
}
|
|
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
698
|
}
|
|
548
699
|
ctx.stdout.write(`${step.input.prompt}\n`);
|
|
549
700
|
ctx.stdout.write("Enter JSON response: ");
|
|
550
701
|
const raw = await readLineFromStream(ctx.stdin, {
|
|
551
702
|
timeoutMs: parseApprovalTimeoutMs(ctx.env),
|
|
703
|
+
signal: ctx.signal,
|
|
552
704
|
});
|
|
553
705
|
const parsed = parseResponseJson(String(raw ?? "").trim());
|
|
554
706
|
validateInputResponse({
|
|
@@ -564,130 +716,168 @@ export async function runWorkflowFile({ filePath, args, ctx, resume, approved, r
|
|
|
564
716
|
lastStepId = step.id;
|
|
565
717
|
continue;
|
|
566
718
|
}
|
|
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
719
|
const env = mergeEnv(ctx.env, workflow.env, step.env, resolvedArgs, results);
|
|
666
720
|
const cwd = resolveCwd(step.cwd ?? workflow.cwd, resolvedArgs) ?? ctx.cwd;
|
|
667
721
|
const execution = getStepExecution(step);
|
|
668
722
|
const retryConfig = resolveRetryConfig(step.retry);
|
|
669
723
|
const executeStepAttempt = async () => {
|
|
724
|
+
let attemptHadNonRetryableSideEffect = false;
|
|
725
|
+
const markNonRetryableSideEffect = () => {
|
|
726
|
+
attemptHadNonRetryableSideEffect = true;
|
|
727
|
+
ctx._onNonRetryableSideEffect?.();
|
|
728
|
+
};
|
|
729
|
+
ctx.signal?.throwIfAborted();
|
|
670
730
|
// Combine external cancellation and optional per-step timeout into one signal.
|
|
671
731
|
let stepSignal = ctx.signal;
|
|
672
732
|
let timeoutId;
|
|
733
|
+
let stepTimeoutController;
|
|
673
734
|
if (step.timeout_ms) {
|
|
674
|
-
|
|
675
|
-
timeoutId = setTimeout(() =>
|
|
735
|
+
stepTimeoutController = new AbortController();
|
|
736
|
+
timeoutId = setTimeout(() => stepTimeoutController.abort(new Error(`Step '${step.id}' timed out after ${step.timeout_ms}ms`)), step.timeout_ms);
|
|
676
737
|
stepSignal = ctx.signal
|
|
677
|
-
? AbortSignal.any([ctx.signal,
|
|
678
|
-
:
|
|
738
|
+
? AbortSignal.any([ctx.signal, stepTimeoutController.signal])
|
|
739
|
+
: stepTimeoutController.signal;
|
|
679
740
|
}
|
|
680
741
|
let result;
|
|
681
742
|
let parallelBranchResults = null;
|
|
682
743
|
try {
|
|
683
|
-
if (
|
|
744
|
+
if (typeof step.for_each === "string" && Array.isArray(step.steps)) {
|
|
745
|
+
const itemsRef = resolveInputValue(step.for_each, resolvedArgs, results);
|
|
746
|
+
if (!Array.isArray(itemsRef)) {
|
|
747
|
+
throw new Error(`Workflow step ${step.id} for_each: expected array, got ${typeof itemsRef}`);
|
|
748
|
+
}
|
|
749
|
+
const itemVar = step.item_var ?? "item";
|
|
750
|
+
const indexVar = step.index_var ?? "index";
|
|
751
|
+
const batchSize = step.batch_size ?? 1;
|
|
752
|
+
const iterationResults = [];
|
|
753
|
+
for (let itemIdx = 0; itemIdx < itemsRef.length; itemIdx++) {
|
|
754
|
+
stepSignal?.throwIfAborted();
|
|
755
|
+
if (step.pause_ms && itemIdx > 0 && itemIdx % batchSize === 0) {
|
|
756
|
+
await abortableSleep(step.pause_ms, stepSignal);
|
|
757
|
+
}
|
|
758
|
+
const item = itemsRef[itemIdx];
|
|
759
|
+
const scopedResults = createRecord(results);
|
|
760
|
+
scopedResults[itemVar] = {
|
|
761
|
+
id: itemVar,
|
|
762
|
+
json: item,
|
|
763
|
+
stdout: typeof item === "string" ? item : JSON.stringify(item),
|
|
764
|
+
};
|
|
765
|
+
scopedResults[indexVar] = {
|
|
766
|
+
id: indexVar,
|
|
767
|
+
json: itemIdx,
|
|
768
|
+
stdout: String(itemIdx),
|
|
769
|
+
};
|
|
770
|
+
for (const subStep of step.steps) {
|
|
771
|
+
stepSignal?.throwIfAborted();
|
|
772
|
+
if (!evaluateCondition(subStep.when ?? subStep.condition, scopedResults)) {
|
|
773
|
+
scopedResults[subStep.id] = { id: subStep.id, skipped: true };
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
const loopEnvBase = mergeEnv(ctx.env, workflow.env, step.env, resolvedArgs, scopedResults);
|
|
777
|
+
const subEnv = subStep.env
|
|
778
|
+
? mergeEnv(loopEnvBase, undefined, subStep.env, resolvedArgs, scopedResults)
|
|
779
|
+
: loopEnvBase;
|
|
780
|
+
const subCwd = resolveCwd(subStep.cwd ?? step.cwd ?? workflow.cwd, resolvedArgs) ?? ctx.cwd;
|
|
781
|
+
const subExecution = getStepExecution(subStep);
|
|
782
|
+
let subResult;
|
|
783
|
+
if (subExecution.kind === "shell") {
|
|
784
|
+
const command = resolveTemplate(subExecution.value, resolvedArgs, scopedResults);
|
|
785
|
+
const stdinValue = resolveShellStdin(subStep.stdin, resolvedArgs, scopedResults);
|
|
786
|
+
await markExecutionStarted(stepSignal);
|
|
787
|
+
const { stdout } = await runShellCommand({
|
|
788
|
+
command,
|
|
789
|
+
stdin: stdinValue,
|
|
790
|
+
env: subEnv,
|
|
791
|
+
cwd: subCwd,
|
|
792
|
+
signal: stepSignal,
|
|
793
|
+
forceTerminationSignal: ctx.forceTerminationSignal,
|
|
794
|
+
killSignal: () => stepTimeoutController?.signal.aborted
|
|
795
|
+
? "SIGKILL"
|
|
796
|
+
: undefined,
|
|
797
|
+
});
|
|
798
|
+
subResult = { id: subStep.id, stdout, json: parseJson(stdout) };
|
|
799
|
+
}
|
|
800
|
+
else if (subExecution.kind === "pipeline") {
|
|
801
|
+
if (!ctx.registry) {
|
|
802
|
+
throw new Error(`Workflow step ${step.id} for_each sub-step ${subStep.id} requires a command registry for pipeline execution`);
|
|
803
|
+
}
|
|
804
|
+
const pipelineText = resolveTemplate(subExecution.value, resolvedArgs, scopedResults);
|
|
805
|
+
const inputValue = resolveInputValue(subStep.stdin, resolvedArgs, scopedResults);
|
|
806
|
+
subResult = await runPipelineStep({
|
|
807
|
+
stepId: subStep.id,
|
|
808
|
+
pipelineText,
|
|
809
|
+
inputValue,
|
|
810
|
+
ctx: {
|
|
811
|
+
...ctx,
|
|
812
|
+
signal: stepSignal,
|
|
813
|
+
_onNonRetryableSideEffect: markNonRetryableSideEffect,
|
|
814
|
+
},
|
|
815
|
+
llmSpendLedger,
|
|
816
|
+
env: subEnv,
|
|
817
|
+
cwd: subCwd,
|
|
818
|
+
requestInputEnabled: false,
|
|
819
|
+
onExecutionStart: () => markExecutionStarted(stepSignal),
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
else {
|
|
823
|
+
const inputValue = resolveInputValue(subStep.stdin, resolvedArgs, scopedResults);
|
|
824
|
+
subResult = createSyntheticStepResult(subStep.id, inputValue);
|
|
825
|
+
}
|
|
826
|
+
scopedResults[subStep.id] = subResult;
|
|
827
|
+
trackStepCost(costTracker, `${step.id}.${subStep.id}`, subResult, llmSpendLedger);
|
|
828
|
+
// Before the next step runs: a charge no item carried is spend the step really made,
|
|
829
|
+
// and leaving it for the end would let `cost_limit` wave through every step after the
|
|
830
|
+
// one that blew the budget.
|
|
831
|
+
settleUnbilledCharges(costTracker, llmSpendLedger);
|
|
832
|
+
if (workflow.cost_limit) {
|
|
833
|
+
costTracker.checkLimit(workflow.cost_limit, ctx.stderr);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
const iterResult = createRecord();
|
|
837
|
+
iterResult[itemVar] = item;
|
|
838
|
+
iterResult[indexVar] = itemIdx;
|
|
839
|
+
for (const subStep of step.steps) {
|
|
840
|
+
const subResult = scopedResults[subStep.id];
|
|
841
|
+
if (subResult && !subResult.skipped) {
|
|
842
|
+
iterResult[subStep.id] =
|
|
843
|
+
subResult.json !== undefined ? subResult.json : subResult.stdout;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
iterationResults.push(Object.fromEntries(Object.entries(iterResult)));
|
|
847
|
+
}
|
|
848
|
+
result = {
|
|
849
|
+
id: step.id,
|
|
850
|
+
json: iterationResults,
|
|
851
|
+
stdout: JSON.stringify(iterationResults),
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
else if (execution.kind === "parallel") {
|
|
684
855
|
const parallel = execution.value;
|
|
685
856
|
const wait = parallel.wait ?? "all";
|
|
686
857
|
const branchAbortController = new AbortController();
|
|
687
|
-
const
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
858
|
+
const parallelTimeoutController = new AbortController();
|
|
859
|
+
const parallelSignal = stepSignal
|
|
860
|
+
? AbortSignal.any([stepSignal, parallelTimeoutController.signal])
|
|
861
|
+
: parallelTimeoutController.signal;
|
|
862
|
+
const branchSignal = AbortSignal.any([parallelSignal, branchAbortController.signal]);
|
|
863
|
+
const timeoutKillSignal = () => stepTimeoutController?.signal.aborted || parallelTimeoutController.signal.aborted
|
|
864
|
+
? "SIGKILL"
|
|
865
|
+
: undefined;
|
|
866
|
+
// `wait: "any"` keeps the first branch to answer and abandons the rest, so a
|
|
867
|
+
// losing branch spends against a buffer of its own. Only the winner's charges
|
|
868
|
+
// reach the run. `wait: "all"` keeps every branch, so those bill straight
|
|
869
|
+
// through as before.
|
|
870
|
+
const branchLedgers = new Map();
|
|
871
|
+
const ledgerFor = (branchId) => {
|
|
872
|
+
if (wait !== "any")
|
|
873
|
+
return llmSpendLedger;
|
|
874
|
+
let buffered = branchLedgers.get(branchId);
|
|
875
|
+
if (!buffered) {
|
|
876
|
+
buffered = branchSpendBuffer(llmSpendLedger);
|
|
877
|
+
branchLedgers.set(branchId, buffered);
|
|
878
|
+
}
|
|
879
|
+
return buffered;
|
|
880
|
+
};
|
|
691
881
|
const runBranch = async (branch) => {
|
|
692
882
|
const mergedBranchEnv = { ...(step.env ?? {}), ...(branch.env ?? {}) };
|
|
693
883
|
const branchEnv = mergeEnv(ctx.env, workflow.env, mergedBranchEnv, resolvedArgs, results);
|
|
@@ -701,13 +891,15 @@ export async function runWorkflowFile({ filePath, args, ctx, resume, approved, r
|
|
|
701
891
|
if (branchExec.kind === "shell") {
|
|
702
892
|
const command = resolveTemplate(branchExec.value, resolvedArgs, results);
|
|
703
893
|
const stdinValue = resolveShellStdin(branch.stdin, resolvedArgs, results);
|
|
894
|
+
await markExecutionStarted(branchSignal);
|
|
704
895
|
const { stdout } = await runShellCommand({
|
|
705
896
|
command,
|
|
706
897
|
stdin: stdinValue,
|
|
707
898
|
env: branchEnv,
|
|
708
899
|
cwd: branchCwd,
|
|
709
900
|
signal: branchSignal,
|
|
710
|
-
|
|
901
|
+
forceTerminationSignal: ctx.forceTerminationSignal,
|
|
902
|
+
killSignal: timeoutKillSignal,
|
|
711
903
|
});
|
|
712
904
|
return {
|
|
713
905
|
branchId: branch.id,
|
|
@@ -724,10 +916,16 @@ export async function runWorkflowFile({ filePath, args, ctx, resume, approved, r
|
|
|
724
916
|
stepId: branch.id,
|
|
725
917
|
pipelineText,
|
|
726
918
|
inputValue,
|
|
727
|
-
ctx: {
|
|
919
|
+
ctx: {
|
|
920
|
+
...ctx,
|
|
921
|
+
signal: branchSignal,
|
|
922
|
+
_onNonRetryableSideEffect: markNonRetryableSideEffect,
|
|
923
|
+
},
|
|
924
|
+
llmSpendLedger: ledgerFor(branch.id),
|
|
728
925
|
env: branchEnv,
|
|
729
926
|
cwd: branchCwd,
|
|
730
927
|
requestInputEnabled: false,
|
|
928
|
+
onExecutionStart: () => markExecutionStarted(branchSignal),
|
|
731
929
|
});
|
|
732
930
|
return { branchId: branch.id, result: branchResult };
|
|
733
931
|
}
|
|
@@ -737,26 +935,27 @@ export async function runWorkflowFile({ filePath, args, ctx, resume, approved, r
|
|
|
737
935
|
const timeoutPromise = parallel.timeout_ms
|
|
738
936
|
? new Promise((_resolve, reject) => {
|
|
739
937
|
parallelTimeoutId = setTimeout(() => {
|
|
740
|
-
|
|
938
|
+
parallelTimeoutController.abort(new Error(`Parallel step ${step.id} timed out after ${parallel.timeout_ms}ms`));
|
|
741
939
|
reject(new Error(`Parallel step ${step.id} timed out after ${parallel.timeout_ms}ms`));
|
|
742
940
|
}, parallel.timeout_ms);
|
|
743
941
|
})
|
|
744
942
|
: null;
|
|
943
|
+
const branchPromises = parallel.branches.map((branch) => runBranch(branch));
|
|
944
|
+
const branchesSettled = Promise.allSettled(branchPromises);
|
|
745
945
|
try {
|
|
746
946
|
if (wait === "any") {
|
|
747
|
-
const branchPromises = parallel.branches.map((branch) => runBranch(branch));
|
|
748
947
|
const winner = (await (timeoutPromise
|
|
749
948
|
? Promise.race([...branchPromises, timeoutPromise])
|
|
750
949
|
: Promise.race(branchPromises)));
|
|
751
|
-
parallelBranchResults =
|
|
752
|
-
|
|
950
|
+
parallelBranchResults = createRecord();
|
|
951
|
+
parallelBranchResults[winner.branchId] = winner.result;
|
|
952
|
+
branchLedgers.get(winner.branchId)?.release();
|
|
753
953
|
}
|
|
754
954
|
else {
|
|
755
|
-
const branchPromises = parallel.branches.map((branch) => runBranch(branch));
|
|
756
955
|
const settled = (await (timeoutPromise
|
|
757
956
|
? Promise.race([Promise.allSettled(branchPromises), timeoutPromise])
|
|
758
|
-
:
|
|
759
|
-
parallelBranchResults =
|
|
957
|
+
: branchesSettled));
|
|
958
|
+
parallelBranchResults = createRecord();
|
|
760
959
|
for (const entry of settled) {
|
|
761
960
|
if (entry.status === "rejected") {
|
|
762
961
|
throw new Error(`Parallel branch failed: ${entry.reason?.message ?? String(entry.reason)}`);
|
|
@@ -768,11 +967,18 @@ export async function runWorkflowFile({ filePath, args, ctx, resume, approved, r
|
|
|
768
967
|
finally {
|
|
769
968
|
if (parallelTimeoutId !== undefined)
|
|
770
969
|
clearTimeout(parallelTimeoutId);
|
|
970
|
+
const branchesCancelled = wait === "any" || parallelSignal.aborted;
|
|
971
|
+
if (branchesCancelled)
|
|
972
|
+
branchAbortController.abort();
|
|
973
|
+
if (branchesCancelled)
|
|
974
|
+
await waitForCancelledParallelBranches(branchesSettled);
|
|
975
|
+
else
|
|
976
|
+
await branchesSettled;
|
|
771
977
|
}
|
|
772
|
-
const merged = {}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
978
|
+
const merged = Object.fromEntries(Object.entries(parallelBranchResults ?? {}).map(([branchId, branchResult]) => [
|
|
979
|
+
branchId,
|
|
980
|
+
branchResult.json,
|
|
981
|
+
]));
|
|
776
982
|
result = {
|
|
777
983
|
id: step.id,
|
|
778
984
|
json: merged,
|
|
@@ -800,10 +1006,25 @@ export async function runWorkflowFile({ filePath, args, ctx, resume, approved, r
|
|
|
800
1006
|
const childActive = new Set(activeWorkflows);
|
|
801
1007
|
childActive.add(canonicalWorkflowPath);
|
|
802
1008
|
const subArgs = resolveWorkflowStepArgs(step.workflow_args, resolvedArgs, results);
|
|
1009
|
+
stepSignal?.throwIfAborted();
|
|
803
1010
|
const subResult = await runWorkflowFile({
|
|
804
1011
|
filePath: resolvedWorkflowPath,
|
|
805
1012
|
args: subArgs,
|
|
806
|
-
ctx: {
|
|
1013
|
+
ctx: {
|
|
1014
|
+
...ctx,
|
|
1015
|
+
env,
|
|
1016
|
+
cwd,
|
|
1017
|
+
signal: stepSignal,
|
|
1018
|
+
_activeWorkflows: childActive,
|
|
1019
|
+
_llmSpendLedger: stepScopedLedger(llmSpendLedger, step.id),
|
|
1020
|
+
_onExecutionStarted: async () => {
|
|
1021
|
+
await markExecutionStarted(stepSignal);
|
|
1022
|
+
},
|
|
1023
|
+
_onExecutionInterrupted: () => {
|
|
1024
|
+
ctx._onExecutionInterrupted?.();
|
|
1025
|
+
},
|
|
1026
|
+
_onNonRetryableSideEffect: markNonRetryableSideEffect,
|
|
1027
|
+
},
|
|
807
1028
|
});
|
|
808
1029
|
if (subResult.status === "needs_approval" || subResult.status === "needs_input") {
|
|
809
1030
|
const resumeToken = subResult.requiresApproval?.resumeToken ?? subResult.requiresInput?.resumeToken;
|
|
@@ -823,17 +1044,26 @@ export async function runWorkflowFile({ filePath, args, ctx, resume, approved, r
|
|
|
823
1044
|
const json = subResult.output.length === 1 ? subResult.output[0] : subResult.output;
|
|
824
1045
|
const stdout = subResult.output.length ? serializeValueForStdout(json) : "";
|
|
825
1046
|
result = { id: step.id, stdout, json };
|
|
1047
|
+
// A child ending in a rendered pipeline (`llm.invoke | json`) prints its items
|
|
1048
|
+
// and returns JSON parsed back out of that print, which no symbol survives.
|
|
1049
|
+
// Carry the child's own items across the composition boundary so this run
|
|
1050
|
+
// applies to them the same replay rule it applies to its own pipelines.
|
|
1051
|
+
const composedItems = pipelineSourceItems(subResult);
|
|
1052
|
+
if (composedItems)
|
|
1053
|
+
attachPipelineSourceItems(result, composedItems);
|
|
826
1054
|
}
|
|
827
1055
|
else if (execution.kind === "shell") {
|
|
828
1056
|
const command = resolveTemplate(execution.value, resolvedArgs, results);
|
|
829
1057
|
const stdinValue = resolveShellStdin(step.stdin, resolvedArgs, results);
|
|
1058
|
+
await markExecutionStarted(stepSignal);
|
|
830
1059
|
const { stdout } = await runShellCommand({
|
|
831
1060
|
command,
|
|
832
1061
|
stdin: stdinValue,
|
|
833
1062
|
env,
|
|
834
1063
|
cwd,
|
|
835
1064
|
signal: stepSignal,
|
|
836
|
-
|
|
1065
|
+
forceTerminationSignal: ctx.forceTerminationSignal,
|
|
1066
|
+
killSignal: () => stepTimeoutController?.signal.aborted ? "SIGKILL" : undefined,
|
|
837
1067
|
});
|
|
838
1068
|
result = { id: step.id, stdout, json: parseJson(stdout) };
|
|
839
1069
|
}
|
|
@@ -847,7 +1077,12 @@ export async function runWorkflowFile({ filePath, args, ctx, resume, approved, r
|
|
|
847
1077
|
stepId: step.id,
|
|
848
1078
|
pipelineText,
|
|
849
1079
|
inputValue,
|
|
850
|
-
ctx: {
|
|
1080
|
+
ctx: {
|
|
1081
|
+
...ctx,
|
|
1082
|
+
signal: stepSignal,
|
|
1083
|
+
_onNonRetryableSideEffect: markNonRetryableSideEffect,
|
|
1084
|
+
},
|
|
1085
|
+
llmSpendLedger,
|
|
851
1086
|
env,
|
|
852
1087
|
cwd,
|
|
853
1088
|
resume: resumedPipelineInput?.stepId === step.id
|
|
@@ -857,14 +1092,23 @@ export async function runWorkflowFile({ filePath, args, ctx, resume, approved, r
|
|
|
857
1092
|
onConsumed: resumedPipelineInput.onConsumed,
|
|
858
1093
|
}
|
|
859
1094
|
: undefined,
|
|
1095
|
+
onExecutionStart: () => markExecutionStarted(stepSignal),
|
|
860
1096
|
});
|
|
861
1097
|
}
|
|
862
1098
|
else {
|
|
1099
|
+
if (execution.kind !== "none")
|
|
1100
|
+
await markExecutionStarted(stepSignal);
|
|
863
1101
|
const inputValue = resolveInputValue(step.stdin, resolvedArgs, results);
|
|
864
1102
|
result = createSyntheticStepResult(step.id, inputValue);
|
|
865
1103
|
}
|
|
866
1104
|
return { result, parallelBranchResults };
|
|
867
1105
|
}
|
|
1106
|
+
catch (error) {
|
|
1107
|
+
if (attemptHadNonRetryableSideEffect) {
|
|
1108
|
+
throw markStepErrorNonRetryable(error);
|
|
1109
|
+
}
|
|
1110
|
+
throw error;
|
|
1111
|
+
}
|
|
868
1112
|
finally {
|
|
869
1113
|
if (timeoutId !== undefined)
|
|
870
1114
|
clearTimeout(timeoutId);
|
|
@@ -878,8 +1122,19 @@ export async function runWorkflowFile({ filePath, args, ctx, resume, approved, r
|
|
|
878
1122
|
signal: ctx.signal,
|
|
879
1123
|
shouldRetry: (error) => {
|
|
880
1124
|
if (error instanceof WorkflowPipelineInputSuspension ||
|
|
881
|
-
error instanceof RequestInputResumeError
|
|
1125
|
+
error instanceof RequestInputResumeError ||
|
|
1126
|
+
error instanceof CostLimitExceededError) {
|
|
1127
|
+
return false;
|
|
1128
|
+
}
|
|
1129
|
+
if (isStepErrorNonRetryable(error)) {
|
|
1130
|
+
return false;
|
|
1131
|
+
}
|
|
1132
|
+
if (ctx.signal?.aborted)
|
|
882
1133
|
return false;
|
|
1134
|
+
if (workflow.cost_limit?.action === "stop") {
|
|
1135
|
+
// Failed pipelines can discard output after the provider was paid.
|
|
1136
|
+
settleUnbilledCharges(costTracker, llmSpendLedger);
|
|
1137
|
+
costTracker.checkLimit(workflow.cost_limit, ctx.stderr);
|
|
883
1138
|
}
|
|
884
1139
|
const message = error?.message ?? String(error);
|
|
885
1140
|
return !/halted (for approval inside|before completion at) pipeline/.test(message);
|
|
@@ -902,40 +1157,67 @@ export async function runWorkflowFile({ filePath, args, ctx, resume, approved, r
|
|
|
902
1157
|
subject: err.request.subject,
|
|
903
1158
|
maxEnvelopeBytes: resolveToolEnvelopeMaxBytes(ctx.env),
|
|
904
1159
|
});
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
1160
|
+
let stateKey;
|
|
1161
|
+
try {
|
|
1162
|
+
stateKey = await saveWorkflowResumeState(ctx.env, {
|
|
1163
|
+
filePath: resolvedFilePath,
|
|
1164
|
+
resumeAtIndex: idx,
|
|
1165
|
+
steps: results,
|
|
1166
|
+
args: resolvedArgs,
|
|
1167
|
+
inputStepId: err.stepId,
|
|
1168
|
+
inputKind: "pipeline_command",
|
|
1169
|
+
inputSchema: err.request.responseSchema,
|
|
1170
|
+
inputSubject: err.request.subject,
|
|
1171
|
+
pipelineInput: err.pipelineInput,
|
|
1172
|
+
...(costTracker.hasUsage() ? { cost: costTracker.getSummary() } : null),
|
|
1173
|
+
...(llmSpendLedger.outstanding().length
|
|
1174
|
+
? { llmSpend: llmSpendLedger.outstanding() }
|
|
1175
|
+
: null),
|
|
1176
|
+
...(llmSpendLedger.settled().length
|
|
1177
|
+
? { llmBilled: llmSpendLedger.settled() }
|
|
1178
|
+
: null),
|
|
1179
|
+
supersededResumeStateKeys: collectSupersededWorkflowResumeStateKeys(consumedResumeStateKey, resumeState),
|
|
1180
|
+
createdAt: new Date().toISOString(),
|
|
1181
|
+
}, ctx.signal);
|
|
1182
|
+
const replaced = await replaceWorkflowResumeState({
|
|
1183
|
+
env: ctx.env,
|
|
1184
|
+
previousStateKey: consumedResumeStateKey,
|
|
1185
|
+
expectedPreviousState: resumeState,
|
|
1186
|
+
previousStateConsumed: resumeStateConsumed,
|
|
1187
|
+
replacementStateKey: stateKey,
|
|
1188
|
+
signal: ctx.signal,
|
|
1189
|
+
});
|
|
1190
|
+
if (!replaced) {
|
|
1191
|
+
throw new Error("Workflow resume state not found");
|
|
1192
|
+
}
|
|
1193
|
+
const resumeToken = encodeToken({
|
|
1194
|
+
protocolVersion: 1,
|
|
1195
|
+
v: 1,
|
|
1196
|
+
kind: "workflow-file",
|
|
1197
|
+
stateKey,
|
|
1198
|
+
});
|
|
1199
|
+
return {
|
|
1200
|
+
status: "needs_input",
|
|
1201
|
+
output: [],
|
|
1202
|
+
requiresInput: {
|
|
1203
|
+
...inputRequest,
|
|
1204
|
+
resumeToken,
|
|
1205
|
+
},
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
catch (stateError) {
|
|
1209
|
+
if (stateKey)
|
|
1210
|
+
await discardWorkflowResumeState(ctx.env, stateKey);
|
|
1211
|
+
throw stateError;
|
|
919
1212
|
}
|
|
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
1213
|
}
|
|
935
1214
|
if (err instanceof RequestInputResumeError) {
|
|
936
1215
|
throw err;
|
|
937
1216
|
}
|
|
938
|
-
if (
|
|
1217
|
+
if (err instanceof CostLimitExceededError) {
|
|
1218
|
+
throw err;
|
|
1219
|
+
}
|
|
1220
|
+
if (ctx.signal?.aborted) {
|
|
939
1221
|
throw err;
|
|
940
1222
|
}
|
|
941
1223
|
if (err?.message &&
|
|
@@ -956,20 +1238,36 @@ export async function runWorkflowFile({ filePath, args, ctx, resume, approved, r
|
|
|
956
1238
|
error: true,
|
|
957
1239
|
errorMessage,
|
|
958
1240
|
};
|
|
1241
|
+
// The step failed after paying a provider: a call it made before failing is spend
|
|
1242
|
+
// this run really made, and its items are gone with the failure. Settling here is
|
|
1243
|
+
// what stops a budget already blown from letting the next step run its side
|
|
1244
|
+
// effects, which is the whole point of `action: stop`.
|
|
1245
|
+
settleUnbilledCharges(costTracker, llmSpendLedger);
|
|
1246
|
+
if (workflow.cost_limit) {
|
|
1247
|
+
costTracker.checkLimit(workflow.cost_limit, ctx.stderr);
|
|
1248
|
+
}
|
|
959
1249
|
if (policy === "skip_rest") {
|
|
960
1250
|
break;
|
|
961
1251
|
}
|
|
962
1252
|
continue;
|
|
963
1253
|
}
|
|
1254
|
+
// One accounting group per step: its parallel branches and its own result settle
|
|
1255
|
+
// together, so no branch can be billed for a charge another branch is about to claim.
|
|
1256
|
+
const deferredCharges = [];
|
|
964
1257
|
if (parallelBranchResults) {
|
|
965
1258
|
for (const [branchId, branchResult] of Object.entries(parallelBranchResults)) {
|
|
966
1259
|
results[branchId] = branchResult;
|
|
967
|
-
trackStepCost(costTracker, branchId, branchResult);
|
|
1260
|
+
trackStepCost(costTracker, branchId, branchResult, llmSpendLedger, deferredCharges);
|
|
968
1261
|
}
|
|
969
1262
|
}
|
|
970
1263
|
results[step.id] = result;
|
|
971
1264
|
lastStepId = step.id;
|
|
972
|
-
trackStepCost(costTracker, step.id, result);
|
|
1265
|
+
trackStepCost(costTracker, step.id, result, llmSpendLedger, deferredCharges);
|
|
1266
|
+
settleDeferredCosts(costTracker, deferredCharges, llmSpendLedger);
|
|
1267
|
+
// Before the next step runs: a charge no item carried is spend the step really made,
|
|
1268
|
+
// and leaving it for the end would let `cost_limit` wave through every step after the
|
|
1269
|
+
// one that blew the budget.
|
|
1270
|
+
settleUnbilledCharges(costTracker, llmSpendLedger);
|
|
973
1271
|
if (workflow.cost_limit) {
|
|
974
1272
|
costTracker.checkLimit(workflow.cost_limit, ctx.stderr);
|
|
975
1273
|
}
|
|
@@ -980,45 +1278,64 @@ export async function runWorkflowFile({ filePath, args, ctx, resume, approved, r
|
|
|
980
1278
|
results[step.id].initiatedBy = approvalIdentity.initiatedBy;
|
|
981
1279
|
}
|
|
982
1280
|
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;
|
|
1281
|
+
let stateKey;
|
|
993
1282
|
try {
|
|
994
|
-
|
|
1283
|
+
stateKey = await saveWorkflowResumeState(ctx.env, {
|
|
1284
|
+
filePath: resolvedFilePath,
|
|
1285
|
+
resumeAtIndex: idx + 1,
|
|
1286
|
+
steps: results,
|
|
1287
|
+
args: resolvedArgs,
|
|
1288
|
+
approvalStepId: step.id,
|
|
1289
|
+
approvalIdentity,
|
|
1290
|
+
...(costTracker.hasUsage() ? { cost: costTracker.getSummary() } : null),
|
|
1291
|
+
...(llmSpendLedger.outstanding().length
|
|
1292
|
+
? { llmSpend: llmSpendLedger.outstanding() }
|
|
1293
|
+
: null),
|
|
1294
|
+
...(llmSpendLedger.settled().length
|
|
1295
|
+
? { llmBilled: llmSpendLedger.settled() }
|
|
1296
|
+
: null),
|
|
1297
|
+
supersededResumeStateKeys: collectSupersededWorkflowResumeStateKeys(consumedResumeStateKey, resumeState),
|
|
1298
|
+
createdAt: new Date().toISOString(),
|
|
1299
|
+
}, ctx.signal);
|
|
1300
|
+
const approvalId = await createApprovalIndex({ env: ctx.env, stateKey });
|
|
1301
|
+
ctx.signal?.throwIfAborted();
|
|
1302
|
+
const replaced = await replaceWorkflowResumeState({
|
|
1303
|
+
env: ctx.env,
|
|
1304
|
+
previousStateKey: consumedResumeStateKey,
|
|
1305
|
+
expectedPreviousState: resumeState,
|
|
1306
|
+
previousStateConsumed: resumeStateConsumed,
|
|
1307
|
+
replacementStateKey: stateKey,
|
|
1308
|
+
signal: ctx.signal,
|
|
1309
|
+
});
|
|
1310
|
+
if (!replaced) {
|
|
1311
|
+
throw new Error("Workflow resume state not found");
|
|
1312
|
+
}
|
|
1313
|
+
const resumeToken = encodeToken({
|
|
1314
|
+
protocolVersion: 1,
|
|
1315
|
+
v: 1,
|
|
1316
|
+
kind: "workflow-file",
|
|
1317
|
+
stateKey,
|
|
1318
|
+
});
|
|
1319
|
+
return {
|
|
1320
|
+
status: "needs_approval",
|
|
1321
|
+
output: [],
|
|
1322
|
+
requiresApproval: {
|
|
1323
|
+
...approval,
|
|
1324
|
+
resumeToken,
|
|
1325
|
+
...(approvalId ? { approvalId } : null),
|
|
1326
|
+
},
|
|
1327
|
+
};
|
|
995
1328
|
}
|
|
996
1329
|
catch (err) {
|
|
997
|
-
|
|
1330
|
+
if (stateKey)
|
|
1331
|
+
await discardWorkflowResumeState(ctx.env, stateKey);
|
|
998
1332
|
throw err;
|
|
999
1333
|
}
|
|
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
1334
|
}
|
|
1019
1335
|
ctx.stdout.write(`${approval.prompt} [y/N] `);
|
|
1020
1336
|
const answer = await readLineFromStream(ctx.stdin, {
|
|
1021
1337
|
timeoutMs: parseApprovalTimeoutMs(ctx.env),
|
|
1338
|
+
signal: ctx.signal,
|
|
1022
1339
|
});
|
|
1023
1340
|
if (!/^y(es)?$/i.test(String(answer).trim())) {
|
|
1024
1341
|
throw new Error("Not approved");
|
|
@@ -1034,17 +1351,81 @@ export async function runWorkflowFile({ filePath, args, ctx, resume, approved, r
|
|
|
1034
1351
|
results[step.id].approvedBy = approvedBy;
|
|
1035
1352
|
}
|
|
1036
1353
|
}
|
|
1354
|
+
// Whatever a skipped or failed step left open, and anything from a run with no step left
|
|
1355
|
+
// to settle it against. A paused run does not reach this line -- its charges travel in
|
|
1356
|
+
// the resume state instead -- so this settles only calls with nowhere left to be billed.
|
|
1357
|
+
settleUnbilledCharges(costTracker, llmSpendLedger);
|
|
1358
|
+
if (workflow.cost_limit) {
|
|
1359
|
+
costTracker.checkLimit(workflow.cost_limit, ctx.stderr);
|
|
1360
|
+
}
|
|
1037
1361
|
const output = lastStepId ? toOutputItems(results[lastStepId]) : [];
|
|
1038
1362
|
if (consumedResumeStateKey) {
|
|
1039
|
-
|
|
1363
|
+
if (resumeStateConsumed) {
|
|
1364
|
+
ctx.signal?.throwIfAborted();
|
|
1365
|
+
await deleteStateJson({
|
|
1366
|
+
env: ctx.env,
|
|
1367
|
+
key: consumedResumeStateKey,
|
|
1368
|
+
signal: ctx.signal,
|
|
1369
|
+
});
|
|
1370
|
+
ctx.signal?.throwIfAborted();
|
|
1371
|
+
}
|
|
1372
|
+
else {
|
|
1373
|
+
const deleted = await deleteResumeStateWithRollback({
|
|
1374
|
+
env: ctx.env,
|
|
1375
|
+
key: consumedResumeStateKey,
|
|
1376
|
+
expectedState: resumeState,
|
|
1377
|
+
signal: ctx.signal,
|
|
1378
|
+
});
|
|
1379
|
+
if (!deleted) {
|
|
1380
|
+
throw new Error("Workflow resume state not found");
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
await cleanupSupersededWorkflowResumeStates(ctx.env, resumeState?.supersededResumeStateKeys);
|
|
1384
|
+
}
|
|
1385
|
+
else {
|
|
1386
|
+
ctx.signal?.throwIfAborted();
|
|
1040
1387
|
}
|
|
1041
1388
|
const runResult = { status: "ok", output };
|
|
1389
|
+
// `output` is what the last step could serialize; a run composing this one also needs the
|
|
1390
|
+
// items themselves, because that is where replay provenance lives.
|
|
1391
|
+
const lastItems = lastStepId ? pipelineSourceItems(results[lastStepId]) : undefined;
|
|
1392
|
+
if (lastItems)
|
|
1393
|
+
attachPipelineSourceItems(runResult, lastItems);
|
|
1042
1394
|
if (costTracker.hasUsage()) {
|
|
1043
1395
|
runResult._meta = { cost: costTracker.getSummary() };
|
|
1044
1396
|
}
|
|
1045
1397
|
return runResult;
|
|
1046
1398
|
}
|
|
1399
|
+
catch (err) {
|
|
1400
|
+
if (resumedExecutionStarted && consumedResumeStateKey) {
|
|
1401
|
+
try {
|
|
1402
|
+
if (ctx.signal?.aborted) {
|
|
1403
|
+
await deleteStateJsonWithBoundedResumeCleanup({
|
|
1404
|
+
env: ctx.env,
|
|
1405
|
+
key: consumedResumeStateKey,
|
|
1406
|
+
});
|
|
1407
|
+
}
|
|
1408
|
+
else {
|
|
1409
|
+
await deleteStateJson({ env: ctx.env, key: consumedResumeStateKey });
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
catch {
|
|
1413
|
+
// Leave the tombstone in place if cleanup fails: it is the durable
|
|
1414
|
+
// record that prevents the consumed token from replaying an effect.
|
|
1415
|
+
}
|
|
1416
|
+
// A consumed marker remains non-replayable even when its bounded cleanup
|
|
1417
|
+
// times out, so its short approval ID can be retired independently.
|
|
1418
|
+
await cleanupApprovalIndexByStateKey({
|
|
1419
|
+
env: ctx.env,
|
|
1420
|
+
stateKey: consumedResumeStateKey,
|
|
1421
|
+
}).catch(() => { });
|
|
1422
|
+
}
|
|
1423
|
+
throw err;
|
|
1424
|
+
}
|
|
1047
1425
|
finally {
|
|
1426
|
+
for (const { signal, onAbort } of executionSignalListeners) {
|
|
1427
|
+
signal.removeEventListener("abort", onAbort);
|
|
1428
|
+
}
|
|
1048
1429
|
ctx._activeWorkflows?.delete(canonicalFilePath);
|
|
1049
1430
|
}
|
|
1050
1431
|
}
|
|
@@ -1129,7 +1510,7 @@ function dryRunWorkflow({ steps, resolvedArgs, results, startIndex, ctx, }) {
|
|
|
1129
1510
|
if (step.pause_ms)
|
|
1130
1511
|
lines.push(` pause_ms: ${step.pause_ms}`);
|
|
1131
1512
|
lines.push(` sub-steps: ${step.steps.length}`);
|
|
1132
|
-
const loopScopedResults =
|
|
1513
|
+
const loopScopedResults = createRecord(results);
|
|
1133
1514
|
loopScopedResults[dryItemVar] = { id: dryItemVar, json: { _placeholder: true } };
|
|
1134
1515
|
loopScopedResults[dryIndexVar] = { id: dryIndexVar, json: 0 };
|
|
1135
1516
|
for (let subIdx = 0; subIdx < step.steps.length; subIdx++) {
|
|
@@ -1172,6 +1553,16 @@ function dryRunWorkflow({ steps, resolvedArgs, results, startIndex, ctx, }) {
|
|
|
1172
1553
|
}
|
|
1173
1554
|
loopScopedResults[sub.id] = { id: sub.id };
|
|
1174
1555
|
}
|
|
1556
|
+
if (step.timeout_ms)
|
|
1557
|
+
lines.push(` timeout: ${step.timeout_ms}ms`);
|
|
1558
|
+
if (step.on_error && step.on_error !== "stop")
|
|
1559
|
+
lines.push(` on_error: ${step.on_error}`);
|
|
1560
|
+
if (step.retry && typeof step.retry === "object") {
|
|
1561
|
+
const rc = resolveRetryConfig(step.retry);
|
|
1562
|
+
if (rc.max > 1) {
|
|
1563
|
+
lines.push(` retry: up to ${rc.max} attempts, ${rc.backoff} backoff (base: ${rc.delay_ms}ms${rc.jitter ? ", jitter" : ""})`);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1175
1566
|
results[step.id] = { id: step.id };
|
|
1176
1567
|
continue;
|
|
1177
1568
|
}
|
|
@@ -1319,12 +1710,87 @@ export function decodeWorkflowResumePayload(payload) {
|
|
|
1319
1710
|
throw new Error("Invalid workflow token");
|
|
1320
1711
|
return data;
|
|
1321
1712
|
}
|
|
1322
|
-
async function saveWorkflowResumeState(env, state) {
|
|
1713
|
+
async function saveWorkflowResumeState(env, state, signal) {
|
|
1323
1714
|
const stateKey = `workflow_resume_${randomUUID()}`;
|
|
1324
|
-
|
|
1325
|
-
|
|
1715
|
+
try {
|
|
1716
|
+
signal?.throwIfAborted();
|
|
1717
|
+
await writeStateJson({ env, key: stateKey, value: state, signal });
|
|
1718
|
+
signal?.throwIfAborted();
|
|
1719
|
+
return stateKey;
|
|
1720
|
+
}
|
|
1721
|
+
catch (err) {
|
|
1722
|
+
if (signal?.aborted)
|
|
1723
|
+
await discardWorkflowResumeState(env, stateKey);
|
|
1724
|
+
throw err;
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
async function replaceWorkflowResumeState({ env, previousStateKey, expectedPreviousState, previousStateConsumed, replacementStateKey, signal, }) {
|
|
1728
|
+
if (!previousStateKey || previousStateKey === replacementStateKey) {
|
|
1729
|
+
signal?.throwIfAborted();
|
|
1730
|
+
return true;
|
|
1731
|
+
}
|
|
1732
|
+
if (previousStateConsumed) {
|
|
1733
|
+
signal?.throwIfAborted();
|
|
1734
|
+
// The successor is already durable. Retire the predecessor's short ID here,
|
|
1735
|
+
// rather than when the preceding unsafe execution is only being claimed.
|
|
1736
|
+
await cleanupApprovalIndexByStateKey({ env, stateKey: previousStateKey }).catch(() => { });
|
|
1737
|
+
return true;
|
|
1738
|
+
}
|
|
1739
|
+
if (!expectedPreviousState)
|
|
1740
|
+
return false;
|
|
1741
|
+
let claimId;
|
|
1742
|
+
try {
|
|
1743
|
+
const consumption = await consumeResumeState({
|
|
1744
|
+
env,
|
|
1745
|
+
key: previousStateKey,
|
|
1746
|
+
expectedState: expectedPreviousState,
|
|
1747
|
+
signal,
|
|
1748
|
+
});
|
|
1749
|
+
if (!consumption.consumed)
|
|
1750
|
+
return false;
|
|
1751
|
+
claimId = consumption.claimId;
|
|
1752
|
+
// Keep the consumed marker until terminal cleanup. It is the atomic
|
|
1753
|
+
// predecessor-to-successor handoff and prevents another resume from
|
|
1754
|
+
// creating a competing successor after this call returns.
|
|
1755
|
+
signal?.throwIfAborted();
|
|
1756
|
+
await cleanupApprovalIndexByStateKey({ env, stateKey: previousStateKey }).catch(() => { });
|
|
1757
|
+
return true;
|
|
1758
|
+
}
|
|
1759
|
+
catch (err) {
|
|
1760
|
+
if (claimId && signal?.aborted) {
|
|
1761
|
+
await restoreConsumedResumeState({
|
|
1762
|
+
env,
|
|
1763
|
+
key: previousStateKey,
|
|
1764
|
+
expectedState: expectedPreviousState,
|
|
1765
|
+
claimId,
|
|
1766
|
+
}).catch(() => { });
|
|
1767
|
+
}
|
|
1768
|
+
throw err;
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
function collectSupersededWorkflowResumeStateKeys(previousStateKey, previousState) {
|
|
1772
|
+
return [
|
|
1773
|
+
...(previousState?.supersededResumeStateKeys ?? []),
|
|
1774
|
+
...(previousStateKey ? [previousStateKey] : []),
|
|
1775
|
+
].filter((stateKey, index, all) => stateKey && all.indexOf(stateKey) === index);
|
|
1776
|
+
}
|
|
1777
|
+
async function cleanupSupersededWorkflowResumeStates(env, stateKeys) {
|
|
1778
|
+
for (const stateKey of stateKeys ?? []) {
|
|
1779
|
+
try {
|
|
1780
|
+
if (isConsumedResumeState(await readStateJsonWithLock({ env, key: stateKey }))) {
|
|
1781
|
+
await deleteStateJson({ env, key: stateKey });
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
catch {
|
|
1785
|
+
// The completed successor makes retained ancestor markers safe.
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
async function discardWorkflowResumeState(env, stateKey) {
|
|
1790
|
+
await cleanupApprovalIndexByStateKey({ env, stateKey }).catch(() => { });
|
|
1791
|
+
await deleteStateJson({ env, key: stateKey }).catch(() => { });
|
|
1326
1792
|
}
|
|
1327
|
-
function alternateWorkflowResumeStateKey(stateKey) {
|
|
1793
|
+
export function alternateWorkflowResumeStateKey(stateKey) {
|
|
1328
1794
|
if (stateKey.includes("workflow-resume_")) {
|
|
1329
1795
|
return stateKey.replace("workflow-resume_", "workflow_resume_");
|
|
1330
1796
|
}
|
|
@@ -1333,8 +1799,8 @@ function alternateWorkflowResumeStateKey(stateKey) {
|
|
|
1333
1799
|
}
|
|
1334
1800
|
return null;
|
|
1335
1801
|
}
|
|
1336
|
-
async function resolveWorkflowResumeStateKey(env, stateKey) {
|
|
1337
|
-
const stored = await
|
|
1802
|
+
async function resolveWorkflowResumeStateKey(env, stateKey, signal) {
|
|
1803
|
+
const stored = await readStateJsonWithLock({ env, key: stateKey, signal });
|
|
1338
1804
|
if (stored && typeof stored === "object") {
|
|
1339
1805
|
return stateKey;
|
|
1340
1806
|
}
|
|
@@ -1342,18 +1808,24 @@ async function resolveWorkflowResumeStateKey(env, stateKey) {
|
|
|
1342
1808
|
if (!altKey) {
|
|
1343
1809
|
return stateKey;
|
|
1344
1810
|
}
|
|
1345
|
-
const altStored = await
|
|
1811
|
+
const altStored = await readStateJsonWithLock({ env, key: altKey, signal });
|
|
1346
1812
|
if (altStored && typeof altStored === "object") {
|
|
1347
1813
|
return altKey;
|
|
1348
1814
|
}
|
|
1349
1815
|
return stateKey;
|
|
1350
1816
|
}
|
|
1351
|
-
async function loadWorkflowResumeState(env, stateKey) {
|
|
1352
|
-
let stored = await
|
|
1817
|
+
async function loadWorkflowResumeState(env, stateKey, signal) {
|
|
1818
|
+
let stored = await readStateJsonWithLock({ env, key: stateKey, signal });
|
|
1819
|
+
if (isConsumedResumeState(stored)) {
|
|
1820
|
+
throw new Error("Workflow resume state not found");
|
|
1821
|
+
}
|
|
1353
1822
|
if ((!stored || typeof stored !== "object") && typeof stateKey === "string") {
|
|
1354
1823
|
const altKey = alternateWorkflowResumeStateKey(stateKey);
|
|
1355
1824
|
if (altKey) {
|
|
1356
|
-
stored = await
|
|
1825
|
+
stored = await readStateJsonWithLock({ env, key: altKey, signal });
|
|
1826
|
+
if (isConsumedResumeState(stored)) {
|
|
1827
|
+
throw new Error("Workflow resume state not found");
|
|
1828
|
+
}
|
|
1357
1829
|
}
|
|
1358
1830
|
}
|
|
1359
1831
|
if (!stored || typeof stored !== "object") {
|
|
@@ -1368,6 +1840,11 @@ async function loadWorkflowResumeState(env, stateKey) {
|
|
|
1368
1840
|
throw new Error("Invalid workflow resume state");
|
|
1369
1841
|
if (!data.args || typeof data.args !== "object")
|
|
1370
1842
|
throw new Error("Invalid workflow resume state");
|
|
1843
|
+
if (data.supersededResumeStateKeys !== undefined &&
|
|
1844
|
+
(!Array.isArray(data.supersededResumeStateKeys) ||
|
|
1845
|
+
data.supersededResumeStateKeys.some((stateKey) => typeof stateKey !== "string"))) {
|
|
1846
|
+
throw new Error("Invalid workflow resume state");
|
|
1847
|
+
}
|
|
1371
1848
|
if (data.inputKind !== undefined &&
|
|
1372
1849
|
!["workflow_step", "pipeline_command"].includes(data.inputKind)) {
|
|
1373
1850
|
throw new Error("Invalid workflow resume state");
|
|
@@ -1418,7 +1895,7 @@ function validateWorkflowPipelineShape(pipeline) {
|
|
|
1418
1895
|
}
|
|
1419
1896
|
}
|
|
1420
1897
|
function mergeEnv(base, workflowEnv, stepEnv, args, results) {
|
|
1421
|
-
const env =
|
|
1898
|
+
const env = createRecord(base);
|
|
1422
1899
|
// Expose resolved args as env vars so shell commands can safely reference them
|
|
1423
1900
|
// without embedding raw values into the command string.
|
|
1424
1901
|
// Example: $LOBSTER_ARG_TEXT
|
|
@@ -1449,7 +1926,14 @@ function normalizeArgEnvKey(key) {
|
|
|
1449
1926
|
return null;
|
|
1450
1927
|
// Keep it predictable for shells: uppercase and [A-Z0-9_]
|
|
1451
1928
|
const up = trimmed.toUpperCase();
|
|
1452
|
-
|
|
1929
|
+
let normalized = up.replace(/[^A-Z0-9]+/g, "_");
|
|
1930
|
+
let start = 0;
|
|
1931
|
+
while (normalized[start] === "_")
|
|
1932
|
+
start++;
|
|
1933
|
+
let end = normalized.length;
|
|
1934
|
+
while (end > start && normalized[end - 1] === "_")
|
|
1935
|
+
end--;
|
|
1936
|
+
normalized = normalized.slice(start, end);
|
|
1453
1937
|
return normalized || null;
|
|
1454
1938
|
}
|
|
1455
1939
|
function resolveCwd(cwd, args) {
|
|
@@ -1478,8 +1962,8 @@ function resolveTemplate(input, args, results) {
|
|
|
1478
1962
|
}
|
|
1479
1963
|
function resolveWorkflowStepArgs(workflowArgs, parentArgs, results) {
|
|
1480
1964
|
if (!workflowArgs)
|
|
1481
|
-
return
|
|
1482
|
-
const resolved =
|
|
1965
|
+
return createRecord();
|
|
1966
|
+
const resolved = createRecord();
|
|
1483
1967
|
for (const [key, value] of Object.entries(workflowArgs)) {
|
|
1484
1968
|
if (typeof value === "string") {
|
|
1485
1969
|
resolved[key] = resolveTemplate(value, parentArgs, results);
|
|
@@ -1492,7 +1976,7 @@ function resolveWorkflowStepArgs(workflowArgs, parentArgs, results) {
|
|
|
1492
1976
|
}
|
|
1493
1977
|
function resolveArgsTemplate(input, args) {
|
|
1494
1978
|
return input.replace(/\$\{([A-Za-z0-9_-]+)\}/g, (match, key) => {
|
|
1495
|
-
if (key
|
|
1979
|
+
if (Object.hasOwn(args, key))
|
|
1496
1980
|
return String(args[key]);
|
|
1497
1981
|
return match;
|
|
1498
1982
|
});
|
|
@@ -1671,21 +2155,222 @@ function parseBoolLike(value) {
|
|
|
1671
2155
|
return false;
|
|
1672
2156
|
return undefined;
|
|
1673
2157
|
}
|
|
1674
|
-
|
|
1675
|
-
|
|
2158
|
+
// The objects a step's pipeline produced, held apart from the serializable step result.
|
|
2159
|
+
// A rendered pipeline such as `llm.invoke | json` prints its items and returns an empty
|
|
2160
|
+
// stream, so the step's `json` is parsed back out of stdout and only carries what JSON can
|
|
2161
|
+
// express. A symbol key survives neither `JSON.stringify` nor `JSON.parse`, so this holds
|
|
2162
|
+
// exactly the items this process made, and no workflow input can put one here.
|
|
2163
|
+
const PIPELINE_SOURCE_ITEMS = Symbol("lobster.workflow.pipelineSourceItems");
|
|
2164
|
+
function attachPipelineSourceItems(target, items) {
|
|
2165
|
+
Object.defineProperty(target, PIPELINE_SOURCE_ITEMS, {
|
|
2166
|
+
value: items,
|
|
2167
|
+
enumerable: false,
|
|
2168
|
+
configurable: true,
|
|
2169
|
+
});
|
|
2170
|
+
return target;
|
|
2171
|
+
}
|
|
2172
|
+
function pipelineSourceItems(target) {
|
|
2173
|
+
if (!target)
|
|
2174
|
+
return undefined;
|
|
2175
|
+
const items = target[PIPELINE_SOURCE_ITEMS];
|
|
2176
|
+
return Array.isArray(items) ? items : undefined;
|
|
2177
|
+
}
|
|
2178
|
+
// The ledger of the run that invoked this one as a `workflow:` step. A composed run bills its
|
|
2179
|
+
// own steps and the composing run bills the output handed back to it, so a live call has to be
|
|
2180
|
+
// open in both ledgers for each of them to count it once. Read defensively: this arrives
|
|
2181
|
+
// through the same context object a caller supplies.
|
|
2182
|
+
function composingSpendLedger(ctx) {
|
|
2183
|
+
const ledger = ctx._llmSpendLedger;
|
|
2184
|
+
if (!ledger || typeof ledger !== "object")
|
|
2185
|
+
return null;
|
|
2186
|
+
return typeof ledger.record === "function" && typeof ledger.claim === "function" ? ledger : null;
|
|
2187
|
+
}
|
|
2188
|
+
/**
|
|
2189
|
+
* One `wait: "any"` branch's view of the run ledger. The race returns as soon as a branch
|
|
2190
|
+
* answers and leaves the others running, so a losing branch can pay a provider long after the
|
|
2191
|
+
* step settled. Writing that charge straight to the run ledger makes the workflow's own total
|
|
2192
|
+
* depend on how long the run happened to live afterwards: land before the run ends and the
|
|
2193
|
+
* discarded branch's spend is billed, land after and it is billed nowhere.
|
|
2194
|
+
*
|
|
2195
|
+
* So a branch opens its charges here instead, and only the winner releases them into the run.
|
|
2196
|
+
* The output of a losing branch is thrown away; its accounting goes with it. Reads pass
|
|
2197
|
+
* through, because they settle charges the run opened before the race and are made by the
|
|
2198
|
+
* workflow after a branch has returned, never from inside one.
|
|
2199
|
+
*/
|
|
2200
|
+
function branchSpendBuffer(ledger) {
|
|
2201
|
+
const held = [];
|
|
2202
|
+
let released = false;
|
|
2203
|
+
return {
|
|
2204
|
+
record: (cacheKey, charge) => {
|
|
2205
|
+
if (released)
|
|
2206
|
+
ledger.record(cacheKey, charge);
|
|
2207
|
+
else
|
|
2208
|
+
held.push({ cacheKey, charge });
|
|
2209
|
+
},
|
|
2210
|
+
claim: (cacheKey, cost) => ledger.claim(cacheKey, cost),
|
|
2211
|
+
billCopy: (cacheKey, model, usage) => ledger.billCopy(cacheKey, model, usage),
|
|
2212
|
+
outstanding: () => ledger.outstanding(),
|
|
2213
|
+
restore: (charges) => ledger.restore(charges),
|
|
2214
|
+
settled: () => ledger.settled(),
|
|
2215
|
+
restoreSettled: (charges) => ledger.restoreSettled(charges),
|
|
2216
|
+
release: () => {
|
|
2217
|
+
if (released)
|
|
2218
|
+
return;
|
|
2219
|
+
released = true;
|
|
2220
|
+
for (const { cacheKey, charge } of held)
|
|
2221
|
+
ledger.record(cacheKey, charge);
|
|
2222
|
+
held.length = 0;
|
|
2223
|
+
},
|
|
2224
|
+
};
|
|
2225
|
+
}
|
|
2226
|
+
/**
|
|
2227
|
+
* The run ledger seen by one step, so a charge opened inside it knows where it came from. A
|
|
2228
|
+
* composed run's charges reach the composing run through this too, which is why the step id is
|
|
2229
|
+
* overwritten rather than kept: in the parent's totals the spend belongs to the `workflow:`
|
|
2230
|
+
* step, not to whatever the child called its own.
|
|
2231
|
+
*/
|
|
2232
|
+
function stepScopedLedger(ledger, stepId) {
|
|
2233
|
+
return {
|
|
2234
|
+
record: (cacheKey, charge) => ledger.record(cacheKey, { ...charge, stepId }),
|
|
2235
|
+
claim: (cacheKey, cost) => ledger.claim(cacheKey, cost),
|
|
2236
|
+
billCopy: (cacheKey, model, usage) => ledger.billCopy(cacheKey, model, usage),
|
|
2237
|
+
outstanding: () => ledger.outstanding(),
|
|
2238
|
+
restore: (charges) => ledger.restore(charges),
|
|
2239
|
+
settled: () => ledger.settled(),
|
|
2240
|
+
restoreSettled: (charges) => ledger.restoreSettled(charges),
|
|
2241
|
+
};
|
|
2242
|
+
}
|
|
2243
|
+
/**
|
|
2244
|
+
* Bills the calls that ended the run unclaimed. An item is the usual carrier of usage, but a
|
|
2245
|
+
* renderer can consume the pipeline before a later stage emits items of its own, an `ask` gate
|
|
2246
|
+
* can swallow the only item, and a composed run can end on a step that carries none. The
|
|
2247
|
+
* charge outlives all three, and dropping it would leave real spend out of `_meta.cost` and
|
|
2248
|
+
* out of `cost_limit`.
|
|
2249
|
+
*/
|
|
2250
|
+
function settleUnbilledCharges(costTracker, llmSpendLedger) {
|
|
2251
|
+
for (const charge of llmSpendLedger.outstanding()) {
|
|
2252
|
+
if (!charge.usage)
|
|
2253
|
+
continue;
|
|
2254
|
+
if (!llmSpendLedger.claim(charge.cacheKey))
|
|
2255
|
+
continue;
|
|
2256
|
+
costTracker.recordUsage(charge.stepId ?? "llm", charge.model ?? null, charge.usage);
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
function trackStepCost(costTracker, stepId, result, llmSpendLedger, deferredCharges) {
|
|
2260
|
+
// Bill the items the pipeline produced rather than the JSON re-read from its stdout: a
|
|
2261
|
+
// renderer serializes them, and replay provenance does not survive that round trip. Steps
|
|
2262
|
+
// without a pipeline (shell commands) have no such items and are read from `json` as before.
|
|
2263
|
+
const json = pipelineSourceItems(result) ?? result.json;
|
|
1676
2264
|
if (!json || typeof json !== "object")
|
|
1677
2265
|
return;
|
|
2266
|
+
// Replays wait for the live results accounted with them. Branches of one parallel step are
|
|
2267
|
+
// billed in declaration order, so a cached replay declared before an identical `--refresh`
|
|
2268
|
+
// branch would otherwise claim the charge that branch had just opened — and the live branch,
|
|
2269
|
+
// billed whatever its own claim returns, would be billed for it too. One provider call, two
|
|
2270
|
+
// charges, and a `cost_limit` tripped on spend that never happened.
|
|
2271
|
+
const deferred = deferredCharges ?? [];
|
|
1678
2272
|
const items = Array.isArray(json) ? json : [json];
|
|
1679
2273
|
for (const item of items) {
|
|
1680
2274
|
if (!item || typeof item !== "object")
|
|
1681
2275
|
continue;
|
|
1682
|
-
const
|
|
2276
|
+
const record = item;
|
|
2277
|
+
const usage = record.usage;
|
|
1683
2278
|
if (!usage || typeof usage !== "object")
|
|
1684
2279
|
continue;
|
|
1685
|
-
const modelValue =
|
|
2280
|
+
const modelValue = record.model;
|
|
1686
2281
|
const model = typeof modelValue === "string" ? modelValue : null;
|
|
2282
|
+
// Provenance the LLM commands attach in-process, never a field in the JSON: a step that
|
|
2283
|
+
// prints a replay-shaped object stays billed, so `cost_limit` cannot be evaded. A
|
|
2284
|
+
// projection can drop the item's own mark while carrying its usage record across, so the
|
|
2285
|
+
// record is read too — both marks are symbol keys built in this process.
|
|
2286
|
+
const provenance = llmProvenanceOf(record) ?? llmProvenanceOf(usage);
|
|
2287
|
+
if (provenance) {
|
|
2288
|
+
if (provenance.replayed) {
|
|
2289
|
+
deferred.push({
|
|
2290
|
+
kind: "replay",
|
|
2291
|
+
stepId,
|
|
2292
|
+
cacheKey: provenance.cacheKey,
|
|
2293
|
+
model,
|
|
2294
|
+
usage: usage,
|
|
2295
|
+
});
|
|
2296
|
+
continue;
|
|
2297
|
+
}
|
|
2298
|
+
// Settles the charge this live call opened -- the one that cost what this item cost,
|
|
2299
|
+
// not merely the first under the key. A step retried after an attempt that failed
|
|
2300
|
+
// after paying leaves more than one charge open, and they did not cost the same.
|
|
2301
|
+
const settled = llmSpendLedger.claim(provenance.cacheKey, {
|
|
2302
|
+
model,
|
|
2303
|
+
usage: usage,
|
|
2304
|
+
});
|
|
2305
|
+
// Bill what the provider was really asked for, not what the item says by the time it
|
|
2306
|
+
// gets here. A pipeline can drop the model on the way -- `| pick usage` hands on the
|
|
2307
|
+
// marked usage record without it -- or rewrite it, and the step is then priced at the
|
|
2308
|
+
// wrong rate or at nothing at all while `cost_limit` reads the difference as room left.
|
|
2309
|
+
// The charge the call opened is the same witness a replay is already billed from. A
|
|
2310
|
+
// charge restored from resume state written before it carried a cost falls back.
|
|
2311
|
+
if (settled?.usage) {
|
|
2312
|
+
costTracker.recordUsage(stepId, settled.model ?? null, settled.usage);
|
|
2313
|
+
continue;
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
else {
|
|
2317
|
+
// No mark, but numbers: a stage that JSON round-trips the stream hands on a copy that
|
|
2318
|
+
// kept `model` and `usage` and lost everything this process attached -- sometimes the
|
|
2319
|
+
// cache key with it. Held back like a replay, and for the same reason: a copy that
|
|
2320
|
+
// lost its key is read by cost alone, and until the marked results have settled their
|
|
2321
|
+
// own charges an identical parallel call's charge is still open to be mistaken for
|
|
2322
|
+
// the one this copy came from.
|
|
2323
|
+
deferred.push({
|
|
2324
|
+
kind: "copy",
|
|
2325
|
+
stepId,
|
|
2326
|
+
cacheKey: typeof record.cacheKey === "string" && record.cacheKey ? record.cacheKey : null,
|
|
2327
|
+
model,
|
|
2328
|
+
usage: usage,
|
|
2329
|
+
});
|
|
2330
|
+
continue;
|
|
2331
|
+
}
|
|
1687
2332
|
costTracker.recordUsage(stepId, model, usage);
|
|
1688
2333
|
}
|
|
2334
|
+
if (!deferredCharges)
|
|
2335
|
+
settleDeferredCosts(costTracker, deferred, llmSpendLedger);
|
|
2336
|
+
}
|
|
2337
|
+
/**
|
|
2338
|
+
* Bills what a step held back, now that every marked live result accounted alongside them has
|
|
2339
|
+
* settled its own charge. Replays go first: one carries the mark of the call it stands in for,
|
|
2340
|
+
* which is better evidence than the cost a copy carries on its own.
|
|
2341
|
+
*
|
|
2342
|
+
* A replay carries the usage of the call that produced it, so counting it again charges a run
|
|
2343
|
+
* for tokens no provider was asked to spend. It is billed only when the call it replays is
|
|
2344
|
+
* still unpaid for: a step is costed once it succeeds, so a step that failed after its LLM
|
|
2345
|
+
* call and was retried leaves its real spend riding on the replay the retry produced.
|
|
2346
|
+
*
|
|
2347
|
+
* A copy is billed either way — it is a record of a call, and nothing in a JSON payload can be
|
|
2348
|
+
* trusted to say otherwise — but the ledger still decides whether it settles a charge, so the
|
|
2349
|
+
* step settlement does not add a second record of the same call.
|
|
2350
|
+
*/
|
|
2351
|
+
function settleDeferredCosts(costTracker, deferred, llmSpendLedger) {
|
|
2352
|
+
for (const replay of deferred) {
|
|
2353
|
+
if (replay.kind !== "replay" || replay.cacheKey === null)
|
|
2354
|
+
continue;
|
|
2355
|
+
const settled = llmSpendLedger.claim(replay.cacheKey);
|
|
2356
|
+
if (!settled)
|
|
2357
|
+
continue;
|
|
2358
|
+
// Bill what the call this settles actually cost, not what the replay carries. Identical
|
|
2359
|
+
// calls that raced on a cold cache are separate charges under one key, and every replay
|
|
2360
|
+
// of them repeats whichever single answer was stored -- so costing each replay by its
|
|
2361
|
+
// own copy reports all of those calls at the price of the one that won the cache write.
|
|
2362
|
+
// A charge restored from resume state written before it carried a cost falls back.
|
|
2363
|
+
const cost = typeof settled === "object" && settled.usage ? settled : null;
|
|
2364
|
+
costTracker.recordUsage(replay.stepId, cost ? (cost.model ?? null) : replay.model, cost?.usage ?? replay.usage);
|
|
2365
|
+
}
|
|
2366
|
+
for (const copy of deferred) {
|
|
2367
|
+
if (copy.kind !== "copy")
|
|
2368
|
+
continue;
|
|
2369
|
+
if (llmSpendLedger.billCopy(copy.cacheKey, copy.model, copy.usage)) {
|
|
2370
|
+
costTracker.recordUsage(copy.stepId, copy.model, copy.usage);
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
deferred.length = 0;
|
|
1689
2374
|
}
|
|
1690
2375
|
function parseJson(stdout) {
|
|
1691
2376
|
const trimmed = stdout.trim();
|
|
@@ -1713,7 +2398,7 @@ function toOutputItems(result) {
|
|
|
1713
2398
|
return [];
|
|
1714
2399
|
}
|
|
1715
2400
|
function cloneResults(results) {
|
|
1716
|
-
const out =
|
|
2401
|
+
const out = createRecord();
|
|
1717
2402
|
for (const [key, value] of Object.entries(results)) {
|
|
1718
2403
|
out[key] = { ...value };
|
|
1719
2404
|
}
|
|
@@ -2141,39 +2826,22 @@ function parseQuotedConditionString(expression, startIndex, quoteChar) {
|
|
|
2141
2826
|
}
|
|
2142
2827
|
throw new Error(`Unsupported condition: ${expression}`);
|
|
2143
2828
|
}
|
|
2144
|
-
async function runShellCommand({ command, stdin, env, cwd, signal, killSignal, }) {
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
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
|
-
});
|
|
2829
|
+
async function runShellCommand({ command, stdin, env, cwd, signal, forceTerminationSignal, killSignal, }) {
|
|
2830
|
+
signal?.throwIfAborted();
|
|
2831
|
+
signal?.throwIfAborted();
|
|
2832
|
+
const { stdout, stderr, code } = await runAbortableProcess({
|
|
2833
|
+
shellCommand: command,
|
|
2834
|
+
env,
|
|
2835
|
+
cwd,
|
|
2836
|
+
stdin,
|
|
2837
|
+
signal,
|
|
2838
|
+
forceTerminationSignal,
|
|
2839
|
+
killSignal,
|
|
2840
|
+
notFoundMessage: "workflow shell not found; check LOBSTER_SHELL or ComSpec",
|
|
2176
2841
|
});
|
|
2842
|
+
if (code === 0)
|
|
2843
|
+
return { stdout, stderr };
|
|
2844
|
+
throw new Error(`workflow command failed (${code}): ${stderr.trim() || stdout.trim() || command}`);
|
|
2177
2845
|
}
|
|
2178
2846
|
function getStepExecution(step) {
|
|
2179
2847
|
if (step.parallel && typeof step.parallel === "object" && !Array.isArray(step.parallel)) {
|
|
@@ -2191,7 +2859,7 @@ function getStepExecution(step) {
|
|
|
2191
2859
|
}
|
|
2192
2860
|
return { kind: "none" };
|
|
2193
2861
|
}
|
|
2194
|
-
async function runPipelineStep({ stepId, pipelineText, inputValue, ctx, env, cwd, resume, requestInputEnabled = true, }) {
|
|
2862
|
+
async function runPipelineStep({ stepId, pipelineText, inputValue, ctx, llmSpendLedger, env, cwd, resume, requestInputEnabled = true, onExecutionStart, }) {
|
|
2195
2863
|
let pipeline;
|
|
2196
2864
|
try {
|
|
2197
2865
|
const currentPipeline = parsePipeline(pipelineText);
|
|
@@ -2228,7 +2896,11 @@ async function runPipelineStep({ stepId, pipelineText, inputValue, ctx, env, cwd
|
|
|
2228
2896
|
mode: ctx.mode,
|
|
2229
2897
|
cwd,
|
|
2230
2898
|
signal: ctx.signal,
|
|
2899
|
+
forceTerminationSignal: ctx.forceTerminationSignal,
|
|
2900
|
+
haltAfterStageOnAbort: true,
|
|
2231
2901
|
llmAdapters: ctx.llmAdapters,
|
|
2902
|
+
// Scoped so a charge nothing bills can still name the step that opened it.
|
|
2903
|
+
llmSpendLedger: llmSpendLedger ? stepScopedLedger(llmSpendLedger, stepId) : undefined,
|
|
2232
2904
|
input: resume ? resume.pipelineInput.items : inputValueToPipelineItems(inputValue),
|
|
2233
2905
|
requestInputEnabled,
|
|
2234
2906
|
requestInputResume: resume
|
|
@@ -2238,8 +2910,11 @@ async function runPipelineStep({ stepId, pipelineText, inputValue, ctx, env, cwd
|
|
|
2238
2910
|
onConsumed: resume.onConsumed,
|
|
2239
2911
|
}
|
|
2240
2912
|
: undefined,
|
|
2913
|
+
onExecutionStart,
|
|
2914
|
+
onNonRetryableSideEffect: ctx._onNonRetryableSideEffect,
|
|
2241
2915
|
});
|
|
2242
2916
|
stdout.end();
|
|
2917
|
+
ctx.signal?.throwIfAborted();
|
|
2243
2918
|
if (result.halted) {
|
|
2244
2919
|
const haltedName = result.haltedAt?.stage?.name ?? "unknown";
|
|
2245
2920
|
if (result.items.length === 1 && result.items[0]?.type === "approval_request") {
|
|
@@ -2273,11 +2948,16 @@ async function runPipelineStep({ stepId, pipelineText, inputValue, ctx, env, cwd
|
|
|
2273
2948
|
? result.items[0]
|
|
2274
2949
|
: result.items
|
|
2275
2950
|
: parseJson(renderedStdout);
|
|
2276
|
-
|
|
2951
|
+
const stepResult = {
|
|
2277
2952
|
id: stepId,
|
|
2278
2953
|
stdout: normalizedStdout,
|
|
2279
2954
|
json,
|
|
2280
2955
|
};
|
|
2956
|
+
// Only when a renderer consumed the pipeline: otherwise `json` already is those items.
|
|
2957
|
+
if (!result.items.length && result.renderedItems.length) {
|
|
2958
|
+
attachPipelineSourceItems(stepResult, result.renderedItems);
|
|
2959
|
+
}
|
|
2960
|
+
return stepResult;
|
|
2281
2961
|
}
|
|
2282
2962
|
function createSyntheticStepResult(stepId, value) {
|
|
2283
2963
|
if (value === null || value === undefined) {
|