@openclaw/lobster 2026.9.3 → 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 -0
- package/package.json +10 -7
|
@@ -2,8 +2,10 @@ import path from "node:path";
|
|
|
2
2
|
import { promises as fsp } from "node:fs";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { Ajv } from "ajv";
|
|
5
|
-
import {
|
|
5
|
+
import { billableTokens } from "../../core/cost_tracker.js";
|
|
6
|
+
import { diffAndStore, ensureDirectory, atomicWriteWasPublished, isJsonSyntaxError, readStateJsonWithLock, stableStringify, withFileLock, writeFileAtomic, } from "../../state/store.js";
|
|
6
7
|
import { createCompileCached } from "../../validation.js";
|
|
8
|
+
import { httpResponseLimitFromEnv, readResponseTextCapped } from "../../read_response_text.js";
|
|
7
9
|
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
8
10
|
const compileCachedLocal = createCompileCached(ajv);
|
|
9
11
|
const artifactSchema = {
|
|
@@ -89,6 +91,369 @@ const validatePayload = ajv.compile(payloadSchema);
|
|
|
89
91
|
const validateResponseEnvelope = ajv.compile(responseSchema);
|
|
90
92
|
const DEFAULT_MAX_VALIDATION_RETRIES = 1;
|
|
91
93
|
const STATE_VERSION = 1;
|
|
94
|
+
// Identity version of the response cache key. See computeCacheKey.
|
|
95
|
+
const CACHE_KEY_VERSION = 2;
|
|
96
|
+
// Provenance for an item this module emitted: which stored answer it belongs to, and whether
|
|
97
|
+
// it came from a provider call or from run state / the response cache.
|
|
98
|
+
// A symbol key cannot come out of `JSON.parse`, so the JSON a workflow step reads from a
|
|
99
|
+
// command's stdout can never carry it: only objects built here, in this process, take part in
|
|
100
|
+
// the replay exemption in workflow cost accounting.
|
|
101
|
+
const LLM_PROVENANCE = Symbol("lobster.llm.provenance");
|
|
102
|
+
/**
|
|
103
|
+
* Stamps provenance on a value this module is emitting. The property is enumerable so a
|
|
104
|
+
* downstream `{ ...value }` keeps it, and `JSON.stringify` ignores symbol keys, so serialized
|
|
105
|
+
* output — including what is written to the cache — is unchanged.
|
|
106
|
+
*/
|
|
107
|
+
function markProvenance(value, provenance) {
|
|
108
|
+
return Object.defineProperty(value, LLM_PROVENANCE, {
|
|
109
|
+
value: provenance,
|
|
110
|
+
enumerable: true,
|
|
111
|
+
configurable: true,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Marks an item this module is emitting, and its usage record with it. A projection such as
|
|
116
|
+
* `pick model,usage` builds a new object out of named fields, so the item's own mark does not
|
|
117
|
+
* reach the consumer — but the usage record crosses by reference, and the usage record is what
|
|
118
|
+
* gets billed. Marking both means provenance survives an in-process projection without the
|
|
119
|
+
* consumer having to recognize every command that can build one.
|
|
120
|
+
*/
|
|
121
|
+
function markLlmItem(item, provenance) {
|
|
122
|
+
if (item.usage && typeof item.usage === "object") {
|
|
123
|
+
markProvenance(item.usage, provenance);
|
|
124
|
+
}
|
|
125
|
+
return markProvenance(item, provenance);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The provenance of an item — or of a usage record — this module produced in the current
|
|
129
|
+
* process, and null for anything else. The public `replayed` field is deliberately not
|
|
130
|
+
* consulted: any command can print it beside a real `usage` object, and honoring that would
|
|
131
|
+
* drop real spend from `_meta.cost` and `cost_limit`.
|
|
132
|
+
*/
|
|
133
|
+
export function llmProvenanceOf(value) {
|
|
134
|
+
if (!value || typeof value !== "object")
|
|
135
|
+
return null;
|
|
136
|
+
const provenance = value[LLM_PROVENANCE];
|
|
137
|
+
if (!provenance || typeof provenance !== "object")
|
|
138
|
+
return null;
|
|
139
|
+
const { cacheKey, replayed } = provenance;
|
|
140
|
+
if (typeof cacheKey !== "string" || typeof replayed !== "boolean")
|
|
141
|
+
return null;
|
|
142
|
+
return { cacheKey, replayed };
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Marks `target` — a structurally identical value rebuilt from `source`'s own JSON — as standing
|
|
146
|
+
* in for whatever calls `source` was marked with.
|
|
147
|
+
*
|
|
148
|
+
* The marks are deliberately not in that JSON, and re-deriving them from it would mean trusting a
|
|
149
|
+
* payload, which the accounting must never do. Carrying them is a different act: the caller holds
|
|
150
|
+
* both values in this process and knows one was built from the other, which nothing on disk can
|
|
151
|
+
* claim for itself.
|
|
152
|
+
*
|
|
153
|
+
* What is carried is always a replay, whatever the source was. A value read back out of storage
|
|
154
|
+
* re-emits an answer that already exists; the call behind it was made once, and this is not that
|
|
155
|
+
* call happening again.
|
|
156
|
+
*/
|
|
157
|
+
export function carryLlmProvenance(source, target) {
|
|
158
|
+
if (!source || typeof source !== "object" || !target || typeof target !== "object")
|
|
159
|
+
return;
|
|
160
|
+
const provenance = llmProvenanceOf(source);
|
|
161
|
+
if (provenance)
|
|
162
|
+
markProvenance(target, { cacheKey: provenance.cacheKey, replayed: true });
|
|
163
|
+
if (Array.isArray(source) || Array.isArray(target)) {
|
|
164
|
+
if (!Array.isArray(source) || !Array.isArray(target))
|
|
165
|
+
return;
|
|
166
|
+
for (let index = 0; index < Math.min(source.length, target.length); index++) {
|
|
167
|
+
carryLlmProvenance(source[index], target[index]);
|
|
168
|
+
}
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
for (const key of Object.keys(source)) {
|
|
172
|
+
carryLlmProvenance(source[key], target[key]);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Restores private replay provenance on completed step results loaded from Lobster's own
|
|
177
|
+
* workflow-resume state. Resume state is the narrow trusted boundary: ordinary command JSON
|
|
178
|
+
* never reaches this helper, so copying a public cache key cannot suppress its usage record.
|
|
179
|
+
*/
|
|
180
|
+
export function restoreLlmProvenance(target, charges) {
|
|
181
|
+
if (!target || typeof target !== "object" || !Array.isArray(charges))
|
|
182
|
+
return;
|
|
183
|
+
const settled = charges.filter((charge) => charge &&
|
|
184
|
+
typeof charge === "object" &&
|
|
185
|
+
typeof charge.cacheKey === "string" &&
|
|
186
|
+
charge.cacheKey &&
|
|
187
|
+
charge.usage &&
|
|
188
|
+
typeof charge.usage === "object");
|
|
189
|
+
if (!settled.length)
|
|
190
|
+
return;
|
|
191
|
+
const visit = (value) => {
|
|
192
|
+
if (!value || typeof value !== "object")
|
|
193
|
+
return;
|
|
194
|
+
if (Array.isArray(value)) {
|
|
195
|
+
for (const item of value)
|
|
196
|
+
visit(item);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const record = value;
|
|
200
|
+
const cacheKey = typeof record.cacheKey === "string" ? record.cacheKey : null;
|
|
201
|
+
const model = typeof record.model === "string" ? record.model : null;
|
|
202
|
+
const usage = record.usage;
|
|
203
|
+
if (cacheKey && usage && typeof usage === "object") {
|
|
204
|
+
const matches = settled.some((charge) => charge.cacheKey === cacheKey &&
|
|
205
|
+
(charge.model ?? null) === model &&
|
|
206
|
+
sameBillableUsage(charge.usage, usage));
|
|
207
|
+
if (matches) {
|
|
208
|
+
const provenance = { cacheKey, replayed: true };
|
|
209
|
+
markProvenance(usage, provenance);
|
|
210
|
+
markProvenance(record, provenance);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
for (const child of Object.values(record))
|
|
214
|
+
visit(child);
|
|
215
|
+
};
|
|
216
|
+
visit(target);
|
|
217
|
+
}
|
|
218
|
+
// A call nothing ever bills — a step that failed outright — leaves its key behind, so the
|
|
219
|
+
// oldest are dropped to keep a long run's ledger bounded.
|
|
220
|
+
const MAX_UNBILLED_LIVE_INVOCATIONS = 256;
|
|
221
|
+
/**
|
|
222
|
+
* Creates the ledger for a single run. The run that paid for a call is the only one that can
|
|
223
|
+
* recover its charge, because no other run holds this ledger: a workflow reusing a cache entry
|
|
224
|
+
* written by an earlier run, or by an SDK caller outside cost accounting, finds nothing to
|
|
225
|
+
* claim and is billed nothing. A live call made without a ledger in `ctx` opens no charge.
|
|
226
|
+
*
|
|
227
|
+
* A run composed by another one — a `workflow:` step — passes the composing run's ledger as
|
|
228
|
+
* `parent`. Both bill the same answer at their own boundary: the child bills its step, the
|
|
229
|
+
* parent bills the output handed back to it. So a charge is opened in both and each settles
|
|
230
|
+
* its own copy exactly once, while a replay neither of them paid for still claims nothing.
|
|
231
|
+
*/
|
|
232
|
+
export function createLlmSpendLedger(parent) {
|
|
233
|
+
// Outstanding charges per cache key, held one entry per call rather than as a count: two
|
|
234
|
+
// identical calls that race on a cold cache are two provider charges under one key, the
|
|
235
|
+
// replays that later stand in for them have to be able to settle both, and each carries the
|
|
236
|
+
// cost of the call that opened it.
|
|
237
|
+
const unbilled = new Map();
|
|
238
|
+
// Charges this run has already accounted for. A copy of an item that lost its mark can turn
|
|
239
|
+
// up in any later step, and the only way to tell it from a call nobody has billed yet is to
|
|
240
|
+
// remember what has been billed. Bounded like the open charges, oldest key first.
|
|
241
|
+
const billed = new Map();
|
|
242
|
+
function settle(cacheKey, charge) {
|
|
243
|
+
const seen = billed.get(cacheKey) ?? [];
|
|
244
|
+
seen.push(charge ?? {});
|
|
245
|
+
billed.set(cacheKey, seen);
|
|
246
|
+
for (const oldest of billed.keys()) {
|
|
247
|
+
if (billed.size <= MAX_UNBILLED_LIVE_INVOCATIONS)
|
|
248
|
+
break;
|
|
249
|
+
billed.delete(oldest);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
record(cacheKey, charge) {
|
|
254
|
+
if (!cacheKey)
|
|
255
|
+
return;
|
|
256
|
+
parent?.record(cacheKey, charge);
|
|
257
|
+
const open = unbilled.get(cacheKey) ?? [];
|
|
258
|
+
open.push({ ...charge });
|
|
259
|
+
unbilled.set(cacheKey, open);
|
|
260
|
+
for (const oldest of unbilled.keys()) {
|
|
261
|
+
if (unbilled.size <= MAX_UNBILLED_LIVE_INVOCATIONS)
|
|
262
|
+
break;
|
|
263
|
+
unbilled.delete(oldest);
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
/**
|
|
267
|
+
* Settles one outstanding charge and hands back what that call cost, or null for the
|
|
268
|
+
* caller that must not bill anything. A live item and every replay of it draw on the same
|
|
269
|
+
* charges, so a provider call is billed exactly once however many steps re-emit its
|
|
270
|
+
* answer — and N calls under one key can be billed N times, never fewer.
|
|
271
|
+
*
|
|
272
|
+
* The cost comes back because a replay is not a reliable witness of it: identical calls
|
|
273
|
+
* that raced on a cold cache each paid their own way, and every replay of them carries
|
|
274
|
+
* whichever single answer was stored.
|
|
275
|
+
*/
|
|
276
|
+
claim(cacheKey, cost) {
|
|
277
|
+
const open = unbilled.get(cacheKey);
|
|
278
|
+
if (!open?.length)
|
|
279
|
+
return null;
|
|
280
|
+
// A caller that knows what its own call cost settles that call's charge. A step
|
|
281
|
+
// retried after an attempt that failed *after* paying has more than one charge under
|
|
282
|
+
// the key, and they did not cost the same: settling the wrong one leaves the other to
|
|
283
|
+
// be billed at this call's price instead of its own.
|
|
284
|
+
const own = cost
|
|
285
|
+
? open.findIndex((charge) => (charge.model ?? null) === (cost.model ?? null) &&
|
|
286
|
+
sameBillableUsage(charge.usage, cost.usage))
|
|
287
|
+
: -1;
|
|
288
|
+
// Otherwise: a charge that records no cost settles nothing anyone can bill, so it
|
|
289
|
+
// must not be the one handed to a caller with a real call to account for. Charges
|
|
290
|
+
// restored from resume state written before they carried a cost are the ones this
|
|
291
|
+
// can be.
|
|
292
|
+
const index = own >= 0
|
|
293
|
+
? own
|
|
294
|
+
: Math.max(open.findIndex((charge) => charge.usage !== undefined), 0);
|
|
295
|
+
const [charge] = open.splice(index, 1);
|
|
296
|
+
if (!open.length)
|
|
297
|
+
unbilled.delete(cacheKey);
|
|
298
|
+
settle(cacheKey, charge);
|
|
299
|
+
return charge ?? {};
|
|
300
|
+
},
|
|
301
|
+
/**
|
|
302
|
+
* Whether an item carrying a call's numbers but not its in-process mark should be billed.
|
|
303
|
+
* A matching open charge is settled so the same provider call is not recorded again by
|
|
304
|
+
* end-of-step cleanup. Once no charge is open, however, public fields can never suppress
|
|
305
|
+
* usage: only the private provenance symbol can prove an item is a replay.
|
|
306
|
+
*/
|
|
307
|
+
billCopy(cacheKey, model, usage) {
|
|
308
|
+
const isSameCall = (charge) => (charge.model ?? null) === model && sameBillableUsage(charge.usage, usage);
|
|
309
|
+
// A copy that still names a cache key is read against that key alone: the key is
|
|
310
|
+
// evidence of which call it came from, and honoring it keeps one call's copy from
|
|
311
|
+
// settling another call's charge. A transform can emit `{ model, usage }` and drop
|
|
312
|
+
// the key with the symbols, leaving the cost as the only evidence there is.
|
|
313
|
+
for (const key of cacheKey === null ? [...unbilled.keys()] : [cacheKey]) {
|
|
314
|
+
const open = unbilled.get(key);
|
|
315
|
+
const index = open?.findIndex(isSameCall) ?? -1;
|
|
316
|
+
if (!open || index < 0)
|
|
317
|
+
continue;
|
|
318
|
+
settle(key, open.splice(index, 1)[0]);
|
|
319
|
+
if (!open.length)
|
|
320
|
+
unbilled.delete(key);
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
return true;
|
|
324
|
+
},
|
|
325
|
+
outstanding() {
|
|
326
|
+
const charges = [];
|
|
327
|
+
for (const [cacheKey, open] of unbilled) {
|
|
328
|
+
for (const charge of open)
|
|
329
|
+
charges.push({ cacheKey, count: 1, ...charge });
|
|
330
|
+
}
|
|
331
|
+
return charges;
|
|
332
|
+
},
|
|
333
|
+
/**
|
|
334
|
+
* Reopens charges a paused run had not billed. A pipeline can suspend mid-step — at an
|
|
335
|
+
* `ask` gate — after its LLM call has been paid for but before the step succeeded, so
|
|
336
|
+
* without this the charge would exist in no run: the paused one never billed it, and the
|
|
337
|
+
* resumed one would exempt the replay that stands in for it.
|
|
338
|
+
*/
|
|
339
|
+
restore(charges) {
|
|
340
|
+
if (!Array.isArray(charges))
|
|
341
|
+
return;
|
|
342
|
+
for (const charge of charges) {
|
|
343
|
+
if (!charge || typeof charge !== "object")
|
|
344
|
+
continue;
|
|
345
|
+
if (typeof charge.cacheKey !== "string" || !charge.cacheKey)
|
|
346
|
+
continue;
|
|
347
|
+
const count = Math.floor(Number(charge.count ?? 0));
|
|
348
|
+
if (!Number.isFinite(count) || count < 1)
|
|
349
|
+
continue;
|
|
350
|
+
// State written before this field existed carries no cost, and a hand-edited file
|
|
351
|
+
// should not be able to invent one: only a plain object of numbers is taken.
|
|
352
|
+
const { cacheKey, count: _count, ...cost } = charge;
|
|
353
|
+
const restored = sanitizeChargeCost(cost);
|
|
354
|
+
for (let i = 0; i < count; i++)
|
|
355
|
+
this.record(cacheKey, restored);
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
/**
|
|
359
|
+
* The calls this run has already billed. A run that pauses carries this the way it carries
|
|
360
|
+
* what it spent: the total says how much, and this says which calls it was for.
|
|
361
|
+
*/
|
|
362
|
+
settled() {
|
|
363
|
+
const charges = [];
|
|
364
|
+
for (const [cacheKey, seen] of billed) {
|
|
365
|
+
for (const charge of seen)
|
|
366
|
+
charges.push({ cacheKey, count: 1, ...charge });
|
|
367
|
+
}
|
|
368
|
+
return charges;
|
|
369
|
+
},
|
|
370
|
+
/**
|
|
371
|
+
* Restores what a paused run had billed, so the two halves of its accounting agree after
|
|
372
|
+
* the pause. `cost` brings the money back; without this the run that resumes has no record
|
|
373
|
+
* of what the money was for, and a later step that re-emits a completed LLM output — `head`
|
|
374
|
+
* over `$live.json` — hands on a copy the fresh ledger has never heard of. It is billed on
|
|
375
|
+
* top of the restored total: one provider call, twice in `_meta.cost` and against
|
|
376
|
+
* `cost_limit`.
|
|
377
|
+
*
|
|
378
|
+
* A settled charge is only ever read against a cache key, so what this restores can excuse
|
|
379
|
+
* a copy of a named call and nothing else. It cannot touch a live call — those settle out
|
|
380
|
+
* of the open charges — and the spend it is reconstructing is already in the `cost` this
|
|
381
|
+
* same state carries.
|
|
382
|
+
*/
|
|
383
|
+
restoreSettled(charges) {
|
|
384
|
+
if (!Array.isArray(charges))
|
|
385
|
+
return;
|
|
386
|
+
for (const charge of charges) {
|
|
387
|
+
if (!charge || typeof charge !== "object")
|
|
388
|
+
continue;
|
|
389
|
+
if (typeof charge.cacheKey !== "string" || !charge.cacheKey)
|
|
390
|
+
continue;
|
|
391
|
+
const count = Math.floor(Number(charge.count ?? 0));
|
|
392
|
+
if (!Number.isFinite(count) || count < 1)
|
|
393
|
+
continue;
|
|
394
|
+
const { cacheKey, count: _count, ...cost } = charge;
|
|
395
|
+
const restored = sanitizeChargeCost(cost);
|
|
396
|
+
for (let i = 0; i < count; i++)
|
|
397
|
+
settle(cacheKey, restored);
|
|
398
|
+
}
|
|
399
|
+
},
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Whether two usage records bill the same, asked of the accounting that bills them. Comparing
|
|
404
|
+
* the records themselves would answer a different question twice over: a live item's usage
|
|
405
|
+
* carries this module's provenance symbol, which a copy that went through JSON never can, and a
|
|
406
|
+
* stage that rebuilds an item can drop or recompute a field nothing is charged for.
|
|
407
|
+
*/
|
|
408
|
+
function sameBillableUsage(left, right) {
|
|
409
|
+
if (!left || typeof left !== "object" || !right || typeof right !== "object")
|
|
410
|
+
return false;
|
|
411
|
+
const billed = billableTokens(left);
|
|
412
|
+
const other = billableTokens(right);
|
|
413
|
+
return billed.inputTokens === other.inputTokens && billed.outputTokens === other.outputTokens;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Opens a charge for a live call, and only for one that costs something. An answer that reports
|
|
417
|
+
* no usage is billed nowhere however many times it is replayed, so a charge for it would sit in
|
|
418
|
+
* the ledger with nothing to settle it -- and would be handed to the next caller with a real
|
|
419
|
+
* call to account for, leaving that one's charge open to be billed a second time.
|
|
420
|
+
*/
|
|
421
|
+
function recordLiveCharge(ctx, cacheKey, item) {
|
|
422
|
+
const cost = chargeCostOf(item);
|
|
423
|
+
if (!cost)
|
|
424
|
+
return;
|
|
425
|
+
ledgerFrom(ctx)?.record(cacheKey, cost);
|
|
426
|
+
}
|
|
427
|
+
/** The cost of a live call, read from the item this module just built for it. */
|
|
428
|
+
function chargeCostOf(item) {
|
|
429
|
+
const usage = item?.usage;
|
|
430
|
+
if (!usage || typeof usage !== "object")
|
|
431
|
+
return undefined;
|
|
432
|
+
return { model: typeof item?.model === "string" ? item.model : null, usage };
|
|
433
|
+
}
|
|
434
|
+
function sanitizeChargeCost(cost) {
|
|
435
|
+
const usage = cost.usage;
|
|
436
|
+
if (!usage || typeof usage !== "object" || Array.isArray(usage))
|
|
437
|
+
return {};
|
|
438
|
+
const numbers = {};
|
|
439
|
+
for (const [key, value] of Object.entries(usage)) {
|
|
440
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
441
|
+
numbers[key] = value;
|
|
442
|
+
}
|
|
443
|
+
if (!Object.keys(numbers).length)
|
|
444
|
+
return {};
|
|
445
|
+
return {
|
|
446
|
+
...(typeof cost.stepId === "string" ? { stepId: cost.stepId } : null),
|
|
447
|
+
model: typeof cost.model === "string" ? cost.model : null,
|
|
448
|
+
usage: numbers,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
function ledgerFrom(ctx) {
|
|
452
|
+
const ledger = ctx?.llmSpendLedger;
|
|
453
|
+
if (!ledger || typeof ledger !== "object")
|
|
454
|
+
return null;
|
|
455
|
+
return typeof ledger.record === "function" && typeof ledger.claim === "function" ? ledger : null;
|
|
456
|
+
}
|
|
92
457
|
export const llmInvokeCommand = createLlmInvokeCommand({
|
|
93
458
|
name: "llm.invoke",
|
|
94
459
|
itemKind: "llm.invoke",
|
|
@@ -164,7 +529,7 @@ export function createLlmInvokeCommand(config) {
|
|
|
164
529
|
"schema-version": { type: "string", description: "Logical schema version for caching" },
|
|
165
530
|
"max-validation-retries": {
|
|
166
531
|
type: "number",
|
|
167
|
-
description: "
|
|
532
|
+
description: "Extra model calls allowed after the first when schema validation fails (default 1)",
|
|
168
533
|
},
|
|
169
534
|
temperature: { type: "number", description: "Sampling temperature" },
|
|
170
535
|
"max-output-tokens": { type: "number", description: "Max completion tokens" },
|
|
@@ -190,7 +555,8 @@ export function createLlmInvokeCommand(config) {
|
|
|
190
555
|
"Features:",
|
|
191
556
|
" - Typed payload validation before invoking the adapter.",
|
|
192
557
|
" - Run-state + file cache so resumes do not re-call the LLM.",
|
|
193
|
-
" - Optional JSON-schema enforcement
|
|
558
|
+
" - Optional JSON-schema enforcement, retried at most --max-validation-retries times",
|
|
559
|
+
" after the first call.",
|
|
194
560
|
"",
|
|
195
561
|
"Config:",
|
|
196
562
|
...config.helpConfig.map((line) => ` - ${line}`),
|
|
@@ -204,6 +570,10 @@ export function createLlmInvokeCommand(config) {
|
|
|
204
570
|
}
|
|
205
571
|
async function runLlmInvoke({ input, args, ctx, config, }) {
|
|
206
572
|
const env = ctx.env ?? process.env;
|
|
573
|
+
const signal = ctx?.signal;
|
|
574
|
+
// Run-state and cache hits return before any adapter call, so a cancelled run
|
|
575
|
+
// would otherwise still finish as a success.
|
|
576
|
+
throwIfCancelled(signal);
|
|
207
577
|
const provider = resolveProvider(args, env, config.defaultProvider, ctx);
|
|
208
578
|
const adapter = resolveAdapter({ provider, env, args, config, ctx });
|
|
209
579
|
const prompt = extractPrompt(args);
|
|
@@ -236,6 +606,9 @@ async function runLlmInvoke({ input, args, ctx, config, }) {
|
|
|
236
606
|
const inputArtifacts = [];
|
|
237
607
|
for await (const item of input)
|
|
238
608
|
inputArtifacts.push(item);
|
|
609
|
+
// Draining pipeline input waits on the upstream step, so a timeout can fire
|
|
610
|
+
// here. Re-check before the reuse lookups below can answer with a success.
|
|
611
|
+
throwIfCancelled(signal);
|
|
239
612
|
const normalizedArtifacts = [...inputArtifacts, ...providedArtifacts].map(normalizeArtifact);
|
|
240
613
|
const artifactHashes = normalizedArtifacts.map(hashArtifact);
|
|
241
614
|
const cacheKey = computeCacheKey({
|
|
@@ -245,21 +618,29 @@ async function runLlmInvoke({ input, args, ctx, config, }) {
|
|
|
245
618
|
schemaVersion,
|
|
246
619
|
artifactHashes,
|
|
247
620
|
outputSchema: userOutputSchema,
|
|
621
|
+
temperature,
|
|
622
|
+
maxOutputTokens,
|
|
248
623
|
});
|
|
249
624
|
if (stateKey && !forceRefresh) {
|
|
250
|
-
const stored = await readReusableLlmState(env, stateKey);
|
|
625
|
+
const stored = await readReusableLlmState(env, stateKey, ctx.signal);
|
|
626
|
+
// Reading run state is I/O of unbounded duration; a signal that aborted
|
|
627
|
+
// during it must not be overtaken by the replay below.
|
|
628
|
+
throwIfCancelled(signal);
|
|
251
629
|
const reused = pickReusableState(stored, cacheKey, config.stateType);
|
|
252
630
|
if (reused) {
|
|
631
|
+
const replay = { cacheKey, replayed: true };
|
|
253
632
|
return {
|
|
254
|
-
output: streamOf(reused.items.map((item) => ({ ...item, source: "run_state", cached: true }))),
|
|
633
|
+
output: streamOf(reused.items.map((item) => markLlmItem({ ...item, source: "run_state", cached: true, replayed: true }, replay))),
|
|
255
634
|
};
|
|
256
635
|
}
|
|
257
636
|
}
|
|
258
637
|
if (!disableCache && !forceRefresh) {
|
|
259
|
-
const cache = await readCacheEntry(env, cacheKey, config.cacheNamespace);
|
|
638
|
+
const cache = await readCacheEntry(env, cacheKey, config.cacheNamespace, ctx.signal);
|
|
639
|
+
throwIfCancelled(signal);
|
|
260
640
|
if (cache) {
|
|
641
|
+
const replay = { cacheKey, replayed: true };
|
|
261
642
|
return {
|
|
262
|
-
output: streamOf(cache.items.map((item) => ({ ...item, source: "cache", cached: true }))),
|
|
643
|
+
output: streamOf(cache.items.map((item) => markLlmItem({ ...item, source: "cache", cached: true, replayed: true }, replay))),
|
|
263
644
|
};
|
|
264
645
|
}
|
|
265
646
|
}
|
|
@@ -286,6 +667,7 @@ async function runLlmInvoke({ input, args, ctx, config, }) {
|
|
|
286
667
|
let attempt = 0;
|
|
287
668
|
let lastValidationErrors = [];
|
|
288
669
|
while (true) {
|
|
670
|
+
throwIfCancelled(signal);
|
|
289
671
|
attempt += 1;
|
|
290
672
|
if (attempt > 1) {
|
|
291
673
|
payload.retryContext = {
|
|
@@ -298,9 +680,16 @@ async function runLlmInvoke({ input, args, ctx, config, }) {
|
|
|
298
680
|
}
|
|
299
681
|
let responseEnvelope;
|
|
300
682
|
try {
|
|
301
|
-
|
|
683
|
+
ctx.signal?.throwIfAborted();
|
|
684
|
+
responseEnvelope = await abortable(adapter.invoke({ env, args, payload, signal: ctx.signal }), signal);
|
|
685
|
+
ctx.signal?.throwIfAborted();
|
|
302
686
|
}
|
|
303
687
|
catch (err) {
|
|
688
|
+
// Cancellation is the caller's error, not an adapter failure: surface it
|
|
689
|
+
// as an abort so workflow timeout and abort handling still recognizes it,
|
|
690
|
+
// rather than wrapping it in a "request failed" adapter error.
|
|
691
|
+
if (signal?.aborted)
|
|
692
|
+
throw asCancellation(err, signal);
|
|
304
693
|
throw new Error(`${config.name} request failed: ${err?.message ?? String(err)}`);
|
|
305
694
|
}
|
|
306
695
|
if (!validateResponseEnvelope(responseEnvelope)) {
|
|
@@ -319,37 +708,112 @@ async function runLlmInvoke({ input, args, ctx, config, }) {
|
|
|
319
708
|
attempt,
|
|
320
709
|
itemKind: config.itemKind,
|
|
321
710
|
});
|
|
711
|
+
const live = { cacheKey, replayed: false };
|
|
712
|
+
for (const item of normalized)
|
|
713
|
+
markLlmItem(item, live);
|
|
714
|
+
// The provider has answered and been paid, so the charge is opened here rather than at
|
|
715
|
+
// either of the returns below. An attempt the local validator rejects never reaches one
|
|
716
|
+
// of them -- it goes round the loop and asks again -- and its call was as real as the one
|
|
717
|
+
// that eventually satisfies the schema. Opening it here also puts it before the writes
|
|
718
|
+
// that store the answer: either can fail once run state already holds a replayable copy,
|
|
719
|
+
// and the retry that replays it must still find a charge to settle.
|
|
720
|
+
recordLiveCharge(ctx, cacheKey, normalized[0]);
|
|
322
721
|
if (!validator) {
|
|
722
|
+
ctx.signal?.throwIfAborted();
|
|
323
723
|
await persistOutputs({
|
|
324
724
|
env,
|
|
325
725
|
stateKey,
|
|
326
726
|
cacheKey,
|
|
327
727
|
items: normalized,
|
|
328
728
|
stateType: config.stateType,
|
|
729
|
+
signal: ctx.signal,
|
|
730
|
+
afterStore: disableCache
|
|
731
|
+
? undefined
|
|
732
|
+
: () => writeCacheEntry(env, cacheKey, normalized, config.cacheNamespace, ctx.signal),
|
|
329
733
|
});
|
|
330
|
-
if (!disableCache)
|
|
331
|
-
await writeCacheEntry(env, cacheKey, normalized, config.cacheNamespace);
|
|
332
734
|
return { output: streamOf(normalized) };
|
|
333
735
|
}
|
|
334
736
|
const structured = normalized[0]?.output?.data ?? null;
|
|
335
737
|
if (validator(structured)) {
|
|
738
|
+
ctx.signal?.throwIfAborted();
|
|
336
739
|
await persistOutputs({
|
|
337
740
|
env,
|
|
338
741
|
stateKey,
|
|
339
742
|
cacheKey,
|
|
340
743
|
items: normalized,
|
|
341
744
|
stateType: config.stateType,
|
|
745
|
+
signal: ctx.signal,
|
|
746
|
+
afterStore: disableCache
|
|
747
|
+
? undefined
|
|
748
|
+
: () => writeCacheEntry(env, cacheKey, normalized, config.cacheNamespace, ctx.signal),
|
|
342
749
|
});
|
|
343
|
-
if (!disableCache)
|
|
344
|
-
await writeCacheEntry(env, cacheKey, normalized, config.cacheNamespace);
|
|
345
750
|
return { output: streamOf(normalized) };
|
|
346
751
|
}
|
|
347
752
|
lastValidationErrors = collectAjvErrors(validator.errors);
|
|
348
|
-
if (attempt > maxValidationRetries
|
|
753
|
+
if (attempt > maxValidationRetries) {
|
|
349
754
|
throw new Error(`${config.name} output failed schema validation: ${lastValidationErrors.join("; ")}`);
|
|
350
755
|
}
|
|
351
756
|
}
|
|
352
757
|
}
|
|
758
|
+
/**
|
|
759
|
+
* Build the error a cancelled run rejects with.
|
|
760
|
+
*
|
|
761
|
+
* The workflow runner only treats an error named `AbortError` or coded
|
|
762
|
+
* `ABORT_ERR` as cancellation; everything else follows the step's retry and
|
|
763
|
+
* `on_error` policy. A host may abort with any reason it likes, so passing
|
|
764
|
+
* `signal.reason` straight through means `controller.abort(new Error("stop"))`
|
|
765
|
+
* leaves a cancelled step looking like an ordinary failure -- retried, or
|
|
766
|
+
* swallowed by `on_error: continue` and reported as a successful run. Keep the
|
|
767
|
+
* host's message and hang its reason off `cause`, but mark the rejection so the
|
|
768
|
+
* runner recognizes it.
|
|
769
|
+
*/
|
|
770
|
+
function cancellationError(signal) {
|
|
771
|
+
const reason = signal.reason;
|
|
772
|
+
if (reason === undefined || reason === null) {
|
|
773
|
+
return new DOMException("The operation was aborted.", "AbortError");
|
|
774
|
+
}
|
|
775
|
+
if (reason?.name === "AbortError" || reason?.code === "ABORT_ERR")
|
|
776
|
+
return reason;
|
|
777
|
+
const message = typeof reason?.message === "string" && reason.message ? reason.message : String(reason);
|
|
778
|
+
const error = new Error(message, { cause: reason });
|
|
779
|
+
error.name = "AbortError";
|
|
780
|
+
error.code = "ABORT_ERR";
|
|
781
|
+
return error;
|
|
782
|
+
}
|
|
783
|
+
/** Rethrow `err` when it already reads as cancellation, else the run's reason. */
|
|
784
|
+
function asCancellation(err, signal) {
|
|
785
|
+
if (err?.name === "AbortError" || err?.code === "ABORT_ERR")
|
|
786
|
+
return err;
|
|
787
|
+
return cancellationError(signal);
|
|
788
|
+
}
|
|
789
|
+
function throwIfCancelled(signal) {
|
|
790
|
+
if (signal?.aborted)
|
|
791
|
+
throw cancellationError(signal);
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Reject as soon as the run is cancelled instead of waiting for `promise`.
|
|
795
|
+
* HTTP adapters are cancelled at the socket, but an injected `ctx.llmAdapters`
|
|
796
|
+
* adapter may ignore `ctx.signal` entirely; without this, one of those keeps a
|
|
797
|
+
* timed-out step waiting for as long as it likes.
|
|
798
|
+
*/
|
|
799
|
+
function abortable(promise, signal) {
|
|
800
|
+
if (!signal)
|
|
801
|
+
return promise;
|
|
802
|
+
return new Promise((resolve, reject) => {
|
|
803
|
+
const onAbort = () => reject(cancellationError(signal));
|
|
804
|
+
// Observe `promise` before anything can settle the wrapper, including the
|
|
805
|
+
// already-aborted case below. An adapter can cancel the run from inside its own
|
|
806
|
+
// `invoke` and reject afterwards; rejecting here without watching that promise
|
|
807
|
+
// leaves the rejection unhandled, which ends the process under Node's default
|
|
808
|
+
// handling -- long after this step was cancelled cleanly.
|
|
809
|
+
promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
|
|
810
|
+
if (signal.aborted) {
|
|
811
|
+
onAbort();
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
815
|
+
});
|
|
816
|
+
}
|
|
353
817
|
function resolveProvider(args, env, defaultProvider, ctx) {
|
|
354
818
|
const explicit = String(args.provider ?? env.LOBSTER_LLM_PROVIDER ?? "")
|
|
355
819
|
.trim()
|
|
@@ -385,11 +849,12 @@ function resolveAdapter({ provider, env, args, config, ctx, }) {
|
|
|
385
849
|
return {
|
|
386
850
|
provider,
|
|
387
851
|
source: typeof direct === "function" ? provider : (direct.source ?? provider),
|
|
388
|
-
async invoke({ payload }) {
|
|
389
|
-
return invoke({ env, args, payload, ctx });
|
|
852
|
+
async invoke({ payload, signal }) {
|
|
853
|
+
return invoke({ env, args, payload, ctx, signal });
|
|
390
854
|
},
|
|
391
855
|
};
|
|
392
856
|
}
|
|
857
|
+
const maxResponseBytes = httpResponseLimitFromEnv(env);
|
|
393
858
|
if (provider === "openclaw") {
|
|
394
859
|
const openclawUrl = String(env.OPENCLAW_URL ?? env.CLAWD_URL ?? "").trim();
|
|
395
860
|
if (!openclawUrl) {
|
|
@@ -400,8 +865,8 @@ function resolveAdapter({ provider, env, args, config, ctx, }) {
|
|
|
400
865
|
return {
|
|
401
866
|
provider,
|
|
402
867
|
source: config.sourceForProvider?.(provider) ?? "openclaw",
|
|
403
|
-
async invoke({ payload }) {
|
|
404
|
-
return invokeOpenClawAdapter({ endpoint, token, payload });
|
|
868
|
+
async invoke({ payload, signal }) {
|
|
869
|
+
return invokeOpenClawAdapter({ endpoint, token, payload, signal, maxResponseBytes });
|
|
405
870
|
},
|
|
406
871
|
};
|
|
407
872
|
}
|
|
@@ -414,8 +879,14 @@ function resolveAdapter({ provider, env, args, config, ctx, }) {
|
|
|
414
879
|
return {
|
|
415
880
|
provider,
|
|
416
881
|
source: config.sourceForProvider?.(provider) ?? "pi",
|
|
417
|
-
async invoke({ payload }) {
|
|
418
|
-
return invokeHttpAdapter({
|
|
882
|
+
async invoke({ payload, signal }) {
|
|
883
|
+
return invokeHttpAdapter({
|
|
884
|
+
endpoint: buildAdapterEndpoint(adapterUrl),
|
|
885
|
+
maxResponseBytes,
|
|
886
|
+
token,
|
|
887
|
+
payload,
|
|
888
|
+
signal,
|
|
889
|
+
});
|
|
419
890
|
},
|
|
420
891
|
};
|
|
421
892
|
}
|
|
@@ -427,8 +898,14 @@ function resolveAdapter({ provider, env, args, config, ctx, }) {
|
|
|
427
898
|
return {
|
|
428
899
|
provider,
|
|
429
900
|
source: config.sourceForProvider?.(provider) ?? "http",
|
|
430
|
-
async invoke({ payload }) {
|
|
431
|
-
return invokeHttpAdapter({
|
|
901
|
+
async invoke({ payload, signal }) {
|
|
902
|
+
return invokeHttpAdapter({
|
|
903
|
+
endpoint: buildAdapterEndpoint(adapterUrl),
|
|
904
|
+
maxResponseBytes,
|
|
905
|
+
token,
|
|
906
|
+
payload,
|
|
907
|
+
signal,
|
|
908
|
+
});
|
|
432
909
|
},
|
|
433
910
|
};
|
|
434
911
|
}
|
|
@@ -451,9 +928,10 @@ function buildAdapterEndpoint(rawUrl) {
|
|
|
451
928
|
}
|
|
452
929
|
return endpoint;
|
|
453
930
|
}
|
|
454
|
-
async function invokeOpenClawAdapter({ endpoint, token, payload, }) {
|
|
931
|
+
async function invokeOpenClawAdapter({ endpoint, token, payload, signal, maxResponseBytes, }) {
|
|
455
932
|
const res = await fetch(endpoint, {
|
|
456
933
|
method: "POST",
|
|
934
|
+
signal,
|
|
457
935
|
headers: {
|
|
458
936
|
"content-type": "application/json",
|
|
459
937
|
...(token ? { authorization: `Bearer ${token}` } : null),
|
|
@@ -464,7 +942,7 @@ async function invokeOpenClawAdapter({ endpoint, token, payload, }) {
|
|
|
464
942
|
args: payload,
|
|
465
943
|
}),
|
|
466
944
|
});
|
|
467
|
-
const text = await res
|
|
945
|
+
const text = await readResponseTextCapped(res, maxResponseBytes);
|
|
468
946
|
if (!res.ok) {
|
|
469
947
|
throw new Error(`${res.status} ${res.statusText}: ${text.slice(0, 400)}`);
|
|
470
948
|
}
|
|
@@ -488,16 +966,17 @@ async function invokeOpenClawAdapter({ endpoint, token, payload, }) {
|
|
|
488
966
|
}
|
|
489
967
|
return { ok: true, result: parsed };
|
|
490
968
|
}
|
|
491
|
-
async function invokeHttpAdapter({ endpoint, token, payload, }) {
|
|
969
|
+
async function invokeHttpAdapter({ endpoint, token, payload, signal, maxResponseBytes, }) {
|
|
492
970
|
const res = await fetch(endpoint, {
|
|
493
971
|
method: "POST",
|
|
972
|
+
signal,
|
|
494
973
|
headers: {
|
|
495
974
|
"content-type": "application/json",
|
|
496
975
|
...(token ? { authorization: `Bearer ${token}` } : null),
|
|
497
976
|
},
|
|
498
977
|
body: JSON.stringify(payload),
|
|
499
978
|
});
|
|
500
|
-
const text = await res
|
|
979
|
+
const text = await readResponseTextCapped(res, maxResponseBytes);
|
|
501
980
|
if (!res.ok) {
|
|
502
981
|
throw new Error(`${res.status} ${res.statusText}: ${text.slice(0, 400)}`);
|
|
503
982
|
}
|
|
@@ -603,14 +1082,24 @@ function hashArtifact(artifact) {
|
|
|
603
1082
|
const stable = stableStringify(artifact);
|
|
604
1083
|
return createHash("sha256").update(stable).digest("hex");
|
|
605
1084
|
}
|
|
606
|
-
function computeCacheKey({ provider, prompt, model, schemaVersion, artifactHashes, outputSchema, }) {
|
|
1085
|
+
function computeCacheKey({ provider, prompt, model, schemaVersion, artifactHashes, outputSchema, temperature, maxOutputTokens, }) {
|
|
1086
|
+
// `null` is the identity of an omitted parameter, distinct from any value a caller can
|
|
1087
|
+
// pass, so an omitted request can never resolve to an entry written with an explicit one.
|
|
1088
|
+
// The version separates this identity from the keys earlier releases wrote, where the two
|
|
1089
|
+
// parameters were absent from the payload and a sampled answer therefore shared a key with
|
|
1090
|
+
// an unsampled request. Bump it whenever the fields below change: entries under older
|
|
1091
|
+
// versions become unreachable, which costs one re-invocation and never a wrong replay.
|
|
607
1092
|
const payload = {
|
|
1093
|
+
cacheKeyVersion: CACHE_KEY_VERSION,
|
|
608
1094
|
provider,
|
|
609
1095
|
prompt,
|
|
610
1096
|
model: model || `${provider}-default`,
|
|
611
1097
|
schemaVersion,
|
|
612
1098
|
artifactHashes,
|
|
613
1099
|
outputSchema: outputSchema ?? null,
|
|
1100
|
+
// The same predicate that decides whether each parameter is sent to the adapter.
|
|
1101
|
+
temperature: Number.isFinite(temperature ?? NaN) ? Number(temperature) : null,
|
|
1102
|
+
maxOutputTokens: Number.isFinite(maxOutputTokens ?? NaN) ? Number(maxOutputTokens) : null,
|
|
614
1103
|
};
|
|
615
1104
|
return createHash("sha256").update(stableStringify(payload)).digest("hex");
|
|
616
1105
|
}
|
|
@@ -646,9 +1135,11 @@ function normalizeResult({ envelope, cacheKey, schemaVersion, artifactHashes, so
|
|
|
646
1135
|
};
|
|
647
1136
|
return [item];
|
|
648
1137
|
}
|
|
649
|
-
async function persistOutputs({ env, stateKey, cacheKey, items, stateType, }) {
|
|
650
|
-
if (!stateKey)
|
|
1138
|
+
async function persistOutputs({ env, stateKey, cacheKey, items, stateType, signal, afterStore, }) {
|
|
1139
|
+
if (!stateKey) {
|
|
1140
|
+
await afterStore?.();
|
|
651
1141
|
return;
|
|
1142
|
+
}
|
|
652
1143
|
const record = {
|
|
653
1144
|
type: stateType,
|
|
654
1145
|
version: STATE_VERSION,
|
|
@@ -656,11 +1147,17 @@ async function persistOutputs({ env, stateKey, cacheKey, items, stateType, }) {
|
|
|
656
1147
|
items,
|
|
657
1148
|
storedAt: new Date().toISOString(),
|
|
658
1149
|
};
|
|
659
|
-
await
|
|
1150
|
+
await diffAndStore({
|
|
1151
|
+
env,
|
|
1152
|
+
key: stateKey,
|
|
1153
|
+
value: record,
|
|
1154
|
+
signal,
|
|
1155
|
+
afterStore: afterStore ? () => afterStore() : undefined,
|
|
1156
|
+
});
|
|
660
1157
|
}
|
|
661
|
-
async function readReusableLlmState(env, stateKey) {
|
|
1158
|
+
async function readReusableLlmState(env, stateKey, signal) {
|
|
662
1159
|
try {
|
|
663
|
-
return await
|
|
1160
|
+
return await readStateJsonWithLock({ env, key: stateKey, signal });
|
|
664
1161
|
}
|
|
665
1162
|
catch (err) {
|
|
666
1163
|
if (isJsonSyntaxError(err))
|
|
@@ -684,28 +1181,85 @@ function collectAjvErrors(errors) {
|
|
|
684
1181
|
return [];
|
|
685
1182
|
return errors.map((err) => `${err.instancePath || "/"} ${err.message ?? ""}`.trim());
|
|
686
1183
|
}
|
|
687
|
-
async function readCacheEntry(env, key, cacheNamespace) {
|
|
1184
|
+
async function readCacheEntry(env, key, cacheNamespace, signal) {
|
|
688
1185
|
const filePath = path.join(getCacheDir(env), cacheNamespace, `${key}.json`);
|
|
1186
|
+
const read = async () => {
|
|
1187
|
+
try {
|
|
1188
|
+
const text = await fsp.readFile(filePath, "utf8");
|
|
1189
|
+
const parsed = JSON.parse(text);
|
|
1190
|
+
if (parsed?.cacheKey !== key || !Array.isArray(parsed.items))
|
|
1191
|
+
return null;
|
|
1192
|
+
return parsed;
|
|
1193
|
+
}
|
|
1194
|
+
catch (err) {
|
|
1195
|
+
if (err?.code === "ENOENT")
|
|
1196
|
+
return null;
|
|
1197
|
+
if (isJsonSyntaxError(err))
|
|
1198
|
+
return null;
|
|
1199
|
+
throw err;
|
|
1200
|
+
}
|
|
1201
|
+
};
|
|
689
1202
|
try {
|
|
690
|
-
|
|
691
|
-
const parsed = JSON.parse(text);
|
|
692
|
-
if (parsed?.cacheKey !== key || !Array.isArray(parsed.items))
|
|
693
|
-
return null;
|
|
694
|
-
return parsed;
|
|
1203
|
+
return await withFileLock({ filePath, signal, task: read });
|
|
695
1204
|
}
|
|
696
1205
|
catch (err) {
|
|
697
|
-
if (err?.code === "ENOENT")
|
|
698
|
-
return null;
|
|
699
|
-
if (isJsonSyntaxError(err))
|
|
1206
|
+
if (err?.code === "ENOENT" || err?.code === "ENOTDIR")
|
|
700
1207
|
return null;
|
|
1208
|
+
// A cache mounted read-only cannot have an active local writer because a
|
|
1209
|
+
// writer first creates the same coordination lock. Preserve its reusable
|
|
1210
|
+
// entries instead of requiring a lock-directory write for a read.
|
|
1211
|
+
if (["EACCES", "EPERM", "EROFS"].includes(err?.code))
|
|
1212
|
+
return read();
|
|
701
1213
|
throw err;
|
|
702
1214
|
}
|
|
703
1215
|
}
|
|
704
|
-
async function writeCacheEntry(env, key, items, cacheNamespace) {
|
|
1216
|
+
async function writeCacheEntry(env, key, items, cacheNamespace, signal) {
|
|
705
1217
|
const dir = path.join(getCacheDir(env), cacheNamespace);
|
|
1218
|
+
signal?.throwIfAborted();
|
|
706
1219
|
await ensureDirectory(dir);
|
|
707
1220
|
const filePath = path.join(dir, `${key}.json`);
|
|
708
|
-
|
|
1221
|
+
const content = JSON.stringify({ items, cacheKey: key, storedAt: new Date().toISOString() }, null, 2) + "\n";
|
|
1222
|
+
await withFileLock({
|
|
1223
|
+
filePath,
|
|
1224
|
+
signal,
|
|
1225
|
+
task: async () => {
|
|
1226
|
+
let previousContent = null;
|
|
1227
|
+
try {
|
|
1228
|
+
previousContent = await fsp.readFile(filePath);
|
|
1229
|
+
}
|
|
1230
|
+
catch (err) {
|
|
1231
|
+
if (err?.code !== "ENOENT")
|
|
1232
|
+
throw err;
|
|
1233
|
+
}
|
|
1234
|
+
const restorePreviousContent = async () => {
|
|
1235
|
+
// No cache reader or competing cache writer can observe this entry
|
|
1236
|
+
// until this lock is released. Restore an entry replaced by a refresh;
|
|
1237
|
+
// only remove the just-published file when none existed beforehand.
|
|
1238
|
+
if (previousContent === null)
|
|
1239
|
+
await fsp.rm(filePath, { force: true });
|
|
1240
|
+
else
|
|
1241
|
+
await writeFileAtomic(filePath, previousContent);
|
|
1242
|
+
};
|
|
1243
|
+
let cacheWasPublished = false;
|
|
1244
|
+
try {
|
|
1245
|
+
signal?.throwIfAborted();
|
|
1246
|
+
const result = await writeFileAtomic(filePath, content, { signal });
|
|
1247
|
+
cacheWasPublished = true;
|
|
1248
|
+
if (result?.signalAbortedAfterCommit || signal?.aborted) {
|
|
1249
|
+
await restorePreviousContent();
|
|
1250
|
+
cacheWasPublished = false;
|
|
1251
|
+
signal?.throwIfAborted();
|
|
1252
|
+
throw new Error("LLM cache publication cancelled");
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
catch (err) {
|
|
1256
|
+
if (cacheWasPublished || atomicWriteWasPublished(err)) {
|
|
1257
|
+
await restorePreviousContent();
|
|
1258
|
+
}
|
|
1259
|
+
throw err;
|
|
1260
|
+
}
|
|
1261
|
+
},
|
|
1262
|
+
});
|
|
709
1263
|
}
|
|
710
1264
|
function getCacheDir(env) {
|
|
711
1265
|
if (env?.LOBSTER_CACHE_DIR)
|