@warlock.js/ai 4.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -0
- package/cjs/index.cjs +8625 -0
- package/cjs/index.cjs.map +1 -0
- package/esm/agent/agent-config.type.d.mts +160 -0
- package/esm/agent/agent-config.type.d.mts.map +1 -0
- package/esm/agent/agent-input-builder.mjs +111 -0
- package/esm/agent/agent-input-builder.mjs.map +1 -0
- package/esm/agent/agent-log-event.mjs +101 -0
- package/esm/agent/agent-log-event.mjs.map +1 -0
- package/esm/agent/agent-stream.d.mts +47 -0
- package/esm/agent/agent-stream.d.mts.map +1 -0
- package/esm/agent/agent-stream.mjs +82 -0
- package/esm/agent/agent-stream.mjs.map +1 -0
- package/esm/agent/agent-to-stream-event.mjs +86 -0
- package/esm/agent/agent-to-stream-event.mjs.map +1 -0
- package/esm/agent/agent.d.mts +38 -0
- package/esm/agent/agent.d.mts.map +1 -0
- package/esm/agent/agent.mjs +974 -0
- package/esm/agent/agent.mjs.map +1 -0
- package/esm/agent/index.d.mts +3 -0
- package/esm/agent/index.mjs +4 -0
- package/esm/agent/json-stream-guard.mjs +400 -0
- package/esm/agent/json-stream-guard.mjs.map +1 -0
- package/esm/ai.d.mts +64 -0
- package/esm/ai.d.mts.map +1 -0
- package/esm/ai.mjs +65 -0
- package/esm/ai.mjs.map +1 -0
- package/esm/config.d.mts +79 -0
- package/esm/config.d.mts.map +1 -0
- package/esm/config.mjs +43 -0
- package/esm/config.mjs.map +1 -0
- package/esm/contracts/agent/agent-options.type.d.mts +132 -0
- package/esm/contracts/agent/agent-options.type.d.mts.map +1 -0
- package/esm/contracts/agent/agent.contract.d.mts +92 -0
- package/esm/contracts/agent/agent.contract.d.mts.map +1 -0
- package/esm/contracts/agent/index.d.mts +2 -0
- package/esm/contracts/attachment.type.d.mts +100 -0
- package/esm/contracts/attachment.type.d.mts.map +1 -0
- package/esm/contracts/content-part.type.d.mts +55 -0
- package/esm/contracts/content-part.type.d.mts.map +1 -0
- package/esm/contracts/conversation-message.type.d.mts +51 -0
- package/esm/contracts/conversation-message.type.d.mts.map +1 -0
- package/esm/contracts/embedder.contract.d.mts +69 -0
- package/esm/contracts/embedder.contract.d.mts.map +1 -0
- package/esm/contracts/end.type.d.mts +53 -0
- package/esm/contracts/end.type.d.mts.map +1 -0
- package/esm/contracts/end.type.mjs +47 -0
- package/esm/contracts/end.type.mjs.map +1 -0
- package/esm/contracts/events/agent-events.type.d.mts +71 -0
- package/esm/contracts/events/agent-events.type.d.mts.map +1 -0
- package/esm/contracts/events/complete-event.type.d.mts +33 -0
- package/esm/contracts/events/complete-event.type.d.mts.map +1 -0
- package/esm/contracts/events/event-identity.type.d.mts +30 -0
- package/esm/contracts/events/event-identity.type.d.mts.map +1 -0
- package/esm/contracts/events/event-map.type.d.mts +80 -0
- package/esm/contracts/events/event-map.type.d.mts.map +1 -0
- package/esm/contracts/events/index.d.mts +7 -0
- package/esm/contracts/events/supervisor-events.type.d.mts +162 -0
- package/esm/contracts/events/supervisor-events.type.d.mts.map +1 -0
- package/esm/contracts/events/usage-event.type.d.mts +39 -0
- package/esm/contracts/events/usage-event.type.d.mts.map +1 -0
- package/esm/contracts/events/workflow-events.type.d.mts +51 -0
- package/esm/contracts/events/workflow-events.type.d.mts.map +1 -0
- package/esm/contracts/executable.contract.d.mts +25 -0
- package/esm/contracts/executable.contract.d.mts.map +1 -0
- package/esm/contracts/finish-reason.type.d.mts +17 -0
- package/esm/contracts/finish-reason.type.d.mts.map +1 -0
- package/esm/contracts/index.d.mts +63 -0
- package/esm/contracts/middleware/index.d.mts +3 -0
- package/esm/contracts/middleware/middleware-context.type.d.mts +99 -0
- package/esm/contracts/middleware/middleware-context.type.d.mts.map +1 -0
- package/esm/contracts/middleware/middleware-state.type.d.mts +34 -0
- package/esm/contracts/middleware/middleware-state.type.d.mts.map +1 -0
- package/esm/contracts/middleware/middleware.contract.d.mts +140 -0
- package/esm/contracts/middleware/middleware.contract.d.mts.map +1 -0
- package/esm/contracts/model-tool-call-request.type.d.mts +58 -0
- package/esm/contracts/model-tool-call-request.type.d.mts.map +1 -0
- package/esm/contracts/model.contract.d.mts +164 -0
- package/esm/contracts/model.contract.d.mts.map +1 -0
- package/esm/contracts/placeholders.type.d.mts +23 -0
- package/esm/contracts/placeholders.type.d.mts.map +1 -0
- package/esm/contracts/result/agent-result.type.d.mts +33 -0
- package/esm/contracts/result/agent-result.type.d.mts.map +1 -0
- package/esm/contracts/result/attempt-entry.type.d.mts +40 -0
- package/esm/contracts/result/attempt-entry.type.d.mts.map +1 -0
- package/esm/contracts/result/base-report.type.d.mts +137 -0
- package/esm/contracts/result/base-report.type.d.mts.map +1 -0
- package/esm/contracts/result/base-report.type.mjs +17 -0
- package/esm/contracts/result/base-report.type.mjs.map +1 -0
- package/esm/contracts/result/base-result.type.d.mts +26 -0
- package/esm/contracts/result/base-result.type.d.mts.map +1 -0
- package/esm/contracts/result/execute-result.type.d.mts +40 -0
- package/esm/contracts/result/execute-result.type.d.mts.map +1 -0
- package/esm/contracts/result/execution-report.type.d.mts +50 -0
- package/esm/contracts/result/execution-report.type.d.mts.map +1 -0
- package/esm/contracts/result/index.d.mts +14 -0
- package/esm/contracts/result/llm-trip.type.d.mts +36 -0
- package/esm/contracts/result/llm-trip.type.d.mts.map +1 -0
- package/esm/contracts/result/model-pricing.type.d.mts +51 -0
- package/esm/contracts/result/model-pricing.type.d.mts.map +1 -0
- package/esm/contracts/result/session-send-result.type.d.mts +29 -0
- package/esm/contracts/result/session-send-result.type.d.mts.map +1 -0
- package/esm/contracts/result/step-result.type.d.mts +43 -0
- package/esm/contracts/result/step-result.type.d.mts.map +1 -0
- package/esm/contracts/result/supervisor-result.type.d.mts +86 -0
- package/esm/contracts/result/supervisor-result.type.d.mts.map +1 -0
- package/esm/contracts/result/tool-call.type.d.mts +44 -0
- package/esm/contracts/result/tool-call.type.d.mts.map +1 -0
- package/esm/contracts/result/usage.type.d.mts +57 -0
- package/esm/contracts/result/usage.type.d.mts.map +1 -0
- package/esm/contracts/result/workflow-result.type.d.mts +45 -0
- package/esm/contracts/result/workflow-result.type.d.mts.map +1 -0
- package/esm/contracts/sdk-adapter.contract.d.mts +77 -0
- package/esm/contracts/sdk-adapter.contract.d.mts.map +1 -0
- package/esm/contracts/stream/stream-event.type.d.mts +68 -0
- package/esm/contracts/stream/stream-event.type.d.mts.map +1 -0
- package/esm/contracts/stream/stream.contract.d.mts +61 -0
- package/esm/contracts/stream/stream.contract.d.mts.map +1 -0
- package/esm/contracts/streaming-tool-guard-config.type.d.mts +78 -0
- package/esm/contracts/streaming-tool-guard-config.type.d.mts.map +1 -0
- package/esm/contracts/supervisor/ack-entry.type.d.mts +97 -0
- package/esm/contracts/supervisor/ack-entry.type.d.mts.map +1 -0
- package/esm/contracts/supervisor/classifier-context.type.d.mts +189 -0
- package/esm/contracts/supervisor/classifier-context.type.d.mts.map +1 -0
- package/esm/contracts/supervisor/dispatch-context.type.d.mts +235 -0
- package/esm/contracts/supervisor/dispatch-context.type.d.mts.map +1 -0
- package/esm/contracts/supervisor/evaluate-context.type.d.mts +124 -0
- package/esm/contracts/supervisor/evaluate-context.type.d.mts.map +1 -0
- package/esm/contracts/supervisor/index.d.mts +16 -0
- package/esm/contracts/supervisor/intent-entry.type.d.mts +254 -0
- package/esm/contracts/supervisor/intent-entry.type.d.mts.map +1 -0
- package/esm/contracts/supervisor/iteration-snapshot.type.d.mts +117 -0
- package/esm/contracts/supervisor/iteration-snapshot.type.d.mts.map +1 -0
- package/esm/contracts/supervisor/next.type.d.mts +25 -0
- package/esm/contracts/supervisor/next.type.d.mts.map +1 -0
- package/esm/contracts/supervisor/route-context.type.d.mts +91 -0
- package/esm/contracts/supervisor/route-context.type.d.mts.map +1 -0
- package/esm/contracts/supervisor/router-entry.type.d.mts +67 -0
- package/esm/contracts/supervisor/router-entry.type.d.mts.map +1 -0
- package/esm/contracts/supervisor/supervisor-config.type.d.mts +356 -0
- package/esm/contracts/supervisor/supervisor-config.type.d.mts.map +1 -0
- package/esm/contracts/supervisor/supervisor-execute-options.type.d.mts +90 -0
- package/esm/contracts/supervisor/supervisor-execute-options.type.d.mts.map +1 -0
- package/esm/contracts/supervisor/supervisor-input.type.d.mts +24 -0
- package/esm/contracts/supervisor/supervisor-input.type.d.mts.map +1 -0
- package/esm/contracts/supervisor/supervisor-snapshot.type.d.mts +49 -0
- package/esm/contracts/supervisor/supervisor-snapshot.type.d.mts.map +1 -0
- package/esm/contracts/supervisor/supervisor-stream-event.type.d.mts +16 -0
- package/esm/contracts/supervisor/supervisor-stream-event.type.d.mts.map +1 -0
- package/esm/contracts/supervisor/supervisor.contract.d.mts +126 -0
- package/esm/contracts/supervisor/supervisor.contract.d.mts.map +1 -0
- package/esm/contracts/system-prompt.contract.d.mts +118 -0
- package/esm/contracts/system-prompt.contract.d.mts.map +1 -0
- package/esm/contracts/tool.contract.d.mts +161 -0
- package/esm/contracts/tool.contract.d.mts.map +1 -0
- package/esm/contracts/workflow/index.d.mts +6 -0
- package/esm/contracts/workflow/next-step-result.type.d.mts +17 -0
- package/esm/contracts/workflow/next-step-result.type.d.mts.map +1 -0
- package/esm/contracts/workflow/retry-config.type.d.mts +19 -0
- package/esm/contracts/workflow/retry-config.type.d.mts.map +1 -0
- package/esm/contracts/workflow/step.contract.d.mts +189 -0
- package/esm/contracts/workflow/step.contract.d.mts.map +1 -0
- package/esm/contracts/workflow/workflow-context.type.d.mts +45 -0
- package/esm/contracts/workflow/workflow-context.type.d.mts.map +1 -0
- package/esm/contracts/workflow/workflow-snapshot.type.d.mts +24 -0
- package/esm/contracts/workflow/workflow-snapshot.type.d.mts.map +1 -0
- package/esm/contracts/workflow/workflow.contract.d.mts +142 -0
- package/esm/contracts/workflow/workflow.contract.d.mts.map +1 -0
- package/esm/errors/agent-cancelled-error.d.mts +49 -0
- package/esm/errors/agent-cancelled-error.d.mts.map +1 -0
- package/esm/errors/agent-cancelled-error.mjs +43 -0
- package/esm/errors/agent-cancelled-error.mjs.map +1 -0
- package/esm/errors/agent-execution-error.d.mts +32 -0
- package/esm/errors/agent-execution-error.d.mts.map +1 -0
- package/esm/errors/agent-execution-error.mjs +35 -0
- package/esm/errors/agent-execution-error.mjs.map +1 -0
- package/esm/errors/agent-max-trips-error.d.mts +45 -0
- package/esm/errors/agent-max-trips-error.d.mts.map +1 -0
- package/esm/errors/agent-max-trips-error.mjs +41 -0
- package/esm/errors/agent-max-trips-error.mjs.map +1 -0
- package/esm/errors/ai-error.d.mts +94 -0
- package/esm/errors/ai-error.d.mts.map +1 -0
- package/esm/errors/ai-error.mjs +68 -0
- package/esm/errors/ai-error.mjs.map +1 -0
- package/esm/errors/budget-exceeded-error.d.mts +41 -0
- package/esm/errors/budget-exceeded-error.d.mts.map +1 -0
- package/esm/errors/budget-exceeded-error.mjs +31 -0
- package/esm/errors/budget-exceeded-error.mjs.map +1 -0
- package/esm/errors/content-filter-error.d.mts +33 -0
- package/esm/errors/content-filter-error.d.mts.map +1 -0
- package/esm/errors/content-filter-error.mjs +28 -0
- package/esm/errors/content-filter-error.mjs.map +1 -0
- package/esm/errors/context-length-exceeded-error.d.mts +38 -0
- package/esm/errors/context-length-exceeded-error.d.mts.map +1 -0
- package/esm/errors/context-length-exceeded-error.mjs +31 -0
- package/esm/errors/context-length-exceeded-error.mjs.map +1 -0
- package/esm/errors/error-category.type.d.mts +30 -0
- package/esm/errors/error-category.type.d.mts.map +1 -0
- package/esm/errors/error-code.type.d.mts +19 -0
- package/esm/errors/error-code.type.d.mts.map +1 -0
- package/esm/errors/guardrail-violation-error.d.mts +55 -0
- package/esm/errors/guardrail-violation-error.d.mts.map +1 -0
- package/esm/errors/guardrail-violation-error.mjs +41 -0
- package/esm/errors/guardrail-violation-error.mjs.map +1 -0
- package/esm/errors/index.d.mts +29 -0
- package/esm/errors/index.mjs +29 -0
- package/esm/errors/invalid-request-error.d.mts +27 -0
- package/esm/errors/invalid-request-error.d.mts.map +1 -0
- package/esm/errors/invalid-request-error.mjs +31 -0
- package/esm/errors/invalid-request-error.mjs.map +1 -0
- package/esm/errors/max-iterations-error.d.mts +31 -0
- package/esm/errors/max-iterations-error.d.mts.map +1 -0
- package/esm/errors/max-iterations-error.mjs +32 -0
- package/esm/errors/max-iterations-error.mjs.map +1 -0
- package/esm/errors/max-steps-exceeded-error.d.mts +20 -0
- package/esm/errors/max-steps-exceeded-error.d.mts.map +1 -0
- package/esm/errors/max-steps-exceeded-error.mjs +21 -0
- package/esm/errors/max-steps-exceeded-error.mjs.map +1 -0
- package/esm/errors/provider-auth-error.d.mts +22 -0
- package/esm/errors/provider-auth-error.d.mts.map +1 -0
- package/esm/errors/provider-auth-error.mjs +26 -0
- package/esm/errors/provider-auth-error.mjs.map +1 -0
- package/esm/errors/provider-error.d.mts +38 -0
- package/esm/errors/provider-error.d.mts.map +1 -0
- package/esm/errors/provider-error.mjs +42 -0
- package/esm/errors/provider-error.mjs.map +1 -0
- package/esm/errors/provider-rate-limit-error.d.mts +34 -0
- package/esm/errors/provider-rate-limit-error.d.mts.map +1 -0
- package/esm/errors/provider-rate-limit-error.mjs +31 -0
- package/esm/errors/provider-rate-limit-error.mjs.map +1 -0
- package/esm/errors/provider-timeout-error.d.mts +22 -0
- package/esm/errors/provider-timeout-error.d.mts.map +1 -0
- package/esm/errors/provider-timeout-error.mjs +26 -0
- package/esm/errors/provider-timeout-error.mjs.map +1 -0
- package/esm/errors/quota-exceeded-error.d.mts +36 -0
- package/esm/errors/quota-exceeded-error.d.mts.map +1 -0
- package/esm/errors/quota-exceeded-error.mjs +40 -0
- package/esm/errors/quota-exceeded-error.mjs.map +1 -0
- package/esm/errors/routing-error.d.mts +23 -0
- package/esm/errors/routing-error.d.mts.map +1 -0
- package/esm/errors/routing-error.mjs +23 -0
- package/esm/errors/routing-error.mjs.map +1 -0
- package/esm/errors/schema-validation-error.d.mts +40 -0
- package/esm/errors/schema-validation-error.d.mts.map +1 -0
- package/esm/errors/schema-validation-error.mjs +37 -0
- package/esm/errors/schema-validation-error.mjs.map +1 -0
- package/esm/errors/step-failed-error.d.mts +20 -0
- package/esm/errors/step-failed-error.d.mts.map +1 -0
- package/esm/errors/step-failed-error.mjs +19 -0
- package/esm/errors/step-failed-error.mjs.map +1 -0
- package/esm/errors/supervisor-cancelled-error.d.mts +29 -0
- package/esm/errors/supervisor-cancelled-error.d.mts.map +1 -0
- package/esm/errors/supervisor-cancelled-error.mjs +29 -0
- package/esm/errors/supervisor-cancelled-error.mjs.map +1 -0
- package/esm/errors/supervisor-drift-error.d.mts +31 -0
- package/esm/errors/supervisor-drift-error.d.mts.map +1 -0
- package/esm/errors/supervisor-drift-error.mjs +30 -0
- package/esm/errors/supervisor-drift-error.mjs.map +1 -0
- package/esm/errors/supervisor-failed-error.d.mts +40 -0
- package/esm/errors/supervisor-failed-error.d.mts.map +1 -0
- package/esm/errors/supervisor-failed-error.mjs +43 -0
- package/esm/errors/supervisor-failed-error.mjs.map +1 -0
- package/esm/errors/supervisor-routing-error.d.mts +44 -0
- package/esm/errors/supervisor-routing-error.d.mts.map +1 -0
- package/esm/errors/supervisor-routing-error.mjs +39 -0
- package/esm/errors/supervisor-routing-error.mjs.map +1 -0
- package/esm/errors/tool-execution-error.d.mts +35 -0
- package/esm/errors/tool-execution-error.d.mts.map +1 -0
- package/esm/errors/tool-execution-error.mjs +32 -0
- package/esm/errors/tool-execution-error.mjs.map +1 -0
- package/esm/errors/workflow-cancelled-error.d.mts +23 -0
- package/esm/errors/workflow-cancelled-error.d.mts.map +1 -0
- package/esm/errors/workflow-cancelled-error.mjs +23 -0
- package/esm/errors/workflow-cancelled-error.mjs.map +1 -0
- package/esm/errors/workflow-drift-error.d.mts +25 -0
- package/esm/errors/workflow-drift-error.d.mts.map +1 -0
- package/esm/errors/workflow-drift-error.mjs +24 -0
- package/esm/errors/workflow-drift-error.mjs.map +1 -0
- package/esm/errors/workflow-error.d.mts +15 -0
- package/esm/errors/workflow-error.d.mts.map +1 -0
- package/esm/errors/workflow-error.mjs +18 -0
- package/esm/errors/workflow-error.mjs.map +1 -0
- package/esm/index.d.mts +137 -0
- package/esm/index.mjs +81 -0
- package/esm/middleware/builtins/budget.d.mts +103 -0
- package/esm/middleware/builtins/budget.d.mts.map +1 -0
- package/esm/middleware/builtins/budget.mjs +100 -0
- package/esm/middleware/builtins/budget.mjs.map +1 -0
- package/esm/middleware/builtins/guardrail.d.mts +92 -0
- package/esm/middleware/builtins/guardrail.d.mts.map +1 -0
- package/esm/middleware/builtins/guardrail.mjs +76 -0
- package/esm/middleware/builtins/guardrail.mjs.map +1 -0
- package/esm/middleware/builtins/semantic-cache.d.mts +106 -0
- package/esm/middleware/builtins/semantic-cache.d.mts.map +1 -0
- package/esm/middleware/builtins/semantic-cache.mjs +149 -0
- package/esm/middleware/builtins/semantic-cache.mjs.map +1 -0
- package/esm/middleware/helpers/compose.d.mts +42 -0
- package/esm/middleware/helpers/compose.d.mts.map +1 -0
- package/esm/middleware/helpers/compose.mjs +52 -0
- package/esm/middleware/helpers/compose.mjs.map +1 -0
- package/esm/middleware/helpers/for-tool.d.mts +55 -0
- package/esm/middleware/helpers/for-tool.d.mts.map +1 -0
- package/esm/middleware/helpers/for-tool.mjs +80 -0
- package/esm/middleware/helpers/for-tool.mjs.map +1 -0
- package/esm/middleware/helpers/index.d.mts +2 -0
- package/esm/middleware/index.d.mts +8 -0
- package/esm/middleware/index.mjs +10 -0
- package/esm/middleware/pipeline.d.mts +70 -0
- package/esm/middleware/pipeline.d.mts.map +1 -0
- package/esm/middleware/pipeline.mjs +100 -0
- package/esm/middleware/pipeline.mjs.map +1 -0
- package/esm/middleware/utils/extract-user-text.d.mts +33 -0
- package/esm/middleware/utils/extract-user-text.d.mts.map +1 -0
- package/esm/middleware/utils/extract-user-text.mjs +40 -0
- package/esm/middleware/utils/extract-user-text.mjs.map +1 -0
- package/esm/middleware/utils/index.d.mts +2 -0
- package/esm/middleware/utils/namespaced-state.d.mts +60 -0
- package/esm/middleware/utils/namespaced-state.d.mts.map +1 -0
- package/esm/middleware/utils/namespaced-state.mjs +60 -0
- package/esm/middleware/utils/namespaced-state.mjs.map +1 -0
- package/esm/mock/index.d.mts +4 -0
- package/esm/mock/index.mjs +5 -0
- package/esm/mock/mock-agent.d.mts +41 -0
- package/esm/mock/mock-agent.d.mts.map +1 -0
- package/esm/mock/mock-agent.mjs +39 -0
- package/esm/mock/mock-agent.mjs.map +1 -0
- package/esm/mock/mock-config.type.d.mts +31 -0
- package/esm/mock/mock-config.type.d.mts.map +1 -0
- package/esm/mock/mock-model.d.mts +94 -0
- package/esm/mock/mock-model.d.mts.map +1 -0
- package/esm/mock/mock-model.mjs +150 -0
- package/esm/mock/mock-model.mjs.map +1 -0
- package/esm/mock/mock-sdk.d.mts +25 -0
- package/esm/mock/mock-sdk.d.mts.map +1 -0
- package/esm/mock/mock-sdk.mjs +37 -0
- package/esm/mock/mock-sdk.mjs.map +1 -0
- package/esm/supervisor/as-tool.d.mts +45 -0
- package/esm/supervisor/as-tool.d.mts.map +1 -0
- package/esm/supervisor/as-tool.mjs +80 -0
- package/esm/supervisor/as-tool.mjs.map +1 -0
- package/esm/supervisor/cancellation.d.mts +12 -0
- package/esm/supervisor/cancellation.d.mts.map +1 -0
- package/esm/supervisor/cancellation.mjs +22 -0
- package/esm/supervisor/cancellation.mjs.map +1 -0
- package/esm/supervisor/decide.mjs +272 -0
- package/esm/supervisor/decide.mjs.map +1 -0
- package/esm/supervisor/emitter.d.mts +27 -0
- package/esm/supervisor/emitter.d.mts.map +1 -0
- package/esm/supervisor/emitter.mjs +47 -0
- package/esm/supervisor/emitter.mjs.map +1 -0
- package/esm/supervisor/entries.d.mts +141 -0
- package/esm/supervisor/entries.d.mts.map +1 -0
- package/esm/supervisor/entries.mjs +206 -0
- package/esm/supervisor/entries.mjs.map +1 -0
- package/esm/supervisor/execution.d.mts +637 -0
- package/esm/supervisor/execution.d.mts.map +1 -0
- package/esm/supervisor/execution.mjs +1868 -0
- package/esm/supervisor/execution.mjs.map +1 -0
- package/esm/supervisor/index.d.mts +11 -0
- package/esm/supervisor/index.mjs +12 -0
- package/esm/supervisor/router-prompt.d.mts +56 -0
- package/esm/supervisor/router-prompt.d.mts.map +1 -0
- package/esm/supervisor/router-prompt.mjs +52 -0
- package/esm/supervisor/router-prompt.mjs.map +1 -0
- package/esm/supervisor/signature.d.mts +28 -0
- package/esm/supervisor/signature.d.mts.map +1 -0
- package/esm/supervisor/signature.mjs +89 -0
- package/esm/supervisor/signature.mjs.map +1 -0
- package/esm/supervisor/snapshot.d.mts +54 -0
- package/esm/supervisor/snapshot.d.mts.map +1 -0
- package/esm/supervisor/snapshot.mjs +77 -0
- package/esm/supervisor/snapshot.mjs.map +1 -0
- package/esm/supervisor/supervisor-stream.d.mts +29 -0
- package/esm/supervisor/supervisor-stream.d.mts.map +1 -0
- package/esm/supervisor/supervisor-stream.mjs +89 -0
- package/esm/supervisor/supervisor-stream.mjs.map +1 -0
- package/esm/supervisor/supervisor.d.mts +27 -0
- package/esm/supervisor/supervisor.d.mts.map +1 -0
- package/esm/supervisor/supervisor.mjs +138 -0
- package/esm/supervisor/supervisor.mjs.map +1 -0
- package/esm/system-prompt/instruction.d.mts +52 -0
- package/esm/system-prompt/instruction.d.mts.map +1 -0
- package/esm/system-prompt/instruction.mjs +57 -0
- package/esm/system-prompt/instruction.mjs.map +1 -0
- package/esm/system-prompt/persona.d.mts +52 -0
- package/esm/system-prompt/persona.d.mts.map +1 -0
- package/esm/system-prompt/persona.mjs +57 -0
- package/esm/system-prompt/persona.mjs.map +1 -0
- package/esm/system-prompt/render-placeholders.d.mts +31 -0
- package/esm/system-prompt/render-placeholders.d.mts.map +1 -0
- package/esm/system-prompt/render-placeholders.mjs +58 -0
- package/esm/system-prompt/render-placeholders.mjs.map +1 -0
- package/esm/system-prompt/system-prompt.d.mts +117 -0
- package/esm/system-prompt/system-prompt.d.mts.map +1 -0
- package/esm/system-prompt/system-prompt.mjs +137 -0
- package/esm/system-prompt/system-prompt.mjs.map +1 -0
- package/esm/tool/index.d.mts +1 -0
- package/esm/tool/index.mjs +3 -0
- package/esm/tool/tool.d.mts +64 -0
- package/esm/tool/tool.d.mts.map +1 -0
- package/esm/tool/tool.mjs +184 -0
- package/esm/tool/tool.mjs.map +1 -0
- package/esm/utils/compute-cost.d.mts +58 -0
- package/esm/utils/compute-cost.d.mts.map +1 -0
- package/esm/utils/compute-cost.mjs +87 -0
- package/esm/utils/compute-cost.mjs.map +1 -0
- package/esm/utils/extract-json-payload.d.mts +33 -0
- package/esm/utils/extract-json-payload.d.mts.map +1 -0
- package/esm/utils/extract-json-payload.mjs +39 -0
- package/esm/utils/extract-json-payload.mjs.map +1 -0
- package/esm/utils/generate-run-id.d.mts +20 -0
- package/esm/utils/generate-run-id.d.mts.map +1 -0
- package/esm/utils/generate-run-id.mjs +23 -0
- package/esm/utils/generate-run-id.mjs.map +1 -0
- package/esm/utils/index.d.mts +9 -0
- package/esm/utils/index.mjs +11 -0
- package/esm/utils/json-schema.d.mts +57 -0
- package/esm/utils/json-schema.d.mts.map +1 -0
- package/esm/utils/json-schema.mjs +89 -0
- package/esm/utils/json-schema.mjs.map +1 -0
- package/esm/utils/prepare-attachment-part.d.mts +36 -0
- package/esm/utils/prepare-attachment-part.d.mts.map +1 -0
- package/esm/utils/prepare-attachment-part.mjs +140 -0
- package/esm/utils/prepare-attachment-part.mjs.map +1 -0
- package/esm/utils/resolve-attachment.d.mts +33 -0
- package/esm/utils/resolve-attachment.d.mts.map +1 -0
- package/esm/utils/resolve-attachment.mjs +73 -0
- package/esm/utils/resolve-attachment.mjs.map +1 -0
- package/esm/utils/safe-json-parse.d.mts +14 -0
- package/esm/utils/safe-json-parse.d.mts.map +1 -0
- package/esm/utils/safe-json-parse.mjs +22 -0
- package/esm/utils/safe-json-parse.mjs.map +1 -0
- package/esm/utils/stamp-report-lineage.d.mts +61 -0
- package/esm/utils/stamp-report-lineage.d.mts.map +1 -0
- package/esm/utils/stamp-report-lineage.mjs +47 -0
- package/esm/utils/stamp-report-lineage.mjs.map +1 -0
- package/esm/utils/token-count.d.mts +15 -0
- package/esm/utils/token-count.d.mts.map +1 -0
- package/esm/utils/token-count.mjs +18 -0
- package/esm/utils/token-count.mjs.map +1 -0
- package/esm/workflow/as-tool.mjs +55 -0
- package/esm/workflow/as-tool.mjs.map +1 -0
- package/esm/workflow/cancellation.mjs +44 -0
- package/esm/workflow/cancellation.mjs.map +1 -0
- package/esm/workflow/emitter.mjs +40 -0
- package/esm/workflow/emitter.mjs.map +1 -0
- package/esm/workflow/engine.mjs +362 -0
- package/esm/workflow/engine.mjs.map +1 -0
- package/esm/workflow/index.d.mts +3 -0
- package/esm/workflow/index.mjs +5 -0
- package/esm/workflow/retry.mjs +32 -0
- package/esm/workflow/retry.mjs.map +1 -0
- package/esm/workflow/router.mjs +56 -0
- package/esm/workflow/router.mjs.map +1 -0
- package/esm/workflow/run-scoped-emitter.mjs +32 -0
- package/esm/workflow/run-scoped-emitter.mjs.map +1 -0
- package/esm/workflow/signature.d.mts +7 -0
- package/esm/workflow/signature.d.mts.map +1 -0
- package/esm/workflow/signature.mjs +34 -0
- package/esm/workflow/signature.mjs.map +1 -0
- package/esm/workflow/snapshot.mjs +71 -0
- package/esm/workflow/snapshot.mjs.map +1 -0
- package/esm/workflow/state.mjs +37 -0
- package/esm/workflow/state.mjs.map +1 -0
- package/esm/workflow/step-runner.mjs +371 -0
- package/esm/workflow/step-runner.mjs.map +1 -0
- package/esm/workflow/step.d.mts +19 -0
- package/esm/workflow/step.d.mts.map +1 -0
- package/esm/workflow/step.mjs +37 -0
- package/esm/workflow/step.mjs.map +1 -0
- package/esm/workflow/workflow.d.mts +12 -0
- package/esm/workflow/workflow.d.mts.map +1 -0
- package/esm/workflow/workflow.mjs +116 -0
- package/esm/workflow/workflow.mjs.map +1 -0
- package/llms-full.txt +2293 -0
- package/llms.txt +20 -0
- package/package.json +37 -0
- package/skills/README.md +53 -0
- package/skills/ai-basics/SKILL.md +95 -0
- package/skills/attach-ai-middleware/SKILL.md +184 -0
- package/skills/define-ai-tool/SKILL.md +186 -0
- package/skills/embed-text/SKILL.md +105 -0
- package/skills/handle-ai-errors/SKILL.md +194 -0
- package/skills/log-ai-calls/SKILL.md +141 -0
- package/skills/persist-ai-data/SKILL.md +207 -0
- package/skills/pick-ai-provider/SKILL.md +164 -0
- package/skills/run-ai-agent/SKILL.md +267 -0
- package/skills/run-ai-workflow/SKILL.md +258 -0
- package/skills/run-supervisor/SKILL.md +309 -0
- package/skills/write-system-prompt/SKILL.md +129 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent.mjs","names":[],"sources":["../../../../../../@warlock.js/ai/src/agent/agent.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { log, type Logger } from \"@warlock.js/logger\";\nimport type {\n AgentContract,\n AgentEventHandler,\n AgentEventMap,\n AgentExecuteOptions,\n AgentResult,\n BaseReport,\n CompleteEvent,\n FinishReason,\n LLMTrip,\n Message,\n MiddlewareExecuteContext,\n MiddlewareState,\n MiddlewareToolContext,\n MiddlewareTripContext,\n ModelResponse,\n ModelToolCallRequest,\n StreamContract,\n StreamEventBody,\n StreamingToolGuardConfig,\n ToolCall,\n ToolEventMeta,\n Usage,\n UsageEvent,\n WithoutIdentity,\n} from \"../contracts\";\nimport {\n AgentCancelledError,\n AgentExecutionError,\n AgentMaxTripsError,\n AIError,\n SchemaValidationError,\n} from \"../errors\";\nimport { runPipeline } from \"../middleware\";\nimport type { ToolContract, ToolInvokeResult } from \"../tool/tool\";\nimport {\n accumulateCost,\n computeCost,\n extractJsonPayload,\n generateRunId,\n safeJsonParse,\n stampReportLineage,\n} from \"../utils\";\nimport type { AgentConfig } from \"./agent-config.type\";\nimport { buildAgentInputMessages } from \"./agent-input-builder\";\nimport { logAgentEvent } from \"./agent-log-event\";\nimport { createAgentStream, type StreamController } from \"./agent-stream\";\nimport { agentEventToStreamEvent } from \"./agent-to-stream-event\";\nimport { JsonStreamGuard } from \"./json-stream-guard\";\n\nconst LOG_MODULE = \"ai.agent\";\n\n/**\n * Detect abort-flavored errors surfaced by SDK HTTP layers — the\n * DOM `AbortError`, axios `ERR_CANCELED`, node-fetch's own\n * `AbortError`. Used to classify them as cancellation rather than\n * generic agent-execution failures.\n */\nfunction isAbortLike(err: unknown): boolean {\n if (!err || typeof err !== \"object\") return false;\n\n const e = err as { name?: unknown; code?: unknown };\n\n return e.name === \"AbortError\" || e.code === \"ERR_CANCELED\" || e.code === \"ABORT_ERR\";\n}\n\n/**\n * Readable synthetic name for agents constructed without an explicit\n * `name`. Format: `anon_<provider>_<model>[_<tool1>+<tool2>+...]` —\n * deterministic (same config → same name across restarts) and\n * human-readable in logs / workflow snapshots.\n *\n * Keeps drift detection honest for the `ai.agent({ model })`\n * one-liner without punishing it with a hashed id nobody can read.\n */\nfunction synthesizeAgentName<T>(config: AgentConfig<T>): string {\n const provider = (config.model as unknown as { provider?: string })?.provider ?? \"unknown\";\n const model = config.model?.name ?? \"unknown\";\n const tools = (config.tools ?? [])\n .map((tool) => tool.name)\n .sort()\n .join(\"+\");\n\n const base = `anon_${sanitize(provider)}_${sanitize(model)}`;\n return tools ? `${base}_${sanitize(tools, { keepPlus: true })}` : base;\n}\n\nfunction sanitize(value: string, opts: { keepPlus?: boolean } = {}): string {\n const allowed = opts.keepPlus ? /[^a-zA-Z0-9._+-]/g : /[^a-zA-Z0-9._-]/g;\n return value.replace(allowed, \"-\");\n}\n\n/**\n * Authoring-time check on the middleware array. Throws an\n * `AgentExecutionError` with `context: { authoring: true }` the\n * moment an invalid entry is found — the agent factory surface is\n * where config bugs should surface, not ten trips into a run.\n *\n * Validates:\n * - Every entry is a non-null object with a non-empty string `name`.\n * - No two entries share the same `name` (would silently collide on\n * `ctx.state` keys and produce impossible-to-debug behavior).\n *\n * Does NOT validate that hook maps contain callable functions —\n * that would catch late-binding bugs but also reject legitimate\n * patterns like `before` being conditionally `undefined`. Runtime\n * dispatch handles missing hooks safely.\n */\nfunction validateMiddleware(middleware: ReadonlyArray<unknown> | undefined): void {\n if (!middleware || middleware.length === 0) {\n return;\n }\n\n const seen = new Set<string>();\n\n for (let index = 0; index < middleware.length; index++) {\n const entry = middleware[index];\n\n if (!entry || typeof entry !== \"object\") {\n throw new AgentExecutionError(\n `middleware[${index}] must be an object; received ${entry === null ? \"null\" : typeof entry}`,\n { context: { authoring: true, index } },\n );\n }\n\n const name = (entry as { name?: unknown }).name;\n\n if (typeof name !== \"string\" || name.length === 0) {\n throw new AgentExecutionError(`middleware[${index}] must have a non-empty string \"name\"`, {\n context: { authoring: true, index },\n });\n }\n\n if (seen.has(name)) {\n throw new AgentExecutionError(\n `duplicate middleware name \"${name}\" — each middleware needs a unique name so ctx.state keys do not collide`,\n { context: { authoring: true, index, name } },\n );\n }\n\n seen.add(name);\n }\n}\n\n/**\n * Creates an executable AI agent from the given configuration.\n *\n * The agent runs a bounded trip loop: each trip calls the model, dispatches\n * any requested tool calls, then loops until the model stops or `maxTrips`\n * is reached. Each `execute()` / `stream()` call spawns a fresh internal\n * `Execution` instance — the factory itself holds no state across calls.\n *\n * `execute()` never throws — any error is attached to the returned result\n * under `result.error`. `stream()` surfaces errors both on the terminal\n * `error` stream event and via the `stream.result` promise.\n *\n * @example\n * const myAgent = agent({\n * model: openai.model({ name: \"gpt-4o\" }),\n * systemPrompt: \"You are a helpful assistant.\",\n * tools: [searchTool],\n * });\n *\n * const result = await myAgent.execute(\"What is the capital of Egypt?\");\n *\n * @example\n * const stream = myAgent.stream(\"Write a haiku about Cairo.\");\n *\n * for await (const event of stream) {\n * if (event.type === \"streaming\") process.stdout.write(event.delta);\n * }\n *\n * const result = await stream.result;\n */\nexport function agent<TOutput = unknown>(config: AgentConfig<TOutput>): AgentContract<TOutput> {\n // Authoring-time validation of the middleware array. Rejects two\n // classes of bug that would otherwise surface as opaque failures\n // mid-execution: (a) entries that aren't proper middleware\n // objects (null, undefined, missing `name`), and (b) two\n // middlewares sharing the same `name`, which would silently\n // collide on `ctx.state` keys. Fail fast, fail loud — per the\n // authoring-time rules in `domains/ai/conventions/errors.md`.\n validateMiddleware(config.middleware);\n\n // Resolve the agent's identity. Explicit `name` wins; otherwise we\n // synthesize a DETERMINISTIC fingerprint from the config's\n // identity-defining fields (model + provider + tool names). Same\n // config across process restarts produces the same synthetic name,\n // so workflow signature drift detection stays honest for agents\n // composed into workflows without explicit names.\n const isAnonymous = !config.name || typeof config.name !== \"string\";\n const name = isAnonymous ? synthesizeAgentName(config) : (config.name as string);\n const resolvedConfig: AgentConfig<TOutput> = { ...config, name };\n\n // Instance-level handlers registered via `.on()`. Stored here\n // (factory-scope) so every `execute()` / `stream()` call on this\n // agent sees the same set. Each event name gets its own Set so\n // `off()` can remove a specific handler without disturbing others.\n const instanceHandlers = new Map<\n keyof AgentEventMap,\n Set<AgentEventHandler<keyof AgentEventMap>>\n >();\n\n function on<K extends keyof AgentEventMap>(event: K, handler: AgentEventHandler<K>): () => void {\n const existing = instanceHandlers.get(event);\n const bucket = existing ?? new Set<AgentEventHandler<keyof AgentEventMap>>();\n\n if (!existing) {\n instanceHandlers.set(event, bucket);\n }\n\n bucket.add(handler as AgentEventHandler<keyof AgentEventMap>);\n\n return () => off(event, handler);\n }\n\n function off<K extends keyof AgentEventMap>(event: K, handler: AgentEventHandler<K>): void {\n const bucket = instanceHandlers.get(event);\n\n if (!bucket) {\n return;\n }\n\n bucket.delete(handler as AgentEventHandler<keyof AgentEventMap>);\n\n if (bucket.size === 0) {\n instanceHandlers.delete(event);\n }\n }\n\n return {\n name,\n isAnonymous,\n description: config.description,\n async execute(\n input: string,\n options?: AgentExecuteOptions<TOutput>,\n ): Promise<AgentResult<TOutput>> {\n return new Execution<TOutput>(\n resolvedConfig,\n input,\n options,\n undefined,\n instanceHandlers,\n ).run();\n },\n\n stream(\n input: string,\n options?: AgentExecuteOptions<TOutput>,\n ): StreamContract<AgentResult<TOutput>> {\n const { controller, stream } = createAgentStream<AgentResult<TOutput>>();\n\n const execution = new Execution<TOutput>(\n resolvedConfig,\n input,\n options,\n controller,\n instanceHandlers,\n );\n\n void execution.run();\n\n return stream;\n },\n\n on,\n off,\n };\n}\n\n/**\n * Name → handler-set map shared between the `agent()` factory and its\n * per-call `Execution`. Each `.on()` registration mutates this map;\n * every `Execution` reads from the same reference so additions and\n * removals take effect mid-flight.\n */\ntype InstanceHandlerMap = Map<keyof AgentEventMap, Set<AgentEventHandler<keyof AgentEventMap>>>;\n\n/**\n * Per-call driver that owns the full lifecycle of a single\n * `agent.execute()` or `agent.stream()` invocation.\n *\n * **Role.** An `Execution` is the short-lived state container and phase\n * orchestrator for one agent run. The public `agent()` factory stays purely\n * functional — all mutable bookkeeping (trips, tool calls, usage totals,\n * message history, terminal error, parsed output) lives here so each call\n * gets a fresh, isolated instance.\n *\n * **Responsibility.**\n * - Owns: building the initial message list, driving the bounded trip loop,\n * dispatching tool calls safely, parsing the final output against the\n * caller's schema, emitting lifecycle events (to both the user handler\n * and, in stream mode, the `StreamController`), and producing the\n * `AgentResult`.\n * - Does NOT own: how the model produces responses (delegated to\n * `ModelContract.complete` / `ModelContract.stream`), how tools execute\n * (delegated to `ToolContract.invoke`), the async-queue plumbing for\n * streaming (delegated to `createAgentStream`), or any cross-call state\n * (factory-level concerns live in `agent()`).\n *\n * Streaming mode is opt-in via the fourth constructor argument: pass a\n * `StreamController` and every event is mirrored into it while model calls\n * are driven via `model.stream()` instead of `model.complete()`. The public\n * contract of `execute()` says it never throws — `Execution` enforces that\n * by funneling every unexpected error into `this.error` and returning a\n * well-formed result regardless of what went wrong.\n *\n * Not exported — consumers interact only with the `agent()` factory (see\n * §4.2 of code-style.md — \"per-call execution state across phases\").\n *\n * @example\n * // Non-streaming — inside agent.execute():\n * const result = await new Execution(config, input, options).run();\n *\n * @example\n * // Streaming — inside agent.stream():\n * const { controller, stream } = createAgentStream();\n * void new Execution(config, input, options, controller).run();\n * return stream;\n */\nclass Execution<TOutput> {\n private readonly trips: LLMTrip[] = [];\n private readonly toolCalls: ToolCall[] = [];\n private readonly usage: Usage = { input: 0, output: 0, total: 0 };\n private readonly messages: Message[] = [];\n private readonly maxTrips: number;\n private readonly startedAt = new Date();\n private readonly start = performance.now();\n private readonly runId = generateRunId(\"agent\");\n private readonly logger: Logger = log;\n private readonly middleware: ReadonlyArray<\n NonNullable<AgentConfig<TOutput>[\"middleware\"]>[number]\n >;\n private readonly middlewareState: MiddlewareState = new Map();\n\n private error?: AIError;\n private data?: TOutput;\n private responseSchema?: Record<string, unknown>;\n\n public constructor(\n private readonly config: AgentConfig<TOutput>,\n private readonly input: string,\n private readonly options?: AgentExecuteOptions<TOutput>,\n private readonly streamController?: StreamController<AgentResult<TOutput>>,\n private readonly instanceHandlers?: InstanceHandlerMap,\n ) {\n this.maxTrips = config.maxTrips ?? 10;\n this.middleware = config.middleware ?? [];\n }\n\n /**\n * Base middleware context shared by every level. `state` is the\n * single mutable bag threaded through `execute`, `trip`, and `tool`\n * hooks for the lifetime of this execution — fresh per `execute()`\n * call, never reused across runs.\n */\n private buildExecuteContext(): MiddlewareExecuteContext {\n return {\n agent: {\n name: this.config.name ?? this.config.model.name,\n isAnonymous: !this.config.name,\n },\n model: {\n name: this.config.model.name,\n provider: this.config.model.provider,\n },\n input: this.input,\n options: this.options as AgentExecuteOptions<unknown> | undefined,\n state: this.middlewareState,\n signal: this.options?.signal,\n };\n }\n\n /**\n * Entry point for a single agent execution. Wraps the real work\n * (`runCore`) in the `execute`-level middleware pipeline, then\n * emits the terminal `agent.completed` / `agent.error` events and\n * closes the stream (if any) with the post-pipeline result — so\n * middleware that short-circuits or transforms the final result\n * still produces a well-formed public outcome.\n *\n * Must never throw: any error that escapes the pipeline is\n * converted into an `AgentResult` with `error` populated before\n * returning, preserving the `agent.execute()` public contract.\n */\n public async run(): Promise<AgentResult<TOutput>> {\n const context = this.buildExecuteContext();\n\n let result: AgentResult<TOutput>;\n\n try {\n result = (await runPipeline(\n this.middleware,\n \"execute\",\n context,\n () => this.runCore(),\n this.logger,\n )) as AgentResult<TOutput>;\n } catch (thrown) {\n this.error = this.toAIError(thrown);\n result = this.buildResult();\n }\n\n if (result.error) {\n this.emit(\"agent.error\", { error: result.error });\n }\n\n this.emit(\"agent.completed\", { result });\n\n // Fire the `onComplete` hook with a flat payload (runId +\n // durationMs pre-extracted) for audit-log consumers. Awaited but\n // errors swallowed so consumer bugs cannot crash the agent or\n // interfere with the result returned to the caller.\n await this.fireCompleteHook(result);\n\n this.streamController?.end(result);\n\n return result;\n }\n\n /**\n * Inner body wrapped by the `execute`-level pipeline. Drives the\n * full lifecycle — build messages → emit starting → run trip loop\n * → parse output → build result. Catches any unexpected throw and\n * funnels it into `this.error` so the returned result is always\n * well-formed; `execute`-level `after` hooks receive the result,\n * with `error` populated when things went wrong.\n */\n private async runCore(): Promise<AgentResult<TOutput>> {\n try {\n await this.buildInitialMessages();\n\n this.emit(\"agent.starting\", { input: this.input });\n\n await this.runTripLoop();\n\n const parseOutcome = await this.parseOutput();\n\n if (parseOutcome === \"failed\" && this.options?.repair) {\n await this.runRepairLoop();\n }\n } catch (thrown) {\n this.error = this.toAIError(thrown);\n }\n\n return this.buildResult();\n }\n\n /**\n * Resolve the system prompt (string or `SystemPromptContract`), merge\n * placeholders from config + execute options, inject a structured-output\n * instruction when the caller wants typed output but the model can't\n * enforce it natively, prepend any conversation history, and append the\n * user input. Produces the initial `messages` array the first trip sends\n * to the model. Runs exactly once per execution.\n */\n private async buildInitialMessages(): Promise<void> {\n const { messages, responseSchema } = await buildAgentInputMessages({\n config: this.config,\n input: this.input,\n options: this.options,\n });\n this.messages.push(...messages);\n this.responseSchema = responseSchema;\n }\n\n /**\n * Drive sequential trips up to `maxTrips`. Each trip may stop the loop\n * naturally (model returned a non-tool-call finish), abort it (model\n * threw), or continue it (model requested tools). When the loop exits\n * after the cap without a natural stop, records a \"Max trips exceeded\"\n * error so the caller can distinguish runaway tool loops from a real result.\n */\n private async runTripLoop(): Promise<void> {\n for (let tripIndex = 0; tripIndex < this.maxTrips; tripIndex++) {\n if (this.options?.signal?.aborted) {\n this.error = this.makeCancelledError();\n return;\n }\n\n const tripInput = tripIndex === 0 ? this.input : \"[tool results]\";\n const outcome = await this.runTrip(tripIndex, tripInput);\n\n if (outcome === \"error\" || outcome === \"stop\") {\n return;\n }\n }\n\n const lastTrip = this.trips[this.trips.length - 1];\n\n if (lastTrip?.finishReason === \"tool_calls\") {\n this.error = new AgentMaxTripsError(\"Max trips exceeded\", {\n maxTrips: this.maxTrips,\n });\n }\n }\n\n /**\n * Execute one round-trip to the model. Aggregates usage into the running\n * total, dispatches any requested tool calls, appends the assistant +\n * tool-result messages for the next trip, and records an `LLMTrip`.\n * Returns an outcome that tells `runTripLoop` whether to continue, stop,\n * or abort.\n */\n private async runTrip(\n tripIndex: number,\n tripInput: string,\n ): Promise<\"continue\" | \"stop\" | \"error\"> {\n this.emit(\"agent.trip.started\", { tripIndex, input: tripInput });\n\n const tripStartedAt = new Date();\n const tripStart = performance.now();\n\n let response: ModelResponse;\n\n try {\n response = await this.runTripThroughPipeline(tripIndex);\n } catch (thrown) {\n this.error = this.toAIError(thrown);\n\n const failedTrip: LLMTrip = {\n index: tripIndex,\n input: tripInput,\n output: \"\",\n finishReason: \"error\",\n startedAt: tripStartedAt.toISOString(),\n endedAt: new Date().toISOString(),\n duration: performance.now() - tripStart,\n usage: { input: 0, output: 0, total: 0 },\n error: this.error,\n };\n\n this.trips.push(failedTrip);\n\n this.emit(\"agent.trip.completed\", { trip: failedTrip });\n this.emit(\"agent.error\", { error: this.error });\n\n return \"error\";\n }\n\n // Attach per-trip cost breakdown using the model's pricing table\n // (when configured). Done at the framework boundary so stored trip\n // records carry historical cost — Panoptic and other archive\n // consumers never re-derive against today's pricing, and the\n // input/output/cached split stays queryable without joining to a\n // pricing table at all.\n if (response.usage.cost === undefined) {\n response.usage.cost = computeCost(response.usage, this.config.model.pricing);\n }\n\n this.usage.input += response.usage.input;\n this.usage.output += response.usage.output;\n this.usage.total += response.usage.total;\n if (response.usage.cachedTokens !== undefined) {\n this.usage.cachedTokens = (this.usage.cachedTokens ?? 0) + response.usage.cachedTokens;\n }\n this.usage.cost = accumulateCost(this.usage.cost, response.usage.cost);\n\n // Fire the `onUsage` hook with a flat, pre-packaged payload so\n // cost-ledger code receives stable identity (runId, model+provider)\n // without joining from elsewhere. Awaited but errors swallowed.\n await this.fireUsageHook(tripIndex, response.usage);\n\n const isToolCallTrip =\n response.finishReason === \"tool_calls\" &&\n response.toolCalls !== undefined &&\n response.toolCalls.length > 0;\n\n const tripToolCalls: ToolCall[] = [];\n\n if (isToolCallTrip) {\n this.messages.push({\n role: \"assistant\",\n content: response.content,\n toolCalls: response.toolCalls,\n });\n\n for (const toolCallRequest of response.toolCalls!) {\n const record = await this.dispatchToolCall(toolCallRequest, tripIndex);\n\n tripToolCalls.push(record);\n }\n }\n\n const trip: LLMTrip = {\n index: tripIndex,\n input: tripInput,\n output: response.content,\n finishReason: response.finishReason,\n startedAt: tripStartedAt.toISOString(),\n endedAt: new Date().toISOString(),\n duration: performance.now() - tripStart,\n usage: response.usage,\n toolCalls: tripToolCalls.length > 0 ? tripToolCalls : undefined,\n };\n\n this.trips.push(trip);\n\n this.emit(\"agent.trip.completed\", { trip });\n\n if (!isToolCallTrip) {\n return \"stop\";\n }\n\n // Terminate the trip loop when EVERY tool call this trip is\n // `mode: \"silent\"`. Silent tools don't feed their result back\n // to the model — the prose the model streamed alongside the\n // tool call IS the final reply. The \"all\" rule is load-bearing:\n // if any feedback tool was called too, its result still needs\n // to round-trip, so we must continue.\n //\n // Composite (`asTool`-wrapped) tools never set `mode: \"silent\"`\n // in v1 — silent-composite mechanics are deferred per plan\n // 2026-05-07-silent-tools.md (Q4). They behave as feedback.\n const allSilent = response.toolCalls!.every((request) => {\n const registered = this.config.tools?.find((tool) => tool.name === request.name);\n return registered?.mode === \"silent\";\n });\n\n return allSilent ? \"stop\" : \"continue\";\n }\n\n /**\n * Route `getModelResponse` through the `trip`-level middleware\n * pipeline. `trip.before` hooks can short-circuit the trip by\n * returning a synthetic `ModelResponse` (semantic cache hit).\n * `trip.after` hooks can transform the response before the trip\n * record is built or any tool calls are dispatched. `trip.onError`\n * hooks can recover from provider failures (fallback chain).\n */\n private async runTripThroughPipeline(tripIndex: number): Promise<ModelResponse> {\n const context: MiddlewareTripContext = {\n ...this.buildExecuteContext(),\n tripIndex,\n messages: this.messages,\n };\n\n return (await runPipeline(\n this.middleware,\n \"trip\",\n context,\n () => this.getModelResponse(tripIndex),\n this.logger,\n )) as ModelResponse;\n }\n\n /**\n * Produce the `ModelResponse` for the current trip. In non-streaming\n * mode, delegates straight to `model.complete()`. In streaming mode,\n * drains `model.stream()` while emitting `streaming` events per delta\n * and accumulates the chunks into the same `ModelResponse` shape, so the\n * rest of the trip pipeline (tool dispatch, trip record, usage\n * aggregation) stays identical between the two modes.\n */\n private async getModelResponse(tripIndex: number): Promise<ModelResponse> {\n const callOptions = {\n ...this.config.modelOptions,\n tools: this.config.tools ?? [],\n ...(this.responseSchema ? { responseSchema: this.responseSchema } : {}),\n ...(this.options?.signal ? { signal: this.options.signal } : {}),\n };\n\n if (!this.streamController) {\n return this.config.model.complete(this.messages, callOptions);\n }\n\n let content = \"\";\n let finishReason: FinishReason = \"stop\";\n let usage: Usage = { input: 0, output: 0, total: 0 };\n const toolCalls: ModelToolCallRequest[] = [];\n const recoveredCalls: ModelToolCallRequest[] = [];\n\n const guardConfig = this.resolveStreamingToolGuard();\n const guard = guardConfig\n ? new JsonStreamGuard({\n tools: (this.config.tools ?? []) as ReadonlyArray<ToolContract<unknown, unknown>>,\n maxBufferBytes: guardConfig.maxBufferBytes,\n onSafeDelta: (delta) => {\n content += delta;\n\n this.emit(\"agent.trip.streaming\", { delta, tripIndex });\n },\n onRecoveredCall: (request) => {\n recoveredCalls.push(request);\n },\n })\n : undefined;\n\n for await (const chunk of this.config.model.stream(this.messages, callOptions)) {\n // Mid-stream abort — break out cleanly instead of continuing to\n // consume the iterator. The underlying fetch is already\n // cancelled via `signal` forwarded in callOptions; this covers\n // adapters that don't honor signal natively and keeps mock\n // models consistent under cancellation tests.\n if (this.options?.signal?.aborted) {\n throw this.makeCancelledError();\n }\n\n if (chunk.type === \"delta\") {\n if (guard) {\n await guard.feed(chunk.content);\n } else {\n content += chunk.content;\n\n this.emit(\"agent.trip.streaming\", { delta: chunk.content, tripIndex });\n }\n\n continue;\n }\n\n if (chunk.type === \"tool-call\") {\n toolCalls.push({\n id: chunk.id,\n name: chunk.name,\n input: chunk.input,\n ...(chunk.providerMetadata ? { providerMetadata: chunk.providerMetadata } : {}),\n });\n\n continue;\n }\n\n finishReason = chunk.finishReason;\n usage = chunk.usage;\n }\n\n if (guard) {\n await guard.finalize();\n }\n\n // Dedupe synthesized calls against real ones the provider streamed\n // structurally — the model occasionally emits BOTH channels for\n // the same call (real tool-call chunk + narrated JSON envelope).\n // Real wins; the synthesized duplicate is dropped so dispatch\n // doesn't run twice. See plan 2026-05-22 §Q5.\n const dedupedRecovered = recoveredCalls.filter(\n (recovered) => !isDuplicateToolCall(recovered, toolCalls),\n );\n\n const mergedToolCalls = [...toolCalls, ...dedupedRecovered];\n\n // When the guard recovered any calls but the model reported a\n // natural `\"stop\"`, override to `\"tool_calls\"` so the agent's\n // dispatch loop (`runTrip` → `isToolCallTrip`) actually fires.\n // Without this the guard silently suppresses the leaked JSON but\n // never dispatches the real action — chips never render.\n const resolvedFinishReason: FinishReason =\n dedupedRecovered.length > 0 && finishReason === \"stop\" ? \"tool_calls\" : finishReason;\n\n return {\n content,\n finishReason: resolvedFinishReason,\n usage,\n toolCalls: mergedToolCalls.length > 0 ? mergedToolCalls : undefined,\n };\n }\n\n /**\n * Resolve the effective `streamingToolGuard` for this trip.\n * Per-call options win over the agent-level config when the key is\n * explicitly present on options (including the explicit `undefined`\n * \"disable for this call\" form). Returns `undefined` when no guard\n * should run.\n */\n private resolveStreamingToolGuard(): StreamingToolGuardConfig | undefined {\n if (\n this.options !== undefined &&\n Object.prototype.hasOwnProperty.call(this.options, \"streamingToolGuard\")\n ) {\n return this.options.streamingToolGuard;\n }\n\n return this.config.streamingToolGuard;\n }\n\n /**\n * Dispatch a single tool call requested by the model. Looks up the tool\n * by name, invokes it via the safe `ToolContract.invoke` entry, pushes a\n * matching tool-result message into `this.messages` so the next trip can\n * see it, and emits the right lifecycle event (`tool-called` on success,\n * `tool-calling-failed` when the tool is unregistered or invoke returned\n * an error). Never throws — always returns a `ToolCall` record.\n */\n private async dispatchToolCall(\n toolCallRequest: ModelToolCallRequest,\n tripIndex: number,\n ): Promise<ToolCall> {\n const registeredTool = this.config.tools?.find((tool) => tool.name === toolCallRequest.name);\n\n if (!registeredTool) {\n const error = new AgentExecutionError(`Tool not registered: ${toolCallRequest.name}`, {\n context: { toolName: toolCallRequest.name, tripIndex },\n });\n\n const nowIso = new Date().toISOString();\n\n const record: ToolCall = {\n runId: generateRunId(\"tool\"),\n rootRunId: this.runId,\n name: toolCallRequest.name,\n type: \"tool\",\n status: \"failed\",\n startedAt: nowIso,\n endedAt: nowIso,\n duration: 0,\n usage: { input: 0, output: 0, total: 0 },\n children: [],\n tripIndex,\n input: toolCallRequest.input,\n error,\n ...(toolCallRequest.recoveredFrom ? { recoveredFrom: toolCallRequest.recoveredFrom } : {}),\n };\n\n this.toolCalls.push(record);\n\n this.messages.push({\n role: \"tool\",\n toolCallId: toolCallRequest.id,\n content: JSON.stringify({ error: error.message }),\n });\n\n // Stub meta — there's no real tool to describe. Carries the\n // requested name for log correlation and an explanatory\n // description so consumers don't see an empty string.\n this.emit(\"agent.tool.failed\", {\n tool: {\n name: toolCallRequest.name,\n description: \"(unregistered tool — no description available)\",\n },\n input: toolCallRequest.input,\n error,\n tripIndex,\n });\n\n return record;\n }\n\n // Build the lightweight event meta once. Resolves `action` to a\n // string here so consumers receive plain data rather than having\n // to re-evaluate a callback on every event.\n const toolMeta: ToolEventMeta = {\n name: registeredTool.name,\n description: registeredTool.description,\n action: resolveToolAction(registeredTool, toolCallRequest.input),\n };\n\n this.emit(\"agent.tool.calling\", {\n tool: toolMeta,\n input: toolCallRequest.input,\n tripIndex,\n });\n\n const toolContext: MiddlewareToolContext = {\n ...this.buildExecuteContext(),\n tripIndex,\n messages: this.messages,\n tool: {\n name: registeredTool.name,\n description: registeredTool.description,\n mode: registeredTool.mode,\n },\n request: toolCallRequest,\n };\n\n let invokeResult: ToolInvokeResult<unknown>;\n\n try {\n invokeResult = (await runPipeline(\n this.middleware,\n \"tool\",\n toolContext,\n () => registeredTool.invoke(toolCallRequest.input, this.options?.toolCtx),\n this.logger,\n )) as ToolInvokeResult<unknown>;\n } catch (thrown) {\n // A `tool`-level middleware hook threw. The real invoke never\n // throws (it funnels errors into `result.error`), so only a\n // middleware abort or a bug reaches this branch. Synthesize a\n // failed-invoke record so the tool-call trace stays consistent.\n const error = this.toAIError(thrown);\n const nowIso = new Date().toISOString();\n const emptyUsage: Usage = { input: 0, output: 0, total: 0 };\n\n const failedRunId = generateRunId(\"tool\");\n invokeResult = {\n error,\n usage: emptyUsage,\n report: {\n runId: failedRunId,\n rootRunId: failedRunId,\n name: registeredTool.name,\n version: registeredTool.version,\n type: \"tool\",\n status: \"failed\",\n startedAt: nowIso,\n endedAt: nowIso,\n duration: 0,\n usage: emptyUsage,\n children: [],\n },\n };\n }\n\n // The agent-level ToolCall record merges the tool's own invocation\n // report with agent-side enrichments (tripIndex, input, output,\n // error). When the underlying tool was an `asTool`-wrapped\n // composite, its inner report becomes the sole child of this\n // ToolCall — preserving the full nested tree while keeping this\n // node's own `type` as `\"tool\"` (from the agent's POV it *was* a\n // tool dispatch).\n const innerReport = invokeResult.report;\n const isComposite = innerReport.type !== \"tool\";\n\n const record: ToolCall = {\n runId: innerReport.runId,\n rootRunId: this.runId,\n name: toolCallRequest.name,\n version: registeredTool.version,\n type: \"tool\",\n status: innerReport.status,\n startedAt: innerReport.startedAt,\n endedAt: innerReport.endedAt,\n duration: innerReport.duration,\n usage: invokeResult.usage,\n children: isComposite ? [innerReport] : innerReport.children,\n tripIndex,\n input: toolCallRequest.input,\n output: invokeResult.data,\n error: invokeResult.error,\n ...(toolCallRequest.recoveredFrom ? { recoveredFrom: toolCallRequest.recoveredFrom } : {}),\n };\n\n this.toolCalls.push(record);\n\n // Roll child usage into the agent's accumulator. Leaf tools\n // contribute zero; `asTool`-wrapped composites contribute the\n // full cost of the inner agent/workflow/supervisor run.\n this.usage.input += invokeResult.usage.input;\n this.usage.output += invokeResult.usage.output;\n this.usage.total += invokeResult.usage.total;\n this.usage.cost = accumulateCost(this.usage.cost, invokeResult.usage.cost);\n\n this.messages.push({\n role: \"tool\",\n toolCallId: toolCallRequest.id,\n content: invokeResult.error\n ? JSON.stringify({ error: invokeResult.error.message })\n : JSON.stringify(invokeResult.data ?? null),\n });\n\n if (invokeResult.error) {\n this.emit(\"agent.tool.failed\", {\n tool: toolMeta,\n input: toolCallRequest.input,\n error: invokeResult.error,\n tripIndex,\n });\n } else {\n this.emit(\"agent.tool.called\", { ...record, tool: toolMeta });\n }\n\n return record;\n }\n\n /**\n * Parse the final trip output against the user-supplied schema (if any).\n * Failures populate `this.error` but never throw. Returns an outcome the\n * caller uses to decide whether self-repair is worth attempting:\n *\n * - `\"skipped\"` — no schema, or a prior trip-level error already set\n * `this.error` (model crash, max trips). Not repairable; the failure\n * isn't a parse problem the model can fix by re-asking.\n * - `\"failed\"` — schema present, output text either failed JSON.parse\n * or failed `~standard.validate`. Repairable via `runRepairLoop`.\n * - `\"success\"` — parsed and validated; `this.data` populated.\n */\n private async parseOutput(): Promise<\"success\" | \"failed\" | \"skipped\"> {\n const schema = this.options?.output ?? this.config.output;\n\n if (!schema || this.error) {\n return \"skipped\";\n }\n\n const finalTrip = this.trips[this.trips.length - 1];\n const text = finalTrip?.output ?? \"\";\n\n if (!text) {\n return \"skipped\";\n }\n\n const payload = extractJsonPayload(text);\n const sentinel = Symbol(\"parse-failed\");\n const parsed = safeJsonParse<unknown>(payload, sentinel);\n\n if (parsed === sentinel) {\n this.error = new SchemaValidationError(\"Failed to parse model output as JSON\", {\n context: { text },\n });\n return \"failed\";\n }\n\n const validation = await (schema as StandardSchemaV1<TOutput>)[\"~standard\"].validate(parsed);\n\n if (validation.issues) {\n const summary = validation.issues.map((issue) => issue.message).join(\"; \");\n this.error = new SchemaValidationError(summary, {\n issues: validation.issues,\n });\n return \"failed\";\n }\n\n this.data = validation.value;\n return \"success\";\n }\n\n /**\n * Opt-in self-repair loop for `output` schema failures. Triggered only\n * when `options.repair` is set and `parseOutput()` returned `\"failed\"`.\n *\n * Each attempt:\n * 1. Pushes the bad assistant response into `this.messages` (so the\n * model can see what it just produced).\n * 2. Pushes a corrective user message naming the validation/parse error.\n * 3. Runs another trip — counted against the same `maxTrips` cap as\n * normal trips so a stuck model can't loop forever.\n * 4. Re-parses. Stops on success, on a trip-level error, or when\n * either `maxAttempts` or `maxTrips` is exhausted.\n *\n * Resets `this.error` and `this.data` before each attempt so the final\n * outcome (success or last failure) is what surfaces to the caller.\n */\n private async runRepairLoop(): Promise<void> {\n const maxAttempts = this.options?.repair?.maxAttempts ?? 1;\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n if (this.trips.length >= this.maxTrips) {\n return;\n }\n\n const lastTrip = this.trips[this.trips.length - 1];\n const badResponse = lastTrip?.output ?? \"\";\n const failureReason = this.error?.message ?? \"unknown validation failure\";\n\n this.error = undefined;\n this.data = undefined;\n\n this.messages.push({ role: \"assistant\", content: badResponse });\n\n this.messages.push({\n role: \"user\",\n content: [\n `Your previous response failed validation: ${failureReason}.`,\n \"Respond again with valid JSON only — no prose, no markdown fences, no commentary.\",\n ].join(\" \"),\n });\n\n const tripIndex = this.trips.length;\n\n this.logger.warn(LOG_MODULE, \"repair.attempting\", \"retrying after validation failure\", {\n attempt: attempt + 1,\n maxAttempts,\n reason: failureReason,\n });\n\n const outcome = await this.runTrip(tripIndex, \"[repair attempt]\");\n\n if (outcome === \"error\") {\n return;\n }\n\n const parseOutcome = await this.parseOutput();\n\n if (parseOutcome === \"success\") {\n return;\n }\n }\n }\n\n /**\n * Build the final `AgentResult` snapshot from accumulated state\n * (trips, tool calls, data/error, usage, timing).\n *\n * Pure — no side effects. `run()` owns terminal event emission and\n * stream closure so the post-pipeline result (possibly transformed\n * or short-circuited by an `execute`-level middleware) is what\n * flows out to consumers and listeners.\n *\n * Trips, tool calls, status, and timing live under `report` so the\n * root stays focused on the four things callers reach for most:\n * `data`, `text`, `usage`, `error`.\n */\n private buildResult(): AgentResult<TOutput> {\n const finalTrip = this.trips[this.trips.length - 1];\n const endedAt = new Date();\n\n const agentName = this.config.name ?? this.config.model.name;\n const status: BaseReport[\"status\"] = this.error\n ? this.error instanceof AgentCancelledError\n ? \"cancelled\"\n : \"failed\"\n : \"completed\";\n\n const report = {\n runId: this.runId,\n rootRunId: this.runId,\n name: agentName,\n version: this.config.version,\n type: \"agent\" as const,\n status,\n startedAt: this.startedAt.toISOString(),\n endedAt: endedAt.toISOString(),\n duration: performance.now() - this.start,\n usage: this.usage,\n children: this.toolCalls,\n model: {\n name: this.config.model.name,\n provider: this.config.model.provider,\n },\n trips: this.trips,\n };\n\n // Stamp lineage on the assembled tree exactly once per run.\n // Rewrites any inner self-roots from composite children to this\n // run's id, stamps `reportSchemaVersion` on the root, and\n // propagates `sessionId` to every node.\n stampReportLineage(report, {\n rootRunId: this.runId,\n sessionId: this.options?.sessionId,\n });\n\n return {\n type: \"agent\",\n data: this.data,\n text: finalTrip?.output,\n report,\n usage: this.usage,\n error: this.error,\n };\n }\n\n /**\n * Normalize any thrown value into an `AIError`. `AIError` instances\n * pass through untouched; provider-adapter SDK errors are caught by\n * the adapter and already arrive typed, so this branch mainly\n * handles runtime crashes (TypeError, ReferenceError) inside\n * model.complete / model.stream and non-Error values (`throw \"bad\"`).\n */\n private toAIError(thrown: unknown): AIError {\n if (thrown instanceof AIError) {\n return thrown;\n }\n\n // Classify abort-flavored errors (DOMException \"AbortError\",\n // node-fetch's `FetchError` with name \"AbortError\", `ERR_CANCELED`\n // from the OpenAI SDK's axios-ish layer) as cancelled instead of\n // a generic exec failure so callers can route retries correctly.\n if (isAbortLike(thrown)) {\n return this.makeCancelledError();\n }\n\n const message = thrown instanceof Error ? thrown.message : String(thrown);\n\n return new AgentExecutionError(message, { cause: thrown });\n }\n\n /**\n * Build the typed cancelled error that both the trip-loop guard\n * and the mid-stream guard emit. Captures the abort reason when\n * one was supplied to `controller.abort(reason)` so logs and\n * telemetry can see what cancelled the run.\n */\n private makeCancelledError(): AgentCancelledError {\n const reason = this.options?.signal?.reason;\n const reasonText = reason === undefined ? \"\" : String(reason);\n\n return new AgentCancelledError(\"agent execution cancelled\", {\n cause: reason,\n cancelledAt: new Date().toISOString(),\n reason: reasonText,\n });\n }\n\n /**\n * Fire a single event through all three subscription tiers in order\n * — factory → instance → per-call — and mirror it into the\n * `StreamController` when streaming is active. A throwing user\n * handler must never crash the agent, so every dispatch is wrapped\n * in `safeCall`. Stream events are converted from the internal\n * `AgentEventMap` payload to the public `StreamEvent` shape because\n * some of them differ (e.g. the tool-called payload vs stream\n * event).\n */\n private emit<K extends keyof AgentEventMap>(\n event: K,\n payload: WithoutIdentity<AgentEventMap[K]>,\n ): void {\n // Inject run identity once, here, so every subscription tier and\n // the stream see it. `rootRunId === runId` for a standalone run;\n // nested propagation lands in a follow-up.\n const fullPayload = {\n ...payload,\n runId: this.runId,\n rootRunId: this.runId,\n } as AgentEventMap[K];\n\n this.logEvent(event, fullPayload);\n\n const factoryHandler = this.config.on?.[event] as AgentEventHandler<K> | undefined;\n\n if (factoryHandler) {\n safeCall(factoryHandler, fullPayload);\n }\n\n const bucket = this.instanceHandlers?.get(event);\n\n if (bucket) {\n for (const handler of bucket) {\n safeCall(handler as AgentEventHandler<K>, fullPayload);\n }\n }\n\n const perCallHandler = this.options?.on?.[event] as AgentEventHandler<K> | undefined;\n\n if (perCallHandler) {\n safeCall(perCallHandler, fullPayload);\n }\n\n if (this.streamController) {\n const body = this.toStreamEvent(event, fullPayload);\n\n if (body) {\n this.streamController.push({\n runId: this.runId,\n rootRunId: this.runId,\n ...body,\n });\n }\n }\n }\n\n /**\n * Emit a structured log line for a lifecycle event. The action\n * string mirrors the event name with the `agent.` prefix stripped\n * (`agent.trip.started` → `trip.started`) so log grep filters and\n * event handlers read the same vocabulary. Level mapping follows\n * the convention documented on `@warlock.js/logger`'s `Logger`.\n */\n private logEvent<K extends keyof AgentEventMap>(event: K, payload: AgentEventMap[K]): void {\n const agentName = this.config.name || this.config.model.name;\n logAgentEvent(\n this.logger,\n {\n module: `${LOG_MODULE}.${agentName}`,\n maxTrips: this.maxTrips,\n modelName: this.config.model.name,\n totalUsage: this.usage,\n totalDurationMs: performance.now() - this.start,\n trips: this.trips,\n toolCalls: this.toolCalls,\n },\n event,\n payload,\n );\n }\n\n private toStreamEvent<K extends keyof AgentEventMap>(\n event: K,\n payload: AgentEventMap[K],\n ): StreamEventBody | undefined {\n return agentEventToStreamEvent(event, payload);\n }\n\n /**\n * Invoke the `onUsage` hook (when configured) with a flat payload\n * carrying stable identity. Awaits the handler so async ledger\n * writes complete before the next trip starts; swallows any throw\n * so consumer bugs cannot crash the agent. Sync handlers wrapped\n * via `Promise.resolve()` so the await is safe in either case.\n */\n private async fireUsageHook(tripIndex: number, tripUsage: Usage): Promise<void> {\n const handler = this.config.onUsage;\n if (!handler) return;\n\n const event: UsageEvent = {\n runId: this.runId,\n tripIndex,\n model: {\n name: this.config.model.name,\n provider: this.config.model.provider,\n },\n usage: { ...tripUsage },\n timestamp: new Date().toISOString(),\n };\n\n try {\n await Promise.resolve(handler(event));\n } catch (err) {\n this.logger.warn(LOG_MODULE, \"onUsage.hook.error\", \"onUsage handler threw\", {\n runId: this.runId,\n tripIndex,\n error: err instanceof Error ? err.message : String(err),\n });\n }\n }\n\n /**\n * Invoke the `onComplete` hook (when configured) once at the end\n * of every run. Receives the full `AgentResult` plus pre-extracted\n * `runId` and `durationMs`. Same swallow-and-log error policy as\n * `fireUsageHook`.\n */\n private async fireCompleteHook(result: AgentResult<TOutput>): Promise<void> {\n const handler = this.config.onComplete;\n if (!handler) return;\n\n const event: CompleteEvent<TOutput> = {\n result,\n runId: this.runId,\n durationMs: performance.now() - this.start,\n };\n\n try {\n await Promise.resolve(handler(event));\n } catch (err) {\n this.logger.warn(LOG_MODULE, \"onComplete.hook.error\", \"onComplete handler threw\", {\n runId: this.runId,\n error: err instanceof Error ? err.message : String(err),\n });\n }\n }\n}\n\n/**\n * Decide whether a guard-synthesized tool call duplicates a real one\n * the provider already streamed structurally. Match key is\n * `name + key-sorted JSON of input` so identical calls (regardless of\n * argument key order) collapse, but two legitimate calls to the same\n * tool with different inputs still both dispatch.\n */\nfunction isDuplicateToolCall(\n recovered: ModelToolCallRequest,\n realCalls: ReadonlyArray<ModelToolCallRequest>,\n): boolean {\n const recoveredKey = `${recovered.name}|${stableStringify(recovered.input)}`;\n\n for (const real of realCalls) {\n const realKey = `${real.name}|${stableStringify(real.input)}`;\n\n if (realKey === recoveredKey) {\n return true;\n }\n }\n\n return false;\n}\n\n/**\n * `JSON.stringify` variant that sorts object keys at every nesting\n * level so structurally-equal inputs serialize to identical strings.\n * Used only for dedupe-key comparison; never surfaces to consumers.\n */\nfunction stableStringify(value: unknown): string {\n return JSON.stringify(value, (_key, val) => {\n if (val !== null && typeof val === \"object\" && !Array.isArray(val)) {\n const source = val as Record<string, unknown>;\n const sorted: Record<string, unknown> = {};\n\n for (const key of Object.keys(source).sort()) {\n sorted[key] = source[key];\n }\n\n return sorted;\n }\n\n return val;\n });\n}\n\n/**\n * Invoke a user-supplied event handler without letting exceptions\n * escape the agent. Thrown errors are swallowed — structured logging\n * attaches here in Phase 0.5 so operators still see the failure.\n */\nfunction safeCall<T>(handler: (payload: T) => void, payload: T): void {\n try {\n handler(payload);\n } catch {\n // Intentionally swallowed — user handlers never crash the agent.\n }\n}\n\n/**\n * Resolve a tool's `action` declaration into a plain string for\n * inclusion in `ToolEventMeta`. Static strings pass through;\n * function-shaped actions are invoked with the validated input.\n *\n * Defensive: if the user's callback throws, swallow and return\n * `undefined` rather than crashing the agent — UI strings are not\n * worth aborting an LLM dispatch over.\n */\nfunction resolveToolAction(\n tool: ToolContract<unknown, unknown>,\n input: unknown,\n): string | undefined {\n if (tool.action === undefined) return undefined;\n if (typeof tool.action === \"string\") return tool.action;\n try {\n return tool.action(input);\n } catch {\n return undefined;\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
//#region ../../@warlock.js/ai/src/agent/json-stream-guard.ts
|
|
2
|
+
/**
|
|
3
|
+
* Default cap on bytes accumulated in a single suspect buffer before
|
|
4
|
+
* the guard gives up, flushes as text, and resets to pass-through.
|
|
5
|
+
* Real envelope payloads observed in production leaks are well under
|
|
6
|
+
* 1 KB; this is a safety valve against runaway / adversarial input.
|
|
7
|
+
*/
|
|
8
|
+
const DEFAULT_MAX_BUFFER_BYTES = 4096;
|
|
9
|
+
/**
|
|
10
|
+
* Fence opener the guard recognizes in pass-through mode. Targets the
|
|
11
|
+
* lowercase form ```` ```json ```` only — that is the form models
|
|
12
|
+
* actually emit in the wild when they fence-wrap a JSON tool envelope.
|
|
13
|
+
* Other languages / casings flush as plain text.
|
|
14
|
+
*/
|
|
15
|
+
const FENCE_OPENER = "```json";
|
|
16
|
+
/**
|
|
17
|
+
* Closing fence sequence inside `bufferingFence` mode. Three backticks,
|
|
18
|
+
* no language tag.
|
|
19
|
+
*/
|
|
20
|
+
const FENCE_CLOSER = "```";
|
|
21
|
+
/**
|
|
22
|
+
* Per-trip state machine that intercepts streamed text deltas, detects
|
|
23
|
+
* JSON envelopes the model has emitted as plain text (the
|
|
24
|
+
* tool-call-leakage symptom), and synthesizes real `ModelToolCallRequest`
|
|
25
|
+
* entries for them while suppressing the JSON from visible output.
|
|
26
|
+
*
|
|
27
|
+
* **Role.** A `JsonStreamGuard` is the per-trip implementation of the
|
|
28
|
+
* opt-in `streamingToolGuard` config. It sits between the model
|
|
29
|
+
* adapter's `delta` chunks and the agent's `agent.trip.streaming`
|
|
30
|
+
* emit + `content` accumulator — text that survives the guard is what
|
|
31
|
+
* the consumer sees and what the trip records as `output`.
|
|
32
|
+
*
|
|
33
|
+
* **Responsibility.**
|
|
34
|
+
* - Owns: a small character-level state machine (pass-through,
|
|
35
|
+
* brace-buffering, fence-buffering), string-literal-aware brace
|
|
36
|
+
* tracking, fence-opener / fence-closer detection, named-envelope
|
|
37
|
+
* matching against registered tool schemas, buffer-cap enforcement.
|
|
38
|
+
* - Does NOT own: event emission (delegated via callbacks), tool
|
|
39
|
+
* dispatch, `finishReason` normalization, dedupe vs. real tool
|
|
40
|
+
* calls — the agent loop handles all four.
|
|
41
|
+
*
|
|
42
|
+
* **Matcher tier — named envelope only (v1).** A buffer matches when
|
|
43
|
+
* it parses as a JSON object containing both:
|
|
44
|
+
* - a `name` or `tool` key resolving to a registered tool name, AND
|
|
45
|
+
* - an `arguments` or `input` key whose value validates against the
|
|
46
|
+
* resolved tool's `~standard` schema.
|
|
47
|
+
* Bare-object matching (where any registered tool's schema is the
|
|
48
|
+
* sole signal) is deferred until tool input schemas are tight enough
|
|
49
|
+
* to distinguish — `v.record(v.any())` would match everything.
|
|
50
|
+
*
|
|
51
|
+
* **Per-trip lifecycle.** One instance per trip. The agent loop calls
|
|
52
|
+
* `feed(chunk)` for every `delta` chunk and `finalize()` exactly once
|
|
53
|
+
* after the stream's `done` chunk. Mid-stream cancellation: the loop
|
|
54
|
+
* simply stops calling `feed`; any open buffer is discarded with the
|
|
55
|
+
* guard instance.
|
|
56
|
+
*
|
|
57
|
+
* Modeled as a class (see §4.2 of code-style.md — per-call execution
|
|
58
|
+
* state across phases): the machine has 3 states, accumulators for
|
|
59
|
+
* brace depth, string-literal escape tracking, and a synthesized-call
|
|
60
|
+
* counter for stable ids across the trip.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* // Inside the agent's streaming trip body:
|
|
64
|
+
* const guard = new JsonStreamGuard({
|
|
65
|
+
* tools: this.config.tools ?? [],
|
|
66
|
+
* maxBufferBytes: guardConfig.maxBufferBytes,
|
|
67
|
+
* onSafeDelta: (delta) => {
|
|
68
|
+
* content += delta;
|
|
69
|
+
* this.emit("agent.trip.streaming", { delta, tripIndex });
|
|
70
|
+
* },
|
|
71
|
+
* onRecoveredCall: (request) => recoveredCalls.push(request),
|
|
72
|
+
* });
|
|
73
|
+
*
|
|
74
|
+
* for await (const chunk of model.stream(messages, callOptions)) {
|
|
75
|
+
* if (chunk.type === "delta") await guard.feed(chunk.content);
|
|
76
|
+
* // ... other chunk types
|
|
77
|
+
* }
|
|
78
|
+
*
|
|
79
|
+
* await guard.finalize();
|
|
80
|
+
*/
|
|
81
|
+
var JsonStreamGuard = class {
|
|
82
|
+
constructor(options) {
|
|
83
|
+
this.mode = "passThrough";
|
|
84
|
+
this.holdback = "";
|
|
85
|
+
this.buffer = "";
|
|
86
|
+
this.braceDepth = 0;
|
|
87
|
+
this.inString = false;
|
|
88
|
+
this.escapeNext = false;
|
|
89
|
+
this.fenceCloseTail = "";
|
|
90
|
+
this.recoveredCount = 0;
|
|
91
|
+
this.tools = options.tools;
|
|
92
|
+
this.maxBufferBytes = options.maxBufferBytes ?? DEFAULT_MAX_BUFFER_BYTES;
|
|
93
|
+
this.onSafeDelta = options.onSafeDelta;
|
|
94
|
+
this.onRecoveredCall = options.onRecoveredCall;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Feed the next raw delta from the model. Splits the chunk into
|
|
98
|
+
* characters and runs each through the state machine, awaiting
|
|
99
|
+
* envelope classification whenever a buffer closes mid-chunk.
|
|
100
|
+
*
|
|
101
|
+
* The hot path (pass-through prose with no `{` / `` ` ``) is fully
|
|
102
|
+
* synchronous — `await` here only blocks at buffer-close points,
|
|
103
|
+
* which are rare in normal traffic.
|
|
104
|
+
*/
|
|
105
|
+
async feed(chunk) {
|
|
106
|
+
for (let i = 0; i < chunk.length; i++) await this.processChar(chunk[i]);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Stream ended. Anything still in the holdback was prose
|
|
110
|
+
* misclassified as a partial fence opener — emit it. Anything still
|
|
111
|
+
* in an open buffer never closed — emit it as text too (a leak
|
|
112
|
+
* truncated mid-flight is still text the user partially saw).
|
|
113
|
+
*/
|
|
114
|
+
async finalize() {
|
|
115
|
+
if (this.holdback.length > 0) {
|
|
116
|
+
this.onSafeDelta(this.holdback);
|
|
117
|
+
this.holdback = "";
|
|
118
|
+
}
|
|
119
|
+
if (this.mode === "bufferingBrace") {
|
|
120
|
+
this.flushBraceBufferAsText();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (this.mode === "bufferingFence") this.flushFenceBufferAsText();
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* True when at least one envelope was recovered this trip. The
|
|
127
|
+
* agent loop reads this to override `finishReason` from `"stop"` to
|
|
128
|
+
* `"tool_calls"` when the model reported a natural stop but the
|
|
129
|
+
* guard found tool calls hiding in the text channel.
|
|
130
|
+
*/
|
|
131
|
+
hasRecoveredCalls() {
|
|
132
|
+
return this.recoveredCount > 0;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Route a single character based on the current mode. The
|
|
136
|
+
* `passThrough` branch handles holdback expansion / flushing
|
|
137
|
+
* iteratively (no recursion) so a character that "breaks" a fence
|
|
138
|
+
* opener can be re-evaluated as a fresh pass-through input in the
|
|
139
|
+
* same call.
|
|
140
|
+
*/
|
|
141
|
+
async processChar(char) {
|
|
142
|
+
if (this.mode === "bufferingBrace") {
|
|
143
|
+
await this.processBraceChar(char);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (this.mode === "bufferingFence") {
|
|
147
|
+
await this.processFenceChar(char);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
let current = char;
|
|
151
|
+
while (true) {
|
|
152
|
+
if (this.holdback.length === 0 && current === "{") {
|
|
153
|
+
this.openBraceBuffer(current);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const extended = this.holdback + current;
|
|
157
|
+
if (this.isFenceOpenerPrefix(extended)) {
|
|
158
|
+
this.holdback = extended;
|
|
159
|
+
if (extended === FENCE_OPENER) this.openFenceBuffer();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (this.holdback.length === 0) {
|
|
163
|
+
this.onSafeDelta(current);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
this.onSafeDelta(this.holdback);
|
|
167
|
+
this.holdback = "";
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Recognize any strict prefix of {@link FENCE_OPENER} including the
|
|
172
|
+
* full string. Used to decide whether to keep extending the holdback
|
|
173
|
+
* or flush it as plain text.
|
|
174
|
+
*/
|
|
175
|
+
isFenceOpenerPrefix(candidate) {
|
|
176
|
+
return candidate.length <= 7 && FENCE_OPENER.startsWith(candidate);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Enter `bufferingBrace` mode with the seed `{` as the first buffer
|
|
180
|
+
* character and the initial brace depth set to one. Any holdback at
|
|
181
|
+
* this point was already a non-fence sequence so it stays empty.
|
|
182
|
+
*/
|
|
183
|
+
openBraceBuffer(seed) {
|
|
184
|
+
this.mode = "bufferingBrace";
|
|
185
|
+
this.buffer = seed;
|
|
186
|
+
this.braceDepth = 1;
|
|
187
|
+
this.inString = false;
|
|
188
|
+
this.escapeNext = false;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Enter `bufferingFence` mode immediately after the opener
|
|
192
|
+
* ```` ```json ```` matched in the holdback. Holdback resets;
|
|
193
|
+
* subsequent characters accumulate into the buffer until the
|
|
194
|
+
* closing fence is seen.
|
|
195
|
+
*/
|
|
196
|
+
openFenceBuffer() {
|
|
197
|
+
this.mode = "bufferingFence";
|
|
198
|
+
this.buffer = "";
|
|
199
|
+
this.fenceCloseTail = "";
|
|
200
|
+
this.holdback = "";
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Process one character while accumulating a brace-delimited JSON
|
|
204
|
+
* object. Tracks string-literal context so `{` / `}` inside `"..."`
|
|
205
|
+
* do not skew brace depth. Closes (and classifies) on balanced
|
|
206
|
+
* braces; flushes-as-text on cap overflow.
|
|
207
|
+
*/
|
|
208
|
+
async processBraceChar(char) {
|
|
209
|
+
this.buffer += char;
|
|
210
|
+
if (this.inString) {
|
|
211
|
+
if (this.escapeNext) {
|
|
212
|
+
this.escapeNext = false;
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (char === "\\") {
|
|
216
|
+
this.escapeNext = true;
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (char === "\"") this.inString = false;
|
|
220
|
+
this.guardBufferCap("brace");
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (char === "\"") {
|
|
224
|
+
this.inString = true;
|
|
225
|
+
this.guardBufferCap("brace");
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (char === "{") {
|
|
229
|
+
this.braceDepth++;
|
|
230
|
+
this.guardBufferCap("brace");
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (char === "}") {
|
|
234
|
+
this.braceDepth--;
|
|
235
|
+
if (this.braceDepth === 0) {
|
|
236
|
+
await this.closeBraceBuffer();
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
this.guardBufferCap("brace");
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
this.guardBufferCap("brace");
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Process one character while accumulating a fence-delimited JSON
|
|
246
|
+
* block. The closing fence ```` ``` ```` ends the block; the closing
|
|
247
|
+
* characters are NOT included in the classified buffer (they are
|
|
248
|
+
* re-emitted only when the block flushes back to text).
|
|
249
|
+
*/
|
|
250
|
+
async processFenceChar(char) {
|
|
251
|
+
this.fenceCloseTail += char;
|
|
252
|
+
if (this.fenceCloseTail.length > 3) this.fenceCloseTail = this.fenceCloseTail.slice(-3);
|
|
253
|
+
if (this.fenceCloseTail === FENCE_CLOSER) {
|
|
254
|
+
const innerLength = this.buffer.length - 2;
|
|
255
|
+
this.buffer = this.buffer.slice(0, Math.max(0, innerLength));
|
|
256
|
+
await this.closeFenceBuffer();
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
this.buffer += char;
|
|
260
|
+
this.guardBufferCap("fence");
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Enforce the buffer-byte cap. When the current buffer exceeds the
|
|
264
|
+
* cap, flush it back to the consumer as plain text and reset to
|
|
265
|
+
* pass-through. Acts as a runaway / adversarial-input safety valve.
|
|
266
|
+
*/
|
|
267
|
+
guardBufferCap(source) {
|
|
268
|
+
if (this.buffer.length <= this.maxBufferBytes) return;
|
|
269
|
+
if (source === "brace") {
|
|
270
|
+
this.flushBraceBufferAsText();
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
this.flushFenceBufferAsText();
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Run the envelope matcher against the closed brace buffer. On a
|
|
277
|
+
* match, synthesize a recovered `ModelToolCallRequest`; on no
|
|
278
|
+
* match, flush the buffer back as plain text. Resets state to
|
|
279
|
+
* pass-through either way.
|
|
280
|
+
*/
|
|
281
|
+
async closeBraceBuffer() {
|
|
282
|
+
const closed = this.buffer;
|
|
283
|
+
this.resetToPassThrough();
|
|
284
|
+
if (await this.tryMatchEnvelope(closed)) return;
|
|
285
|
+
this.onSafeDelta(closed);
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Run the envelope matcher against the closed fence buffer. On a
|
|
289
|
+
* match, synthesize a recovered call; on no match, flush as text
|
|
290
|
+
* **with** the original opener and closer reconstructed so the
|
|
291
|
+
* customer sees exactly the markdown the model emitted.
|
|
292
|
+
*/
|
|
293
|
+
async closeFenceBuffer() {
|
|
294
|
+
const closed = this.buffer;
|
|
295
|
+
this.resetToPassThrough();
|
|
296
|
+
if (await this.tryMatchEnvelope(closed)) return;
|
|
297
|
+
this.onSafeDelta(`${FENCE_OPENER}${closed}${FENCE_CLOSER}`);
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Emit the brace-buffer verbatim as text and reset to pass-through.
|
|
301
|
+
* Used on cap overflow and on `finalize()` for an unclosed buffer.
|
|
302
|
+
*/
|
|
303
|
+
flushBraceBufferAsText() {
|
|
304
|
+
const closed = this.buffer;
|
|
305
|
+
this.resetToPassThrough();
|
|
306
|
+
this.onSafeDelta(closed);
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Emit the fence-buffer verbatim as text, reconstructing the
|
|
310
|
+
* opener and closer so the original markdown structure is
|
|
311
|
+
* preserved for the consumer.
|
|
312
|
+
*/
|
|
313
|
+
flushFenceBufferAsText() {
|
|
314
|
+
const closed = this.buffer;
|
|
315
|
+
this.resetToPassThrough();
|
|
316
|
+
this.onSafeDelta(`${FENCE_OPENER}${closed}`);
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Reset all per-buffer state back to the pass-through baseline.
|
|
320
|
+
* Called whenever a buffer closes — by recovery, by flush, or by
|
|
321
|
+
* cap overflow — so the next character starts a fresh scan.
|
|
322
|
+
*/
|
|
323
|
+
resetToPassThrough() {
|
|
324
|
+
this.mode = "passThrough";
|
|
325
|
+
this.buffer = "";
|
|
326
|
+
this.braceDepth = 0;
|
|
327
|
+
this.inString = false;
|
|
328
|
+
this.escapeNext = false;
|
|
329
|
+
this.fenceCloseTail = "";
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Attempt to classify a closed buffer as a tool-call envelope. On
|
|
333
|
+
* success, invoke `onRecoveredCall` with a synthesized request and
|
|
334
|
+
* return `true`; on failure return `false` so the caller can flush
|
|
335
|
+
* the buffer back as text.
|
|
336
|
+
*/
|
|
337
|
+
async tryMatchEnvelope(raw) {
|
|
338
|
+
const parsed = safeParseJson(raw);
|
|
339
|
+
if (parsed === void 0 || typeof parsed !== "object" || parsed === null) return false;
|
|
340
|
+
const envelope = parsed;
|
|
341
|
+
const candidateName = readString(envelope, "name") ?? readString(envelope, "tool");
|
|
342
|
+
const candidateInput = readObject(envelope, "arguments") ?? readObject(envelope, "input");
|
|
343
|
+
if (!candidateName || !candidateInput) return false;
|
|
344
|
+
const tool = this.tools.find((entry) => entry.name === candidateName);
|
|
345
|
+
if (!tool || !tool.input) return false;
|
|
346
|
+
const schema = tool.input;
|
|
347
|
+
let validationResult;
|
|
348
|
+
try {
|
|
349
|
+
validationResult = await schema["~standard"].validate(candidateInput);
|
|
350
|
+
} catch {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
if (validationResult.issues) return false;
|
|
354
|
+
this.recoveredCount++;
|
|
355
|
+
this.onRecoveredCall({
|
|
356
|
+
id: `synth_${candidateName}_${this.recoveredCount}`,
|
|
357
|
+
name: candidateName,
|
|
358
|
+
input: validationResult.value,
|
|
359
|
+
recoveredFrom: "stream-text"
|
|
360
|
+
});
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
/**
|
|
365
|
+
* Parse a JSON string returning `undefined` on any failure. Local to
|
|
366
|
+
* the guard so it can distinguish "not JSON" from a parsed `null`
|
|
367
|
+
* value, which `safeJsonParse` cannot — a parsed `null` is a valid
|
|
368
|
+
* JSON value but not a valid envelope, and we want the difference.
|
|
369
|
+
*/
|
|
370
|
+
function safeParseJson(raw) {
|
|
371
|
+
try {
|
|
372
|
+
return JSON.parse(raw);
|
|
373
|
+
} catch {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Read a string-typed field from an envelope candidate. Returns
|
|
379
|
+
* `undefined` when the key is missing or the value is non-string —
|
|
380
|
+
* the matcher rejects either case.
|
|
381
|
+
*/
|
|
382
|
+
function readString(envelope, key) {
|
|
383
|
+
const value = envelope[key];
|
|
384
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Read an object-typed field from an envelope candidate. Returns
|
|
388
|
+
* `undefined` when the key is missing or the value is not a
|
|
389
|
+
* plain object (rejects arrays, primitives, null) — tool input
|
|
390
|
+
* schemas always validate against an object root.
|
|
391
|
+
*/
|
|
392
|
+
function readObject(envelope, key) {
|
|
393
|
+
const value = envelope[key];
|
|
394
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return;
|
|
395
|
+
return value;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
//#endregion
|
|
399
|
+
export { JsonStreamGuard };
|
|
400
|
+
//# sourceMappingURL=json-stream-guard.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json-stream-guard.mjs","names":[],"sources":["../../../../../../@warlock.js/ai/src/agent/json-stream-guard.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport type { ModelToolCallRequest } from \"../contracts/model-tool-call-request.type\";\nimport type { ToolContract } from \"../tool/tool\";\n\n/**\n * Default cap on bytes accumulated in a single suspect buffer before\n * the guard gives up, flushes as text, and resets to pass-through.\n * Real envelope payloads observed in production leaks are well under\n * 1 KB; this is a safety valve against runaway / adversarial input.\n */\nconst DEFAULT_MAX_BUFFER_BYTES = 4096;\n\n/**\n * Fence opener the guard recognizes in pass-through mode. Targets the\n * lowercase form ```` ```json ```` only — that is the form models\n * actually emit in the wild when they fence-wrap a JSON tool envelope.\n * Other languages / casings flush as plain text.\n */\nconst FENCE_OPENER = \"```json\";\n\n/**\n * Closing fence sequence inside `bufferingFence` mode. Three backticks,\n * no language tag.\n */\nconst FENCE_CLOSER = \"```\";\n\n/**\n * Options passed when constructing a `JsonStreamGuard`.\n *\n * The guard is deliberately framework-agnostic of *how* deltas are\n * emitted or how recovered calls are dispatched — callers wire those\n * via `onSafeDelta` / `onRecoveredCall`. This keeps the unit-testable\n * surface tiny and lets the agent loop own all event-emission policy.\n */\nexport type JsonStreamGuardOptions = {\n /** Tools the agent has registered for this trip. Envelope lookups use `.name`. */\n tools: ReadonlyArray<ToolContract<unknown, unknown>>;\n /**\n * Hard cap on a single suspect buffer's size. When exceeded, the\n * buffer is flushed verbatim as text and the guard returns to\n * pass-through. Defaults to {@link DEFAULT_MAX_BUFFER_BYTES}.\n */\n maxBufferBytes?: number;\n /**\n * Called for every chunk of text that survived the guard — exactly\n * what the consumer should treat as the visible delta. May be\n * called many times per `feed()` call, possibly with a single\n * character or with a multi-character flush.\n */\n onSafeDelta: (delta: string) => void;\n /**\n * Called once per envelope the guard successfully classifies as a\n * tool-call recovery. The request carries `recoveredFrom:\n * \"stream-text\"` so downstream consumers can distinguish synthesized\n * calls from real ones.\n */\n onRecoveredCall: (request: ModelToolCallRequest) => void;\n};\n\n/**\n * Per-trip state machine that intercepts streamed text deltas, detects\n * JSON envelopes the model has emitted as plain text (the\n * tool-call-leakage symptom), and synthesizes real `ModelToolCallRequest`\n * entries for them while suppressing the JSON from visible output.\n *\n * **Role.** A `JsonStreamGuard` is the per-trip implementation of the\n * opt-in `streamingToolGuard` config. It sits between the model\n * adapter's `delta` chunks and the agent's `agent.trip.streaming`\n * emit + `content` accumulator — text that survives the guard is what\n * the consumer sees and what the trip records as `output`.\n *\n * **Responsibility.**\n * - Owns: a small character-level state machine (pass-through,\n * brace-buffering, fence-buffering), string-literal-aware brace\n * tracking, fence-opener / fence-closer detection, named-envelope\n * matching against registered tool schemas, buffer-cap enforcement.\n * - Does NOT own: event emission (delegated via callbacks), tool\n * dispatch, `finishReason` normalization, dedupe vs. real tool\n * calls — the agent loop handles all four.\n *\n * **Matcher tier — named envelope only (v1).** A buffer matches when\n * it parses as a JSON object containing both:\n * - a `name` or `tool` key resolving to a registered tool name, AND\n * - an `arguments` or `input` key whose value validates against the\n * resolved tool's `~standard` schema.\n * Bare-object matching (where any registered tool's schema is the\n * sole signal) is deferred until tool input schemas are tight enough\n * to distinguish — `v.record(v.any())` would match everything.\n *\n * **Per-trip lifecycle.** One instance per trip. The agent loop calls\n * `feed(chunk)` for every `delta` chunk and `finalize()` exactly once\n * after the stream's `done` chunk. Mid-stream cancellation: the loop\n * simply stops calling `feed`; any open buffer is discarded with the\n * guard instance.\n *\n * Modeled as a class (see §4.2 of code-style.md — per-call execution\n * state across phases): the machine has 3 states, accumulators for\n * brace depth, string-literal escape tracking, and a synthesized-call\n * counter for stable ids across the trip.\n *\n * @example\n * // Inside the agent's streaming trip body:\n * const guard = new JsonStreamGuard({\n * tools: this.config.tools ?? [],\n * maxBufferBytes: guardConfig.maxBufferBytes,\n * onSafeDelta: (delta) => {\n * content += delta;\n * this.emit(\"agent.trip.streaming\", { delta, tripIndex });\n * },\n * onRecoveredCall: (request) => recoveredCalls.push(request),\n * });\n *\n * for await (const chunk of model.stream(messages, callOptions)) {\n * if (chunk.type === \"delta\") await guard.feed(chunk.content);\n * // ... other chunk types\n * }\n *\n * await guard.finalize();\n */\nexport class JsonStreamGuard {\n private readonly tools: ReadonlyArray<ToolContract<unknown, unknown>>;\n private readonly maxBufferBytes: number;\n private readonly onSafeDelta: (delta: string) => void;\n private readonly onRecoveredCall: (request: ModelToolCallRequest) => void;\n\n private mode: \"passThrough\" | \"bufferingBrace\" | \"bufferingFence\" = \"passThrough\";\n\n /**\n * Characters held back in pass-through mode while we resolve whether\n * a partial fence opener (`` ` ``, `` `` ``, `` ``` ``, `` ```j ``, …)\n * will complete or break. Always a strict prefix of {@link FENCE_OPENER};\n * emptied (and emitted verbatim) the moment a non-matching character\n * arrives.\n */\n private holdback = \"\";\n\n /**\n * Accumulator while `mode === \"bufferingBrace\"` or `\"bufferingFence\"`.\n * In brace mode it carries the JSON including the outermost `{`/`}`.\n * In fence mode it carries everything between the opener and the\n * closer (the opener and closer themselves are NOT in the buffer —\n * they are reconstructed only on a flush-as-text fallback).\n */\n private buffer = \"\";\n\n /**\n * Brace-depth counter for `bufferingBrace` mode. Increments on `{`,\n * decrements on `}` — but only when {@link inString} is false, so a\n * `{` inside a JSON string literal does not skew the depth. Buffer\n * closes when depth returns to zero.\n */\n private braceDepth = 0;\n\n /** True while the scanner is inside a `\"...\"` JSON string literal. */\n private inString = false;\n\n /**\n * True when the previous character inside a string literal was a\n * backslash, so the current character is escaped (`\\\"` does not end\n * the string; `\\\\` resets the flag without escaping anything else).\n */\n private escapeNext = false;\n\n /**\n * Trailing tail of the fence buffer used to detect the closing\n * ```` ``` ```` sequence. Length capped at the closer length; rotated\n * forward as new characters arrive.\n */\n private fenceCloseTail = \"\";\n\n /**\n * Count of envelopes the guard has successfully synthesized this\n * trip. Used to assign deterministic, collision-free ids on\n * recovered `ModelToolCallRequest` entries.\n */\n private recoveredCount = 0;\n\n public constructor(options: JsonStreamGuardOptions) {\n this.tools = options.tools;\n this.maxBufferBytes = options.maxBufferBytes ?? DEFAULT_MAX_BUFFER_BYTES;\n this.onSafeDelta = options.onSafeDelta;\n this.onRecoveredCall = options.onRecoveredCall;\n }\n\n /**\n * Feed the next raw delta from the model. Splits the chunk into\n * characters and runs each through the state machine, awaiting\n * envelope classification whenever a buffer closes mid-chunk.\n *\n * The hot path (pass-through prose with no `{` / `` ` ``) is fully\n * synchronous — `await` here only blocks at buffer-close points,\n * which are rare in normal traffic.\n */\n public async feed(chunk: string): Promise<void> {\n for (let i = 0; i < chunk.length; i++) {\n await this.processChar(chunk[i]);\n }\n }\n\n /**\n * Stream ended. Anything still in the holdback was prose\n * misclassified as a partial fence opener — emit it. Anything still\n * in an open buffer never closed — emit it as text too (a leak\n * truncated mid-flight is still text the user partially saw).\n */\n public async finalize(): Promise<void> {\n if (this.holdback.length > 0) {\n this.onSafeDelta(this.holdback);\n this.holdback = \"\";\n }\n\n if (this.mode === \"bufferingBrace\") {\n this.flushBraceBufferAsText();\n return;\n }\n\n if (this.mode === \"bufferingFence\") {\n this.flushFenceBufferAsText();\n }\n }\n\n /**\n * True when at least one envelope was recovered this trip. The\n * agent loop reads this to override `finishReason` from `\"stop\"` to\n * `\"tool_calls\"` when the model reported a natural stop but the\n * guard found tool calls hiding in the text channel.\n */\n public hasRecoveredCalls(): boolean {\n return this.recoveredCount > 0;\n }\n\n /**\n * Route a single character based on the current mode. The\n * `passThrough` branch handles holdback expansion / flushing\n * iteratively (no recursion) so a character that \"breaks\" a fence\n * opener can be re-evaluated as a fresh pass-through input in the\n * same call.\n */\n private async processChar(char: string): Promise<void> {\n if (this.mode === \"bufferingBrace\") {\n await this.processBraceChar(char);\n return;\n }\n\n if (this.mode === \"bufferingFence\") {\n await this.processFenceChar(char);\n return;\n }\n\n let current = char;\n\n while (true) {\n if (this.holdback.length === 0 && current === \"{\") {\n this.openBraceBuffer(current);\n return;\n }\n\n const extended = this.holdback + current;\n\n if (this.isFenceOpenerPrefix(extended)) {\n this.holdback = extended;\n\n if (extended === FENCE_OPENER) {\n this.openFenceBuffer();\n }\n\n return;\n }\n\n if (this.holdback.length === 0) {\n this.onSafeDelta(current);\n return;\n }\n\n this.onSafeDelta(this.holdback);\n this.holdback = \"\";\n }\n }\n\n /**\n * Recognize any strict prefix of {@link FENCE_OPENER} including the\n * full string. Used to decide whether to keep extending the holdback\n * or flush it as plain text.\n */\n private isFenceOpenerPrefix(candidate: string): boolean {\n return candidate.length <= FENCE_OPENER.length && FENCE_OPENER.startsWith(candidate);\n }\n\n /**\n * Enter `bufferingBrace` mode with the seed `{` as the first buffer\n * character and the initial brace depth set to one. Any holdback at\n * this point was already a non-fence sequence so it stays empty.\n */\n private openBraceBuffer(seed: string): void {\n this.mode = \"bufferingBrace\";\n this.buffer = seed;\n this.braceDepth = 1;\n this.inString = false;\n this.escapeNext = false;\n }\n\n /**\n * Enter `bufferingFence` mode immediately after the opener\n * ```` ```json ```` matched in the holdback. Holdback resets;\n * subsequent characters accumulate into the buffer until the\n * closing fence is seen.\n */\n private openFenceBuffer(): void {\n this.mode = \"bufferingFence\";\n this.buffer = \"\";\n this.fenceCloseTail = \"\";\n this.holdback = \"\";\n }\n\n /**\n * Process one character while accumulating a brace-delimited JSON\n * object. Tracks string-literal context so `{` / `}` inside `\"...\"`\n * do not skew brace depth. Closes (and classifies) on balanced\n * braces; flushes-as-text on cap overflow.\n */\n private async processBraceChar(char: string): Promise<void> {\n this.buffer += char;\n\n if (this.inString) {\n if (this.escapeNext) {\n this.escapeNext = false;\n return;\n }\n\n if (char === \"\\\\\") {\n this.escapeNext = true;\n return;\n }\n\n if (char === '\"') {\n this.inString = false;\n }\n\n this.guardBufferCap(\"brace\");\n return;\n }\n\n if (char === '\"') {\n this.inString = true;\n this.guardBufferCap(\"brace\");\n return;\n }\n\n if (char === \"{\") {\n this.braceDepth++;\n this.guardBufferCap(\"brace\");\n return;\n }\n\n if (char === \"}\") {\n this.braceDepth--;\n\n if (this.braceDepth === 0) {\n await this.closeBraceBuffer();\n return;\n }\n\n this.guardBufferCap(\"brace\");\n return;\n }\n\n this.guardBufferCap(\"brace\");\n }\n\n /**\n * Process one character while accumulating a fence-delimited JSON\n * block. The closing fence ```` ``` ```` ends the block; the closing\n * characters are NOT included in the classified buffer (they are\n * re-emitted only when the block flushes back to text).\n */\n private async processFenceChar(char: string): Promise<void> {\n this.fenceCloseTail += char;\n\n if (this.fenceCloseTail.length > FENCE_CLOSER.length) {\n this.fenceCloseTail = this.fenceCloseTail.slice(-FENCE_CLOSER.length);\n }\n\n if (this.fenceCloseTail === FENCE_CLOSER) {\n const innerLength = this.buffer.length - (FENCE_CLOSER.length - 1);\n this.buffer = this.buffer.slice(0, Math.max(0, innerLength));\n\n await this.closeFenceBuffer();\n return;\n }\n\n this.buffer += char;\n this.guardBufferCap(\"fence\");\n }\n\n /**\n * Enforce the buffer-byte cap. When the current buffer exceeds the\n * cap, flush it back to the consumer as plain text and reset to\n * pass-through. Acts as a runaway / adversarial-input safety valve.\n */\n private guardBufferCap(source: \"brace\" | \"fence\"): void {\n if (this.buffer.length <= this.maxBufferBytes) {\n return;\n }\n\n if (source === \"brace\") {\n this.flushBraceBufferAsText();\n return;\n }\n\n this.flushFenceBufferAsText();\n }\n\n /**\n * Run the envelope matcher against the closed brace buffer. On a\n * match, synthesize a recovered `ModelToolCallRequest`; on no\n * match, flush the buffer back as plain text. Resets state to\n * pass-through either way.\n */\n private async closeBraceBuffer(): Promise<void> {\n const closed = this.buffer;\n\n this.resetToPassThrough();\n\n const matched = await this.tryMatchEnvelope(closed);\n\n if (matched) {\n return;\n }\n\n this.onSafeDelta(closed);\n }\n\n /**\n * Run the envelope matcher against the closed fence buffer. On a\n * match, synthesize a recovered call; on no match, flush as text\n * **with** the original opener and closer reconstructed so the\n * customer sees exactly the markdown the model emitted.\n */\n private async closeFenceBuffer(): Promise<void> {\n const closed = this.buffer;\n\n this.resetToPassThrough();\n\n const matched = await this.tryMatchEnvelope(closed);\n\n if (matched) {\n return;\n }\n\n this.onSafeDelta(`${FENCE_OPENER}${closed}${FENCE_CLOSER}`);\n }\n\n /**\n * Emit the brace-buffer verbatim as text and reset to pass-through.\n * Used on cap overflow and on `finalize()` for an unclosed buffer.\n */\n private flushBraceBufferAsText(): void {\n const closed = this.buffer;\n this.resetToPassThrough();\n this.onSafeDelta(closed);\n }\n\n /**\n * Emit the fence-buffer verbatim as text, reconstructing the\n * opener and closer so the original markdown structure is\n * preserved for the consumer.\n */\n private flushFenceBufferAsText(): void {\n const closed = this.buffer;\n this.resetToPassThrough();\n this.onSafeDelta(`${FENCE_OPENER}${closed}`);\n }\n\n /**\n * Reset all per-buffer state back to the pass-through baseline.\n * Called whenever a buffer closes — by recovery, by flush, or by\n * cap overflow — so the next character starts a fresh scan.\n */\n private resetToPassThrough(): void {\n this.mode = \"passThrough\";\n this.buffer = \"\";\n this.braceDepth = 0;\n this.inString = false;\n this.escapeNext = false;\n this.fenceCloseTail = \"\";\n }\n\n /**\n * Attempt to classify a closed buffer as a tool-call envelope. On\n * success, invoke `onRecoveredCall` with a synthesized request and\n * return `true`; on failure return `false` so the caller can flush\n * the buffer back as text.\n */\n private async tryMatchEnvelope(raw: string): Promise<boolean> {\n const parsed = safeParseJson(raw);\n\n if (parsed === undefined || typeof parsed !== \"object\" || parsed === null) {\n return false;\n }\n\n const envelope = parsed as Record<string, unknown>;\n const candidateName = readString(envelope, \"name\") ?? readString(envelope, \"tool\");\n const candidateInput = readObject(envelope, \"arguments\") ?? readObject(envelope, \"input\");\n\n if (!candidateName || !candidateInput) {\n return false;\n }\n\n const tool = this.tools.find((entry) => entry.name === candidateName);\n\n if (!tool || !tool.input) {\n return false;\n }\n\n const schema = tool.input as StandardSchemaV1<unknown>;\n\n let validationResult: StandardSchemaV1.Result<unknown>;\n\n try {\n validationResult = await schema[\"~standard\"].validate(candidateInput);\n } catch {\n return false;\n }\n\n if (validationResult.issues) {\n return false;\n }\n\n this.recoveredCount++;\n\n this.onRecoveredCall({\n id: `synth_${candidateName}_${this.recoveredCount}`,\n name: candidateName,\n input: validationResult.value,\n recoveredFrom: \"stream-text\",\n });\n\n return true;\n }\n}\n\n/**\n * Parse a JSON string returning `undefined` on any failure. Local to\n * the guard so it can distinguish \"not JSON\" from a parsed `null`\n * value, which `safeJsonParse` cannot — a parsed `null` is a valid\n * JSON value but not a valid envelope, and we want the difference.\n */\nfunction safeParseJson(raw: string): unknown {\n try {\n return JSON.parse(raw);\n } catch {\n return undefined;\n }\n}\n\n/**\n * Read a string-typed field from an envelope candidate. Returns\n * `undefined` when the key is missing or the value is non-string —\n * the matcher rejects either case.\n */\nfunction readString(envelope: Record<string, unknown>, key: string): string | undefined {\n const value = envelope[key];\n\n return typeof value === \"string\" && value.length > 0 ? value : undefined;\n}\n\n/**\n * Read an object-typed field from an envelope candidate. Returns\n * `undefined` when the key is missing or the value is not a\n * plain object (rejects arrays, primitives, null) — tool input\n * schemas always validate against an object root.\n */\nfunction readObject(\n envelope: Record<string, unknown>,\n key: string,\n): Record<string, unknown> | undefined {\n const value = envelope[key];\n\n if (value === null || typeof value !== \"object\" || Array.isArray(value)) {\n return undefined;\n }\n\n return value as Record<string, unknown>;\n}\n"],"mappings":"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"}
|