@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,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the turn-recovery digest (issue #366).
|
|
3
|
+
*
|
|
4
|
+
* The digest is the replacement agent's only account of the work its lost
|
|
5
|
+
* predecessor did, so these pins cover the three doctrine properties it
|
|
6
|
+
* inherits from the DD-013 bridge composer — bounded lines, drop-oldest
|
|
7
|
+
* budget enforcement with disclosure, never-throw — plus the rendering
|
|
8
|
+
* contract per message/tool-call kind.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect } from "vitest";
|
|
12
|
+
import { create } from "@bufbuild/protobuf";
|
|
13
|
+
import {
|
|
14
|
+
AgentMessageSchema,
|
|
15
|
+
ToolCallSchema,
|
|
16
|
+
type AgentMessage,
|
|
17
|
+
type ToolCall,
|
|
18
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
19
|
+
import { MessageType, ToolCallStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
20
|
+
|
|
21
|
+
import { composeTurnRecoveryDigest, formatTurnRecoveryText } from "../turn-recovery.js";
|
|
22
|
+
|
|
23
|
+
function aiMessage(content: string, toolCalls: ToolCall[] = []): AgentMessage {
|
|
24
|
+
return create(AgentMessageSchema, { type: MessageType.MESSAGE_AI, content, toolCalls });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type ToolCallFields = Partial<
|
|
28
|
+
Pick<ToolCall, "name" | "argsPreview" | "approvalMessage" | "status" | "error">
|
|
29
|
+
>;
|
|
30
|
+
|
|
31
|
+
function toolCall(overrides: ToolCallFields): ToolCall {
|
|
32
|
+
return create(ToolCallSchema, {
|
|
33
|
+
id: "tc-1",
|
|
34
|
+
name: "Shell",
|
|
35
|
+
status: ToolCallStatus.TOOL_CALL_COMPLETED,
|
|
36
|
+
...overrides,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe("composeTurnRecoveryDigest", () => {
|
|
41
|
+
it("renders assistant text and tool calls oldest-first", () => {
|
|
42
|
+
const digest = composeTurnRecoveryDigest([
|
|
43
|
+
aiMessage("Let me check the config first.", [
|
|
44
|
+
toolCall({ name: "Read", argsPreview: '{"path":"config.yaml"}' }),
|
|
45
|
+
]),
|
|
46
|
+
aiMessage("The port is wrong — fixing it."),
|
|
47
|
+
]);
|
|
48
|
+
expect(digest).toBe(
|
|
49
|
+
[
|
|
50
|
+
"Assistant: Let me check the config first.",
|
|
51
|
+
'Tool: Read({"path":"config.yaml"}) — completed',
|
|
52
|
+
"Assistant: The port is wrong — fixing it.",
|
|
53
|
+
].join("\n"),
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("prefers the resolved approval message for a tool line — the same description the user approved against", () => {
|
|
58
|
+
const digest = composeTurnRecoveryDigest([
|
|
59
|
+
aiMessage("", [
|
|
60
|
+
toolCall({
|
|
61
|
+
approvalMessage: "Write file: gated.txt",
|
|
62
|
+
argsPreview: '{"path":"gated.txt","content":"..."}',
|
|
63
|
+
status: ToolCallStatus.TOOL_CALL_WAITING_APPROVAL,
|
|
64
|
+
}),
|
|
65
|
+
]),
|
|
66
|
+
]);
|
|
67
|
+
expect(digest).toBe("Tool: Write file: gated.txt — paused for user approval");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("maps each terminal status honestly and treats in-flight calls as interrupted", () => {
|
|
71
|
+
const digest = composeTurnRecoveryDigest([
|
|
72
|
+
aiMessage("", [
|
|
73
|
+
toolCall({ name: "A", status: ToolCallStatus.TOOL_CALL_COMPLETED }),
|
|
74
|
+
toolCall({ name: "B", status: ToolCallStatus.TOOL_CALL_FAILED, error: "exit 1" }),
|
|
75
|
+
toolCall({ name: "C", status: ToolCallStatus.TOOL_CALL_FAILED }),
|
|
76
|
+
toolCall({ name: "D", status: ToolCallStatus.TOOL_CALL_SKIPPED }),
|
|
77
|
+
toolCall({ name: "E", status: ToolCallStatus.TOOL_CALL_RUNNING }),
|
|
78
|
+
toolCall({ name: "F", status: ToolCallStatus.TOOL_CALL_PENDING }),
|
|
79
|
+
]),
|
|
80
|
+
]);
|
|
81
|
+
expect(digest).toBe(
|
|
82
|
+
[
|
|
83
|
+
"Tool: A — completed",
|
|
84
|
+
"Tool: B — failed: exit 1",
|
|
85
|
+
"Tool: C — failed",
|
|
86
|
+
"Tool: D — skipped",
|
|
87
|
+
"Tool: E — interrupted before it finished",
|
|
88
|
+
"Tool: F — interrupted before it finished",
|
|
89
|
+
].join("\n"),
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("keeps system notices but skips human messages (already in <user_request>), thinking, and blanks", () => {
|
|
94
|
+
const digest = composeTurnRecoveryDigest([
|
|
95
|
+
create(AgentMessageSchema, { type: MessageType.MESSAGE_HUMAN, content: "Fix the build" }),
|
|
96
|
+
create(AgentMessageSchema, { type: MessageType.MESSAGE_SYSTEM, content: "Budget warning: 80% used" }),
|
|
97
|
+
create(AgentMessageSchema, { type: MessageType.MESSAGE_THINKING, content: "hmm, maybe the lockfile" }),
|
|
98
|
+
aiMessage(" "),
|
|
99
|
+
]);
|
|
100
|
+
expect(digest).toBe("System: Budget warning: 80% used");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("returns undefined when nothing renders", () => {
|
|
104
|
+
expect(composeTurnRecoveryDigest([])).toBeUndefined();
|
|
105
|
+
expect(
|
|
106
|
+
composeTurnRecoveryDigest([
|
|
107
|
+
create(AgentMessageSchema, { type: MessageType.MESSAGE_HUMAN, content: "only the request" }),
|
|
108
|
+
]),
|
|
109
|
+
).toBeUndefined();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("truncates a long assistant line at the per-line budget with an ellipsis", () => {
|
|
113
|
+
const digest = composeTurnRecoveryDigest([aiMessage("x".repeat(1000))]);
|
|
114
|
+
expect(digest).toBe(`Assistant: ${"x".repeat(400)}\u2026`);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("drops the OLDEST lines when over the whole-digest budget and discloses the omission", () => {
|
|
118
|
+
// 30 lines of ~311 chars each (~9.6k total) against the 4000-char budget:
|
|
119
|
+
// the newest lines must survive, the oldest go, and the notice leads.
|
|
120
|
+
const messages = Array.from({ length: 30 }, (_, i) =>
|
|
121
|
+
aiMessage(`step ${String(i).padStart(2, "0")} ${"y".repeat(300)}`),
|
|
122
|
+
);
|
|
123
|
+
const digest = composeTurnRecoveryDigest(messages);
|
|
124
|
+
expect(digest).toBeDefined();
|
|
125
|
+
expect(digest!.length).toBeLessThanOrEqual(4000);
|
|
126
|
+
const lines = digest!.split("\n");
|
|
127
|
+
expect(lines[0]).toBe("[\u2026 earlier activity in this turn omitted for length]");
|
|
128
|
+
// Recency wins: the last line is the newest entry, the first entries are gone.
|
|
129
|
+
expect(lines[lines.length - 1]).toContain("step 29");
|
|
130
|
+
expect(digest).not.toContain("step 00");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("never throws — a malformed message degrades to no digest, not a failed recovery", () => {
|
|
134
|
+
// Force the internal iteration to blow up: content getter that throws.
|
|
135
|
+
const poison = new Proxy(aiMessage("ok"), {
|
|
136
|
+
get(target, prop, receiver) {
|
|
137
|
+
if (prop === "content") throw new Error("corrupt row");
|
|
138
|
+
return Reflect.get(target, prop, receiver);
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
expect(composeTurnRecoveryDigest([poison as AgentMessage])).toBeUndefined();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("formatTurnRecoveryText", () => {
|
|
146
|
+
it("frames a digest with the work-already-done preamble", () => {
|
|
147
|
+
const text = formatTurnRecoveryText("Assistant: did things");
|
|
148
|
+
expect(text).toContain("session holding that conversation was lost");
|
|
149
|
+
expect(text).toContain("do not start the task over");
|
|
150
|
+
expect(text.endsWith("Assistant: did things")).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("still discloses the state loss when there is no transcript — without it the appended decisions would read as reactions to proposals this agent never made", () => {
|
|
154
|
+
for (const empty of [undefined, "", " "]) {
|
|
155
|
+
const text = formatTurnRecoveryText(empty);
|
|
156
|
+
expect(text).toContain("session holding that conversation was lost");
|
|
157
|
+
expect(text).toContain("no transcript of your progress is available");
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -222,6 +222,19 @@ describe("consumeCursorTurnStream", () => {
|
|
|
222
222
|
expect(state.streamErrorMessage).toBe("boom");
|
|
223
223
|
});
|
|
224
224
|
|
|
225
|
+
it("does not capture a non-string stream ERROR message (oss#299 hardening)", async () => {
|
|
226
|
+
// message is untyped at runtime; a structured value assigned here would
|
|
227
|
+
// crash classifyText downstream (.toLowerCase() on a non-string).
|
|
228
|
+
const { deps, state } = buildDeps();
|
|
229
|
+
|
|
230
|
+
await consumeCursorTurnStream(
|
|
231
|
+
mockRun([ev({ type: "status", status: "ERROR", message: { code: 14 } })]),
|
|
232
|
+
deps,
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
expect(state.streamErrorMessage).toBeUndefined();
|
|
236
|
+
});
|
|
237
|
+
|
|
225
238
|
describe("first-denial early stop", () => {
|
|
226
239
|
let hitlDir: string;
|
|
227
240
|
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* "mcpToolPolicies": {
|
|
14
14
|
* "apply_cloud_resource": { "requiresApproval": true, "message": "..." }
|
|
15
15
|
* },
|
|
16
|
+
* "mcpServerEnabledTools": { "planton": ["get_cloud_resource"] },
|
|
16
17
|
* "approvedGrants": [{ "toolName": "edit", "mcpServerSlug": "", "key": "write", "salient": "a.txt", "contentDigest": "<sha256>" }],
|
|
17
18
|
* "approvedGrantTokens": ["<base64(key\nsalient[\ncontentDigest])>"]
|
|
18
19
|
* }
|
|
@@ -174,6 +175,21 @@ export interface ApprovalStateFile {
|
|
|
174
175
|
*/
|
|
175
176
|
leasedCategories: string[];
|
|
176
177
|
mcpToolPolicies: Record<string, McpToolPolicyEntry>;
|
|
178
|
+
/**
|
|
179
|
+
* Per-server effective enabled_tools allow-lists (issue #350), keyed by
|
|
180
|
+
* MCP server slug — ONLY restricted servers appear (an absent slug means
|
|
181
|
+
* unrestricted, so the common case stays an empty object). The Cursor SDK
|
|
182
|
+
* config cannot hide a server's tools, so the hook enforces the manifest
|
|
183
|
+
* instead: on beforeMCPExecution it matches the payload's mcp_server_name
|
|
184
|
+
* against this map and denies a non-listed tool with the non-pausing,
|
|
185
|
+
* permanent "disabled" kind — BEFORE autoApproveAll and grants, because
|
|
186
|
+
* enabled_tools is a capability manifest, not an approval gate (no bypass
|
|
187
|
+
* may resurrect a disabled tool, and no human may be offered "approve" on
|
|
188
|
+
* one). Unlike mcpToolPolicies (name-keyed, server-blind), this map is
|
|
189
|
+
* server-scoped: the hook payload carries the server identity, so equal
|
|
190
|
+
* tool names on different servers cannot cross-grant.
|
|
191
|
+
*/
|
|
192
|
+
mcpServerEnabledTools: Record<string, string[]>;
|
|
177
193
|
approvedGrants: ApprovalGrant[];
|
|
178
194
|
approvedGrantTokens: string[];
|
|
179
195
|
/**
|
|
@@ -381,6 +397,8 @@ function parseArgs(argsPreview: string): Record<string, unknown> | undefined {
|
|
|
381
397
|
* - leasedCategories: built-in categories with a run-lifetime lease
|
|
382
398
|
* - mcpToolPolicies: per-tool policy for MCP tools requiring approval (leased
|
|
383
399
|
* servers are already absent — dropped upstream by mergeApprovalPolicies)
|
|
400
|
+
* - mcpServerEnabledTools: per-server enabled_tools allow-lists (issue #350,
|
|
401
|
+
* restricted servers only) for the hook's permanent "disabled" arm
|
|
384
402
|
* - approvedGrants / approvedGrantTokens: tools approved in the current HITL
|
|
385
403
|
* cycle, allowed through on reinvocation
|
|
386
404
|
*
|
|
@@ -396,6 +414,7 @@ export function buildApprovalState(
|
|
|
396
414
|
captureIgnored = false,
|
|
397
415
|
gitWorkspace = true,
|
|
398
416
|
unattendedSkip = false,
|
|
417
|
+
mcpServerEnabledTools: Record<string, string[]> = {},
|
|
399
418
|
): ApprovalStateFile {
|
|
400
419
|
const approvedGrants = grants ?? [];
|
|
401
420
|
|
|
@@ -411,6 +430,7 @@ export function buildApprovalState(
|
|
|
411
430
|
autoApproveAll: globalBypass,
|
|
412
431
|
leasedCategories: [...leasedCategories],
|
|
413
432
|
mcpToolPolicies,
|
|
433
|
+
mcpServerEnabledTools,
|
|
414
434
|
approvedGrants,
|
|
415
435
|
// The hook matches a tool call's PRIMARY token (content when it can compute a
|
|
416
436
|
// digest from tool_input, else coarse). A content-identified grant authorizes
|
|
@@ -474,12 +494,18 @@ const DENIAL_LEDGER_FILE = "denials.jsonl";
|
|
|
474
494
|
* classification may never have run).
|
|
475
495
|
* - `fail-closed` — the approval state file was missing, so everything gated
|
|
476
496
|
* denied. A turn-level "the gate itself was broken" fact.
|
|
497
|
+
* - `disabled` — the agent's enabled_tools manifest excludes this MCP
|
|
498
|
+
* tool (issue #350). Permanent for the run and
|
|
499
|
+
* mode-independent: NOT an approval (a human must never be
|
|
500
|
+
* offered "approve" on a manifest-disabled tool), so it is
|
|
501
|
+
* non-pausing and the model adapts — the same consumer
|
|
502
|
+
* semantics as `secret`.
|
|
477
503
|
*
|
|
478
504
|
* An unknown kind string is preserved as-is: it is treated as non-pausing (an
|
|
479
505
|
* unknown deny must never manufacture an approval) but still attributes the
|
|
480
506
|
* blocked call to our own hook.
|
|
481
507
|
*/
|
|
482
|
-
export type DenialKind = "approval" | "unattended" | "secret" | "capture-error" | "fail-closed";
|
|
508
|
+
export type DenialKind = "approval" | "unattended" | "secret" | "capture-error" | "fail-closed" | "disabled";
|
|
483
509
|
|
|
484
510
|
/** The one kind that pauses the run for user approval. */
|
|
485
511
|
export const APPROVAL_DENIAL_KIND: DenialKind = "approval";
|
|
@@ -487,6 +513,9 @@ export const APPROVAL_DENIAL_KIND: DenialKind = "approval";
|
|
|
487
513
|
/** The unattended-mode resolution kind (non-pausing; stamped SKIPPED). */
|
|
488
514
|
export const UNATTENDED_DENIAL_KIND: DenialKind = "unattended";
|
|
489
515
|
|
|
516
|
+
/** The enabled_tools manifest denial kind (non-pausing, permanent; issue #350). */
|
|
517
|
+
export const DISABLED_DENIAL_KIND: DenialKind = "disabled";
|
|
518
|
+
|
|
490
519
|
/**
|
|
491
520
|
* One denial recorded by the preToolUse hook. `token` is the call's identity in
|
|
492
521
|
* the same space as grantToken() (base64 of `toolName \n salientArg`), used to
|
|
@@ -22,6 +22,13 @@
|
|
|
22
22
|
* directives are built from the RESOLVED paths, so prompt and filesystem can
|
|
23
23
|
* never disagree.
|
|
24
24
|
*
|
|
25
|
+
* Duplicate filenames are renamed, never overwritten (issue #364): because
|
|
26
|
+
* placement keys purely on the filename, two attachments with the same name
|
|
27
|
+
* contend for one path — the later one takes the platform's `stem-2.ext`
|
|
28
|
+
* rename (shared/attachment-naming.ts, same semantics as the deep-agent
|
|
29
|
+
* injector and the React composer) and the rename is disclosed in the
|
|
30
|
+
* prompt's `<input_files>` section via {@link ResolvedAttachment.renamedFrom}.
|
|
31
|
+
*
|
|
25
32
|
* Error model: fail-hard, matching the native harness's attachment injector.
|
|
26
33
|
* Attachments are explicit user inputs — an execution that silently runs
|
|
27
34
|
* without one produces silently incorrect results (the "plan file wasn't
|
|
@@ -33,6 +40,7 @@ import { mkdir, copyFile, readFile, stat, writeFile } from "node:fs/promises";
|
|
|
33
40
|
import { join, basename } from "node:path";
|
|
34
41
|
import type { Attachment } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/spec_pb";
|
|
35
42
|
import type { ArtifactStorage } from "../../shared/artifact-storage.js";
|
|
43
|
+
import { allocateUniqueName } from "../../shared/attachment-naming.js";
|
|
36
44
|
import {
|
|
37
45
|
isVisionCandidate,
|
|
38
46
|
type VisionBudget,
|
|
@@ -46,9 +54,16 @@ import { ensureStigmerSymlink, STIGMER_LOCAL_STATE_DIR } from "../../shared/work
|
|
|
46
54
|
const INPUTS_SUBDIR = "inputs";
|
|
47
55
|
|
|
48
56
|
export interface ResolvedAttachment {
|
|
57
|
+
/** The final on-disk basename — after any duplicate rename. */
|
|
49
58
|
filename: string;
|
|
50
59
|
/** Workspace-relative path the agent reads (`.stigmer/inputs/{filename}`). */
|
|
51
60
|
relativePath: string;
|
|
61
|
+
/**
|
|
62
|
+
* The attachment's original filename, present only when a duplicate name
|
|
63
|
+
* was renamed (shared/attachment-naming.ts) — rendered as disclosure in
|
|
64
|
+
* the prompt's `<input_files>` section.
|
|
65
|
+
*/
|
|
66
|
+
renamedFrom?: string;
|
|
52
67
|
/** Present when the attachment was accepted into the turn's vision payload. */
|
|
53
68
|
vision?: VisionImage;
|
|
54
69
|
/**
|
|
@@ -111,9 +126,13 @@ export async function resolveAttachments(
|
|
|
111
126
|
// it, but only when the agent has skills).
|
|
112
127
|
await ensureStigmerSymlink(options.primaryWorkspaceDir, platformDir);
|
|
113
128
|
|
|
129
|
+
// Placement keys purely on the filename, so this set is the whole
|
|
130
|
+
// collision domain — sequential resolution means each attachment sees
|
|
131
|
+
// every name claimed before it (see module doc on duplicate handling).
|
|
132
|
+
const takenNames = new Set<string>();
|
|
114
133
|
const results: ResolvedAttachment[] = [];
|
|
115
134
|
for (const attachment of attachments) {
|
|
116
|
-
results.push(await resolveAttachment(attachment, inputsDir, options));
|
|
135
|
+
results.push(await resolveAttachment(attachment, inputsDir, takenNames, options));
|
|
117
136
|
}
|
|
118
137
|
|
|
119
138
|
console.log(
|
|
@@ -127,11 +146,15 @@ export async function resolveAttachments(
|
|
|
127
146
|
async function resolveAttachment(
|
|
128
147
|
attachment: Attachment,
|
|
129
148
|
inputsDir: string,
|
|
149
|
+
takenNames: Set<string>,
|
|
130
150
|
options: AttachmentResolverOptions,
|
|
131
151
|
): Promise<ResolvedAttachment> {
|
|
132
152
|
// Local-mode fast path: the file is already on this machine's disk.
|
|
133
153
|
if (options.mode === "local" && attachment.localPath) {
|
|
134
|
-
const filename =
|
|
154
|
+
const { name: filename, renamedFrom } = allocateUniqueName(
|
|
155
|
+
safeInputName(attachment.filename || attachment.localPath),
|
|
156
|
+
takenNames,
|
|
157
|
+
);
|
|
135
158
|
let vision: VisionOutcome | undefined;
|
|
136
159
|
try {
|
|
137
160
|
vision = await materializeLocalFile(attachment, filename, inputsDir, options.visionBudget);
|
|
@@ -145,6 +168,7 @@ async function resolveAttachment(
|
|
|
145
168
|
return {
|
|
146
169
|
filename,
|
|
147
170
|
relativePath: join(STIGMER_LOCAL_STATE_DIR, INPUTS_SUBDIR, filename),
|
|
171
|
+
...(renamedFrom !== undefined ? { renamedFrom } : {}),
|
|
148
172
|
...visionOutcomeFields(vision),
|
|
149
173
|
};
|
|
150
174
|
}
|
|
@@ -164,7 +188,10 @@ async function resolveAttachment(
|
|
|
164
188
|
);
|
|
165
189
|
}
|
|
166
190
|
|
|
167
|
-
const filename =
|
|
191
|
+
const { name: filename, renamedFrom } = allocateUniqueName(
|
|
192
|
+
safeInputName(attachment.filename || attachment.storageKey),
|
|
193
|
+
takenNames,
|
|
194
|
+
);
|
|
168
195
|
let content: Buffer;
|
|
169
196
|
try {
|
|
170
197
|
content = await options.storage.download(attachment.storageKey);
|
|
@@ -185,6 +212,7 @@ async function resolveAttachment(
|
|
|
185
212
|
return {
|
|
186
213
|
filename,
|
|
187
214
|
relativePath: join(STIGMER_LOCAL_STATE_DIR, INPUTS_SUBDIR, filename),
|
|
215
|
+
...(renamedFrom !== undefined ? { renamedFrom } : {}),
|
|
188
216
|
...visionOutcomeFields(vision),
|
|
189
217
|
};
|
|
190
218
|
}
|
|
@@ -20,6 +20,13 @@ import type { SessionSpec } from "@stigmer/protos/ai/stigmer/agentic/session/v1/
|
|
|
20
20
|
import type { WorkspaceEntry } from "@stigmer/protos/ai/stigmer/agentic/session/v1/workspace_pb";
|
|
21
21
|
import type { ApiResourceReference } from "@stigmer/protos/ai/stigmer/commons/apiresource/io_pb";
|
|
22
22
|
import type { CloudRepo } from "./session-lifecycle.js";
|
|
23
|
+
import { mergeMcpServerUsages } from "../../shared/mcp-resolver.js";
|
|
24
|
+
|
|
25
|
+
// Both harnesses must merge agent + session usages identically (session wins
|
|
26
|
+
// per slug — the usage whose enabled_tools the enforcement honors), so the
|
|
27
|
+
// merge lives in shared/mcp-resolver.ts. Re-exported here for its historical
|
|
28
|
+
// home alongside mergeSkillRefs.
|
|
29
|
+
export { mergeMcpServerUsages } from "../../shared/mcp-resolver.js";
|
|
23
30
|
|
|
24
31
|
/**
|
|
25
32
|
* Path segments that identify runner-internal directories. Any workspace dir
|
|
@@ -130,33 +137,6 @@ export function resolveCloudRepos(workspaceEntries: WorkspaceEntry[]): CloudRepo
|
|
|
130
137
|
// MCP and skill merging
|
|
131
138
|
// ---------------------------------------------------------------------------
|
|
132
139
|
|
|
133
|
-
/**
|
|
134
|
-
* Merge MCP server usages from agent (base) and session (overlay).
|
|
135
|
-
*
|
|
136
|
-
* Replicates session_context_merge.py::merge_mcp_server_usages():
|
|
137
|
-
* - Agent-level usages are the base set
|
|
138
|
-
* - Session-level usages extend or override by mcp_server_ref.slug
|
|
139
|
-
* - If both reference the same slug, session-level takes precedence
|
|
140
|
-
*/
|
|
141
|
-
export function mergeMcpServerUsages(
|
|
142
|
-
agentUsages: McpServerUsage[],
|
|
143
|
-
sessionUsages: McpServerUsage[],
|
|
144
|
-
): McpServerUsage[] {
|
|
145
|
-
const bySlug = new Map<string, McpServerUsage>();
|
|
146
|
-
|
|
147
|
-
for (const usage of agentUsages) {
|
|
148
|
-
const slug = usage.mcpServerRef?.slug;
|
|
149
|
-
if (slug) bySlug.set(slug, usage);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
for (const usage of sessionUsages) {
|
|
153
|
-
const slug = usage.mcpServerRef?.slug;
|
|
154
|
-
if (slug) bySlug.set(slug, usage);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return [...bySlug.values()];
|
|
158
|
-
}
|
|
159
|
-
|
|
160
140
|
/**
|
|
161
141
|
* Merge skill refs from agent and session.
|
|
162
142
|
*
|
|
@@ -25,7 +25,9 @@ export async function resolveExecutionEnv(
|
|
|
25
25
|
): Promise<EnvResult> {
|
|
26
26
|
// A desktop runner exchanges its bootstrap credential for a token scoped to
|
|
27
27
|
// this execution's session, so cloud's decrypt gate binds the read (#156).
|
|
28
|
-
// No-op for cloud sandbox and OSS runners.
|
|
28
|
+
// No-op for cloud sandbox and OSS runners. A failed exchange throws and
|
|
29
|
+
// fails the activity: the bootstrap credential no longer decrypts
|
|
30
|
+
// (stigmer-cloud#218), so proceeding would resolve redacted placeholders.
|
|
29
31
|
const scopedToken = await client.acquireScopedRunnerToken({
|
|
30
32
|
agentExecutionId: executionId,
|
|
31
33
|
});
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* 3. Surface isRetryable for future workflow-level retry decisions
|
|
8
8
|
*
|
|
9
9
|
* Error detail can come from several sources (in priority order):
|
|
10
|
-
* -
|
|
11
|
-
*
|
|
10
|
+
* - Structured fields, either from a thrown CursorSdkError or lifted from a
|
|
11
|
+
* structured run.wait() error value (highest fidelity)
|
|
12
|
+
* - run.wait() error text (SDK-provided string, often bare/generic)
|
|
12
13
|
* - SDKStatusMessage with status "ERROR" from the stream
|
|
13
14
|
* - ConnectError captured from process unhandledRejection
|
|
14
15
|
* - Text extracted from the failing run.conversation() turn
|
|
@@ -39,7 +40,8 @@ export interface ClassifiedError {
|
|
|
39
40
|
|
|
40
41
|
/**
|
|
41
42
|
* Structured fields lifted from a thrown CursorSdkError (errors.d.ts:
|
|
42
|
-
* { code, status, isRetryable, cause, endpoint, requestId, operation })
|
|
43
|
+
* { code, status, isRetryable, cause, endpoint, requestId, operation }) or
|
|
44
|
+
* from a structured run.wait() error value (see extractRunErrorSources).
|
|
43
45
|
* Only the fields used for classification are retained.
|
|
44
46
|
*/
|
|
45
47
|
export interface SdkErrorFields {
|
|
@@ -48,6 +50,110 @@ export interface SdkErrorFields {
|
|
|
48
50
|
message?: string;
|
|
49
51
|
}
|
|
50
52
|
|
|
53
|
+
/**
|
|
54
|
+
* What String() produces for any plain object. Carries zero signal, so it is
|
|
55
|
+
* refused everywhere: extraction never emits it, and classifyFromSources
|
|
56
|
+
* treats it as absent should any other producer leak it through.
|
|
57
|
+
*/
|
|
58
|
+
const OBJECT_JUNK_STRING = "[object Object]";
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Error detail lifted from a run.wait() result, split by fidelity: structured
|
|
62
|
+
* values land in sdkError, plain text in sdkResultFields. At most one of the
|
|
63
|
+
* two is set; both undefined means the result carried nothing usable and the
|
|
64
|
+
* classifier's lower-priority sources should decide.
|
|
65
|
+
*/
|
|
66
|
+
export interface RunErrorSources {
|
|
67
|
+
sdkError: SdkErrorFields | undefined;
|
|
68
|
+
sdkResultFields: string | undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const NO_RUN_ERROR_SOURCES: RunErrorSources = {
|
|
72
|
+
sdkError: undefined,
|
|
73
|
+
sdkResultFields: undefined,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Lift error detail from a run.wait() result whose status is "error".
|
|
78
|
+
*
|
|
79
|
+
* The SDK types RunResult.result as `string`, but structured values (Error
|
|
80
|
+
* instances, { code, message } objects) have been observed at runtime, both
|
|
81
|
+
* in `result` and in the undeclared error/message/reason fields (oss#299).
|
|
82
|
+
* A bare String() on those yields "[object Object]", which both destroys the
|
|
83
|
+
* message users see and — worse — shadows the lower-priority classifier
|
|
84
|
+
* sources (stream, rejection, conversation introspection) that often hold
|
|
85
|
+
* the real reason.
|
|
86
|
+
*
|
|
87
|
+
* Walks the candidate fields in order and answers from the FIRST one that
|
|
88
|
+
* yields usable content: strings keep flowing to the string channel
|
|
89
|
+
* (sdkResultFields), structured values are lifted into the same structured
|
|
90
|
+
* channel a thrown CursorSdkError uses (sdkError), and hopeless values are
|
|
91
|
+
* skipped so a later candidate — or the classifier's fallback sources — can
|
|
92
|
+
* win. (The previous `??` chain stopped at the first non-nullish value, so a
|
|
93
|
+
* hopeless object or empty string hid usable text one field later.)
|
|
94
|
+
*
|
|
95
|
+
* Deliberately NO JSON.stringify fallback for unrecognized object shapes:
|
|
96
|
+
* the error arm already logs the raw result in full server-side, and a JSON
|
|
97
|
+
* blob shown to the user would shadow the introspection sources that exist
|
|
98
|
+
* precisely to recover the real reason.
|
|
99
|
+
*/
|
|
100
|
+
export function extractRunErrorSources(result: unknown): RunErrorSources {
|
|
101
|
+
if (result === null || typeof result !== "object") return NO_RUN_ERROR_SOURCES;
|
|
102
|
+
const r = result as Record<string, unknown>;
|
|
103
|
+
for (const candidate of [r.result, r.error, r.message, r.reason]) {
|
|
104
|
+
const extracted = extractFromCandidate(candidate);
|
|
105
|
+
if (extracted) return extracted;
|
|
106
|
+
}
|
|
107
|
+
return NO_RUN_ERROR_SOURCES;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function extractFromCandidate(v: unknown): RunErrorSources | undefined {
|
|
111
|
+
if (v == null) return undefined;
|
|
112
|
+
if (typeof v === "string") {
|
|
113
|
+
if (v.length === 0 || v === OBJECT_JUNK_STRING) return undefined;
|
|
114
|
+
return { sdkError: undefined, sdkResultFields: v };
|
|
115
|
+
}
|
|
116
|
+
if (typeof v === "object") {
|
|
117
|
+
// Covers Error instances too: their message (and, on SDK/Node error
|
|
118
|
+
// shapes, code/status) are readable as plain properties.
|
|
119
|
+
const o = v as Record<string, unknown>;
|
|
120
|
+
const fields: SdkErrorFields = {};
|
|
121
|
+
if (typeof o.code === "string" && o.code.length > 0) fields.code = o.code;
|
|
122
|
+
if (typeof o.status === "number") fields.status = o.status;
|
|
123
|
+
if (typeof o.message === "string" && o.message.length > 0) fields.message = o.message;
|
|
124
|
+
if (fields.code !== undefined || fields.status !== undefined || fields.message !== undefined) {
|
|
125
|
+
return { sdkError: fields, sdkResultFields: undefined };
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
// Remaining primitives (number, boolean, ...) stringify losslessly.
|
|
130
|
+
const text = String(v);
|
|
131
|
+
return text.length > 0 ? { sdkError: undefined, sdkResultFields: text } : undefined;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Stable lead sentence of the detail-free fallback error (all five detail
|
|
136
|
+
* channels empty — empirically the shape of Cursor-side capacity rejections,
|
|
137
|
+
* oss#492). This is what end users read in embedded surfaces, so it must be
|
|
138
|
+
* actionable product copy, not a diagnostic; the diagnostic context follows
|
|
139
|
+
* in a parenthetical. Mirrors the COST_LIMIT_ERROR_PREFIX convention: a
|
|
140
|
+
* consumer that needs to recognize this failure can match on the prefix.
|
|
141
|
+
* Do not reword without checking consumers, and NEVER include the phrase
|
|
142
|
+
* "retry or resume" — sdk-react's isInterruptedError (MessageThread.tsx)
|
|
143
|
+
* reframes any error containing it as a neutral resumable notice instead of
|
|
144
|
+
* a failure alert.
|
|
145
|
+
*/
|
|
146
|
+
export const DETAIL_FREE_FALLBACK_USER_PREFIX =
|
|
147
|
+
"The model may be temporarily overloaded — please retry, or switch to a different model.";
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Stable lead sentence of the transport-timeout fallback error (0 messages,
|
|
151
|
+
* ~30s duration — the SDK's default timeout with no stream established).
|
|
152
|
+
* Same user-facing rules as DETAIL_FREE_FALLBACK_USER_PREFIX above.
|
|
153
|
+
*/
|
|
154
|
+
export const TRANSPORT_TIMEOUT_USER_PREFIX =
|
|
155
|
+
"The connection to the model could not be established — please retry.";
|
|
156
|
+
|
|
51
157
|
const AUTH_PATTERNS = [
|
|
52
158
|
"unauthenticated", "unauthorized", "401", "forbidden",
|
|
53
159
|
"permission_denied", "invalid api key", "not logged in",
|
|
@@ -168,7 +274,11 @@ function classifyFromSources(opts: SynthesizeErrorOpts): ClassifiedError {
|
|
|
168
274
|
}
|
|
169
275
|
|
|
170
276
|
if (opts.sdkResultFields) {
|
|
171
|
-
|
|
277
|
+
// "Cursor run failed" is the SDK's bare generic; "[object Object]" is
|
|
278
|
+
// String()-coerced junk from any producer that bypassed the shape-aware
|
|
279
|
+
// extraction. Neither carries signal — fall through to better sources.
|
|
280
|
+
const isBareGeneric = opts.sdkResultFields === "Cursor run failed"
|
|
281
|
+
|| opts.sdkResultFields === OBJECT_JUNK_STRING;
|
|
172
282
|
if (!isBareGeneric) {
|
|
173
283
|
const { category, retryable } = classifyText(opts.sdkResultFields);
|
|
174
284
|
return {
|
|
@@ -230,7 +340,10 @@ function classifyFromSources(opts: SynthesizeErrorOpts): ClassifiedError {
|
|
|
230
340
|
const { model, mode, agentId } = opts.fallbackContext;
|
|
231
341
|
return {
|
|
232
342
|
category: "network",
|
|
233
|
-
message:
|
|
343
|
+
message:
|
|
344
|
+
`${TRANSPORT_TIMEOUT_USER_PREFIX} ` +
|
|
345
|
+
`(Transport timeout: ${opts.durationMs}ms, 0 messages received. ` +
|
|
346
|
+
`Model=${model}, mode=${mode}, agentId=${agentId})`,
|
|
234
347
|
retryable: true,
|
|
235
348
|
source: "fallback",
|
|
236
349
|
};
|
|
@@ -245,11 +358,22 @@ function classifyFromSources(opts: SynthesizeErrorOpts): ClassifiedError {
|
|
|
245
358
|
};
|
|
246
359
|
}
|
|
247
360
|
|
|
361
|
+
// The honest last resort: every detail channel was empty. Observed in prod
|
|
362
|
+
// only during provider capacity incidents (oss#492: Composer 2.5 degradation
|
|
363
|
+
// — the SDK rejection that carries ERROR_RESOURCE_EXHAUSTED in Cursor's IDE
|
|
364
|
+
// arrives here detail-free), so the copy leads with the capacity hypothesis
|
|
365
|
+
// hedged ("may be"), and retryable is true: the observed cause is transient
|
|
366
|
+
// by nature, and nothing gates a recovery loop on unknown+retryable. The
|
|
367
|
+
// parenthetical keeps the exact Model=/mode=/agentId= tokens for log-grep
|
|
368
|
+
// continuity and the env integration test's matcher.
|
|
248
369
|
const { model, mode, agentId } = opts.fallbackContext;
|
|
249
370
|
return {
|
|
250
371
|
category: "unknown",
|
|
251
|
-
message:
|
|
252
|
-
|
|
372
|
+
message:
|
|
373
|
+
`${DETAIL_FREE_FALLBACK_USER_PREFIX} ` +
|
|
374
|
+
`(No error detail from the Cursor SDK. ` +
|
|
375
|
+
`Model=${model}, mode=${mode}, agentId=${agentId})`,
|
|
376
|
+
retryable: true,
|
|
253
377
|
source: "fallback",
|
|
254
378
|
};
|
|
255
379
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier-2 structured-output extraction for the Cursor harness.
|
|
3
|
+
*
|
|
4
|
+
* When tier-1 text extraction (shared/extract-json.ts) cannot find JSON in
|
|
5
|
+
* the agent's free-text response, this tier asks an economy-tier LLM to
|
|
6
|
+
* extract it via withStructuredOutput (function-calling), which guarantees
|
|
7
|
+
* schema-conformant output through the API's tool-use mechanism.
|
|
8
|
+
*
|
|
9
|
+
* Lives in its own module (rather than inside execute-cursor/index.ts) so
|
|
10
|
+
* the LangChain construction path stays out of the Cursor activity's module
|
|
11
|
+
* graph until a run actually needs tier 2 — index.ts imports this module
|
|
12
|
+
* lazily at the call site, mirroring its tier-1 import, which is what
|
|
13
|
+
* bundle-slim's deferred evaluation preserves.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { Config } from "../../config.js";
|
|
17
|
+
import { getEconomyModel } from "../../shared/model-registry.js";
|
|
18
|
+
import { buildChatModel } from "../../shared/model-client.js";
|
|
19
|
+
import { checkDirectCredentials } from "../../shared/llm-backend.js";
|
|
20
|
+
import { tryInferProvider } from "../../shared/llm-proxy.js";
|
|
21
|
+
import { jsonSchemaToZod } from "../../shared/json-schema-to-zod.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Extract structured data from an agent's free-text response using an
|
|
25
|
+
* economy-tier LLM with withStructuredOutput (function-calling).
|
|
26
|
+
*
|
|
27
|
+
* Construction (registry-id resolution, provider inference, proxy wiring) is
|
|
28
|
+
* delegated to the shared buildChatModel so the economy model's registry id
|
|
29
|
+
* is always resolved to a provider API id before the call.
|
|
30
|
+
*
|
|
31
|
+
* Throws when no LLM is reachable (no proxy and no credential path for the
|
|
32
|
+
* extraction model's provider) — the caller treats any throw here as "tier 2
|
|
33
|
+
* unavailable", logs it, and returns the agent's text without structured
|
|
34
|
+
* output, so the failure mode is a diagnosable log line, never a lost run.
|
|
35
|
+
*/
|
|
36
|
+
export async function extractStructuredOutput(
|
|
37
|
+
agentResponse: string,
|
|
38
|
+
schema: Record<string, unknown>,
|
|
39
|
+
config: Config,
|
|
40
|
+
primaryModel: string,
|
|
41
|
+
): Promise<unknown | null> {
|
|
42
|
+
const extractionModel = await getEconomyModel(primaryModel);
|
|
43
|
+
const proxyEndpoint = config.proxyEndpoint ?? undefined;
|
|
44
|
+
|
|
45
|
+
if (!proxyEndpoint) {
|
|
46
|
+
const provider = tryInferProvider(extractionModel);
|
|
47
|
+
const missing = provider === null ? null : checkDirectCredentials(provider);
|
|
48
|
+
if (missing !== null) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Structured-output extraction needs the ${provider} model ` +
|
|
51
|
+
`'${extractionModel}' but has no credential path. ${missing}`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const { model: llm } = await buildChatModel({
|
|
57
|
+
modelName: extractionModel,
|
|
58
|
+
proxyEndpoint,
|
|
59
|
+
stigmerToken: config.stigmerToken ?? undefined,
|
|
60
|
+
maxTokens: 4096,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const zodSchema = jsonSchemaToZod(schema);
|
|
64
|
+
const structured = llm.withStructuredOutput(zodSchema);
|
|
65
|
+
|
|
66
|
+
const result = await structured.invoke([
|
|
67
|
+
{ role: "system", content: "Extract the structured data from the agent's response. Return only the data that matches the schema." },
|
|
68
|
+
{ role: "user", content: agentResponse },
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
return result ?? null;
|
|
72
|
+
}
|