@stigmer/runner 3.10.0 → 3.11.1-dev.20260812192248
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 +14 -1
- package/dist/.build-fingerprint +1 -1
- package/dist/activities/call-llm.js +9 -10
- package/dist/activities/call-llm.js.map +1 -1
- package/dist/activities/classify-tool-approvals.d.ts +2 -1
- package/dist/activities/classify-tool-approvals.js +28 -2
- package/dist/activities/classify-tool-approvals.js.map +1 -1
- package/dist/activities/discover-mcp-server.d.ts +39 -0
- package/dist/activities/discover-mcp-server.js +165 -28
- package/dist/activities/discover-mcp-server.js.map +1 -1
- package/dist/activities/emit-event.d.ts +14 -2
- package/dist/activities/emit-event.js +52 -17
- package/dist/activities/emit-event.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.d.ts +8 -0
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js +1 -1
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js.map +1 -1
- package/dist/activities/execute-cursor/approval-state.d.ts +28 -2
- package/dist/activities/execute-cursor/approval-state.js +7 -1
- package/dist/activities/execute-cursor/approval-state.js.map +1 -1
- package/dist/activities/execute-cursor/attachment-resolver.d.ts +14 -0
- package/dist/activities/execute-cursor/attachment-resolver.js +18 -4
- package/dist/activities/execute-cursor/attachment-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/blueprint-resolver.d.ts +1 -9
- package/dist/activities/execute-cursor/blueprint-resolver.js +6 -22
- package/dist/activities/execute-cursor/blueprint-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/env-resolver.js +3 -1
- package/dist/activities/execute-cursor/env-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/error-classifier.d.ts +59 -3
- package/dist/activities/execute-cursor/error-classifier.js +115 -6
- package/dist/activities/execute-cursor/error-classifier.js.map +1 -1
- package/dist/activities/execute-cursor/extract-structured-output.d.ts +29 -0
- package/dist/activities/execute-cursor/extract-structured-output.js +58 -0
- package/dist/activities/execute-cursor/extract-structured-output.js.map +1 -0
- package/dist/activities/execute-cursor/hook-script.d.ts +14 -3
- package/dist/activities/execute-cursor/hook-script.js +72 -10
- package/dist/activities/execute-cursor/hook-script.js.map +1 -1
- package/dist/activities/execute-cursor/index.d.ts +51 -11
- package/dist/activities/execute-cursor/index.js +177 -93
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/mcp-resolver.d.ts +24 -1
- package/dist/activities/execute-cursor/mcp-resolver.js +5 -2
- package/dist/activities/execute-cursor/mcp-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/model-pricing-data.d.ts +2 -0
- package/dist/activities/execute-cursor/model-pricing-data.js +13 -3
- package/dist/activities/execute-cursor/model-pricing-data.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.d.ts +51 -4
- package/dist/activities/execute-cursor/prompt-builder.js +49 -7
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-cursor/turn-recovery.d.ts +52 -0
- package/dist/activities/execute-cursor/turn-recovery.js +193 -0
- package/dist/activities/execute-cursor/turn-recovery.js.map +1 -0
- package/dist/activities/execute-cursor/turn-stream.js +4 -1
- package/dist/activities/execute-cursor/turn-stream.js.map +1 -1
- package/dist/activities/execute-deep-agent/attachment-injector.d.ts +18 -1
- package/dist/activities/execute-deep-agent/attachment-injector.js +68 -23
- package/dist/activities/execute-deep-agent/attachment-injector.js.map +1 -1
- package/dist/activities/execute-deep-agent/environment.js +3 -1
- package/dist/activities/execute-deep-agent/environment.js.map +1 -1
- package/dist/activities/execute-deep-agent/index.js +15 -0
- package/dist/activities/execute-deep-agent/index.js.map +1 -1
- package/dist/activities/execute-deep-agent/prompt-builder.d.ts +7 -7
- package/dist/activities/execute-deep-agent/prompt-builder.js +8 -2
- package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.d.ts +10 -0
- package/dist/activities/execute-deep-agent/setup.js +65 -24
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/shell-env.d.ts +5 -1
- package/dist/activities/execute-deep-agent/shell-env.js +7 -9
- package/dist/activities/execute-deep-agent/shell-env.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-transformer.d.ts +18 -1
- package/dist/activities/execute-deep-agent/subagent-transformer.js +15 -2
- package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +27 -6
- package/dist/activities/execute-deep-agent/subagent-wiring.js +26 -5
- package/dist/activities/execute-deep-agent/subagent-wiring.js.map +1 -1
- package/dist/activities/hydrate-workflow-execution.js +8 -3
- package/dist/activities/hydrate-workflow-execution.js.map +1 -1
- package/dist/activities/run-command.d.ts +5 -2
- package/dist/activities/run-command.js +22 -10
- package/dist/activities/run-command.js.map +1 -1
- package/dist/activities/run-env.d.ts +40 -0
- package/dist/activities/run-env.js +66 -0
- package/dist/activities/run-env.js.map +1 -0
- package/dist/activities/workflow-event-activities.d.ts +28 -10
- package/dist/activities/workflow-event-activities.js +87 -58
- package/dist/activities/workflow-event-activities.js.map +1 -1
- package/dist/bootstrap.js +5 -0
- package/dist/bootstrap.js.map +1 -1
- package/dist/claimcheck/payload-codec.js +21 -1
- package/dist/claimcheck/payload-codec.js.map +1 -1
- package/dist/client/stigmer-client.d.ts +83 -21
- package/dist/client/stigmer-client.js +106 -29
- package/dist/client/stigmer-client.js.map +1 -1
- package/dist/encryption/config.d.ts +64 -0
- package/dist/encryption/config.js +108 -0
- package/dist/encryption/config.js.map +1 -0
- package/dist/encryption/index.d.ts +3 -0
- package/dist/encryption/index.js +3 -0
- package/dist/encryption/index.js.map +1 -0
- package/dist/encryption/payload-codec.d.ts +41 -0
- package/dist/encryption/payload-codec.js +130 -0
- package/dist/encryption/payload-codec.js.map +1 -0
- package/dist/middleware/index.d.ts +3 -0
- package/dist/middleware/index.js +7 -0
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/path-normalization.d.ts +57 -0
- package/dist/middleware/path-normalization.js +109 -0
- package/dist/middleware/path-normalization.js.map +1 -0
- package/dist/middleware/types.d.ts +8 -0
- package/dist/payload-codecs.d.ts +17 -0
- package/dist/payload-codecs.js +42 -0
- package/dist/payload-codecs.js.map +1 -0
- package/dist/preflight.d.ts +31 -0
- package/dist/preflight.js +43 -0
- package/dist/preflight.js.map +1 -1
- package/dist/runner-manager.js +21 -17
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.js +11 -18
- package/dist/runner.js.map +1 -1
- package/dist/shared/approval-policy.d.ts +9 -3
- package/dist/shared/approval-policy.js +15 -6
- package/dist/shared/approval-policy.js.map +1 -1
- package/dist/shared/artifact-storage.d.ts +23 -1
- package/dist/shared/artifact-storage.js +51 -10
- package/dist/shared/artifact-storage.js.map +1 -1
- package/dist/shared/attachment-naming.d.ts +53 -0
- package/dist/shared/attachment-naming.js +59 -0
- package/dist/shared/attachment-naming.js.map +1 -0
- package/dist/shared/caller-identity.d.ts +23 -2
- package/dist/shared/caller-identity.js +36 -5
- package/dist/shared/caller-identity.js.map +1 -1
- package/dist/shared/channel-attachment.js +1 -0
- package/dist/shared/channel-attachment.js.map +1 -1
- package/dist/shared/checkpointer/http-saver.d.ts +26 -1
- package/dist/shared/checkpointer/http-saver.js +54 -8
- package/dist/shared/checkpointer/http-saver.js.map +1 -1
- package/dist/shared/conversation-attachment.js +1 -0
- package/dist/shared/conversation-attachment.js.map +1 -1
- package/dist/shared/conversation-catchup.d.ts +9 -2
- package/dist/shared/conversation-catchup.js +39 -6
- package/dist/shared/conversation-catchup.js.map +1 -1
- package/dist/shared/datastore-attachment.d.ts +50 -7
- package/dist/shared/datastore-attachment.js +93 -11
- package/dist/shared/datastore-attachment.js.map +1 -1
- package/dist/shared/grpc-retry.d.ts +5 -1
- package/dist/shared/grpc-retry.js +5 -1
- package/dist/shared/grpc-retry.js.map +1 -1
- package/dist/shared/http-retry.d.ts +85 -0
- package/dist/shared/http-retry.js +101 -0
- package/dist/shared/http-retry.js.map +1 -0
- package/dist/shared/llm-backend.d.ts +275 -0
- package/dist/shared/llm-backend.js +425 -0
- package/dist/shared/llm-backend.js.map +1 -0
- package/dist/shared/llm-proxy.d.ts +8 -0
- package/dist/shared/llm-proxy.js +15 -0
- package/dist/shared/llm-proxy.js.map +1 -1
- package/dist/shared/mcp-enabled-tools.d.ts +57 -0
- package/dist/shared/mcp-enabled-tools.js +86 -0
- package/dist/shared/mcp-enabled-tools.js.map +1 -0
- package/dist/shared/mcp-manager.d.ts +11 -5
- package/dist/shared/mcp-manager.js +47 -8
- package/dist/shared/mcp-manager.js.map +1 -1
- package/dist/shared/mcp-resolver.d.ts +39 -2
- package/dist/shared/mcp-resolver.js +38 -2
- package/dist/shared/mcp-resolver.js.map +1 -1
- package/dist/shared/mcp-schema-sanitizer.d.ts +70 -0
- package/dist/shared/mcp-schema-sanitizer.js +197 -0
- package/dist/shared/mcp-schema-sanitizer.js.map +1 -0
- package/dist/shared/model-client.d.ts +18 -5
- package/dist/shared/model-client.js +149 -18
- package/dist/shared/model-client.js.map +1 -1
- package/dist/shared/model-error.js +198 -5
- package/dist/shared/model-error.js.map +1 -1
- package/dist/shared/model-pricing-data.d.ts +2 -0
- package/dist/shared/model-pricing-data.js +13 -3
- package/dist/shared/model-pricing-data.js.map +1 -1
- package/dist/shared/model-registry.js +3 -2
- package/dist/shared/model-registry.js.map +1 -1
- package/dist/shared/plan-mode-permissions.d.ts +33 -0
- package/dist/shared/plan-mode-permissions.js +35 -0
- package/dist/shared/plan-mode-permissions.js.map +1 -0
- package/dist/shared/registry-endpoint.d.ts +20 -0
- package/dist/shared/registry-endpoint.js +25 -0
- package/dist/shared/registry-endpoint.js.map +1 -1
- package/dist/shared/runner-credential-keys.d.ts +24 -0
- package/dist/shared/runner-credential-keys.js +47 -0
- package/dist/shared/runner-credential-keys.js.map +1 -0
- package/dist/worker.d.ts +2 -1
- package/dist/worker.js +2 -4
- package/dist/worker.js.map +1 -1
- package/dist/workflow-engine/resolve.d.ts +19 -1
- package/dist/workflow-engine/resolve.js +37 -2
- package/dist/workflow-engine/resolve.js.map +1 -1
- package/dist/workflow-engine/types.d.ts +18 -0
- package/dist/workflow-engine/types.js.map +1 -1
- package/dist/workflows/call-agent-orchestrator.d.ts +9 -0
- package/dist/workflows/call-agent-orchestrator.js +1 -0
- package/dist/workflows/call-agent-orchestrator.js.map +1 -1
- package/dist/workflows/connect-mcp-server.js +9 -0
- package/dist/workflows/connect-mcp-server.js.map +1 -1
- package/dist/workflows/engine-core.js +23 -2
- package/dist/workflows/engine-core.js.map +1 -1
- package/dist/workflows/execute-from-execution.d.ts +1 -1
- package/dist/workflows/execute-from-execution.js +11 -1
- package/dist/workflows/execute-from-execution.js.map +1 -1
- package/dist/workflows/types.d.ts +8 -0
- package/package.json +9 -4
- package/src/__test-utils__/__tests__/vitest-global-setup.test.ts +37 -0
- package/src/__test-utils__/vitest-global-setup.ts +50 -0
- package/src/__tests__/bootstrap.test.ts +43 -0
- package/src/__tests__/claimcheck-codec.test.ts +36 -0
- package/src/__tests__/encryption-codec.test.ts +287 -0
- package/src/__tests__/fixtures/encrypted-payload-fixture.json +15 -0
- package/src/__tests__/history-encryption-e2e.test.ts +243 -0
- package/src/__tests__/preflight.test.ts +65 -2
- package/src/activities/__tests__/call-llm.test.ts +75 -0
- package/src/activities/__tests__/classify-tool-approvals.test.ts +117 -1
- package/src/activities/__tests__/discover-mcp-server.hang.test.ts +103 -0
- package/src/activities/__tests__/discover-mcp-server.test.ts +242 -1
- package/src/activities/__tests__/error-classifier.test.ts +94 -2
- package/src/activities/__tests__/run-command.test.ts +221 -0
- package/src/activities/__tests__/workflow-event-activities.test.ts +107 -8
- package/src/activities/call-llm.ts +9 -16
- package/src/activities/classify-tool-approvals.ts +34 -4
- package/src/activities/discover-mcp-server.ts +203 -31
- package/src/activities/emit-event.ts +65 -21
- package/src/activities/execute-cursor/__test-utils__/cursor-hook-harness.ts +9 -0
- package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +14 -0
- package/src/activities/execute-cursor/__tests__/attachment-resolver.test.ts +53 -0
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +151 -16
- package/src/activities/execute-cursor/__tests__/error-classifier-extraction.test.ts +208 -0
- package/src/activities/execute-cursor/__tests__/extract-structured-output.test.ts +120 -0
- package/src/activities/execute-cursor/__tests__/hook-script.test.ts +93 -0
- package/src/activities/execute-cursor/__tests__/mcp-resolver.test.ts +125 -0
- package/src/activities/execute-cursor/__tests__/model-pricing.test.ts +62 -1
- package/src/activities/execute-cursor/__tests__/prompt-builder-delegation.test.ts +1 -1
- package/src/activities/execute-cursor/__tests__/turn-recovery.test.ts +160 -0
- package/src/activities/execute-cursor/__tests__/turn-stream.test.ts +13 -0
- package/src/activities/execute-cursor/approval-state.ts +30 -1
- package/src/activities/execute-cursor/attachment-resolver.ts +31 -3
- package/src/activities/execute-cursor/blueprint-resolver.ts +7 -27
- package/src/activities/execute-cursor/env-resolver.ts +3 -1
- package/src/activities/execute-cursor/error-classifier.ts +131 -7
- package/src/activities/execute-cursor/extract-structured-output.ts +72 -0
- package/src/activities/execute-cursor/hook-script.ts +74 -10
- package/src/activities/execute-cursor/index.ts +209 -109
- package/src/activities/execute-cursor/mcp-resolver.ts +36 -2
- package/src/activities/execute-cursor/model-pricing-data.ts +22 -3
- package/src/activities/execute-cursor/prompt-builder.ts +85 -9
- package/src/activities/execute-cursor/turn-recovery.ts +208 -0
- package/src/activities/execute-cursor/turn-stream.ts +5 -2
- package/src/activities/execute-deep-agent/__tests__/attachment-injector.test.ts +110 -8
- package/src/activities/execute-deep-agent/__tests__/datastore-degradation.test.ts +104 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +2 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/plan-mode-path-normalization.test.ts +174 -0
- package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +34 -5
- package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/shell-env.test.ts +24 -10
- package/src/activities/execute-deep-agent/__tests__/subagent-plan-mode-permissions.test.ts +178 -0
- package/src/activities/execute-deep-agent/__tests__/subagent-wiring.test.ts +12 -7
- package/src/activities/execute-deep-agent/attachment-injector.ts +94 -30
- package/src/activities/execute-deep-agent/environment.ts +3 -1
- package/src/activities/execute-deep-agent/index.ts +20 -0
- package/src/activities/execute-deep-agent/prompt-builder.ts +20 -10
- package/src/activities/execute-deep-agent/setup.ts +85 -29
- package/src/activities/execute-deep-agent/shell-env.ts +8 -9
- package/src/activities/execute-deep-agent/subagent-transformer.ts +30 -2
- package/src/activities/execute-deep-agent/subagent-wiring.ts +39 -6
- package/src/activities/hydrate-workflow-execution.ts +8 -3
- package/src/activities/run-command.ts +25 -12
- package/src/activities/run-env.ts +79 -0
- package/src/activities/workflow-event-activities.ts +96 -69
- package/src/bootstrap.ts +5 -0
- package/src/claimcheck/payload-codec.ts +33 -1
- package/src/client/__tests__/stigmer-client.test.ts +110 -11
- package/src/client/stigmer-client.ts +150 -37
- package/src/encryption/config.ts +155 -0
- package/src/encryption/index.ts +3 -0
- package/src/encryption/payload-codec.ts +152 -0
- package/src/middleware/__tests__/path-normalization.test.ts +140 -0
- package/src/middleware/index.ts +8 -0
- package/src/middleware/path-normalization.ts +125 -0
- package/src/middleware/types.ts +11 -0
- package/src/payload-codecs.ts +62 -0
- package/src/preflight.ts +45 -0
- package/src/runner-manager.ts +26 -26
- package/src/runner.ts +12 -27
- package/src/shared/__tests__/approval-policy.test.ts +82 -39
- package/src/shared/__tests__/artifact-storage.test.ts +145 -2
- package/src/shared/__tests__/attachment-naming.test.ts +159 -0
- package/src/shared/__tests__/bedrock-adapter.test.ts +213 -0
- package/src/shared/__tests__/bedrock-seam.test.ts +390 -0
- package/src/shared/__tests__/caller-identity.test.ts +25 -0
- package/src/shared/__tests__/channel-attachment.test.ts +1 -1
- package/src/shared/__tests__/connect-backfill.test.ts +1 -0
- package/src/shared/__tests__/conversation-attachment.test.ts +1 -1
- package/src/shared/__tests__/conversation-catchup.test.ts +24 -0
- package/src/shared/__tests__/datastore-attachment.test.ts +129 -1
- package/src/shared/__tests__/foundry-adapter.test.ts +276 -0
- package/src/shared/__tests__/foundry-seam.test.ts +482 -0
- package/src/shared/__tests__/http-retry.test.ts +68 -0
- package/src/shared/__tests__/llm-backend.test.ts +616 -0
- package/src/shared/__tests__/mcp-enabled-tools.test.ts +86 -0
- package/src/shared/__tests__/mcp-manager.test.ts +137 -15
- package/src/shared/__tests__/mcp-resolver.test.ts +146 -3
- package/src/shared/__tests__/mcp-schema-sanitizer.test.ts +267 -0
- package/src/shared/__tests__/model-client.test.ts +200 -0
- package/src/shared/__tests__/model-error.test.ts +289 -1
- package/src/shared/__tests__/model-pricing.test.ts +62 -2
- package/src/shared/__tests__/model-registry.test.ts +22 -0
- package/src/shared/__tests__/synthesized-attachment.test.ts +1 -0
- package/src/shared/__tests__/vertex-adapter.test.ts +169 -0
- package/src/shared/__tests__/vertex-seam.test.ts +295 -0
- package/src/shared/approval-policy.ts +14 -7
- package/src/shared/artifact-storage.ts +75 -10
- package/src/shared/attachment-naming.ts +78 -0
- package/src/shared/caller-identity.ts +40 -5
- package/src/shared/channel-attachment.ts +1 -0
- package/src/shared/checkpointer/__tests__/http-saver.test.ts +196 -1
- package/src/shared/checkpointer/http-saver.ts +71 -8
- package/src/shared/conversation-attachment.ts +1 -0
- package/src/shared/conversation-catchup.ts +39 -6
- package/src/shared/datastore-attachment.ts +106 -11
- package/src/shared/grpc-retry.ts +5 -1
- package/src/shared/http-retry.ts +139 -0
- package/src/shared/llm-backend.ts +544 -0
- package/src/shared/llm-proxy.ts +15 -0
- package/src/shared/mcp-enabled-tools.ts +105 -0
- package/src/shared/mcp-manager.ts +56 -8
- package/src/shared/mcp-resolver.ts +73 -2
- package/src/shared/mcp-schema-sanitizer.ts +224 -0
- package/src/shared/model-client.ts +179 -19
- package/src/shared/model-error.ts +222 -4
- package/src/shared/model-pricing-data.ts +22 -3
- package/src/shared/model-registry.ts +7 -2
- package/src/shared/plan-mode-permissions.ts +37 -0
- package/src/shared/registry-endpoint.ts +27 -0
- package/src/shared/runner-credential-keys.ts +46 -0
- package/src/worker.ts +4 -5
- package/src/workflow-engine/__tests__/tasks/emit-event.test.ts +127 -49
- package/src/workflow-engine/resolve.ts +48 -2
- package/src/workflow-engine/types.ts +18 -0
- package/src/workflows/__tests__/connect-mcp-server.test.ts +25 -0
- package/src/workflows/__tests__/execute-serverless-workflow.test.ts +68 -2
- package/src/workflows/call-agent-orchestrator.ts +10 -0
- package/src/workflows/connect-mcp-server.ts +9 -0
- package/src/workflows/engine-core.ts +23 -2
- package/src/workflows/execute-from-execution.ts +12 -2
- package/src/workflows/types.ts +8 -0
- package/dist/activities/execute-cursor/mcp-config.d.ts +0 -30
- package/dist/activities/execute-cursor/mcp-config.js +0 -39
- package/dist/activities/execute-cursor/mcp-config.js.map +0 -1
- package/src/activities/execute-cursor/mcp-config.ts +0 -66
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter test for the vertex backend branch of buildChatModel — the
|
|
3
|
+
* complement of vertex-seam.test.ts.
|
|
4
|
+
*
|
|
5
|
+
* The seam test pins the REAL AnthropicVertex client's wire behavior under
|
|
6
|
+
* a hand-built ChatAnthropic. This file pins OUR production wiring: a real
|
|
7
|
+
* `buildChatModel` and a real `ChatAnthropic` drive a mocked
|
|
8
|
+
* `@anthropic-ai/vertex-sdk`, so LangChain's lazy `createClient` invocation
|
|
9
|
+
* path (client constructed on first request, once per cached client) is
|
|
10
|
+
* exercised exactly as production does it. Together the two files cover the
|
|
11
|
+
* whole chain without any test-only injection seam in production code.
|
|
12
|
+
*
|
|
13
|
+
* Pinned here:
|
|
14
|
+
* - the factory forwards LangChain's `maxRetries: 0` to the client (a
|
|
15
|
+
* factory that drops it nests SDK retries inside LangChain's own loop),
|
|
16
|
+
* - the factory forwards the request timeout (clientOptions.timeout ->
|
|
17
|
+
* factory options -> SDK constructor; how STIGMER_LLM_REQUEST_TIMEOUT_MS
|
|
18
|
+
* bounds this backend),
|
|
19
|
+
* - construction and invocation succeed with NO ANTHROPIC_API_KEY (Vertex
|
|
20
|
+
* auth is Google's, and ChatAnthropic waives the key when createClient
|
|
21
|
+
* is provided),
|
|
22
|
+
* - the translated `@date` id is what crosses the wire while the returned
|
|
23
|
+
* apiModelId stays canonical (the canonical-id invariant),
|
|
24
|
+
* - usage_metadata survives the adapter (what billing reads).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
28
|
+
import { HumanMessage, AIMessage } from "@langchain/core/messages";
|
|
29
|
+
|
|
30
|
+
const { vertexCtorArgs, createRequests } = vi.hoisted(() => ({
|
|
31
|
+
vertexCtorArgs: [] as Array<{ maxRetries?: number; timeout?: number }>,
|
|
32
|
+
createRequests: [] as Array<Record<string, unknown>>,
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
vi.mock("@anthropic-ai/vertex-sdk", () => ({
|
|
36
|
+
AnthropicVertex: class {
|
|
37
|
+
readonly maxRetries: number | undefined;
|
|
38
|
+
readonly messages = {
|
|
39
|
+
create: async (request: Record<string, unknown>) => {
|
|
40
|
+
createRequests.push(request);
|
|
41
|
+
return {
|
|
42
|
+
id: "msg_vertex_adapter_01",
|
|
43
|
+
type: "message",
|
|
44
|
+
role: "assistant",
|
|
45
|
+
model: request.model,
|
|
46
|
+
content: [{ type: "text", text: "Namaste from Vertex." }],
|
|
47
|
+
stop_reason: "end_turn",
|
|
48
|
+
stop_sequence: null,
|
|
49
|
+
usage: { input_tokens: 7, output_tokens: 5 },
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
constructor(opts: { maxRetries?: number; timeout?: number }) {
|
|
55
|
+
vertexCtorArgs.push(opts);
|
|
56
|
+
this.maxRetries = opts.maxRetries;
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
import { ChatAnthropic } from "@langchain/anthropic";
|
|
62
|
+
|
|
63
|
+
import { buildChatModel } from "../model-client.js";
|
|
64
|
+
import { _resetRegistryCache } from "../model-registry.js";
|
|
65
|
+
import { toVertexModelId } from "../llm-backend.js";
|
|
66
|
+
|
|
67
|
+
describe("buildChatModel vertex adapter", () => {
|
|
68
|
+
const savedEnv = { ...process.env };
|
|
69
|
+
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
_resetRegistryCache();
|
|
72
|
+
vertexCtorArgs.length = 0;
|
|
73
|
+
createRequests.length = 0;
|
|
74
|
+
delete process.env.ANTHROPIC_API_KEY;
|
|
75
|
+
process.env.STIGMER_ANTHROPIC_BACKEND = "vertex";
|
|
76
|
+
process.env.CLOUD_ML_REGION = "asia-south1";
|
|
77
|
+
// Registry offline → resolveToApiModelId passes the id through, keeping
|
|
78
|
+
// this test free of registry fixtures (that path has its own tests).
|
|
79
|
+
vi.spyOn(globalThis, "fetch").mockRejectedValue(new Error("registry offline"));
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
afterEach(() => {
|
|
83
|
+
_resetRegistryCache();
|
|
84
|
+
vi.restoreAllMocks();
|
|
85
|
+
process.env = { ...savedEnv };
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("drives the mocked client through the real createClient path with the invariants intact", async () => {
|
|
89
|
+
const { model, apiModelId } = await buildChatModel({
|
|
90
|
+
modelName: "claude-sonnet-4-5-20250929",
|
|
91
|
+
maxTokens: 1024,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Canonical id returned; the client is not constructed yet (lazy factory).
|
|
95
|
+
expect(apiModelId).toBe("claude-sonnet-4-5-20250929");
|
|
96
|
+
expect(vertexCtorArgs).toHaveLength(0);
|
|
97
|
+
|
|
98
|
+
const result = await model.invoke([new HumanMessage("Weather in Chennai?")]);
|
|
99
|
+
|
|
100
|
+
// First request constructed exactly one (batch) client, with LangChain's
|
|
101
|
+
// maxRetries: 0 honored — LangChain owns retrying.
|
|
102
|
+
expect(vertexCtorArgs).toHaveLength(1);
|
|
103
|
+
expect(vertexCtorArgs[0].maxRetries).toBe(0);
|
|
104
|
+
|
|
105
|
+
// The wire carries the Vertex-translated id; billing already got the
|
|
106
|
+
// canonical one above.
|
|
107
|
+
expect(createRequests).toHaveLength(1);
|
|
108
|
+
expect(createRequests[0].model).toBe("claude-sonnet-4-5@20250929");
|
|
109
|
+
expect(createRequests[0].max_tokens).toBe(1024);
|
|
110
|
+
|
|
111
|
+
// Response and usage flow back through LangChain untouched.
|
|
112
|
+
expect(result).toBeInstanceOf(AIMessage);
|
|
113
|
+
expect(result.text).toBe("Namaste from Vertex.");
|
|
114
|
+
expect((result as AIMessage).usage_metadata).toMatchObject({
|
|
115
|
+
input_tokens: 7,
|
|
116
|
+
output_tokens: 5,
|
|
117
|
+
total_tokens: 12,
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("translated ids inherit the same default maxTokens as canonical ids", () => {
|
|
122
|
+
// setup.ts deliberately omits maxTokens, so the LangChain per-model
|
|
123
|
+
// default applies. That default prefix-matches the model string — and the
|
|
124
|
+
// vertex branch hands ChatAnthropic the TRANSLATED string. This pins the
|
|
125
|
+
// equality the zero-behavior-change criterion rests on (verified 16384 ==
|
|
126
|
+
// 16384 for 4.5-generation ids at @langchain/anthropic 1.4.0); a future
|
|
127
|
+
// bump that diverges the two forms fails here instead of silently capping
|
|
128
|
+
// vertex deployments differently from public ones.
|
|
129
|
+
for (const canonical of [
|
|
130
|
+
"claude-sonnet-4-5-20250929",
|
|
131
|
+
"claude-haiku-4-5-20251001",
|
|
132
|
+
"claude-sonnet-4-6",
|
|
133
|
+
]) {
|
|
134
|
+
const publicModel = new ChatAnthropic({ model: canonical, apiKey: "probe" });
|
|
135
|
+
const vertexModel = new ChatAnthropic({ model: toVertexModelId(canonical), apiKey: "probe" });
|
|
136
|
+
expect(vertexModel.maxTokens, canonical).toBe(publicModel.maxTokens);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("forwards the request timeout to the SDK client (STIGMER_LLM_REQUEST_TIMEOUT_MS path)", async () => {
|
|
141
|
+
// The timeout rides clientOptions -> factory options -> SDK constructor.
|
|
142
|
+
// A factory that drops it silently unbounds the operator's timeout on
|
|
143
|
+
// this backend (the regression T06 repaired for the public path).
|
|
144
|
+
const { model } = await buildChatModel({
|
|
145
|
+
modelName: "claude-sonnet-4-6",
|
|
146
|
+
maxTokens: 256,
|
|
147
|
+
timeoutMs: 5000,
|
|
148
|
+
});
|
|
149
|
+
await model.invoke([new HumanMessage("hi")]);
|
|
150
|
+
|
|
151
|
+
expect(vertexCtorArgs).toHaveLength(1);
|
|
152
|
+
expect(vertexCtorArgs[0].timeout).toBe(5000);
|
|
153
|
+
expect(vertexCtorArgs[0].maxRetries).toBe(0);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("constructs and invokes with no ANTHROPIC_API_KEY anywhere in the environment", async () => {
|
|
157
|
+
expect(process.env.ANTHROPIC_API_KEY).toBeUndefined();
|
|
158
|
+
|
|
159
|
+
const { model } = await buildChatModel({
|
|
160
|
+
modelName: "claude-sonnet-4-6",
|
|
161
|
+
maxTokens: 256,
|
|
162
|
+
});
|
|
163
|
+
const result = await model.invoke([new HumanMessage("hi")]);
|
|
164
|
+
|
|
165
|
+
expect(result).toBeInstanceOf(AIMessage);
|
|
166
|
+
// Dateless 4.6-generation id crossed the wire untranslated.
|
|
167
|
+
expect(createRequests.at(-1)?.model).toBe("claude-sonnet-4-6");
|
|
168
|
+
});
|
|
169
|
+
});
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Characterization test for the ChatAnthropic `createClient` -> AnthropicVertex
|
|
3
|
+
* seam — the integration T02's vertex backend adapter will be built on.
|
|
4
|
+
*
|
|
5
|
+
* This is NOT a unit test of our code (there is no production vertex code
|
|
6
|
+
* yet). It pins the exact cross-package behavior production will rely on:
|
|
7
|
+
* the REAL `ChatAnthropic` (@langchain/anthropic, bundling @anthropic-ai/sdk
|
|
8
|
+
* 0.95.x) driving the REAL `AnthropicVertex` client (@anthropic-ai/vertex-sdk,
|
|
9
|
+
* bundling its own nested @anthropic-ai/sdk >=0.115). If a future bump of
|
|
10
|
+
* either side changes request shaping, streaming event handling, tool-call
|
|
11
|
+
* assembly, or usage accounting across this seam, this suite fails in CI
|
|
12
|
+
* instead of production. See scripts/check-langchain-deps.sh for why two
|
|
13
|
+
* @anthropic-ai/sdk copies coexist and when they collapse to one.
|
|
14
|
+
*
|
|
15
|
+
* Determinism: zero credentials, zero network. Google auth is bypassed by
|
|
16
|
+
* injecting a fake `authClient` (the SDK's supported constructor option —
|
|
17
|
+
* `accessToken` alone does NOT skip the auth client, adaptRequest always
|
|
18
|
+
* awaits it), and transport is a recording `fetch` injected through the
|
|
19
|
+
* SDK's own `fetch` option. No global patching, no SDK mocks.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { describe, it, expect } from "vitest";
|
|
23
|
+
import { ChatAnthropic } from "@langchain/anthropic";
|
|
24
|
+
import { AnthropicVertex } from "@anthropic-ai/vertex-sdk";
|
|
25
|
+
import type { AuthClient } from "google-auth-library";
|
|
26
|
+
import { HumanMessage, AIMessage, AIMessageChunk } from "@langchain/core/messages";
|
|
27
|
+
|
|
28
|
+
// ─── Fixtures ────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
/** Dated pre-4.6 id in Vertex's `@date` form (see llm-backend.ts translation). */
|
|
31
|
+
const VERTEX_MODEL_ID = "claude-sonnet-4-5@20250929";
|
|
32
|
+
const PROJECT_ID = "test-project";
|
|
33
|
+
const REGION = "asia-south1";
|
|
34
|
+
|
|
35
|
+
const WEATHER_TOOL = {
|
|
36
|
+
name: "get_weather",
|
|
37
|
+
description: "Get the current weather for a city.",
|
|
38
|
+
input_schema: {
|
|
39
|
+
type: "object" as const,
|
|
40
|
+
properties: { city: { type: "string" } },
|
|
41
|
+
required: ["city"],
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** Non-streaming (`:rawPredict`) response: text + tool_use + usage. */
|
|
46
|
+
const MESSAGE_RESPONSE = {
|
|
47
|
+
id: "msg_vertex_test_01",
|
|
48
|
+
type: "message",
|
|
49
|
+
role: "assistant",
|
|
50
|
+
model: VERTEX_MODEL_ID,
|
|
51
|
+
content: [
|
|
52
|
+
{ type: "text", text: "I'll check the weather." },
|
|
53
|
+
{ type: "tool_use", id: "toolu_test_01", name: "get_weather", input: { city: "Chennai" } },
|
|
54
|
+
],
|
|
55
|
+
stop_reason: "tool_use",
|
|
56
|
+
stop_sequence: null,
|
|
57
|
+
usage: { input_tokens: 25, output_tokens: 17 },
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Streaming (`:streamRawPredict`) response as Anthropic SSE: a text block,
|
|
62
|
+
* a tool_use block assembled from input_json_delta, then cumulative usage in
|
|
63
|
+
* message_delta — the exact event grammar the nested SDK parses.
|
|
64
|
+
*/
|
|
65
|
+
const SSE_EVENTS: ReadonlyArray<[string, object]> = [
|
|
66
|
+
["message_start", {
|
|
67
|
+
type: "message_start",
|
|
68
|
+
message: {
|
|
69
|
+
id: "msg_vertex_test_02", type: "message", role: "assistant",
|
|
70
|
+
model: VERTEX_MODEL_ID, content: [], stop_reason: null, stop_sequence: null,
|
|
71
|
+
usage: { input_tokens: 25, output_tokens: 1 },
|
|
72
|
+
},
|
|
73
|
+
}],
|
|
74
|
+
["content_block_start", { type: "content_block_start", index: 0, content_block: { type: "text", text: "" } }],
|
|
75
|
+
["content_block_delta", { type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "I'll check the weather." } }],
|
|
76
|
+
["content_block_stop", { type: "content_block_stop", index: 0 }],
|
|
77
|
+
["content_block_start", { type: "content_block_start", index: 1, content_block: { type: "tool_use", id: "toolu_test_02", name: "get_weather", input: {} } }],
|
|
78
|
+
["content_block_delta", { type: "content_block_delta", index: 1, delta: { type: "input_json_delta", partial_json: '{"city":"Chennai"}' } }],
|
|
79
|
+
["content_block_stop", { type: "content_block_stop", index: 1 }],
|
|
80
|
+
["message_delta", { type: "message_delta", delta: { stop_reason: "tool_use", stop_sequence: null }, usage: { output_tokens: 17 } }],
|
|
81
|
+
["message_stop", { type: "message_stop" }],
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
function sseBody(): string {
|
|
85
|
+
return SSE_EVENTS
|
|
86
|
+
.map(([event, data]) => `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`)
|
|
87
|
+
.join("");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ─── Harness ─────────────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
interface RecordedRequest {
|
|
93
|
+
url: string;
|
|
94
|
+
method: string;
|
|
95
|
+
headers: Record<string, string>;
|
|
96
|
+
body: Record<string, unknown>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface SeamHarness {
|
|
100
|
+
model: ChatAnthropic;
|
|
101
|
+
requests: RecordedRequest[];
|
|
102
|
+
/** Options ChatAnthropic passed to the createClient factory. */
|
|
103
|
+
factoryOptions: Array<{ maxRetries?: number }>;
|
|
104
|
+
/** The AnthropicVertex instances the factory constructed. */
|
|
105
|
+
clients: AnthropicVertex[];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Build a real ChatAnthropic wired to a real AnthropicVertex through the
|
|
110
|
+
* `createClient` seam, with transport replaced by a recording fetch.
|
|
111
|
+
*
|
|
112
|
+
* The factory honors `maxRetries` from the incoming options: LangChain owns
|
|
113
|
+
* retrying (its AsyncCaller wraps every request) and passes `maxRetries: 0`
|
|
114
|
+
* so the underlying SDK must not retry underneath it. A factory that ignored
|
|
115
|
+
* this would nest the Vertex SDK's default 2 retries inside LangChain's loop,
|
|
116
|
+
* multiplying every transient failure — the T02 adapter must preserve this.
|
|
117
|
+
*/
|
|
118
|
+
function buildSeamHarness(): SeamHarness {
|
|
119
|
+
const requests: RecordedRequest[] = [];
|
|
120
|
+
const factoryOptions: SeamHarness["factoryOptions"] = [];
|
|
121
|
+
const clients: AnthropicVertex[] = [];
|
|
122
|
+
|
|
123
|
+
const recordingFetch: typeof fetch = async (input, init) => {
|
|
124
|
+
const headers: Record<string, string> = {};
|
|
125
|
+
new Headers(init?.headers).forEach((value, key) => {
|
|
126
|
+
headers[key] = value;
|
|
127
|
+
});
|
|
128
|
+
requests.push({
|
|
129
|
+
url: String(input),
|
|
130
|
+
method: init?.method ?? "GET",
|
|
131
|
+
headers,
|
|
132
|
+
body: JSON.parse(String(init?.body)) as Record<string, unknown>,
|
|
133
|
+
});
|
|
134
|
+
const streaming = String(input).includes(":streamRawPredict");
|
|
135
|
+
return streaming
|
|
136
|
+
? new Response(sseBody(), {
|
|
137
|
+
status: 200,
|
|
138
|
+
headers: { "Content-Type": "text/event-stream" },
|
|
139
|
+
})
|
|
140
|
+
: new Response(JSON.stringify(MESSAGE_RESPONSE), {
|
|
141
|
+
status: 200,
|
|
142
|
+
headers: { "Content-Type": "application/json" },
|
|
143
|
+
});
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Minimal structural fake of google-auth-library's AuthClient — the Vertex
|
|
147
|
+
// SDK's adaptRequest calls exactly `getRequestHeaders()` (merged into the
|
|
148
|
+
// outbound request) and reads `projectId`. The cast is test-only: building
|
|
149
|
+
// a real AuthClient would require credentials, defeating determinism.
|
|
150
|
+
const fakeAuthClient = {
|
|
151
|
+
projectId: PROJECT_ID,
|
|
152
|
+
getRequestHeaders: async () => new Headers({ authorization: "Bearer test-token" }),
|
|
153
|
+
} as unknown as AuthClient;
|
|
154
|
+
|
|
155
|
+
const model = new ChatAnthropic({
|
|
156
|
+
model: VERTEX_MODEL_ID,
|
|
157
|
+
temperature: 0,
|
|
158
|
+
maxTokens: 1024,
|
|
159
|
+
createClient: (options) => {
|
|
160
|
+
factoryOptions.push({ maxRetries: options.maxRetries });
|
|
161
|
+
const client = new AnthropicVertex({
|
|
162
|
+
projectId: PROJECT_ID,
|
|
163
|
+
region: REGION,
|
|
164
|
+
authClient: fakeAuthClient,
|
|
165
|
+
fetch: recordingFetch,
|
|
166
|
+
maxRetries: options.maxRetries,
|
|
167
|
+
});
|
|
168
|
+
clients.push(client);
|
|
169
|
+
return client;
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
return { model, requests, factoryOptions, clients };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const expectedUrl = (specifier: "rawPredict" | "streamRawPredict") =>
|
|
177
|
+
`https://${REGION}-aiplatform.googleapis.com/v1/projects/${PROJECT_ID}` +
|
|
178
|
+
`/locations/${REGION}/publishers/anthropic/models/${VERTEX_MODEL_ID}:${specifier}`;
|
|
179
|
+
|
|
180
|
+
// ─── Tests ───────────────────────────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
describe("ChatAnthropic createClient -> AnthropicVertex seam", () => {
|
|
183
|
+
it("constructs without an Anthropic API key when createClient is provided", () => {
|
|
184
|
+
// chat_models.js waives the "Anthropic API key not found" check for
|
|
185
|
+
// factory-constructed clients — the waiver the vertex backend depends on.
|
|
186
|
+
expect(() => buildSeamHarness()).not.toThrow();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("shapes a non-streaming request into Vertex wire form (:rawPredict)", async () => {
|
|
190
|
+
const h = buildSeamHarness();
|
|
191
|
+
|
|
192
|
+
const result = await h.model.invoke([new HumanMessage("Weather in Chennai?")]);
|
|
193
|
+
|
|
194
|
+
expect(h.requests).toHaveLength(1);
|
|
195
|
+
const req = h.requests[0];
|
|
196
|
+
|
|
197
|
+
// The model id rides in the URL path — with `@` intact — not in the body.
|
|
198
|
+
expect(req.url).toBe(expectedUrl("rawPredict"));
|
|
199
|
+
expect(req.method).toBe("POST");
|
|
200
|
+
expect(req.body).not.toHaveProperty("model");
|
|
201
|
+
expect(req.body.anthropic_version).toBe("vertex-2023-10-16");
|
|
202
|
+
expect(req.body.max_tokens).toBe(1024);
|
|
203
|
+
|
|
204
|
+
// The fake auth client's OAuth header reached the wire.
|
|
205
|
+
expect(req.headers.authorization).toBe("Bearer test-token");
|
|
206
|
+
|
|
207
|
+
expect(result).toBeInstanceOf(AIMessage);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("round-trips tool definitions and tool_use blocks into tool_calls", async () => {
|
|
211
|
+
const h = buildSeamHarness();
|
|
212
|
+
const withTools = h.model.bindTools([WEATHER_TOOL]);
|
|
213
|
+
|
|
214
|
+
const result = (await withTools.invoke([
|
|
215
|
+
new HumanMessage("Weather in Chennai?"),
|
|
216
|
+
])) as AIMessage;
|
|
217
|
+
|
|
218
|
+
// Tool definition survived request shaping through the Vertex adapter.
|
|
219
|
+
const tools = h.requests[0].body.tools as Array<{ name: string }>;
|
|
220
|
+
expect(tools).toHaveLength(1);
|
|
221
|
+
expect(tools[0].name).toBe("get_weather");
|
|
222
|
+
|
|
223
|
+
// The tool_use response block became a LangChain tool_call with its id —
|
|
224
|
+
// the identity the HITL approval flow keys on.
|
|
225
|
+
expect(result.tool_calls).toHaveLength(1);
|
|
226
|
+
expect(result.tool_calls?.[0]).toMatchObject({
|
|
227
|
+
id: "toolu_test_01",
|
|
228
|
+
name: "get_weather",
|
|
229
|
+
args: { city: "Chennai" },
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("reports usage_metadata on non-streaming responses (what billing reads)", async () => {
|
|
234
|
+
const h = buildSeamHarness();
|
|
235
|
+
|
|
236
|
+
const result = (await h.model.invoke([
|
|
237
|
+
new HumanMessage("Weather in Chennai?"),
|
|
238
|
+
])) as AIMessage;
|
|
239
|
+
|
|
240
|
+
expect(result.usage_metadata).toMatchObject({
|
|
241
|
+
input_tokens: 25,
|
|
242
|
+
output_tokens: 17,
|
|
243
|
+
total_tokens: 42,
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("streams via :streamRawPredict, assembling tool_calls and usage from SSE", async () => {
|
|
248
|
+
const h = buildSeamHarness();
|
|
249
|
+
|
|
250
|
+
let final: AIMessageChunk | undefined;
|
|
251
|
+
for await (const chunk of await h.model.stream([
|
|
252
|
+
new HumanMessage("Weather in Chennai?"),
|
|
253
|
+
])) {
|
|
254
|
+
final = final === undefined ? chunk : final.concat(chunk);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
expect(h.requests).toHaveLength(1);
|
|
258
|
+
expect(h.requests[0].url).toBe(expectedUrl("streamRawPredict"));
|
|
259
|
+
expect(h.requests[0].body.stream).toBe(true);
|
|
260
|
+
expect(h.requests[0].body).not.toHaveProperty("model");
|
|
261
|
+
|
|
262
|
+
expect(final).toBeDefined();
|
|
263
|
+
expect(final?.text).toBe("I'll check the weather.");
|
|
264
|
+
expect(final?.tool_calls).toHaveLength(1);
|
|
265
|
+
expect(final?.tool_calls?.[0]).toMatchObject({
|
|
266
|
+
id: "toolu_test_02",
|
|
267
|
+
name: "get_weather",
|
|
268
|
+
args: { city: "Chennai" },
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// input from message_start; output accumulated across message events.
|
|
272
|
+
// Pinned to the observed accumulation so a LangChain bump that changes
|
|
273
|
+
// usage math (billing input) fails here first.
|
|
274
|
+
expect(final?.usage_metadata).toMatchObject({
|
|
275
|
+
input_tokens: 25,
|
|
276
|
+
output_tokens: 18,
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("passes maxRetries: 0 to the factory and the honored value reaches the client", async () => {
|
|
281
|
+
const h = buildSeamHarness();
|
|
282
|
+
|
|
283
|
+
await h.model.invoke([new HumanMessage("hi")]);
|
|
284
|
+
for await (const chunk of await h.model.stream([new HumanMessage("hi")])) {
|
|
285
|
+
void chunk;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// LangChain owns retrying: it must hand the factory maxRetries: 0, once
|
|
289
|
+
// per cached client (batch + streaming are constructed independently).
|
|
290
|
+
expect(h.factoryOptions).toHaveLength(2);
|
|
291
|
+
expect(h.factoryOptions.every((o) => o.maxRetries === 0)).toBe(true);
|
|
292
|
+
expect(h.clients).toHaveLength(2);
|
|
293
|
+
expect(h.clients.every((c) => c.maxRetries === 0)).toBe(true);
|
|
294
|
+
});
|
|
295
|
+
});
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import type { ToolApprovalPolicy } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/spec_pb";
|
|
19
|
-
import type { ToolApprovalOverride } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
|
|
20
19
|
import type { AgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
21
20
|
import { ApprovalAction, ApprovalMode, ApprovalPolicySource } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
22
21
|
import { toolApprovalCategory, type ToolApprovalCategory } from "./tool-kind.js";
|
|
@@ -321,9 +320,16 @@ export interface MergedToolPolicy {
|
|
|
321
320
|
* Policy chain (each level overrides the previous):
|
|
322
321
|
* 1. status.toolApprovals — system-generated defaults; presence = requires approval
|
|
323
322
|
* 2. spec.pinnedToolApprovals — manual overrides; presence = requires approval
|
|
324
|
-
* 3.
|
|
323
|
+
* 3. usage tool_approval_overrides — explicit boolean per tool (enable OR disable)
|
|
325
324
|
* 4. active leases — runtime bypass (highest priority), now scoped
|
|
326
325
|
*
|
|
326
|
+
* Layer 3 is read from {@link ResolvedMcpServer.toolApprovalOverrides} — the
|
|
327
|
+
* overrides ride each server from the usage that resolved it (issue #349), so
|
|
328
|
+
* an override is STRUCTURALLY scoped to its own server. There is deliberately
|
|
329
|
+
* no cross-server override input: a flat list applied inside this per-server
|
|
330
|
+
* loop is how an override once leaked onto — or silently un-gated — a
|
|
331
|
+
* same-named tool on another server.
|
|
332
|
+
*
|
|
327
333
|
* The map carries ONLY the tools that require approval — a tool's absence means
|
|
328
334
|
* "auto-approved". Leases shape that absence:
|
|
329
335
|
* - On a global pre-arm ({@link ActiveLeases.global}) the map is empty.
|
|
@@ -341,7 +347,6 @@ export interface MergedToolPolicy {
|
|
|
341
347
|
*/
|
|
342
348
|
export function mergeApprovalPolicies(
|
|
343
349
|
resolvedServers: ResolvedMcpServer[],
|
|
344
|
-
agentOverrides: ToolApprovalOverride[],
|
|
345
350
|
leases: ActiveLeases,
|
|
346
351
|
): Map<string, MergedToolPolicy> {
|
|
347
352
|
const merged = new Map<string, MergedToolPolicy>();
|
|
@@ -382,10 +387,12 @@ export function mergeApprovalPolicies(
|
|
|
382
387
|
});
|
|
383
388
|
}
|
|
384
389
|
|
|
385
|
-
// Layer 3:
|
|
386
|
-
//
|
|
387
|
-
//
|
|
388
|
-
|
|
390
|
+
// Layer 3: this usage's overrides (explicit boolean, can enable or
|
|
391
|
+
// disable) — scoped to THIS server because they arrived on it (see the
|
|
392
|
+
// function doc). Touching a tool here makes the per-agent layer the
|
|
393
|
+
// responsible source, whether it enables, disables, or re-messages the
|
|
394
|
+
// gate.
|
|
395
|
+
for (const override of server.toolApprovalOverrides) {
|
|
389
396
|
if (!override.toolName) continue;
|
|
390
397
|
const existing = serverPolicies.get(override.toolName);
|
|
391
398
|
if (existing) {
|
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
* The runner never holds R2/S3 credentials — in cloud mode it calls the proxy
|
|
14
14
|
* to obtain a presigned upload URL, then PUTs content over plain HTTPS.
|
|
15
15
|
*
|
|
16
|
+
* Every proxy-backend request rides the shared bounded-backoff loop
|
|
17
|
+
* (http-retry.ts) with a per-request abort timeout, so a momentary
|
|
18
|
+
* stigmer-service interruption is retried and a hung connection cannot
|
|
19
|
+
* stall an agent turn unboundedly (stigmer/stigmer#468).
|
|
20
|
+
*
|
|
16
21
|
* DD-6: No direct R2 backend. Local + Proxy only.
|
|
17
22
|
*/
|
|
18
23
|
|
|
@@ -20,6 +25,8 @@ import { mkdir, writeFile, readFile, access, rm } from "node:fs/promises";
|
|
|
20
25
|
import { dirname, join, resolve, sep } from "node:path";
|
|
21
26
|
import { homedir } from "node:os";
|
|
22
27
|
import type { Config } from "../config.js";
|
|
28
|
+
import type { RetryOptions } from "./grpc-retry.js";
|
|
29
|
+
import { fetchWithRetry, type FetchRetryPolicy } from "./http-retry.js";
|
|
23
30
|
|
|
24
31
|
// ── Interface ────────────────────────────────────────────────────────
|
|
25
32
|
|
|
@@ -117,13 +124,64 @@ export class LocalArtifactStorage implements ArtifactStorage {
|
|
|
117
124
|
|
|
118
125
|
// ── Proxy Backend ────────────────────────────────────────────────────
|
|
119
126
|
|
|
127
|
+
/** Retry policy plus the two per-request bounds; every field is defaulted. */
|
|
128
|
+
export interface ProxyArtifactStorageOptions extends RetryOptions {
|
|
129
|
+
/** Bound for proxy presign calls and the 1-byte exists probe. Default 30 s. */
|
|
130
|
+
readonly requestTimeoutMs?: number;
|
|
131
|
+
/**
|
|
132
|
+
* Bound for the presigned R2 PUT/GET, which move whole artifact payloads
|
|
133
|
+
* (claim-check offloads, tool outputs, file-review captures — MB scale).
|
|
134
|
+
* Deliberately wider than requestTimeoutMs so a legitimate slow transfer
|
|
135
|
+
* is never aborted by a bound sized for JSON round-trips. Default 120 s.
|
|
136
|
+
*/
|
|
137
|
+
readonly transferTimeoutMs?: number;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Default transient-retry policy (stigmer/stigmer#468): the checkpointer's
|
|
142
|
+
* budget — sized to span a pod-restart reroute, ~7.75 s of delays across
|
|
143
|
+
* 5 retries. Retrying is safe at every site: the presign POSTs are
|
|
144
|
+
* idempotent mints, the PUT re-sends identical bytes to the same key (a
|
|
145
|
+
* keyed overwrite), and the GETs are reads.
|
|
146
|
+
*/
|
|
147
|
+
const DEFAULT_RETRY = {
|
|
148
|
+
baseDelayMs: 250,
|
|
149
|
+
backoffFactor: 2,
|
|
150
|
+
maxRetries: 5,
|
|
151
|
+
requestTimeoutMs: 30_000,
|
|
152
|
+
transferTimeoutMs: 120_000,
|
|
153
|
+
} as const;
|
|
154
|
+
|
|
120
155
|
export class ProxyArtifactStorage implements ArtifactStorage {
|
|
121
156
|
private readonly baseUrl: string;
|
|
122
157
|
private readonly authTokenSource: ProxyAuthTokenSource;
|
|
123
|
-
|
|
124
|
-
|
|
158
|
+
/** Governs the proxy presign calls and the exists probe. */
|
|
159
|
+
private readonly requestPolicy: FetchRetryPolicy;
|
|
160
|
+
/** Governs the presigned R2 transfers; same budget, wider timeout. */
|
|
161
|
+
private readonly transferPolicy: FetchRetryPolicy;
|
|
162
|
+
|
|
163
|
+
constructor(
|
|
164
|
+
proxyEndpoint: string,
|
|
165
|
+
authToken: ProxyAuthTokenSource,
|
|
166
|
+
options: ProxyArtifactStorageOptions = {},
|
|
167
|
+
) {
|
|
125
168
|
this.baseUrl = `${proxyEndpoint.replace(/\/+$/, "")}/v1/proxy/artifacts`;
|
|
126
169
|
this.authTokenSource = authToken;
|
|
170
|
+
const shared = {
|
|
171
|
+
label: "ProxyArtifactStorage",
|
|
172
|
+
baseDelayMs: options.baseDelayMs ?? DEFAULT_RETRY.baseDelayMs,
|
|
173
|
+
backoffFactor: options.backoffFactor ?? DEFAULT_RETRY.backoffFactor,
|
|
174
|
+
maxRetries: options.maxRetries ?? DEFAULT_RETRY.maxRetries,
|
|
175
|
+
delayFn: options.delayFn,
|
|
176
|
+
};
|
|
177
|
+
this.requestPolicy = {
|
|
178
|
+
...shared,
|
|
179
|
+
requestTimeoutMs: options.requestTimeoutMs ?? DEFAULT_RETRY.requestTimeoutMs,
|
|
180
|
+
};
|
|
181
|
+
this.transferPolicy = {
|
|
182
|
+
...shared,
|
|
183
|
+
requestTimeoutMs: options.transferTimeoutMs ?? DEFAULT_RETRY.transferTimeoutMs,
|
|
184
|
+
};
|
|
127
185
|
}
|
|
128
186
|
|
|
129
187
|
/**
|
|
@@ -141,14 +199,14 @@ export class ProxyArtifactStorage implements ArtifactStorage {
|
|
|
141
199
|
async upload(key: string, content: Buffer, contentType?: string): Promise<string> {
|
|
142
200
|
const ct = contentType ?? "application/octet-stream";
|
|
143
201
|
|
|
144
|
-
const presignResp = await
|
|
202
|
+
const presignResp = await fetchWithRetry(`${this.baseUrl}/presigned-upload-url`, {
|
|
145
203
|
method: "POST",
|
|
146
204
|
headers: {
|
|
147
205
|
"Authorization": `Bearer ${this.authToken}`,
|
|
148
206
|
"Content-Type": "application/json",
|
|
149
207
|
},
|
|
150
208
|
body: JSON.stringify({ key, content_type: ct }),
|
|
151
|
-
});
|
|
209
|
+
}, this.requestPolicy);
|
|
152
210
|
if (!presignResp.ok) {
|
|
153
211
|
throw new Error(
|
|
154
212
|
`Failed to get presigned upload URL (HTTP ${presignResp.status}): ` +
|
|
@@ -180,11 +238,14 @@ export class ProxyArtifactStorage implements ArtifactStorage {
|
|
|
180
238
|
uploadHeaders["Content-Type"] = ct;
|
|
181
239
|
}
|
|
182
240
|
|
|
183
|
-
|
|
241
|
+
// Retrying the PUT alone (not the presign+PUT pair) is deliberate: the
|
|
242
|
+
// retry budget's few seconds of delays sit far inside presign validity,
|
|
243
|
+
// and re-sending the same Buffer to the same key is a keyed overwrite.
|
|
244
|
+
const putResp = await fetchWithRetry(data.url, {
|
|
184
245
|
method: "PUT",
|
|
185
246
|
headers: uploadHeaders,
|
|
186
247
|
body: content,
|
|
187
|
-
});
|
|
248
|
+
}, this.transferPolicy);
|
|
188
249
|
if (!putResp.ok) {
|
|
189
250
|
throw new Error(
|
|
190
251
|
`Presigned upload failed (HTTP ${putResp.status}): ` +
|
|
@@ -196,14 +257,14 @@ export class ProxyArtifactStorage implements ArtifactStorage {
|
|
|
196
257
|
}
|
|
197
258
|
|
|
198
259
|
async getDownloadUrl(key: string): Promise<string> {
|
|
199
|
-
const resp = await
|
|
260
|
+
const resp = await fetchWithRetry(`${this.baseUrl}/presigned-download-url`, {
|
|
200
261
|
method: "POST",
|
|
201
262
|
headers: {
|
|
202
263
|
"Authorization": `Bearer ${this.authToken}`,
|
|
203
264
|
"Content-Type": "application/json",
|
|
204
265
|
},
|
|
205
266
|
body: JSON.stringify({ key }),
|
|
206
|
-
});
|
|
267
|
+
}, this.requestPolicy);
|
|
207
268
|
if (!resp.ok) {
|
|
208
269
|
throw new Error(
|
|
209
270
|
`Failed to get presigned download URL (HTTP ${resp.status}): ` +
|
|
@@ -216,7 +277,7 @@ export class ProxyArtifactStorage implements ArtifactStorage {
|
|
|
216
277
|
|
|
217
278
|
async download(key: string): Promise<Buffer> {
|
|
218
279
|
const url = await this.getDownloadUrl(key);
|
|
219
|
-
const resp = await
|
|
280
|
+
const resp = await fetchWithRetry(url, undefined, this.transferPolicy);
|
|
220
281
|
if (!resp.ok) {
|
|
221
282
|
throw new Error(
|
|
222
283
|
`Artifact download failed (HTTP ${resp.status}) for key '${key}': ` +
|
|
@@ -242,7 +303,11 @@ export class ProxyArtifactStorage implements ArtifactStorage {
|
|
|
242
303
|
// addressed CAS blob dedup) then re-uploads, which is idempotent.
|
|
243
304
|
return false;
|
|
244
305
|
}
|
|
245
|
-
const resp = await
|
|
306
|
+
const resp = await fetchWithRetry(
|
|
307
|
+
url,
|
|
308
|
+
{ headers: { Range: "bytes=0-0" } },
|
|
309
|
+
this.requestPolicy,
|
|
310
|
+
);
|
|
246
311
|
await resp.arrayBuffer().catch(() => undefined); // drain the <=1-byte body
|
|
247
312
|
if (resp.status === 404) return false;
|
|
248
313
|
if (resp.status === 200 || resp.status === 206 || resp.status === 416) return true;
|