@stigmer/runner 3.40.0 → 3.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/.build-fingerprint +1 -1
- package/dist/__test-utils__/execution-record-fixture.d.ts +2 -1
- package/dist/__test-utils__/execution-record-fixture.js.map +1 -1
- package/dist/activities/call-agent.js +1 -1
- package/dist/activities/discover-mcp-server.d.ts +7 -1
- package/dist/activities/discover-mcp-server.js +13 -29
- package/dist/activities/discover-mcp-server.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/fold.d.ts +4 -2
- package/dist/activities/execute-cursor/__test-utils__/fold.js +3 -4
- package/dist/activities/execute-cursor/__test-utils__/fold.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js +1 -0
- package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js.map +1 -1
- package/dist/activities/execute-cursor/approval-policy.d.ts +1 -29
- package/dist/activities/execute-cursor/approval-policy.js +11 -35
- package/dist/activities/execute-cursor/approval-policy.js.map +1 -1
- package/dist/activities/execute-cursor/boundary-rows.js +4 -4
- package/dist/activities/execute-cursor/boundary-rows.js.map +1 -1
- package/dist/activities/execute-cursor/translator.d.ts +12 -17
- package/dist/activities/execute-cursor/translator.js +13 -30
- package/dist/activities/execute-cursor/translator.js.map +1 -1
- package/dist/activities/execute-cursor/turn-stream.js +4 -2
- package/dist/activities/execute-cursor/turn-stream.js.map +1 -1
- package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.d.ts +11 -1
- package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js +22 -3
- package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js.map +1 -1
- package/dist/activities/execute-deep-agent/inline-publisher.d.ts +33 -2
- package/dist/activities/execute-deep-agent/inline-publisher.js +33 -7
- package/dist/activities/execute-deep-agent/inline-publisher.js.map +1 -1
- package/dist/activities/execute-deep-agent/platform-route.d.ts +48 -13
- package/dist/activities/execute-deep-agent/platform-route.js +87 -19
- package/dist/activities/execute-deep-agent/platform-route.js.map +1 -1
- package/dist/activities/execute-deep-agent/prompt-builder.d.ts +70 -35
- package/dist/activities/execute-deep-agent/prompt-builder.js +70 -55
- package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/streaming-side-effects.d.ts +20 -3
- package/dist/activities/execute-deep-agent/streaming-side-effects.js +26 -4
- package/dist/activities/execute-deep-agent/streaming-side-effects.js.map +1 -1
- package/dist/activities/execute-deep-agent/translator.d.ts +5 -4
- package/dist/activities/execute-deep-agent/translator.js +5 -4
- package/dist/activities/execute-deep-agent/translator.js.map +1 -1
- package/dist/activities/execute-deep-agent/turn-settle.js +6 -6
- package/dist/activities/execute-deep-agent/turn-settle.js.map +1 -1
- package/dist/activities/execute-deep-agent/turn-setup.d.ts +18 -13
- package/dist/activities/execute-deep-agent/turn-setup.js +42 -36
- package/dist/activities/execute-deep-agent/turn-setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/turn-stream.d.ts +10 -6
- package/dist/activities/execute-deep-agent/turn-stream.js +6 -16
- package/dist/activities/execute-deep-agent/turn-stream.js.map +1 -1
- package/dist/activities/execute-deep-agent/turn.d.ts +4 -1
- package/dist/activities/execute-deep-agent/turn.js +15 -6
- package/dist/activities/execute-deep-agent/turn.js.map +1 -1
- package/dist/activities/workflow-event-activities.js +2 -0
- package/dist/activities/workflow-event-activities.js.map +1 -1
- package/dist/config.d.ts +9 -0
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/exit-backstop.d.ts +64 -0
- package/dist/exit-backstop.js +79 -0
- package/dist/exit-backstop.js.map +1 -0
- package/dist/harness/terminal-table.d.ts +12 -5
- package/dist/harness/terminal-table.js +13 -13
- package/dist/harness/terminal-table.js.map +1 -1
- package/dist/harness/transcript/builder.d.ts +21 -0
- package/dist/harness/transcript/builder.js +53 -31
- package/dist/harness/transcript/builder.js.map +1 -1
- package/dist/harness/transcript/events.d.ts +22 -26
- package/dist/harness/transcript/events.js +6 -5
- package/dist/harness/transcript/events.js.map +1 -1
- package/dist/harness/turn-context.d.ts +3 -1
- package/dist/harness/turn-context.js +5 -3
- package/dist/harness/turn-context.js.map +1 -1
- package/dist/main.js +10 -4
- package/dist/main.js.map +1 -1
- package/dist/middleware/cost-advisory.d.ts +6 -3
- package/dist/middleware/cost-advisory.js +6 -3
- package/dist/middleware/cost-advisory.js.map +1 -1
- package/dist/middleware/execution-budget.d.ts +22 -7
- package/dist/middleware/execution-budget.js +57 -35
- package/dist/middleware/execution-budget.js.map +1 -1
- package/dist/middleware/index.d.ts +10 -7
- package/dist/middleware/index.js +10 -7
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/types.d.ts +6 -1
- package/dist/runner-manager.js +3 -2
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.d.ts +25 -0
- package/dist/runner.js +41 -14
- package/dist/runner.js.map +1 -1
- package/dist/shared/approval-policy.d.ts +23 -13
- package/dist/shared/approval-policy.js +47 -14
- package/dist/shared/approval-policy.js.map +1 -1
- package/dist/shared/caller-identity.d.ts +4 -5
- package/dist/shared/caller-identity.js +4 -5
- package/dist/shared/caller-identity.js.map +1 -1
- package/dist/shared/channel-attachment.js +3 -2
- package/dist/shared/channel-attachment.js.map +1 -1
- package/dist/shared/connect-backfill.d.ts +2 -1
- package/dist/shared/connect-backfill.js +2 -2
- package/dist/shared/connect-backfill.js.map +1 -1
- package/dist/shared/mcp-resolver.d.ts +10 -1
- package/dist/shared/mcp-resolver.js +11 -2
- package/dist/shared/mcp-resolver.js.map +1 -1
- package/dist/shared/memory-attachment.js +3 -2
- package/dist/shared/memory-attachment.js.map +1 -1
- package/dist/shared/model-error.d.ts +25 -3
- package/dist/shared/model-error.js +78 -10
- package/dist/shared/model-error.js.map +1 -1
- package/dist/shared/platform-server-address.d.ts +71 -0
- package/dist/shared/platform-server-address.js +103 -0
- package/dist/shared/platform-server-address.js.map +1 -0
- package/dist/shared/prompt-sections.d.ts +6 -28
- package/dist/shared/prompt-sections.js +6 -35
- package/dist/shared/prompt-sections.js.map +1 -1
- package/dist/shared/skill-resolver.d.ts +2 -3
- package/dist/shared/skill-resolver.js +2 -3
- package/dist/shared/skill-resolver.js.map +1 -1
- package/dist/shared/synthesized-attachment.d.ts +0 -5
- package/dist/shared/synthesized-attachment.js +0 -12
- package/dist/shared/synthesized-attachment.js.map +1 -1
- package/dist/shared/tool-rounds.d.ts +51 -23
- package/dist/shared/tool-rounds.js +67 -24
- package/dist/shared/tool-rounds.js.map +1 -1
- package/dist/shared/workspace/platform-dir.d.ts +15 -0
- package/dist/shared/workspace/platform-dir.js +18 -1
- package/dist/shared/workspace/platform-dir.js.map +1 -1
- package/dist/shared/workspace/sources/git.js +13 -6
- package/dist/shared/workspace/sources/git.js.map +1 -1
- package/dist/shared/workspace/stigmer-link.d.ts +21 -14
- package/dist/shared/workspace/stigmer-link.js +91 -29
- package/dist/shared/workspace/stigmer-link.js.map +1 -1
- package/dist/tools/url-guard.d.ts +5 -1
- package/dist/tools/url-guard.js +9 -2
- package/dist/tools/url-guard.js.map +1 -1
- package/dist/worker.d.ts +11 -2
- package/dist/worker.js +1 -1
- package/dist/worker.js.map +1 -1
- package/dist/workflow-engine/tasks/human-input.d.ts +16 -0
- package/dist/workflow-engine/tasks/human-input.js +79 -37
- package/dist/workflow-engine/tasks/human-input.js.map +1 -1
- package/dist/workflow-engine/types.d.ts +14 -4
- package/dist/workflow-engine/types.js.map +1 -1
- package/dist/workflows/call-agent-orchestrator.d.ts +13 -0
- package/dist/workflows/call-agent-orchestrator.js.map +1 -1
- package/dist/workflows/engine-core.d.ts +25 -0
- package/dist/workflows/engine-core.js.map +1 -1
- package/dist/workflows/execute-from-execution.d.ts +3 -0
- package/dist/workflows/execute-from-execution.js.map +1 -1
- package/dist/workflows/human-input-orchestrator.d.ts +9 -12
- package/dist/workflows/human-input-orchestrator.js +12 -44
- package/dist/workflows/human-input-orchestrator.js.map +1 -1
- package/package.json +5 -5
- package/src/__test-utils__/config-fixture.ts +1 -0
- package/src/__test-utils__/execution-record-fixture.ts +2 -1
- package/src/__test-utils__/harness-contract/runtime-contract.ts +5 -3
- package/src/__test-utils__/test-gate.ts +8 -0
- package/src/__tests__/exit-backstop.test.ts +91 -0
- package/src/__tests__/golden-e2e.test.ts +80 -62
- package/src/__tests__/history-encryption-e2e.test.ts +51 -27
- package/src/__tests__/runner.test.ts +60 -2
- package/src/activities/__tests__/classify-tool-approvals.test.ts +1 -0
- package/src/activities/__tests__/discover-mcp-server.hang.test.ts +5 -1
- package/src/activities/__tests__/discover-mcp-server.test.ts +90 -49
- package/src/activities/__tests__/workflow-event-activities.test.ts +28 -7
- package/src/activities/call-agent.ts +1 -1
- package/src/activities/discover-mcp-server.ts +27 -37
- package/src/activities/execute-cursor/__test-utils__/fold.ts +6 -4
- package/src/activities/execute-cursor/__test-utils__/hermetic-cursor.ts +1 -0
- package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +4 -29
- package/src/activities/execute-cursor/__tests__/hermetic/bypassed-tool-call.test.ts +120 -0
- package/src/activities/execute-cursor/__tests__/hermetic/file-review-capture.test.ts +5 -5
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/file-review-capture.status.json +0 -2
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/resolution-error.status.json +1 -1
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.error-delta.status.json +0 -3
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.output-first.status.json +0 -2
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.status.json +0 -2
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/stall.status.json +0 -2
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/unattributed-hook-block.status.json +0 -3
- package/src/activities/execute-cursor/__tests__/hermetic/shell-output-delta.test.ts +9 -7
- package/src/activities/execute-cursor/__tests__/hermetic/thrown-error-arms.test.ts +6 -1
- package/src/activities/execute-cursor/__tests__/hitl-resume-history.test.ts +8 -11
- package/src/activities/execute-cursor/__tests__/sequential-gate-resume.test.ts +1 -3
- package/src/activities/execute-cursor/__tests__/translator.test.ts +68 -15
- package/src/activities/execute-cursor/approval-policy.ts +11 -36
- package/src/activities/execute-cursor/boundary-rows.ts +3 -3
- package/src/activities/execute-cursor/translator.ts +15 -32
- package/src/activities/execute-cursor/turn-stream.ts +4 -1
- package/src/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.ts +33 -2
- package/src/activities/execute-deep-agent/__tests__/adapter.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.build-from-plan.prompt.md +1 -27
- package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.everything.prompt.md +26 -30
- package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.plan-mode.prompt.md +1 -26
- package/src/activities/execute-deep-agent/__tests__/goldens/user-message.everything.md +36 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/file-review-capture.test.ts +4 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/file-review-capture.status.json +1 -1
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/inline-artifact.turn1.status.json +1 -1
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/inline-artifact.turn2.status.json +1 -1
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/recursion-limit.persisted.status.json +8 -73
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/resolution-error.status.json +1 -1
- package/src/activities/execute-deep-agent/__tests__/hermetic/inline-artifact-across-gate.test.ts +17 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/recursion-limit.test.ts +86 -26
- package/src/activities/execute-deep-agent/__tests__/hermetic/resolution-error.test.ts +9 -5
- package/src/activities/execute-deep-agent/__tests__/hermetic/thrown-turn-drains-publishes.test.ts +159 -0
- package/src/activities/execute-deep-agent/__tests__/inline-publisher.test.ts +84 -0
- package/src/activities/execute-deep-agent/__tests__/platform-route.test.ts +47 -10
- package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +127 -199
- package/src/activities/execute-deep-agent/__tests__/prompt-goldens.test.ts +31 -26
- package/src/activities/execute-deep-agent/__tests__/streaming-side-effects.test.ts +34 -0
- package/src/activities/execute-deep-agent/__tests__/turn-stream.test.ts +13 -5
- package/src/activities/execute-deep-agent/inline-publisher.ts +47 -7
- package/src/activities/execute-deep-agent/platform-route.ts +109 -20
- package/src/activities/execute-deep-agent/prompt-builder.ts +104 -92
- package/src/activities/execute-deep-agent/streaming-side-effects.ts +27 -4
- package/src/activities/execute-deep-agent/translator.ts +5 -4
- package/src/activities/execute-deep-agent/turn-settle.ts +6 -6
- package/src/activities/execute-deep-agent/turn-setup.ts +43 -38
- package/src/activities/execute-deep-agent/turn-stream.ts +15 -22
- package/src/activities/execute-deep-agent/turn.ts +16 -7
- package/src/activities/workflow-event-activities.ts +2 -0
- package/src/config.ts +11 -0
- package/src/exit-backstop.ts +97 -0
- package/src/harness/README.md +5 -5
- package/src/harness/terminal-table.ts +13 -13
- package/src/harness/transcript/__tests__/builder.test.ts +93 -21
- package/src/harness/transcript/builder.ts +53 -30
- package/src/harness/transcript/events.ts +22 -27
- package/src/harness/turn-context.ts +5 -3
- package/src/main.ts +9 -5
- package/src/middleware/__tests__/execution-budget.test.ts +181 -103
- package/src/middleware/cost-advisory.ts +6 -3
- package/src/middleware/execution-budget.ts +60 -34
- package/src/middleware/index.ts +10 -7
- package/src/middleware/types.ts +6 -1
- package/src/runner-manager.ts +3 -2
- package/src/runner.ts +57 -15
- package/src/shared/__tests__/approval-policy.test.ts +41 -0
- package/src/shared/__tests__/artifact-storage.test.ts +1 -0
- package/src/shared/__tests__/connect-backfill.test.ts +15 -11
- package/src/shared/__tests__/mcp-resolver.test.ts +72 -10
- package/src/shared/__tests__/model-error.test.ts +71 -3
- package/src/shared/__tests__/platform-server-address.test.ts +122 -0
- package/src/shared/__tests__/prompt-sections.test.ts +2 -54
- package/src/shared/__tests__/tool-rounds.test.ts +64 -14
- package/src/shared/approval-policy.ts +57 -14
- package/src/shared/caller-identity.ts +4 -5
- package/src/shared/channel-attachment.ts +2 -2
- package/src/shared/connect-backfill.ts +5 -1
- package/src/shared/mcp-resolver.ts +11 -1
- package/src/shared/memory-attachment.ts +2 -2
- package/src/shared/model-error.ts +87 -9
- package/src/shared/platform-server-address.ts +123 -0
- package/src/shared/prompt-sections.ts +6 -55
- package/src/shared/skill-resolver.ts +2 -3
- package/src/shared/synthesized-attachment.ts +0 -12
- package/src/shared/tool-rounds.ts +70 -24
- package/src/shared/workspace/__tests__/git-source.test.ts +11 -5
- package/src/shared/workspace/__tests__/stigmer-link.test.ts +107 -10
- package/src/shared/workspace/platform-dir.ts +19 -1
- package/src/shared/workspace/sources/git.ts +14 -8
- package/src/shared/workspace/stigmer-link.ts +99 -29
- package/src/tools/__tests__/url-guard.test.ts +19 -1
- package/src/tools/url-guard.ts +9 -1
- package/src/worker.ts +12 -2
- package/src/workflow-engine/__tests__/golden-execution.test.ts +2 -1
- package/src/workflow-engine/__tests__/loader.test.ts +1 -1
- package/src/workflow-engine/__tests__/tasks/human-input.test.ts +133 -57
- package/src/workflow-engine/tasks/human-input.ts +92 -42
- package/src/workflow-engine/types.ts +16 -4
- package/src/workflows/call-agent-orchestrator.ts +7 -0
- package/src/workflows/engine-core.ts +18 -0
- package/src/workflows/execute-from-execution.ts +2 -1
- package/src/workflows/human-input-orchestrator.ts +15 -56
- package/dist/shared/skill-relevance.d.ts +0 -65
- package/dist/shared/skill-relevance.js +0 -175
- package/dist/shared/skill-relevance.js.map +0 -1
- package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.skills-below-threshold.prompt.md +0 -146
- package/src/shared/__tests__/skill-relevance.test.ts +0 -292
- package/src/shared/skill-relevance.ts +0 -222
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The shared prompt glue (`shared/prompt-sections.ts`): the standing order
|
|
3
|
-
* the input-files lines
|
|
4
|
-
* (the sentence's arms came from `skill-writer.test.ts`, retired with its
|
|
5
|
-
* module in #1096). What each harness renders AROUND these is pinned whole by
|
|
2
|
+
* The shared prompt glue (`shared/prompt-sections.ts`): the standing order
|
|
3
|
+
* and the input-files lines. What each harness renders AROUND these is pinned whole by
|
|
6
4
|
* the two builders' prompt goldens (`execute-deep-agent/__tests__/
|
|
7
5
|
* prompt-goldens.test.ts`, `execute-cursor/__tests__/prompt-goldens.test.ts`);
|
|
8
6
|
* this file pins the glue's own contract, harness-free.
|
|
@@ -10,16 +8,12 @@
|
|
|
10
8
|
|
|
11
9
|
import { describe, it, expect } from "vitest";
|
|
12
10
|
import {
|
|
13
|
-
alsoAvailableSkillsNote,
|
|
14
11
|
inputFileLines,
|
|
15
|
-
selectSkillsForPrompt,
|
|
16
12
|
standingContextSections,
|
|
17
13
|
visionPromptInfoOf,
|
|
18
14
|
type StandingSectionKind,
|
|
19
15
|
} from "../prompt-sections.js";
|
|
20
16
|
import type { ResolvedAttachment } from "../attachment-resolver.js";
|
|
21
|
-
import type { SkillMetadata } from "../skill-resolver.js";
|
|
22
|
-
import { SKILL_COUNT_THRESHOLD } from "../skill-relevance.js";
|
|
23
17
|
import { formatDeclaredPreferencesText } from "../declared-preferences.js";
|
|
24
18
|
|
|
25
19
|
describe("standingContextSections", () => {
|
|
@@ -119,49 +113,3 @@ describe("visionPromptInfoOf", () => {
|
|
|
119
113
|
});
|
|
120
114
|
});
|
|
121
115
|
});
|
|
122
|
-
|
|
123
|
-
describe("selectSkillsForPrompt", () => {
|
|
124
|
-
const skill = (name: string, description: string): SkillMetadata => ({ name, description, path: `.stigmer/skills/${name}/SKILL.md` });
|
|
125
|
-
|
|
126
|
-
it("highlights every skill below the threshold, in mount order, and names none as also-available", () => {
|
|
127
|
-
const skills = [skill("alpha", "Deploy to kubernetes"), skill("beta", "Reshape CSV exports")];
|
|
128
|
-
const selection = selectSkillsForPrompt("deploy the service", skills);
|
|
129
|
-
expect(selection.highlighted).toEqual(skills);
|
|
130
|
-
expect(selection.alsoAvailable).toEqual([]);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("at the threshold, highlights the skills the message reaches and names the rest", () => {
|
|
134
|
-
const skills = Array.from({ length: SKILL_COUNT_THRESHOLD }, (_, i) =>
|
|
135
|
-
i === 0 ? skill("k8s-deploy", "Deploy services to kubernetes clusters") : skill(`other-${i}`, `Unrelated thing number ${i}`),
|
|
136
|
-
);
|
|
137
|
-
const selection = selectSkillsForPrompt("Deploy the payments service to kubernetes", skills);
|
|
138
|
-
expect(selection.highlighted.map((s) => s.name)).toContain("k8s-deploy");
|
|
139
|
-
expect(selection.alsoAvailable.length).toBeGreaterThan(0);
|
|
140
|
-
expect(selection.highlighted.length + selection.alsoAvailable.length).toBe(SKILL_COUNT_THRESHOLD);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it("selects nothing from nothing", () => {
|
|
144
|
-
expect(selectSkillsForPrompt("anything", [])).toEqual({ highlighted: [], alsoAvailable: [] });
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
describe("alsoAvailableSkillsNote", () => {
|
|
149
|
-
it("returns empty for no excluded names", () => {
|
|
150
|
-
expect(alsoAvailableSkillsNote([])).toBe("");
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it("lists excluded skills with backtick formatting", () => {
|
|
154
|
-
const note = alsoAvailableSkillsNote(["alpha", "beta", "gamma"]);
|
|
155
|
-
expect(note).toContain("`alpha`");
|
|
156
|
-
expect(note).toContain("`beta`");
|
|
157
|
-
expect(note).toContain("`gamma`");
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it("includes activation instructions that name no tool, so any harness can carry it", () => {
|
|
161
|
-
const note = alsoAvailableSkillsNote(["some-skill"]);
|
|
162
|
-
expect(note).toContain(".stigmer/skills/<name>/SKILL.md");
|
|
163
|
-
expect(note).toContain("relevant to your task");
|
|
164
|
-
expect(note).not.toMatch(/\b(read|Read) tool\b/);
|
|
165
|
-
expect(note.startsWith("#")).toBe(false);
|
|
166
|
-
});
|
|
167
|
-
});
|
|
@@ -1,35 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* max_tool_rounds resolution and the budget's stop signal.
|
|
3
|
+
*
|
|
4
|
+
* Pins the proto contract's arithmetic (0/unset = unlimited, the 10–1000
|
|
5
|
+
* clamp and its warnings), the backstop that must never bind before the
|
|
6
|
+
* round count, and the classifier the native turn uses to read a thrown
|
|
7
|
+
* error as the `tool_call_limit` outcome. The classifier cases use
|
|
8
|
+
* LangChain's and LangGraph's own error classes, so an engine upgrade that
|
|
9
|
+
* changes how a middleware's throw reaches the turn fails here, not in a
|
|
10
|
+
* production run that would otherwise read the stop as a failure.
|
|
11
|
+
*/
|
|
12
|
+
|
|
1
13
|
import { describe, expect, it, vi, afterEach } from "vitest";
|
|
14
|
+
import { MiddlewareError } from "langchain";
|
|
15
|
+
import { GraphRecursionError } from "@langchain/langgraph";
|
|
2
16
|
import {
|
|
3
17
|
MAX_TOOL_ROUNDS,
|
|
4
18
|
MIN_TOOL_ROUNDS,
|
|
5
|
-
|
|
6
|
-
|
|
19
|
+
SUPER_STEPS_BACKSTOP_PER_ROUND,
|
|
20
|
+
ToolRoundLimitError,
|
|
21
|
+
backstopRecursionLimit,
|
|
22
|
+
isToolCallBudgetStop,
|
|
23
|
+
resolveToolRoundLimit,
|
|
7
24
|
} from "../tool-rounds.js";
|
|
8
25
|
|
|
9
|
-
describe("
|
|
26
|
+
describe("resolveToolRoundLimit", () => {
|
|
10
27
|
afterEach(() => {
|
|
11
28
|
vi.restoreAllMocks();
|
|
12
29
|
});
|
|
13
30
|
|
|
14
31
|
it("returns null for unset (proto contract: 0 = unlimited)", () => {
|
|
15
|
-
expect(
|
|
16
|
-
expect(
|
|
32
|
+
expect(resolveToolRoundLimit(undefined)).toBeNull();
|
|
33
|
+
expect(resolveToolRoundLimit(0)).toBeNull();
|
|
17
34
|
});
|
|
18
35
|
|
|
19
36
|
it("returns null for negative values (treated as unset, never a tiny limit)", () => {
|
|
20
|
-
expect(
|
|
37
|
+
expect(resolveToolRoundLimit(-5)).toBeNull();
|
|
21
38
|
});
|
|
22
39
|
|
|
23
|
-
it("
|
|
24
|
-
expect(
|
|
25
|
-
expect(
|
|
26
|
-
expect(
|
|
40
|
+
it("returns an in-range value as the round count itself", () => {
|
|
41
|
+
expect(resolveToolRoundLimit(10)).toBe(10);
|
|
42
|
+
expect(resolveToolRoundLimit(100)).toBe(100);
|
|
43
|
+
expect(resolveToolRoundLimit(MAX_TOOL_ROUNDS)).toBe(MAX_TOOL_ROUNDS);
|
|
27
44
|
});
|
|
28
45
|
|
|
29
46
|
it("clamps below-range values up to the minimum with a warning", () => {
|
|
30
47
|
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
31
48
|
|
|
32
|
-
expect(
|
|
49
|
+
expect(resolveToolRoundLimit(3)).toBe(MIN_TOOL_ROUNDS);
|
|
33
50
|
|
|
34
51
|
expect(warn).toHaveBeenCalledOnce();
|
|
35
52
|
expect(warn.mock.calls[0][0]).toContain("max_tool_rounds=3");
|
|
@@ -39,7 +56,7 @@ describe("resolveRecursionLimit", () => {
|
|
|
39
56
|
it("clamps above-range values down to the maximum with a warning", () => {
|
|
40
57
|
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
41
58
|
|
|
42
|
-
expect(
|
|
59
|
+
expect(resolveToolRoundLimit(5000)).toBe(MAX_TOOL_ROUNDS);
|
|
43
60
|
|
|
44
61
|
expect(warn).toHaveBeenCalledOnce();
|
|
45
62
|
expect(warn.mock.calls[0][0]).toContain("max_tool_rounds=5000");
|
|
@@ -49,9 +66,42 @@ describe("resolveRecursionLimit", () => {
|
|
|
49
66
|
it("does not warn for in-range values", () => {
|
|
50
67
|
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
51
68
|
|
|
52
|
-
|
|
53
|
-
|
|
69
|
+
resolveToolRoundLimit(MIN_TOOL_ROUNDS);
|
|
70
|
+
resolveToolRoundLimit(MAX_TOOL_ROUNDS);
|
|
54
71
|
|
|
55
72
|
expect(warn).not.toHaveBeenCalled();
|
|
56
73
|
});
|
|
57
74
|
});
|
|
75
|
+
|
|
76
|
+
describe("backstopRecursionLimit", () => {
|
|
77
|
+
it("allows the backstop's super-steps per round, far above what one round costs", () => {
|
|
78
|
+
expect(backstopRecursionLimit(MIN_TOOL_ROUNDS)).toBe(MIN_TOOL_ROUNDS * SUPER_STEPS_BACKSTOP_PER_ROUND);
|
|
79
|
+
expect(backstopRecursionLimit(MAX_TOOL_ROUNDS)).toBe(MAX_TOOL_ROUNDS * SUPER_STEPS_BACKSTOP_PER_ROUND);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("isToolCallBudgetStop", () => {
|
|
84
|
+
it("reads the round count's own error as the budget's stop", () => {
|
|
85
|
+
expect(isToolCallBudgetStop(new ToolRoundLimitError(10))).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("reads it through LangChain's MiddlewareError wrapping, once per wrapModelCall layer", () => {
|
|
89
|
+
let wrapped: unknown = new ToolRoundLimitError(10);
|
|
90
|
+
for (const layer of ["ExecutionBudgetMiddleware", "todoListMiddleware", "SummarizationMiddleware"]) {
|
|
91
|
+
wrapped = MiddlewareError.wrap(wrapped, layer);
|
|
92
|
+
}
|
|
93
|
+
expect(wrapped).toBeInstanceOf(MiddlewareError);
|
|
94
|
+
expect(isToolCallBudgetStop(wrapped)).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("reads the backstop's recursion error as the same stop", () => {
|
|
98
|
+
expect(isToolCallBudgetStop(new GraphRecursionError("Recursion limit of 200 reached without hitting a stop condition."))).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("does not read any other failure as the budget's stop, wrapped or not", () => {
|
|
102
|
+
expect(isToolCallBudgetStop(new Error("upstream 529"))).toBe(false);
|
|
103
|
+
expect(isToolCallBudgetStop(MiddlewareError.wrap(new Error("upstream 529"), "ExecutionBudgetMiddleware"))).toBe(false);
|
|
104
|
+
expect(isToolCallBudgetStop("Recursion limit")).toBe(false);
|
|
105
|
+
expect(isToolCallBudgetStop(undefined)).toBe(false);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -20,6 +20,7 @@ import type { AgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexe
|
|
|
20
20
|
import { ApprovalAction, ApprovalMode, ApprovalPolicySource } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
21
21
|
import { SENSITIVE_ARG_KEYS } from "./args-preview.js";
|
|
22
22
|
import { toolApprovalCategory, type ToolApprovalCategory } from "./tool-kind.js";
|
|
23
|
+
import { extractFilePath } from "./file-tools.js";
|
|
23
24
|
import type { ResolvedMcpServer } from "./mcp-resolver.js";
|
|
24
25
|
|
|
25
26
|
/**
|
|
@@ -480,18 +481,15 @@ export function resolveApprovalMessage(
|
|
|
480
481
|
* THE approval-message template per mutating built-in category — the one
|
|
481
482
|
* table both harnesses word their approval cards from (since #1097).
|
|
482
483
|
* Keyed by category (not raw tool name) so every alias of a mutation renders
|
|
483
|
-
* one message
|
|
484
|
-
* (`
|
|
485
|
-
*
|
|
486
|
-
*
|
|
487
|
-
* Cursor boundary resolves a denied call's message through the same path.
|
|
484
|
+
* one message. It is read only through {@link resolveBuiltInApprovalMessage},
|
|
485
|
+
* which the native gate (`resolveToolApproval`), the Cursor translator's `gate`
|
|
486
|
+
* and the Cursor boundary's denied-call message all call, so the three can
|
|
487
|
+
* never word a card differently.
|
|
488
488
|
*
|
|
489
489
|
* Until then the Cursor harness carried a table of its own and the two differed
|
|
490
490
|
* by a word — `shell` read "Run command" there and "Execute command" here.
|
|
491
491
|
* Unified on the plainer word: a user reads "Run command" and knows
|
|
492
|
-
* what is being asked.
|
|
493
|
-
* `write` resolves `{{args.path}}`, while deepagents' file tools send
|
|
494
|
-
* `file_path`, so a gated native write reads "Write file: <unknown>".
|
|
492
|
+
* what is being asked.
|
|
495
493
|
*/
|
|
496
494
|
export const CATEGORY_APPROVAL_MESSAGE: Record<ToolApprovalCategory, string> = {
|
|
497
495
|
write: "Write file: {{args.path}}",
|
|
@@ -499,6 +497,50 @@ export const CATEGORY_APPROVAL_MESSAGE: Record<ToolApprovalCategory, string> = {
|
|
|
499
497
|
shell: "Run command: {{args.command}}",
|
|
500
498
|
};
|
|
501
499
|
|
|
500
|
+
/**
|
|
501
|
+
* THE approval-card message for a built-in tool of either harness taxonomy, or
|
|
502
|
+
* `undefined` when the tool is not a gated built-in.
|
|
503
|
+
*
|
|
504
|
+
* The `{{args.path}}` a file card reads is filled from `extractFilePath`
|
|
505
|
+
* (`file-tools.ts`), the one home of "which argument names the file" across
|
|
506
|
+
* both taxonomies: deepagents' file tools and the Cursor hook send
|
|
507
|
+
* `file_path`, the Cursor stream sends `path`. Resolving the raw args instead
|
|
508
|
+
* rendered a gated native write as "Write file: <unknown>" (#1112). The
|
|
509
|
+
* template keeps its one placeholder, so the table stays harness-neutral.
|
|
510
|
+
*/
|
|
511
|
+
export function resolveBuiltInApprovalMessage(
|
|
512
|
+
toolName: string,
|
|
513
|
+
args: Record<string, unknown>,
|
|
514
|
+
): string | undefined {
|
|
515
|
+
const category = toolApprovalCategory(toolName);
|
|
516
|
+
return category ? resolveCategoryApprovalMessage(category, toolName, args) : undefined;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function resolveCategoryApprovalMessage(
|
|
520
|
+
category: ToolApprovalCategory,
|
|
521
|
+
toolName: string,
|
|
522
|
+
args: Record<string, unknown>,
|
|
523
|
+
): string {
|
|
524
|
+
return resolveApprovalMessage(CATEGORY_APPROVAL_MESSAGE[category], toolName, categoryTemplateArgs(category, args));
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/** The args a category's template resolves against: a file card reads the file from wherever its taxonomy names it. */
|
|
528
|
+
function categoryTemplateArgs(category: ToolApprovalCategory, args: Record<string, unknown>): Record<string, unknown> {
|
|
529
|
+
switch (category) {
|
|
530
|
+
case "write":
|
|
531
|
+
case "delete": {
|
|
532
|
+
const path = extractFilePath(args);
|
|
533
|
+
return path === null ? args : { ...args, path };
|
|
534
|
+
}
|
|
535
|
+
case "shell":
|
|
536
|
+
return args;
|
|
537
|
+
default: {
|
|
538
|
+
const exhaustive: never = category;
|
|
539
|
+
throw new Error(`categoryTemplateArgs: unknown approval category ${String(exhaustive)}`);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
502
544
|
/** What the gate decides for one call: whether it waits, the message the card shows, and which layer said so. */
|
|
503
545
|
export interface ApprovalRequirement {
|
|
504
546
|
readonly requiresApproval: boolean;
|
|
@@ -508,11 +550,12 @@ export interface ApprovalRequirement {
|
|
|
508
550
|
}
|
|
509
551
|
|
|
510
552
|
/**
|
|
511
|
-
* THE approval decision for one tool call — the
|
|
512
|
-
* (`middleware/approval-gate.ts`) interrupts on
|
|
513
|
-
*
|
|
514
|
-
* about whether a call waits (since #1097; until
|
|
515
|
-
* builder carried its own weaker copy that gated MCP tools
|
|
553
|
+
* THE approval decision for one tool call — the function the native gate
|
|
554
|
+
* (`middleware/approval-gate.ts`) interrupts on. A held call reaches the
|
|
555
|
+
* transcript only through that interrupt (`approval_proposed`), so the row can
|
|
556
|
+
* never disagree with the gate about whether a call waits (since #1097; until
|
|
557
|
+
* then the transcript builder carried its own weaker copy that gated MCP tools
|
|
558
|
+
* only). Its
|
|
516
559
|
* read-side twin is {@link resolveApprovalProvenance}, which answers "which
|
|
517
560
|
* layer governs this call" for EVERY call, gated or not.
|
|
518
561
|
*
|
|
@@ -561,7 +604,7 @@ export function resolveToolApproval(
|
|
|
561
604
|
}
|
|
562
605
|
return {
|
|
563
606
|
requiresApproval: true,
|
|
564
|
-
message:
|
|
607
|
+
message: resolveCategoryApprovalMessage(category, toolName, args),
|
|
565
608
|
source: "builtin_category",
|
|
566
609
|
};
|
|
567
610
|
}
|
|
@@ -139,9 +139,8 @@ export function resolveCallerIdentity(
|
|
|
139
139
|
|
|
140
140
|
/**
|
|
141
141
|
* Return a NEW env map with the reserved caller-identity keys set —
|
|
142
|
-
* platform values are authoritative over same-named user entries (
|
|
143
|
-
*
|
|
144
|
-
* impersonate a caller).
|
|
142
|
+
* platform values are authoritative over same-named user entries (a user
|
|
143
|
+
* env var must never be able to impersonate a caller).
|
|
145
144
|
*
|
|
146
145
|
* Call this on the env map handed to MCP resolution ONLY — never on the
|
|
147
146
|
* map that reaches agent subprocess environments. Per-server opt-in is
|
|
@@ -174,8 +173,8 @@ export function injectCallerIdentityEnv(
|
|
|
174
173
|
* Discovery-context injection: the connect workflow resolves the same
|
|
175
174
|
* header templates with no session, so every declared reserved key gets
|
|
176
175
|
* the anonymous sentinel — otherwise a caller-identity-templating server
|
|
177
|
-
* can never be discovered. Gated on the server's declared keys,
|
|
178
|
-
*
|
|
176
|
+
* can never be discovered. Gated on the server's declared keys, as every
|
|
177
|
+
* platform key is (platform-server-address.ts).
|
|
179
178
|
*/
|
|
180
179
|
export function injectAnonymousCallerIdentityForDiscovery(
|
|
181
180
|
declaredEnvKeys: ReadonlySet<string>,
|
|
@@ -45,8 +45,8 @@ import type {
|
|
|
45
45
|
} from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/message_io_pb";
|
|
46
46
|
import type { StigmerClient } from "../client/stigmer-client.js";
|
|
47
47
|
import type { ResolvedMcpServer } from "./mcp-resolver.js";
|
|
48
|
+
import { grpcTarget, SERVER_ADDRESS_ENV_KEY } from "./platform-server-address.js";
|
|
48
49
|
import {
|
|
49
|
-
grpcTarget,
|
|
50
50
|
stdioCredentialEnv,
|
|
51
51
|
type SynthesizedAttachmentOptions,
|
|
52
52
|
} from "./synthesized-attachment.js";
|
|
@@ -157,7 +157,7 @@ export function synthesizeChannelAttachment(
|
|
|
157
157
|
args: ["mcp-server"],
|
|
158
158
|
env: {
|
|
159
159
|
STIGMER_MCP_ROSTER: "channels",
|
|
160
|
-
|
|
160
|
+
[SERVER_ADDRESS_ENV_KEY]: grpcTarget(options.backendEndpoint),
|
|
161
161
|
...stdioCredentialEnv(options.credential),
|
|
162
162
|
},
|
|
163
163
|
};
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
import type { ResolvedMcpServer } from "./mcp-resolver.js";
|
|
22
22
|
import { resolveMcpServers } from "./mcp-resolver.js";
|
|
23
23
|
import type { McpTransportPosture } from "./mcp-transport-guard.js";
|
|
24
|
+
import type { PlatformEndpoints } from "./platform-server-address.js";
|
|
24
25
|
import { withTimeout } from "./with-timeout.js";
|
|
25
26
|
import type { StigmerClient } from "../client/stigmer-client.js";
|
|
26
27
|
import type { McpServerUsage } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
|
|
@@ -67,6 +68,7 @@ export async function backfillMcpServersIfNeeded(
|
|
|
67
68
|
envVars: Record<string, string>,
|
|
68
69
|
org: string,
|
|
69
70
|
transportPosture: McpTransportPosture,
|
|
71
|
+
platformEndpoints: PlatformEndpoints,
|
|
70
72
|
onHeartbeat?: () => void,
|
|
71
73
|
secretKeys?: ReadonlySet<string>,
|
|
72
74
|
): Promise<ResolvedMcpServer[]> {
|
|
@@ -131,7 +133,9 @@ export async function backfillMcpServersIfNeeded(
|
|
|
131
133
|
|
|
132
134
|
// Same posture as the initial resolution: every server here already
|
|
133
135
|
// passed the transport guard once, so re-resolving cannot newly reject.
|
|
134
|
-
const refreshed = await resolveMcpServers(
|
|
136
|
+
const refreshed = await resolveMcpServers(
|
|
137
|
+
client, usages, envVars, transportPosture, platformEndpoints,
|
|
138
|
+
);
|
|
135
139
|
return refreshed.resolvedServers;
|
|
136
140
|
}
|
|
137
141
|
|
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
* toCursorMcpConfig (execute-cursor/cursor-mcp-config.ts) for the Cursor
|
|
13
13
|
* SDK — so a behavioral change here (like the transport guard) lands in
|
|
14
14
|
* both execution paths by construction.
|
|
15
|
+
*
|
|
16
|
+
* The one platform key filled here is STIGMER_SERVER_ADDRESS
|
|
17
|
+
* (platform-server-address.ts), before the declared-key filter so the filter
|
|
18
|
+
* sees the key present; discovery applies the same fill itself because it
|
|
19
|
+
* resolves a single server without a usage.
|
|
15
20
|
*/
|
|
16
21
|
|
|
17
22
|
import type { McpServerUsage, ToolApprovalOverride } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
|
|
@@ -30,6 +35,7 @@ import {
|
|
|
30
35
|
PlaceholderResolutionError,
|
|
31
36
|
} from "./placeholder-resolver.js";
|
|
32
37
|
import { effectiveEnabledTools } from "./mcp-enabled-tools.js";
|
|
38
|
+
import { fillPlatformServerAddress, type PlatformEndpoints } from "./platform-server-address.js";
|
|
33
39
|
|
|
34
40
|
/**
|
|
35
41
|
* Harness-agnostic intermediate representation of a resolved MCP server.
|
|
@@ -86,12 +92,16 @@ export interface McpResolutionResult {
|
|
|
86
92
|
* resolveMcpTransportPosture(config.mode)). A stdio server under a
|
|
87
93
|
* forbidding posture throws {@link McpTransportError} and fails the
|
|
88
94
|
* whole resolution — never degraded to a skipped server.
|
|
95
|
+
* @param platformEndpoints The runner's endpoints a missing
|
|
96
|
+
* STIGMER_SERVER_ADDRESS is filled from (the runner Config satisfies
|
|
97
|
+
* it). Required so no resolution path can forget the fill.
|
|
89
98
|
*/
|
|
90
99
|
export async function resolveMcpServers(
|
|
91
100
|
client: StigmerClient,
|
|
92
101
|
usages: McpServerUsage[],
|
|
93
102
|
envVars: Record<string, string>,
|
|
94
103
|
transportPosture: McpTransportPosture,
|
|
104
|
+
platformEndpoints: PlatformEndpoints,
|
|
95
105
|
): Promise<McpResolutionResult> {
|
|
96
106
|
const resolved: ResolvedMcpServer[] = [];
|
|
97
107
|
|
|
@@ -103,7 +113,7 @@ export async function resolveMcpServers(
|
|
|
103
113
|
const mcpServer = await client.getMcpServerByReference(ref);
|
|
104
114
|
const serverEnv = filterEnvToDeclaredKeys(
|
|
105
115
|
mcpServer.spec?.env,
|
|
106
|
-
envVars,
|
|
116
|
+
fillPlatformServerAddress(mcpServer, envVars, platformEndpoints),
|
|
107
117
|
ref.slug,
|
|
108
118
|
);
|
|
109
119
|
const server = mcpServerToResolved(
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
|
|
52
52
|
import type { RecalledMemories } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/spec_pb";
|
|
53
53
|
import type { ResolvedMcpServer } from "./mcp-resolver.js";
|
|
54
|
+
import { grpcTarget, SERVER_ADDRESS_ENV_KEY } from "./platform-server-address.js";
|
|
54
55
|
import {
|
|
55
|
-
grpcTarget,
|
|
56
56
|
stdioCredentialEnv,
|
|
57
57
|
type SynthesizedAttachmentOptions,
|
|
58
58
|
} from "./synthesized-attachment.js";
|
|
@@ -159,7 +159,7 @@ export function synthesizeMemoryAttachment(
|
|
|
159
159
|
args: ["mcp-server"],
|
|
160
160
|
env: {
|
|
161
161
|
STIGMER_MCP_ROSTER: "memory",
|
|
162
|
-
|
|
162
|
+
[SERVER_ADDRESS_ENV_KEY]: grpcTarget(options.backendEndpoint),
|
|
163
163
|
...stdioCredentialEnv(options.credential),
|
|
164
164
|
...nonEmptyEntries([
|
|
165
165
|
[MEMORY_ORG_ENV, context.org],
|
|
@@ -19,6 +19,14 @@
|
|
|
19
19
|
* (BYO-key) mode, attributes billing errors to the user's own account
|
|
20
20
|
* with actionable wording.
|
|
21
21
|
*
|
|
22
|
+
* 3. On Stigmer Cloud an organization may bring its own provider key, which
|
|
23
|
+
* the proxy injects upstream in place of the platform's. A failure on
|
|
24
|
+
* that key is the organization's, not the platform's, yet it reaches the
|
|
25
|
+
* runner in proxy mode. The proxy keeps the provider's status and message
|
|
26
|
+
* and marks the message with ORGANIZATION_PROVIDER_KEY_SENTINEL; this
|
|
27
|
+
* module words a marked failure as the organization's key, never as a
|
|
28
|
+
* platform fault.
|
|
29
|
+
*
|
|
22
30
|
* Classification order is load-bearing: the sentinel check must precede
|
|
23
31
|
* status mapping, otherwise the proxy's 503 would classify as a retryable
|
|
24
32
|
* 5xx and waste Temporal retries against a dead platform account.
|
|
@@ -43,6 +51,14 @@ import {
|
|
|
43
51
|
*/
|
|
44
52
|
export const PLATFORM_CAPACITY_SENTINEL = "STIGMER_PLATFORM_MODEL_CAPACITY";
|
|
45
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Machine-readable code the cloud proxy embeds in a provider failure it
|
|
56
|
+
* relays from a call served on the organization's own provider key. The
|
|
57
|
+
* message text carries it, as it carries PLATFORM_CAPACITY_SENTINEL.
|
|
58
|
+
* Duplicated in stigmer-cloud's LLM proxy lane; change both or neither.
|
|
59
|
+
*/
|
|
60
|
+
export const ORGANIZATION_PROVIDER_KEY_SENTINEL = "STIGMER_ORGANIZATION_PROVIDER_KEY";
|
|
61
|
+
|
|
46
62
|
export interface ClassifiedModelError {
|
|
47
63
|
/** Stable machine-readable code (doubles as the Temporal failure type). */
|
|
48
64
|
readonly code: string;
|
|
@@ -72,17 +88,22 @@ export interface ModelErrorContext {
|
|
|
72
88
|
|
|
73
89
|
/**
|
|
74
90
|
* Walk the `cause` chain to the root error. LangChain's MiddlewareError (and
|
|
75
|
-
* anything else that chains causes) preserves the original SDK error there
|
|
76
|
-
*
|
|
91
|
+
* anything else that chains causes) preserves the original SDK error there,
|
|
92
|
+
* and the root is the one link carrying the provider's `.status`.
|
|
93
|
+
*
|
|
94
|
+
* The chain is as long as the agent's middleware stack: LangChain wraps a
|
|
95
|
+
* model call's failure once per middleware that implements `wrapModelCall`
|
|
96
|
+
* (`MiddlewareError.wrap` in its AgentNode), and a native turn stacks
|
|
97
|
+
* deepagents' own middleware, the to-do list and Stigmer's, well past ten
|
|
98
|
+
* layers. So the walk has no depth bound; a cause cycle ends it instead, at
|
|
99
|
+
* the first error seen twice.
|
|
77
100
|
*/
|
|
78
101
|
export function unwrapModelError(err: unknown): unknown {
|
|
102
|
+
const seen = new Set<unknown>([err]);
|
|
79
103
|
let current = err;
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
} else {
|
|
84
|
-
return current;
|
|
85
|
-
}
|
|
104
|
+
while (current instanceof Error && current.cause instanceof Error && !seen.has(current.cause)) {
|
|
105
|
+
current = current.cause;
|
|
106
|
+
seen.add(current);
|
|
86
107
|
}
|
|
87
108
|
return current;
|
|
88
109
|
}
|
|
@@ -99,10 +120,21 @@ export function unwrapModelError(err: unknown): unknown {
|
|
|
99
120
|
*/
|
|
100
121
|
export function classifyModelCallError(
|
|
101
122
|
err: unknown,
|
|
102
|
-
|
|
123
|
+
callerCtx: ModelErrorContext,
|
|
103
124
|
): ClassifiedModelError | undefined {
|
|
104
125
|
const root = unwrapModelError(err);
|
|
105
126
|
const message = root instanceof Error ? root.message : String(root);
|
|
127
|
+
|
|
128
|
+
// 0. Organization-key sentinel — before everything that reads proxyMode:
|
|
129
|
+
// the call went through the proxy but on the organization's own key,
|
|
130
|
+
// so its failures are the organization's. The key-specific failures
|
|
131
|
+
// get key-specific wording; the rest classify as direct mode would.
|
|
132
|
+
const organizationKey = message.includes(ORGANIZATION_PROVIDER_KEY_SENTINEL);
|
|
133
|
+
if (organizationKey) {
|
|
134
|
+
const classified = classifyOrganizationKeyError(root, message, callerCtx);
|
|
135
|
+
if (classified !== undefined) return classified;
|
|
136
|
+
}
|
|
137
|
+
const ctx = organizationKey ? { ...callerCtx, proxyMode: false } : callerCtx;
|
|
106
138
|
const backend = resolveDirectBackend(ctx);
|
|
107
139
|
|
|
108
140
|
// 1. Platform sentinel — before status mapping (see module doc).
|
|
@@ -235,6 +267,52 @@ export function classifyModelCallError(
|
|
|
235
267
|
return undefined;
|
|
236
268
|
}
|
|
237
269
|
|
|
270
|
+
/**
|
|
271
|
+
* The failures an organization's own provider key causes, worded for the
|
|
272
|
+
* organization's admin: out of credit or quota at the provider, the key
|
|
273
|
+
* rejected, or the key's account not permitted the model. Undefined for
|
|
274
|
+
* every other failure, which classifies as direct mode would.
|
|
275
|
+
*/
|
|
276
|
+
function classifyOrganizationKeyError(
|
|
277
|
+
root: unknown,
|
|
278
|
+
message: string,
|
|
279
|
+
ctx: ModelErrorContext,
|
|
280
|
+
): ClassifiedModelError | undefined {
|
|
281
|
+
const provider = providerSubject(ctx);
|
|
282
|
+
if (isProviderBillingMessage(message)) {
|
|
283
|
+
return {
|
|
284
|
+
code: "LLM_PROVIDER_BILLING",
|
|
285
|
+
retryable: false,
|
|
286
|
+
message:
|
|
287
|
+
`Your organization's own ${providerLabel(ctx)} key is out of credits or over quota. ` +
|
|
288
|
+
`Add credits to that provider account, or update the key in Settings → Provider keys. ` +
|
|
289
|
+
`Provider message: ${message}`,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
const status = typeof (root as { status?: unknown }).status === "number"
|
|
293
|
+
? (root as { status: number }).status
|
|
294
|
+
: undefined;
|
|
295
|
+
if (status === 401) {
|
|
296
|
+
return {
|
|
297
|
+
code: "LLM_AUTHENTICATION_ERROR",
|
|
298
|
+
retryable: false,
|
|
299
|
+
message:
|
|
300
|
+
`${provider} rejected your organization's own key (authentication, HTTP 401) for ${modelLabel(ctx)}. ` +
|
|
301
|
+
`Update the key in Settings → Provider keys.`,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
if (status === 403) {
|
|
305
|
+
return {
|
|
306
|
+
code: "LLM_PERMISSION_DENIED",
|
|
307
|
+
retryable: false,
|
|
308
|
+
message:
|
|
309
|
+
`${provider} denied your organization's own key (authorization, HTTP 403) for ${modelLabel(ctx)}. ` +
|
|
310
|
+
`Check that the key's account may use this model, or update the key in Settings → Provider keys.`,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
return undefined;
|
|
314
|
+
}
|
|
315
|
+
|
|
238
316
|
/**
|
|
239
317
|
* Retryability policy (unchanged from the original call-llm classifier):
|
|
240
318
|
* - 4xx (except 429): nonRetryable — client/config errors won't self-heal
|