@namzu/sdk 51.1.0 → 52.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +119 -0
- package/dist/agents/QueryAgent.d.ts.map +1 -1
- package/dist/agents/QueryAgent.js +2 -0
- package/dist/agents/QueryAgent.js.map +1 -1
- package/dist/agents/SupervisorAgent.d.ts +1 -1
- package/dist/agents/SupervisorAgent.d.ts.map +1 -1
- package/dist/agents/SupervisorAgent.js.map +1 -1
- package/dist/agents/examples/SupervisorAgent.d.ts +42 -0
- package/dist/agents/examples/SupervisorAgent.d.ts.map +1 -1
- package/dist/agents/examples/SupervisorAgent.js +80 -4
- package/dist/agents/examples/SupervisorAgent.js.map +1 -1
- package/dist/agents/index.d.ts +1 -1
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js.map +1 -1
- package/dist/authorization/protected-paths.d.ts +36 -0
- package/dist/authorization/protected-paths.d.ts.map +1 -0
- package/dist/authorization/protected-paths.js +107 -0
- package/dist/authorization/protected-paths.js.map +1 -0
- package/dist/authorization/readonly-commands.d.ts +39 -0
- package/dist/authorization/readonly-commands.d.ts.map +1 -0
- package/dist/authorization/readonly-commands.js +797 -0
- package/dist/authorization/readonly-commands.js.map +1 -0
- package/dist/authorization/readonly-flags.d.ts +19 -0
- package/dist/authorization/readonly-flags.d.ts.map +1 -0
- package/dist/authorization/readonly-flags.js +178 -0
- package/dist/authorization/readonly-flags.js.map +1 -0
- package/dist/authorization/removal.d.ts +52 -0
- package/dist/authorization/removal.d.ts.map +1 -0
- package/dist/authorization/removal.js +283 -0
- package/dist/authorization/removal.js.map +1 -0
- package/dist/authorization/shell-paths.d.ts +66 -0
- package/dist/authorization/shell-paths.d.ts.map +1 -0
- package/dist/authorization/shell-paths.js +697 -0
- package/dist/authorization/shell-paths.js.map +1 -0
- package/dist/bridge/acp/update.d.ts.map +1 -1
- package/dist/bridge/acp/update.js +1 -0
- package/dist/bridge/acp/update.js.map +1 -1
- package/dist/compaction/context-window.d.ts +24 -0
- package/dist/compaction/context-window.d.ts.map +1 -1
- package/dist/compaction/context-window.js +34 -0
- package/dist/compaction/context-window.js.map +1 -1
- package/dist/compaction/manual.d.ts.map +1 -1
- package/dist/compaction/manual.js +10 -2
- package/dist/compaction/manual.js.map +1 -1
- package/dist/compaction/plan.d.ts +4 -0
- package/dist/compaction/plan.d.ts.map +1 -1
- package/dist/compaction/plan.js +6 -2
- package/dist/compaction/plan.js.map +1 -1
- package/dist/compaction/verifier.d.ts +11 -1
- package/dist/compaction/verifier.d.ts.map +1 -1
- package/dist/compaction/verifier.js +75 -23
- package/dist/compaction/verifier.js.map +1 -1
- package/dist/config/runtime.d.ts +55 -7
- package/dist/config/runtime.d.ts.map +1 -1
- package/dist/config/runtime.js +23 -2
- package/dist/config/runtime.js.map +1 -1
- package/dist/connector/web/guarded-fetch.d.ts.map +1 -1
- package/dist/connector/web/guarded-fetch.js +24 -0
- package/dist/connector/web/guarded-fetch.js.map +1 -1
- package/dist/connector/web/index.d.ts +1 -1
- package/dist/connector/web/index.d.ts.map +1 -1
- package/dist/connector/web/page.d.ts +59 -0
- package/dist/connector/web/page.d.ts.map +1 -0
- package/dist/connector/web/page.js +112 -0
- package/dist/connector/web/page.js.map +1 -0
- package/dist/connector/web/types.d.ts +47 -0
- package/dist/connector/web/types.d.ts.map +1 -1
- package/dist/connector/web/types.js.map +1 -1
- package/dist/constants/continuation.d.ts +8 -0
- package/dist/constants/continuation.d.ts.map +1 -1
- package/dist/constants/continuation.js +8 -0
- package/dist/constants/continuation.js.map +1 -1
- package/dist/constants/plugin/index.d.ts +1 -1
- package/dist/constants/plugin/index.d.ts.map +1 -1
- package/dist/constants/plugin/index.js +1 -1
- package/dist/constants/plugin/index.js.map +1 -1
- package/dist/contracts/a2a.d.ts +2 -2
- package/dist/manager/agent/lifecycle.d.ts +7 -0
- package/dist/manager/agent/lifecycle.d.ts.map +1 -1
- package/dist/manager/agent/lifecycle.js +11 -2
- package/dist/manager/agent/lifecycle.js.map +1 -1
- package/dist/manager/session/turn-recorder.d.ts.map +1 -1
- package/dist/manager/session/turn-recorder.js +4 -2
- package/dist/manager/session/turn-recorder.js.map +1 -1
- package/dist/plugin/index.d.ts +1 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +1 -1
- package/dist/plugin/index.js.map +1 -1
- package/dist/plugin/lifecycle.d.ts +6 -0
- package/dist/plugin/lifecycle.d.ts.map +1 -1
- package/dist/plugin/lifecycle.js +98 -114
- package/dist/plugin/lifecycle.js.map +1 -1
- package/dist/plugin/shell-hook.d.ts +90 -22
- package/dist/plugin/shell-hook.d.ts.map +1 -1
- package/dist/plugin/shell-hook.js +434 -41
- package/dist/plugin/shell-hook.js.map +1 -1
- package/dist/provider/capabilities.d.ts +9 -0
- package/dist/provider/capabilities.d.ts.map +1 -1
- package/dist/provider/capabilities.js +15 -0
- package/dist/provider/capabilities.js.map +1 -1
- package/dist/provider/errors.d.ts.map +1 -1
- package/dist/provider/errors.js +13 -0
- package/dist/provider/errors.js.map +1 -1
- package/dist/provider/fallback.d.ts.map +1 -1
- package/dist/provider/fallback.js +10 -0
- package/dist/provider/fallback.js.map +1 -1
- package/dist/provider/idle-timeout.d.ts.map +1 -1
- package/dist/provider/idle-timeout.js +5 -0
- package/dist/provider/idle-timeout.js.map +1 -1
- package/dist/provider/image-input-refusal.d.ts +9 -0
- package/dist/provider/image-input-refusal.d.ts.map +1 -0
- package/dist/provider/image-input-refusal.js +54 -0
- package/dist/provider/image-input-refusal.js.map +1 -0
- package/dist/provider/index.d.ts +2 -1
- package/dist/provider/index.d.ts.map +1 -1
- package/dist/provider/index.js +2 -1
- package/dist/provider/index.js.map +1 -1
- package/dist/provider/retry.d.ts +7 -2
- package/dist/provider/retry.d.ts.map +1 -1
- package/dist/provider/retry.js +38 -4
- package/dist/provider/retry.js.map +1 -1
- package/dist/public-runtime.d.ts +5 -2
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +5 -2
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-types.d.ts +2 -2
- package/dist/public-types.d.ts.map +1 -1
- package/dist/runtime/jobs/registry.d.ts +21 -0
- package/dist/runtime/jobs/registry.d.ts.map +1 -1
- package/dist/runtime/jobs/registry.js +15 -2
- package/dist/runtime/jobs/registry.js.map +1 -1
- package/dist/runtime/query/deferred-sandbox.d.ts +54 -0
- package/dist/runtime/query/deferred-sandbox.d.ts.map +1 -0
- package/dist/runtime/query/deferred-sandbox.js +75 -0
- package/dist/runtime/query/deferred-sandbox.js.map +1 -0
- package/dist/runtime/query/executor/tool-call-admission.d.ts +10 -0
- package/dist/runtime/query/executor/tool-call-admission.d.ts.map +1 -1
- package/dist/runtime/query/executor/tool-call-admission.js +74 -13
- package/dist/runtime/query/executor/tool-call-admission.js.map +1 -1
- package/dist/runtime/query/executor.d.ts +51 -2
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +214 -9
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/finalize-turn.d.ts +6 -0
- package/dist/runtime/query/finalize-turn.d.ts.map +1 -1
- package/dist/runtime/query/finalize-turn.js +1 -1
- package/dist/runtime/query/finalize-turn.js.map +1 -1
- package/dist/runtime/query/index.d.ts +29 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +108 -28
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts +57 -0
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +380 -28
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts +61 -0
- package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts.map +1 -0
- package/dist/runtime/query/iteration/phases/compaction-breaker.js +94 -0
- package/dist/runtime/query/iteration/phases/compaction-breaker.js.map +1 -0
- package/dist/runtime/query/iteration/phases/compaction.d.ts +42 -1
- package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.js +203 -23
- package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +10 -0
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.js +165 -2
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/iteration/provider-rejected-image.d.ts +21 -1
- package/dist/runtime/query/iteration/provider-rejected-image.d.ts.map +1 -1
- package/dist/runtime/query/iteration/provider-rejected-image.js +53 -7
- package/dist/runtime/query/iteration/provider-rejected-image.js.map +1 -1
- package/dist/runtime/query/iteration/request-repair.d.ts +108 -0
- package/dist/runtime/query/iteration/request-repair.d.ts.map +1 -0
- package/dist/runtime/query/iteration/request-repair.js +593 -0
- package/dist/runtime/query/iteration/request-repair.js.map +1 -0
- package/dist/runtime/query/iteration/stream-turn.d.ts +6 -0
- package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
- package/dist/runtime/query/iteration/stream-turn.js +2 -2
- package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
- package/dist/runtime/query/path-escalation.d.ts +36 -0
- package/dist/runtime/query/path-escalation.d.ts.map +1 -0
- package/dist/runtime/query/path-escalation.js +184 -0
- package/dist/runtime/query/path-escalation.js.map +1 -0
- package/dist/runtime/query/plugin-hooks.d.ts.map +1 -1
- package/dist/runtime/query/plugin-hooks.js +8 -0
- package/dist/runtime/query/plugin-hooks.js.map +1 -1
- package/dist/runtime/query/reminders.d.ts +35 -0
- package/dist/runtime/query/reminders.d.ts.map +1 -0
- package/dist/runtime/query/reminders.js +7 -0
- package/dist/runtime/query/reminders.js.map +1 -0
- package/dist/runtime/query/request-rich-content.d.ts +5 -1
- package/dist/runtime/query/request-rich-content.d.ts.map +1 -1
- package/dist/runtime/query/request-rich-content.js +67 -25
- package/dist/runtime/query/request-rich-content.js.map +1 -1
- package/dist/runtime/query/resume-pending.d.ts.map +1 -1
- package/dist/runtime/query/resume-pending.js +26 -0
- package/dist/runtime/query/resume-pending.js.map +1 -1
- package/dist/runtime/query/review-policy.d.ts +26 -0
- package/dist/runtime/query/review-policy.d.ts.map +1 -1
- package/dist/runtime/query/review-policy.js +79 -5
- package/dist/runtime/query/review-policy.js.map +1 -1
- package/dist/runtime/query/tool-output-budget.d.ts +46 -0
- package/dist/runtime/query/tool-output-budget.d.ts.map +1 -1
- package/dist/runtime/query/tool-output-budget.js +85 -1
- package/dist/runtime/query/tool-output-budget.js.map +1 -1
- package/dist/runtime/query/tooling.d.ts +1 -0
- package/dist/runtime/query/tooling.d.ts.map +1 -1
- package/dist/runtime/query/tooling.js +5 -0
- package/dist/runtime/query/tooling.js.map +1 -1
- package/dist/session/paths.d.ts +5 -1
- package/dist/session/paths.d.ts.map +1 -1
- package/dist/session/paths.js +8 -0
- package/dist/session/paths.js.map +1 -1
- package/dist/store/memory/disk.d.ts.map +1 -1
- package/dist/store/memory/disk.js +2 -2
- package/dist/store/memory/disk.js.map +1 -1
- package/dist/store/memory/markdown.d.ts.map +1 -1
- package/dist/store/memory/markdown.js +2 -2
- package/dist/store/memory/markdown.js.map +1 -1
- package/dist/store/memory/naming.d.ts +9 -0
- package/dist/store/memory/naming.d.ts.map +1 -1
- package/dist/store/memory/naming.js +12 -0
- package/dist/store/memory/naming.js.map +1 -1
- package/dist/tools/builtins/bash.d.ts +30 -1
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +170 -26
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/builtins/computer-use.d.ts +1 -1
- package/dist/tools/builtins/computer-use.d.ts.map +1 -1
- package/dist/tools/builtins/computer-use.js +12 -5
- package/dist/tools/builtins/computer-use.js.map +1 -1
- package/dist/tools/builtins/content-fingerprint.d.ts +2 -0
- package/dist/tools/builtins/content-fingerprint.d.ts.map +1 -1
- package/dist/tools/builtins/content-fingerprint.js +4 -0
- package/dist/tools/builtins/content-fingerprint.js.map +1 -1
- package/dist/tools/builtins/edit.d.ts.map +1 -1
- package/dist/tools/builtins/edit.js +60 -18
- package/dist/tools/builtins/edit.js.map +1 -1
- package/dist/tools/builtins/grep.d.ts +8 -1
- package/dist/tools/builtins/grep.d.ts.map +1 -1
- package/dist/tools/builtins/grep.js +152 -33
- package/dist/tools/builtins/grep.js.map +1 -1
- package/dist/tools/builtins/read-file.d.ts +1 -0
- package/dist/tools/builtins/read-file.d.ts.map +1 -1
- package/dist/tools/builtins/read-file.js +44 -5
- package/dist/tools/builtins/read-file.js.map +1 -1
- package/dist/tools/builtins/read-render.d.ts +52 -0
- package/dist/tools/builtins/read-render.d.ts.map +1 -1
- package/dist/tools/builtins/read-render.js +143 -0
- package/dist/tools/builtins/read-render.js.map +1 -1
- package/dist/tools/builtins/web.d.ts +13 -2
- package/dist/tools/builtins/web.d.ts.map +1 -1
- package/dist/tools/builtins/web.js +225 -32
- package/dist/tools/builtins/web.js.map +1 -1
- package/dist/tools/coordinator/ask-user-question.d.ts +2 -0
- package/dist/tools/coordinator/ask-user-question.d.ts.map +1 -1
- package/dist/tools/coordinator/ask-user-question.js +256 -92
- package/dist/tools/coordinator/ask-user-question.js.map +1 -1
- package/dist/tools/coordinator/question-options.d.ts +1 -0
- package/dist/tools/coordinator/question-options.d.ts.map +1 -1
- package/dist/tools/coordinator/question-options.js +6 -1
- package/dist/tools/coordinator/question-options.js.map +1 -1
- package/dist/tools/defineTool.d.ts +4 -1
- package/dist/tools/defineTool.d.ts.map +1 -1
- package/dist/tools/defineTool.js +6 -1
- package/dist/tools/defineTool.js.map +1 -1
- package/dist/tools/paths.d.ts +6 -0
- package/dist/tools/paths.d.ts.map +1 -1
- package/dist/tools/paths.js +1 -1
- package/dist/tools/paths.js.map +1 -1
- package/dist/types/agent/query.d.ts +3 -0
- package/dist/types/agent/query.d.ts.map +1 -1
- package/dist/types/errors/catalog.js +1 -1
- package/dist/types/errors/catalog.js.map +1 -1
- package/dist/types/hitl/index.d.ts +104 -0
- package/dist/types/hitl/index.d.ts.map +1 -1
- package/dist/types/hitl/index.js.map +1 -1
- package/dist/types/message/index.d.ts +1 -1
- package/dist/types/message/index.d.ts.map +1 -1
- package/dist/types/message/index.js +2 -0
- package/dist/types/message/index.js.map +1 -1
- package/dist/types/plugin/index.d.ts +93 -19
- package/dist/types/plugin/index.d.ts.map +1 -1
- package/dist/types/plugin/index.js +3 -0
- package/dist/types/plugin/index.js.map +1 -1
- package/dist/types/provider/errors.d.ts +19 -0
- package/dist/types/provider/errors.d.ts.map +1 -1
- package/dist/types/provider/errors.js +54 -5
- package/dist/types/provider/errors.js.map +1 -1
- package/dist/types/provider/interface.d.ts +12 -0
- package/dist/types/provider/interface.d.ts.map +1 -1
- package/dist/types/provider/stream.d.ts +1 -1
- package/dist/types/session/config.d.ts +14 -0
- package/dist/types/session/config.d.ts.map +1 -1
- package/dist/types/session/events.d.ts +27 -4
- package/dist/types/session/events.d.ts.map +1 -1
- package/dist/types/session/events.js.map +1 -1
- package/dist/types/session/records.d.ts +9 -9
- package/dist/types/session/records.d.ts.map +1 -1
- package/dist/types/session/records.js +1 -0
- package/dist/types/session/records.js.map +1 -1
- package/dist/types/session/stop-reason.d.ts +5 -0
- package/dist/types/session/stop-reason.d.ts.map +1 -1
- package/dist/types/tool/index.d.ts +52 -0
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/package.json +4 -2
- package/src/agents/QueryAgent.ts +5 -1
- package/src/agents/SupervisorAgent.ts +1 -1
- package/src/agents/examples/SupervisorAgent.ts +217 -106
- package/src/agents/index.ts +1 -1
- package/src/authorization/protected-paths.ts +110 -0
- package/src/authorization/readonly-commands.ts +771 -0
- package/src/authorization/readonly-flags.ts +196 -0
- package/src/authorization/removal.ts +326 -0
- package/src/authorization/shell-paths.ts +927 -0
- package/src/bridge/acp/update.ts +1 -0
- package/src/compaction/context-window.ts +41 -0
- package/src/compaction/manual.ts +10 -2
- package/src/compaction/plan.ts +11 -2
- package/src/compaction/verifier.ts +84 -22
- package/src/config/runtime.ts +23 -2
- package/src/connector/web/guarded-fetch.ts +26 -0
- package/src/connector/web/index.ts +3 -0
- package/src/connector/web/page.ts +130 -0
- package/src/connector/web/types.ts +47 -0
- package/src/constants/continuation.ts +10 -0
- package/src/constants/plugin/index.ts +1 -1
- package/src/manager/agent/lifecycle.ts +12 -4
- package/src/manager/session/turn-recorder.ts +10 -2
- package/src/plugin/index.ts +1 -0
- package/src/plugin/lifecycle.ts +107 -119
- package/src/plugin/shell-hook.ts +477 -43
- package/src/provider/capabilities.ts +17 -0
- package/src/provider/errors.ts +13 -0
- package/src/provider/fallback.ts +10 -0
- package/src/provider/idle-timeout.ts +5 -0
- package/src/provider/image-input-refusal.ts +54 -0
- package/src/provider/index.ts +2 -0
- package/src/provider/retry.ts +42 -4
- package/src/public-runtime.ts +11 -0
- package/src/public-types.ts +4 -0
- package/src/runtime/jobs/registry.ts +39 -2
- package/src/runtime/query/deferred-sandbox.ts +110 -0
- package/src/runtime/query/executor/tool-call-admission.ts +79 -13
- package/src/runtime/query/executor.ts +267 -11
- package/src/runtime/query/finalize-turn.ts +7 -1
- package/src/runtime/query/index.ts +169 -52
- package/src/runtime/query/iteration/index.ts +439 -35
- package/src/runtime/query/iteration/phases/compaction-breaker.ts +108 -0
- package/src/runtime/query/iteration/phases/compaction.ts +241 -38
- package/src/runtime/query/iteration/phases/context.ts +11 -0
- package/src/runtime/query/iteration/phases/tool-review.ts +177 -2
- package/src/runtime/query/iteration/provider-rejected-image.ts +71 -5
- package/src/runtime/query/iteration/request-repair.ts +743 -0
- package/src/runtime/query/iteration/stream-turn.ts +11 -1
- package/src/runtime/query/path-escalation.ts +204 -0
- package/src/runtime/query/plugin-hooks.ts +8 -0
- package/src/runtime/query/reminders.ts +45 -0
- package/src/runtime/query/request-rich-content.ts +77 -27
- package/src/runtime/query/resume-pending.ts +27 -0
- package/src/runtime/query/review-policy.ts +106 -5
- package/src/runtime/query/tool-output-budget.ts +115 -1
- package/src/runtime/query/tooling.ts +6 -0
- package/src/session/paths.ts +11 -1
- package/src/store/memory/disk.ts +2 -1
- package/src/store/memory/markdown.ts +2 -1
- package/src/store/memory/naming.ts +13 -0
- package/src/tools/builtins/bash.ts +203 -34
- package/src/tools/builtins/computer-use.ts +13 -4
- package/src/tools/builtins/content-fingerprint.ts +5 -0
- package/src/tools/builtins/edit.ts +69 -19
- package/src/tools/builtins/grep.ts +159 -38
- package/src/tools/builtins/read-file.ts +48 -5
- package/src/tools/builtins/read-render.ts +193 -0
- package/src/tools/builtins/web.ts +258 -33
- package/src/tools/coordinator/ask-user-question.ts +309 -103
- package/src/tools/coordinator/question-options.ts +8 -1
- package/src/tools/defineTool.ts +11 -2
- package/src/tools/paths.ts +1 -1
- package/src/types/agent/query.ts +3 -0
- package/src/types/errors/catalog.ts +1 -1
- package/src/types/hitl/index.ts +104 -0
- package/src/types/message/index.ts +2 -0
- package/src/types/plugin/index.ts +101 -18
- package/src/types/provider/errors.ts +66 -5
- package/src/types/provider/interface.ts +13 -0
- package/src/types/provider/stream.ts +1 -1
- package/src/types/session/config.ts +14 -0
- package/src/types/session/events.ts +32 -4
- package/src/types/session/records.ts +1 -0
- package/src/types/session/stop-reason.ts +5 -0
- package/src/types/tool/index.ts +53 -2
|
@@ -0,0 +1,743 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
import { repairToolMessageHistory, toolHistoryRepairChanged } from '../../../compaction/dangling.js'
|
|
4
|
+
import { isProviderRequestError } from '../../../provider/errors.js'
|
|
5
|
+
import { isImageInputRefusal } from '../../../provider/image-input-refusal.js'
|
|
6
|
+
import type {
|
|
7
|
+
AssistantMessage,
|
|
8
|
+
Message,
|
|
9
|
+
MessageAttachment,
|
|
10
|
+
ToolResultBlock,
|
|
11
|
+
} from '../../../types/message/index.js'
|
|
12
|
+
import type {
|
|
13
|
+
ChatCompletionParams,
|
|
14
|
+
LLMProvider,
|
|
15
|
+
StreamChunk,
|
|
16
|
+
} from '../../../types/provider/index.js'
|
|
17
|
+
import { findSingleRequestImage } from '../request-rich-content.js'
|
|
18
|
+
import { isProviderRejectedImageError } from './provider-rejected-image.js'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Repair-and-retry for a request a provider rejects as invalid (a 400 that is
|
|
22
|
+
* not a context overflow), so a conversation does not dead-end on it.
|
|
23
|
+
*
|
|
24
|
+
* Mirrors the reference terminal's table of silent 400 recoveries: pair tool
|
|
25
|
+
* calls with results before a send, strip rejected reasoning, drop a
|
|
26
|
+
* parameter the model refuses, and take rejected media out of the OUTGOING
|
|
27
|
+
* request. Stored history is never edited.
|
|
28
|
+
*
|
|
29
|
+
* **Nothing is remembered on a guess.** Every repair is a guess about what the
|
|
30
|
+
* provider objected to. What it teaches (a parameter to leave out, reasoning to
|
|
31
|
+
* strip, media to omit, a media limit) is kept for the run only once a resend
|
|
32
|
+
* WITHOUT the offending part was accepted. A guess that did not fix the
|
|
33
|
+
* request is forgotten and the original rejection is thrown, so a wrong guess
|
|
34
|
+
* never changes a later request.
|
|
35
|
+
*
|
|
36
|
+
* It decorates one model call of the agent loop (a turn, or the closing
|
|
37
|
+
* summary), so a rejection is only acted on when nothing has been accepted yet:
|
|
38
|
+
* resending then cannot duplicate output. Other callers of a provider, such as
|
|
39
|
+
* compaction summaries, do not pass through it.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
export type RejectedParameter = 'effort' | 'thinking' | 'temperature' | 'topP' | 'responseFormat'
|
|
43
|
+
|
|
44
|
+
export interface MediaRemoval {
|
|
45
|
+
readonly count: number
|
|
46
|
+
readonly kind: 'image' | 'document' | 'media'
|
|
47
|
+
/** Some of it was attached to the person's latest message. */
|
|
48
|
+
readonly current: boolean
|
|
49
|
+
/** It was dropped to fit a count the provider accepts, not because a file was faulty. */
|
|
50
|
+
readonly limit: boolean
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface RequestRepairHooks {
|
|
54
|
+
/** Debug-level record of a silent repair. Never shown to the person. */
|
|
55
|
+
readonly debug?: (message: string) => void
|
|
56
|
+
/** Media was taken out of a request that then went through. Called once per removal. */
|
|
57
|
+
readonly onMediaRemoved?: (removal: MediaRemoval) => void | Promise<void>
|
|
58
|
+
/**
|
|
59
|
+
* The member that serves the next request. A fallback chain moves between
|
|
60
|
+
* members, and what a member refused is a fact about that member, not about
|
|
61
|
+
* the head of the chain.
|
|
62
|
+
*/
|
|
63
|
+
readonly servedBy?: () => { readonly providerId: string; readonly model?: string } | undefined
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type Rejection =
|
|
67
|
+
| { readonly kind: 'tool-pairing' }
|
|
68
|
+
| { readonly kind: 'reasoning' }
|
|
69
|
+
| { readonly kind: 'parameter'; readonly parameters: readonly RejectedParameter[] }
|
|
70
|
+
| {
|
|
71
|
+
readonly kind: 'media'
|
|
72
|
+
readonly generic: boolean
|
|
73
|
+
readonly messageIndex?: number
|
|
74
|
+
readonly contentIndex?: number
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Most repair resends for one original request, across every send it makes. */
|
|
78
|
+
export const MAX_REPAIRS_PER_REQUEST = 5
|
|
79
|
+
const MAX_STRIPS_PER_INDEX = 3
|
|
80
|
+
/** The reference drops media in these steps for an unprocessable request. */
|
|
81
|
+
const GENERIC_MEDIA_LIMITS: readonly number[] = [510, 255, 127]
|
|
82
|
+
|
|
83
|
+
const PARAMETER_CODES: ReadonlySet<string> = new Set([
|
|
84
|
+
'unsupported_parameter',
|
|
85
|
+
'unsupported_value',
|
|
86
|
+
'unknown_parameter',
|
|
87
|
+
])
|
|
88
|
+
|
|
89
|
+
const REASONING_CODES: ReadonlySet<string> = new Set([
|
|
90
|
+
'invalid_encrypted_content',
|
|
91
|
+
'invalid_reasoning',
|
|
92
|
+
'invalid_signature',
|
|
93
|
+
])
|
|
94
|
+
|
|
95
|
+
const TOOL_PAIRING =
|
|
96
|
+
/\btool_use\b.{0,120}\btool_result\b|\btool_result\b.{0,120}\btool_use\b|unexpected\s+`?tool_use_id`?|tool_use\s+ids?\s+must\s+be\s+unique|\btool_call_id\b|role\s+'tool'\s+must\s+be\s+a\s+response|'tool_calls'\s+must\s+be\s+followed|Expected\s+toolResult\s+blocks|function\s+call\s+turn\s+comes\s+immediately|number\s+of\s+function\s+response\s+parts/i
|
|
97
|
+
|
|
98
|
+
const REASONING =
|
|
99
|
+
/\bthought[_ ]?signature\b|`?signature`?\s+in\s+`?(?:thinking|redacted_thinking)|\b(?:thinking|redacted_thinking)\s+blocks?\b.{0,160}\b(?:cannot|can['’]t|must|invalid|modified|expected)|\bexpected\s+`?(?:thinking|redacted_thinking)`?|\bencrypted[_ ]content\b|\breasoning\s+item\b|\bitem\s+'rs_[^']*'|\bof\s+type\s+'reasoning'/i
|
|
100
|
+
|
|
101
|
+
/** A path into the tool list: wording about a tool schema is never about a request parameter or media. */
|
|
102
|
+
const TOOL_SCHEMA_PATH =
|
|
103
|
+
/\b(?:tools?|functions?)[.[]\s*\d+|input_schema|\.properties\b|json\s+schema/i
|
|
104
|
+
|
|
105
|
+
const REFUSAL =
|
|
106
|
+
"(?:unsupported|not\\s+supported|does\\s?n[o']t\\s+support|do\\s+not\\s+support|unrecognized|unknown\\s+(?:request\\s+)?(?:parameter|argument|field)|not\\s+allowed|deprecated|only\\s+the\\s+default)"
|
|
107
|
+
|
|
108
|
+
const PARAMETER_NAMES: Record<RejectedParameter, string> = {
|
|
109
|
+
effort: '(?:reasoning[_. -]?effort|effort)',
|
|
110
|
+
thinking: 'thinking',
|
|
111
|
+
temperature: 'temperature',
|
|
112
|
+
topP: 'top[_ ]?p',
|
|
113
|
+
responseFormat:
|
|
114
|
+
'(?:response_format|output_config(?:\\.format)?|json_schema|json_object|structured\\s+output)',
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const PARAMETER_PATTERNS = Object.fromEntries(
|
|
118
|
+
(Object.entries(PARAMETER_NAMES) as [RejectedParameter, string][]).map(([name, token]) => [
|
|
119
|
+
name,
|
|
120
|
+
{
|
|
121
|
+
named: new RegExp(`\\b${token}\\b`, 'i'),
|
|
122
|
+
refused: new RegExp(`${REFUSAL}.{0,80}\\b${token}\\b|\\b${token}\\b.{0,80}${REFUSAL}`, 'i'),
|
|
123
|
+
},
|
|
124
|
+
]),
|
|
125
|
+
) as Record<RejectedParameter, { named: RegExp; refused: RegExp }>
|
|
126
|
+
|
|
127
|
+
const BOTH_SAMPLING =
|
|
128
|
+
/temperature.{0,60}top[_ ]?p.{0,80}(?:cannot|can['’]t|may\s+not|should\s+not).{0,20}(?:both|together)/i
|
|
129
|
+
|
|
130
|
+
const IMAGE_FAULT =
|
|
131
|
+
/\b(?:images?|pictures?|photos?)\b.{0,100}\b(?:too\s+(?:large|big)|exceeds?|larger\s+than|maximum\s+(?:size|dimensions?)|dimensions?|(?:could|can)\s?not\s+be\s+(?:processed|decoded|loaded|read|fetched|downloaded)|failed\s+to\s+(?:process|decode|load|read|download)|unsupported|not\s+supported|corrupt\w*|malformed|truncated|invalid)\b|\b(?:(?:could|can)\s?not|failed\s+to|unable\s+to)\s+(?:process|decode|load|read|download|fetch)\b.{0,60}\b(?:images?|pictures?)\b|\bunsupported\s+(?:image|media)\b|\binvalid\s+image\b/i
|
|
132
|
+
const DOCUMENT_FAULT =
|
|
133
|
+
/\bdocument_block\b|\b(?:documents?|pdfs?)\b.{0,80}\b(?:too\s+(?:large|big)|exceeds?\s+(?:the\s+)?(?:maximum|size|page)|page\s+limit|password|corrupt\w*|(?:could|can)\s?not\s+be\s+(?:processed|read|decoded)|failed\s+to\s+(?:process|read|decode))\b|\bpassword[- ]protected\s+pdf\b|\bpdf\b.{0,40}\bpages?\b.{0,20}\b(?:limit|maximum|exceeds?)\b/i
|
|
134
|
+
const GENERIC_MEDIA =
|
|
135
|
+
/\bunprocessable\b.{0,100}\b(?:images?|media|documents?)\b|\b(?:images?|media|documents?)\b.{0,100}\bunprocessable\b|\btoo\s+many\s+(?:images?|documents?|media|attachments?)\b|\bmedia_budget\b|\bimage_block\b/i
|
|
136
|
+
const NAMES_MEDIA = /\b(?:images?|pictures?|photos?|documents?|pdfs?|media|attachments?)\b/i
|
|
137
|
+
|
|
138
|
+
function wireIndexes(text: string): { messageIndex?: number; contentIndex?: number } {
|
|
139
|
+
const both = /\bmessages?[.[]\s*(\d+)\s*\]?[.[]\s*content[.[]\s*(\d+)/i.exec(text)
|
|
140
|
+
if (both?.[1] !== undefined && both[2] !== undefined) {
|
|
141
|
+
return { messageIndex: Number(both[1]), contentIndex: Number(both[2]) }
|
|
142
|
+
}
|
|
143
|
+
const only = /\bmessages?[.[]\s*(\d+)\b/i.exec(text)
|
|
144
|
+
return only?.[1] !== undefined ? { messageIndex: Number(only[1]) } : {}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function rejectedParameters(
|
|
148
|
+
text: string,
|
|
149
|
+
code: string | undefined,
|
|
150
|
+
params: ChatCompletionParams,
|
|
151
|
+
): RejectedParameter[] {
|
|
152
|
+
if (TOOL_SCHEMA_PATH.test(text)) return []
|
|
153
|
+
const structured = code !== undefined && PARAMETER_CODES.has(code)
|
|
154
|
+
const set = {
|
|
155
|
+
effort: params.effort !== undefined,
|
|
156
|
+
thinking: params.thinking !== undefined,
|
|
157
|
+
temperature: params.temperature !== undefined,
|
|
158
|
+
topP: params.topP !== undefined,
|
|
159
|
+
responseFormat: params.responseFormat !== undefined,
|
|
160
|
+
} satisfies Record<RejectedParameter, boolean>
|
|
161
|
+
const found: RejectedParameter[] = []
|
|
162
|
+
for (const name of Object.keys(set) as RejectedParameter[]) {
|
|
163
|
+
if (!set[name]) continue
|
|
164
|
+
const pattern = PARAMETER_PATTERNS[name]
|
|
165
|
+
if (structured ? pattern.named.test(text) : pattern.refused.test(text)) found.push(name)
|
|
166
|
+
}
|
|
167
|
+
if (found.length === 0 && set.topP && BOTH_SAMPLING.test(text)) found.push('topP')
|
|
168
|
+
return found
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function requestHasMedia(params: ChatCompletionParams): boolean {
|
|
172
|
+
return listMedia(params.messages).length > 0
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* What kind of invalid request this is, from the provider's structured code
|
|
177
|
+
* where it sent one and from its wording only as a fallback.
|
|
178
|
+
*/
|
|
179
|
+
export function classifyRequestRejection(
|
|
180
|
+
error: unknown,
|
|
181
|
+
params: ChatCompletionParams,
|
|
182
|
+
): Rejection | null {
|
|
183
|
+
if (!isProviderRequestError(error) || error.kind !== 'bad_request') return null
|
|
184
|
+
if (
|
|
185
|
+
error.status !== undefined &&
|
|
186
|
+
error.status !== 400 &&
|
|
187
|
+
error.status !== 413 &&
|
|
188
|
+
error.status !== 422
|
|
189
|
+
) {
|
|
190
|
+
return null
|
|
191
|
+
}
|
|
192
|
+
const text = error.detail ?? error.message
|
|
193
|
+
const code = error.providerCode
|
|
194
|
+
|
|
195
|
+
if (code !== undefined && REASONING_CODES.has(code)) return { kind: 'reasoning' }
|
|
196
|
+
if (REASONING.test(text)) return { kind: 'reasoning' }
|
|
197
|
+
if (TOOL_PAIRING.test(text)) return { kind: 'tool-pairing' }
|
|
198
|
+
|
|
199
|
+
const parameters = rejectedParameters(text, code, params)
|
|
200
|
+
if (parameters.length > 0) return { kind: 'parameter', parameters }
|
|
201
|
+
|
|
202
|
+
if (TOOL_SCHEMA_PATH.test(text)) return null
|
|
203
|
+
if (code === 'invalid_image') return { kind: 'media', generic: false, ...wireIndexes(text) }
|
|
204
|
+
if (GENERIC_MEDIA.test(text)) return { kind: 'media', generic: true }
|
|
205
|
+
// A bare 413 or 422 says nothing about media: it counts only when the
|
|
206
|
+
// request carries some and the provider's words name it.
|
|
207
|
+
if ((error.status === 413 || error.status === 422) && NAMES_MEDIA.test(text)) {
|
|
208
|
+
return requestHasMedia(params) ? { kind: 'media', generic: true } : null
|
|
209
|
+
}
|
|
210
|
+
if (IMAGE_FAULT.test(text) || DOCUMENT_FAULT.test(text)) {
|
|
211
|
+
return { kind: 'media', generic: false, ...wireIndexes(text) }
|
|
212
|
+
}
|
|
213
|
+
return null
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function isAccepted(chunk: StreamChunk): boolean {
|
|
217
|
+
if (chunk.retry !== undefined || chunk.fallback !== undefined || chunk.error !== undefined) {
|
|
218
|
+
return false
|
|
219
|
+
}
|
|
220
|
+
return Boolean(
|
|
221
|
+
chunk.delta.content ||
|
|
222
|
+
chunk.delta.toolCalls?.length ||
|
|
223
|
+
chunk.delta.toolCallEnd ||
|
|
224
|
+
chunk.delta.reasoning ||
|
|
225
|
+
chunk.delta.citation ||
|
|
226
|
+
chunk.finishReason !== undefined ||
|
|
227
|
+
chunk.usage !== undefined ||
|
|
228
|
+
chunk.replayState !== undefined ||
|
|
229
|
+
chunk.textParts !== undefined,
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const mediaFingerprint = (data: string, mediaType: string): string =>
|
|
234
|
+
createHash('sha256').update(mediaType).update('\0').update(data).digest('hex')
|
|
235
|
+
|
|
236
|
+
const FAULT_NOTE =
|
|
237
|
+
'the provider could not process it. The file may be unsupported or corrupt; do not retry reading it. If you need to inspect it, use a shell command instead.'
|
|
238
|
+
const LIMIT_NOTE =
|
|
239
|
+
'this conversation holds more files than the provider accepts at once, so the oldest are left out of the request. They are still in the conversation.'
|
|
240
|
+
const removedNote = (kind: 'image' | 'document', limit: boolean): string =>
|
|
241
|
+
`[${kind} removed: ${limit ? LIMIT_NOTE : FAULT_NOTE}]`
|
|
242
|
+
|
|
243
|
+
interface MediaSlot {
|
|
244
|
+
readonly messageIndex: number
|
|
245
|
+
readonly itemIndex: number
|
|
246
|
+
readonly source: 'user' | 'tool'
|
|
247
|
+
readonly kind: 'image' | 'document'
|
|
248
|
+
readonly fingerprint: string
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function listMedia(messages: readonly Message[]): MediaSlot[] {
|
|
252
|
+
const slots: MediaSlot[] = []
|
|
253
|
+
for (const [messageIndex, message] of messages.entries()) {
|
|
254
|
+
if (message.role === 'user') {
|
|
255
|
+
for (const [itemIndex, attachment] of (message.attachments ?? []).entries()) {
|
|
256
|
+
if (attachment.type === 'stored') continue
|
|
257
|
+
slots.push({
|
|
258
|
+
messageIndex,
|
|
259
|
+
itemIndex,
|
|
260
|
+
source: 'user',
|
|
261
|
+
kind: attachment.type === 'document' ? 'document' : 'image',
|
|
262
|
+
fingerprint: mediaFingerprint(attachment.data, attachment.mediaType),
|
|
263
|
+
})
|
|
264
|
+
}
|
|
265
|
+
} else if (message.role === 'tool' && Array.isArray(message.content)) {
|
|
266
|
+
for (const [itemIndex, block] of message.content.entries()) {
|
|
267
|
+
if (block.type !== 'image' && block.type !== 'document') continue
|
|
268
|
+
slots.push({
|
|
269
|
+
messageIndex,
|
|
270
|
+
itemIndex,
|
|
271
|
+
source: 'tool',
|
|
272
|
+
kind: block.type,
|
|
273
|
+
fingerprint: mediaFingerprint(block.data, block.mediaType),
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return slots
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Replace the given media with a short text note, in a copy. */
|
|
282
|
+
function removeMedia(
|
|
283
|
+
messages: readonly Message[],
|
|
284
|
+
slots: readonly MediaSlot[],
|
|
285
|
+
limit: boolean,
|
|
286
|
+
): Message[] {
|
|
287
|
+
if (slots.length === 0) return messages as Message[]
|
|
288
|
+
const byMessage = new Map<number, Map<number, MediaSlot>>()
|
|
289
|
+
for (const slot of slots) {
|
|
290
|
+
const items = byMessage.get(slot.messageIndex) ?? new Map<number, MediaSlot>()
|
|
291
|
+
items.set(slot.itemIndex, slot)
|
|
292
|
+
byMessage.set(slot.messageIndex, items)
|
|
293
|
+
}
|
|
294
|
+
return messages.map((message, messageIndex) => {
|
|
295
|
+
const items = byMessage.get(messageIndex)
|
|
296
|
+
if (!items) return message
|
|
297
|
+
if (message.role === 'user') {
|
|
298
|
+
const attachments: readonly MessageAttachment[] = message.attachments ?? []
|
|
299
|
+
const kept = attachments.filter((_a, i) => !items.has(i))
|
|
300
|
+
const notes = [...items.values()]
|
|
301
|
+
.sort((a, b) => a.itemIndex - b.itemIndex)
|
|
302
|
+
.map((slot) => removedNote(slot.kind, limit))
|
|
303
|
+
.join('\n\n')
|
|
304
|
+
const { attachments: _drop, ...rest } = message
|
|
305
|
+
return {
|
|
306
|
+
...rest,
|
|
307
|
+
content: message.content.length > 0 ? `${message.content}\n\n${notes}` : notes,
|
|
308
|
+
...(kept.length > 0 ? { attachments: kept } : {}),
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (message.role === 'tool' && Array.isArray(message.content)) {
|
|
312
|
+
const content: ToolResultBlock[] = message.content.map((block, i) => {
|
|
313
|
+
const slot = items.get(i)
|
|
314
|
+
return slot ? { type: 'text', text: removedNote(slot.kind, limit) } : block
|
|
315
|
+
})
|
|
316
|
+
return { ...message, content }
|
|
317
|
+
}
|
|
318
|
+
return message
|
|
319
|
+
})
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function stripReasoning(messages: readonly Message[]): Message[] {
|
|
323
|
+
let changed = false
|
|
324
|
+
const out = messages.map((message) => {
|
|
325
|
+
if (message.role !== 'assistant') return message
|
|
326
|
+
const assistant = message as AssistantMessage
|
|
327
|
+
const hasState =
|
|
328
|
+
assistant.source !== undefined &&
|
|
329
|
+
(assistant.source as { replayState?: unknown }).replayState !== undefined
|
|
330
|
+
if (assistant.reasoning === undefined && !hasState) return message
|
|
331
|
+
changed = true
|
|
332
|
+
const { reasoning: _reasoning, source, ...rest } = assistant
|
|
333
|
+
if (source === undefined) return rest as Message
|
|
334
|
+
const { replayState: _state, ...route } = source as typeof source & {
|
|
335
|
+
replayState?: unknown
|
|
336
|
+
}
|
|
337
|
+
return { ...rest, source: route } as Message
|
|
338
|
+
})
|
|
339
|
+
return changed ? out : (messages as Message[])
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* The media a provider's `messages.N.content.M` points at, mapped back through
|
|
344
|
+
* that driver's own serialisation. Exact for the two wires whose layout is
|
|
345
|
+
* known; anything else returns undefined and the caller does not guess.
|
|
346
|
+
*
|
|
347
|
+
* - the content-block wire: system turns are left out; a run of consecutive tool results
|
|
348
|
+
* is ONE user entry whose blocks are those results; every other message,
|
|
349
|
+
* including a user turn that follows tool results, is its own entry; a user
|
|
350
|
+
* entry's blocks are its text (when not empty) and then each attachment.
|
|
351
|
+
* - the chat-completions wire: one entry per message, system included; only user attachments
|
|
352
|
+
* travel as media (tool results are sent as text); blocks are the text (when
|
|
353
|
+
* not empty) and then each attachment.
|
|
354
|
+
*/
|
|
355
|
+
function mediaAtWireIndex(
|
|
356
|
+
messages: readonly Message[],
|
|
357
|
+
slots: readonly MediaSlot[],
|
|
358
|
+
family: string,
|
|
359
|
+
wireIndex: number,
|
|
360
|
+
contentIndex: number | undefined,
|
|
361
|
+
): MediaSlot[] | undefined {
|
|
362
|
+
if (family !== 'anthropic' && family !== 'openai') return undefined
|
|
363
|
+
const entries: number[][] = []
|
|
364
|
+
let inToolRun = false
|
|
365
|
+
for (const [index, message] of messages.entries()) {
|
|
366
|
+
if (family === 'anthropic') {
|
|
367
|
+
if (message.role === 'system') continue
|
|
368
|
+
if (message.role === 'tool') {
|
|
369
|
+
if (!inToolRun) entries.push([])
|
|
370
|
+
inToolRun = true
|
|
371
|
+
entries[entries.length - 1]?.push(index)
|
|
372
|
+
continue
|
|
373
|
+
}
|
|
374
|
+
inToolRun = false
|
|
375
|
+
}
|
|
376
|
+
entries.push([index])
|
|
377
|
+
}
|
|
378
|
+
const entry = entries[wireIndex]
|
|
379
|
+
if (entry === undefined) return undefined
|
|
380
|
+
const mine = slots.filter((slot) => entry.includes(slot.messageIndex))
|
|
381
|
+
if (mine.length === 0) return undefined
|
|
382
|
+
if (contentIndex === undefined) return mine
|
|
383
|
+
const first = entry[0]
|
|
384
|
+
const message = first === undefined ? undefined : messages[first]
|
|
385
|
+
if (message?.role === 'user' && !(family === 'openai' && mine.some((s) => s.source === 'tool'))) {
|
|
386
|
+
const itemIndex = contentIndex - (message.content.length > 0 ? 1 : 0)
|
|
387
|
+
const hit = mine.filter((slot) => slot.itemIndex === itemIndex)
|
|
388
|
+
return hit.length > 0 ? hit : mine
|
|
389
|
+
}
|
|
390
|
+
if (family === 'anthropic' && message?.role === 'tool') {
|
|
391
|
+
const owner = entry[contentIndex]
|
|
392
|
+
const hit = owner === undefined ? [] : mine.filter((slot) => slot.messageIndex === owner)
|
|
393
|
+
return hit.length > 0 ? hit : mine
|
|
394
|
+
}
|
|
395
|
+
return mine
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
interface Learned {
|
|
399
|
+
readonly latched: Map<string, Set<RejectedParameter>>
|
|
400
|
+
readonly reasoningOff: Set<string>
|
|
401
|
+
readonly rejectedMedia: Set<string>
|
|
402
|
+
mediaLimit: number | undefined
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const emptyLearned = (): Learned => ({
|
|
406
|
+
latched: new Map(),
|
|
407
|
+
reasoningOff: new Set(),
|
|
408
|
+
rejectedMedia: new Set(),
|
|
409
|
+
mediaLimit: undefined,
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
/** The budget and the unconfirmed guesses of one original request, shared by every send it makes. */
|
|
413
|
+
export interface RepairSession {
|
|
414
|
+
readonly spent: Set<string>
|
|
415
|
+
readonly stripsByIndex: Map<number, number>
|
|
416
|
+
pending: Learned
|
|
417
|
+
repairs: number
|
|
418
|
+
removal: MediaRemoval | undefined
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export class RequestRepair {
|
|
422
|
+
private readonly learned: Learned = emptyLearned()
|
|
423
|
+
|
|
424
|
+
constructor(private readonly hooks: RequestRepairHooks = {}) {}
|
|
425
|
+
|
|
426
|
+
/** A fresh budget for one original request. */
|
|
427
|
+
session(): RepairSession {
|
|
428
|
+
return {
|
|
429
|
+
spent: new Set(),
|
|
430
|
+
stripsByIndex: new Map(),
|
|
431
|
+
pending: emptyLearned(),
|
|
432
|
+
repairs: 0,
|
|
433
|
+
removal: undefined,
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
private served(
|
|
438
|
+
provider: Pick<LLMProvider, 'id'>,
|
|
439
|
+
params: ChatCompletionParams,
|
|
440
|
+
): { providerId: string; model: string } {
|
|
441
|
+
const member = this.hooks.servedBy?.()
|
|
442
|
+
return {
|
|
443
|
+
providerId: member?.providerId ?? params.providerRoute?.providerId ?? provider.id,
|
|
444
|
+
model: member?.model ?? params.providerRoute?.model ?? params.model,
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
private routeKey(provider: Pick<LLMProvider, 'id'>, params: ChatCompletionParams): string {
|
|
449
|
+
const { providerId, model } = this.served(provider, params)
|
|
450
|
+
return `${providerId}|${model}`
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** Everything already confirmed, then the tool pairing the provider requires. */
|
|
454
|
+
private prepare(
|
|
455
|
+
provider: Pick<LLMProvider, 'id'>,
|
|
456
|
+
params: ChatCompletionParams,
|
|
457
|
+
): ChatCompletionParams {
|
|
458
|
+
const key = this.routeKey(provider, params)
|
|
459
|
+
let next: ChatCompletionParams = params
|
|
460
|
+
|
|
461
|
+
const latched = this.learned.latched.get(key)
|
|
462
|
+
if (latched) next = withoutParameters(next, latched)
|
|
463
|
+
|
|
464
|
+
let messages = next.messages
|
|
465
|
+
if (this.learned.reasoningOff.has(key)) messages = stripReasoning(messages)
|
|
466
|
+
messages = this.applyMediaMemory(messages)
|
|
467
|
+
messages = this.pair(messages)
|
|
468
|
+
return messages === next.messages ? next : { ...next, messages }
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
private applyMediaMemory(messages: Message[]): Message[] {
|
|
472
|
+
const { rejectedMedia, mediaLimit } = this.learned
|
|
473
|
+
if (rejectedMedia.size === 0 && mediaLimit === undefined) return messages
|
|
474
|
+
const slots = listMedia(messages)
|
|
475
|
+
const faulty = slots.filter((slot) => rejectedMedia.has(slot.fingerprint))
|
|
476
|
+
let out = removeMedia(messages, faulty, false)
|
|
477
|
+
if (mediaLimit !== undefined) {
|
|
478
|
+
const rest = listMedia(out)
|
|
479
|
+
if (rest.length > mediaLimit) {
|
|
480
|
+
out = removeMedia(out, rest.slice(0, rest.length - mediaLimit), true)
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return out
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** The shared tool-history repair, on the outgoing copy. Duplicate call ids fail closed there, so the request goes out as it is. */
|
|
487
|
+
private pair(messages: Message[]): Message[] {
|
|
488
|
+
try {
|
|
489
|
+
const repaired = repairToolMessageHistory(messages)
|
|
490
|
+
if (!toolHistoryRepairChanged(repaired.report)) return messages
|
|
491
|
+
this.hooks.debug?.(`Repaired tool pairing before sending: ${JSON.stringify(repaired.report)}`)
|
|
492
|
+
return repaired.messages
|
|
493
|
+
} catch (error) {
|
|
494
|
+
this.hooks.debug?.(
|
|
495
|
+
`Tool pairing could not be repaired before sending: ${error instanceof Error ? error.message : String(error)}`,
|
|
496
|
+
)
|
|
497
|
+
return messages
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/** What a resend that was accepted has proved. */
|
|
502
|
+
private commit(session: RepairSession): void {
|
|
503
|
+
const { pending } = session
|
|
504
|
+
for (const [key, names] of pending.latched) {
|
|
505
|
+
const set = this.learned.latched.get(key) ?? new Set<RejectedParameter>()
|
|
506
|
+
for (const name of names) set.add(name)
|
|
507
|
+
this.learned.latched.set(key, set)
|
|
508
|
+
}
|
|
509
|
+
for (const key of pending.reasoningOff) this.learned.reasoningOff.add(key)
|
|
510
|
+
for (const fingerprint of pending.rejectedMedia) this.learned.rejectedMedia.add(fingerprint)
|
|
511
|
+
if (pending.mediaLimit !== undefined) {
|
|
512
|
+
this.learned.mediaLimit = Math.min(
|
|
513
|
+
this.learned.mediaLimit ?? pending.mediaLimit,
|
|
514
|
+
pending.mediaLimit,
|
|
515
|
+
)
|
|
516
|
+
}
|
|
517
|
+
session.pending = emptyLearned()
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** The request to send after `rejection`, or null when nothing more can be tried. */
|
|
521
|
+
private next(
|
|
522
|
+
provider: Pick<LLMProvider, 'id'>,
|
|
523
|
+
params: ChatCompletionParams,
|
|
524
|
+
rejection: Rejection,
|
|
525
|
+
session: RepairSession,
|
|
526
|
+
): ChatCompletionParams | null {
|
|
527
|
+
const { spent, pending } = session
|
|
528
|
+
const key = this.routeKey(provider, params)
|
|
529
|
+
switch (rejection.kind) {
|
|
530
|
+
case 'tool-pairing': {
|
|
531
|
+
if (spent.has('tool-pairing')) return null
|
|
532
|
+
spent.add('tool-pairing')
|
|
533
|
+
const repaired = repairOnce(params.messages)
|
|
534
|
+
if (repaired === null) return null
|
|
535
|
+
this.hooks.debug?.(`Retrying after a tool-history rejection (${key})`)
|
|
536
|
+
return { ...params, messages: repaired }
|
|
537
|
+
}
|
|
538
|
+
case 'reasoning': {
|
|
539
|
+
if (spent.has('reasoning')) return null
|
|
540
|
+
spent.add('reasoning')
|
|
541
|
+
const messages = stripReasoning(params.messages)
|
|
542
|
+
if (messages === params.messages) return null
|
|
543
|
+
pending.reasoningOff.add(key)
|
|
544
|
+
let next: ChatCompletionParams = { ...params, messages }
|
|
545
|
+
// With thinking on, a tool turn that carries no thinking block is
|
|
546
|
+
// itself refused ("Expected `thinking` … but found `tool_use`"), so
|
|
547
|
+
// the two go together: neither is asked for in this resend.
|
|
548
|
+
if (params.thinking !== undefined && params.thinking.type !== 'disabled') {
|
|
549
|
+
next = withoutParameters(next, new Set<RejectedParameter>(['thinking']))
|
|
550
|
+
const names = pending.latched.get(key) ?? new Set<RejectedParameter>()
|
|
551
|
+
names.add('thinking')
|
|
552
|
+
pending.latched.set(key, names)
|
|
553
|
+
}
|
|
554
|
+
this.hooks.debug?.(`Retrying without reasoning blocks (${key})`)
|
|
555
|
+
return next
|
|
556
|
+
}
|
|
557
|
+
case 'parameter': {
|
|
558
|
+
const fresh = rejection.parameters.filter((name) => !spent.has(`param:${name}`))
|
|
559
|
+
if (fresh.length === 0) return null
|
|
560
|
+
for (const name of fresh) spent.add(`param:${name}`)
|
|
561
|
+
const names = pending.latched.get(key) ?? new Set<RejectedParameter>()
|
|
562
|
+
for (const name of fresh) names.add(name)
|
|
563
|
+
pending.latched.set(key, names)
|
|
564
|
+
this.hooks.debug?.(
|
|
565
|
+
`Retrying without parameters the model refused (${key}): ${fresh.join(', ')}`,
|
|
566
|
+
)
|
|
567
|
+
return withoutParameters(params, new Set(fresh))
|
|
568
|
+
}
|
|
569
|
+
case 'media':
|
|
570
|
+
return this.nextWithoutMedia(provider, params, rejection, session)
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
private nextWithoutMedia(
|
|
575
|
+
provider: Pick<LLMProvider, 'id'>,
|
|
576
|
+
params: ChatCompletionParams,
|
|
577
|
+
rejection: Extract<Rejection, { kind: 'media' }>,
|
|
578
|
+
session: RepairSession,
|
|
579
|
+
): ChatCompletionParams | null {
|
|
580
|
+
const { spent, stripsByIndex, pending } = session
|
|
581
|
+
const slots = listMedia(params.messages)
|
|
582
|
+
if (slots.length === 0) return null
|
|
583
|
+
|
|
584
|
+
let victims: MediaSlot[] = []
|
|
585
|
+
let limited = false
|
|
586
|
+
if (!rejection.generic) {
|
|
587
|
+
if (rejection.messageIndex !== undefined) {
|
|
588
|
+
const used = stripsByIndex.get(rejection.messageIndex) ?? 0
|
|
589
|
+
if (used < MAX_STRIPS_PER_INDEX) {
|
|
590
|
+
stripsByIndex.set(rejection.messageIndex, used + 1)
|
|
591
|
+
victims =
|
|
592
|
+
mediaAtWireIndex(
|
|
593
|
+
params.messages,
|
|
594
|
+
slots,
|
|
595
|
+
this.served(provider, params).providerId,
|
|
596
|
+
rejection.messageIndex,
|
|
597
|
+
rejection.contentIndex,
|
|
598
|
+
) ?? []
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
if (victims.length === 0) {
|
|
602
|
+
// No exact index: the newest message that carries media.
|
|
603
|
+
const newest = slots[slots.length - 1]
|
|
604
|
+
if (newest === undefined) return null
|
|
605
|
+
const count = (stripsByIndex.get(-1) ?? 0) + 1
|
|
606
|
+
if (count > MAX_STRIPS_PER_INDEX) return null
|
|
607
|
+
stripsByIndex.set(-1, count)
|
|
608
|
+
victims = slots.filter((slot) => slot.messageIndex === newest.messageIndex)
|
|
609
|
+
}
|
|
610
|
+
} else {
|
|
611
|
+
// The reference's steps, then halving for smaller requests, down to
|
|
612
|
+
// none: each step keeps fewer than the request held, newest first.
|
|
613
|
+
const limit =
|
|
614
|
+
GENERIC_MEDIA_LIMITS.find((candidate) => candidate < slots.length) ??
|
|
615
|
+
Math.floor(slots.length / 2)
|
|
616
|
+
spent.add(`limit:${limit}`)
|
|
617
|
+
pending.mediaLimit = Math.min(pending.mediaLimit ?? limit, limit)
|
|
618
|
+
victims = slots.slice(0, slots.length - limit)
|
|
619
|
+
limited = true
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// A count limit teaches a limit, not that these files are faulty.
|
|
623
|
+
if (!limited) for (const slot of victims) pending.rejectedMedia.add(slot.fingerprint)
|
|
624
|
+
this.hooks.debug?.(`Retrying without ${victims.length} media the provider rejected`)
|
|
625
|
+
|
|
626
|
+
let lastUser = -1
|
|
627
|
+
for (const [index, message] of params.messages.entries()) {
|
|
628
|
+
if (message.role === 'user') lastUser = index
|
|
629
|
+
}
|
|
630
|
+
const kinds = new Set(victims.map((slot) => slot.kind))
|
|
631
|
+
const removal: MediaRemoval = {
|
|
632
|
+
count: victims.length,
|
|
633
|
+
kind: kinds.size === 1 ? ([...kinds][0] as 'image' | 'document') : 'media',
|
|
634
|
+
current: victims.some((slot) => slot.source === 'user' && slot.messageIndex === lastUser),
|
|
635
|
+
limit: limited,
|
|
636
|
+
}
|
|
637
|
+
const before = session.removal
|
|
638
|
+
session.removal = before
|
|
639
|
+
? {
|
|
640
|
+
count: before.count + removal.count,
|
|
641
|
+
kind: before.kind === removal.kind ? before.kind : 'media',
|
|
642
|
+
current: before.current || removal.current,
|
|
643
|
+
limit: before.limit && removal.limit,
|
|
644
|
+
}
|
|
645
|
+
: removal
|
|
646
|
+
return { ...params, messages: removeMedia(params.messages, victims, limited) }
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Send `params`, repairing and resending a request the provider rejects as
|
|
651
|
+
* invalid until it is accepted or nothing more can be changed. The original
|
|
652
|
+
* rejection is thrown when nothing applies, and what the guesses taught is
|
|
653
|
+
* forgotten unless a resend was accepted.
|
|
654
|
+
*/
|
|
655
|
+
async *stream(
|
|
656
|
+
provider: LLMProvider,
|
|
657
|
+
params: ChatCompletionParams,
|
|
658
|
+
session: RepairSession = this.session(),
|
|
659
|
+
): AsyncIterable<StreamChunk> {
|
|
660
|
+
let current = this.prepare(provider, params)
|
|
661
|
+
let firstError: unknown
|
|
662
|
+
|
|
663
|
+
for (;;) {
|
|
664
|
+
let produced = false
|
|
665
|
+
let reported = false
|
|
666
|
+
try {
|
|
667
|
+
for await (const chunk of provider.chatStream(current)) {
|
|
668
|
+
if (isAccepted(chunk)) {
|
|
669
|
+
produced = true
|
|
670
|
+
this.commit(session)
|
|
671
|
+
if (session.removal !== undefined && !reported) {
|
|
672
|
+
reported = true
|
|
673
|
+
const removal = session.removal
|
|
674
|
+
session.removal = undefined
|
|
675
|
+
await this.hooks.onMediaRemoved?.(removal)
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
yield chunk
|
|
679
|
+
}
|
|
680
|
+
this.commit(session)
|
|
681
|
+
if (session.removal !== undefined && !reported) {
|
|
682
|
+
const removal = session.removal
|
|
683
|
+
session.removal = undefined
|
|
684
|
+
await this.hooks.onMediaRemoved?.(removal)
|
|
685
|
+
}
|
|
686
|
+
return
|
|
687
|
+
} catch (error) {
|
|
688
|
+
if (current.signal?.aborted) throw current.signal.reason
|
|
689
|
+
if (produced) throw error
|
|
690
|
+
firstError ??= error
|
|
691
|
+
const forget = (): never => {
|
|
692
|
+
session.pending = emptyLearned()
|
|
693
|
+
session.removal = undefined
|
|
694
|
+
throw firstError
|
|
695
|
+
}
|
|
696
|
+
if (session.repairs >= MAX_REPAIRS_PER_REQUEST) return forget()
|
|
697
|
+
if (isHandledElsewhere(error, current)) return forget()
|
|
698
|
+
const rejection = classifyRequestRejection(error, current)
|
|
699
|
+
if (rejection === null) return forget()
|
|
700
|
+
const step = this.next(provider, current, rejection, session)
|
|
701
|
+
if (step === null) return forget()
|
|
702
|
+
session.repairs++
|
|
703
|
+
current = step
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/** Re-run the shared repair on the outgoing copy; null when it changes nothing or cannot. */
|
|
710
|
+
function repairOnce(messages: Message[]): Message[] | null {
|
|
711
|
+
try {
|
|
712
|
+
const repaired = repairToolMessageHistory(messages)
|
|
713
|
+
return toolHistoryRepairChanged(repaired.report) ? repaired.messages : null
|
|
714
|
+
} catch {
|
|
715
|
+
return null
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function withoutParameters(
|
|
720
|
+
params: ChatCompletionParams,
|
|
721
|
+
names: ReadonlySet<RejectedParameter>,
|
|
722
|
+
): ChatCompletionParams {
|
|
723
|
+
if (names.size === 0) return params
|
|
724
|
+
const next = { ...params }
|
|
725
|
+
if (names.has('effort')) next.effort = undefined
|
|
726
|
+
if (names.has('thinking')) next.thinking = undefined
|
|
727
|
+
if (names.has('temperature')) next.temperature = undefined
|
|
728
|
+
if (names.has('topP')) next.topP = undefined
|
|
729
|
+
if (names.has('responseFormat')) next.responseFormat = undefined
|
|
730
|
+
return next
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Rejections another recovery already owns: a model that reads text only, and
|
|
735
|
+
* a single distinct image the provider calls invalid. Those keep their own
|
|
736
|
+
* commit points in `provider-rejected-image.ts`, which wraps this one.
|
|
737
|
+
*/
|
|
738
|
+
function isHandledElsewhere(error: unknown, params: ChatCompletionParams): boolean {
|
|
739
|
+
if (isProviderRejectedImageError(error) && findSingleRequestImage(params.messages) !== null) {
|
|
740
|
+
return true
|
|
741
|
+
}
|
|
742
|
+
return isImageInputRefusal(error)
|
|
743
|
+
}
|