@stigmer/runner 3.9.0 → 3.11.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 +12 -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 +32 -0
- package/dist/activities/discover-mcp-server.js +162 -27
- package/dist/activities/discover-mcp-server.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 +25 -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 +40 -3
- package/dist/activities/execute-cursor/error-classifier.js +81 -3
- 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 +5 -1
- package/dist/activities/execute-cursor/index.js +59 -58
- 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/prompt-builder.d.ts +18 -4
- package/dist/activities/execute-cursor/prompt-builder.js +12 -7
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- 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 -26
- package/dist/activities/execute-deep-agent/setup.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 +8 -1
- package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +11 -4
- package/dist/activities/execute-deep-agent/subagent-wiring.js +13 -4
- package/dist/activities/execute-deep-agent/subagent-wiring.js.map +1 -1
- package/dist/activities/hydrate-workflow-execution.js +3 -1
- package/dist/activities/hydrate-workflow-execution.js.map +1 -1
- 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/claimcheck/payload-codec.js +21 -1
- package/dist/claimcheck/payload-codec.js.map +1 -1
- package/dist/client/stigmer-client.d.ts +9 -4
- package/dist/client/stigmer-client.js +28 -15
- package/dist/client/stigmer-client.js.map +1 -1
- package/dist/encryption/config.d.ts +32 -0
- package/dist/encryption/config.js +68 -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/payload-codecs.d.ts +16 -0
- package/dist/payload-codecs.js +38 -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 +5 -15
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.js +5 -16
- 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/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/attachment-vision.d.ts +43 -2
- package/dist/shared/attachment-vision.js +72 -6
- package/dist/shared/attachment-vision.js.map +1 -1
- 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 +42 -1
- package/dist/shared/checkpointer/http-saver.js +96 -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/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/http-retry.d.ts +43 -0
- package/dist/shared/http-retry.js +50 -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 +17 -2
- package/dist/shared/mcp-manager.js +37 -25
- 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/model-client.d.ts +12 -5
- package/dist/shared/model-client.js +138 -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-registry.d.ts +20 -2
- package/dist/shared/model-registry.js +37 -2
- package/dist/shared/model-registry.js.map +1 -1
- package/dist/shared/plan-mode-permissions.d.ts +26 -0
- package/dist/shared/plan-mode-permissions.js +28 -0
- package/dist/shared/plan-mode-permissions.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/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 +7 -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/package.json +8 -2
- package/src/__tests__/claimcheck-codec.test.ts +36 -0
- package/src/__tests__/encryption-codec.test.ts +234 -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 +50 -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 +203 -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 +190 -32
- 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 +92 -0
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +40 -14
- 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__/prompt-builder-delegation.test.ts +1 -1
- 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 +38 -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 +91 -4
- 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 +63 -72
- package/src/activities/execute-cursor/mcp-resolver.ts +36 -2
- package/src/activities/execute-cursor/prompt-builder.ts +34 -9
- package/src/activities/execute-cursor/turn-stream.ts +5 -2
- package/src/activities/execute-deep-agent/__tests__/attachment-injector.test.ts +132 -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__/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__/subagent-plan-mode-permissions.test.ts +173 -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 +86 -31
- package/src/activities/execute-deep-agent/subagent-transformer.ts +23 -1
- package/src/activities/execute-deep-agent/subagent-wiring.ts +14 -4
- package/src/activities/hydrate-workflow-execution.ts +3 -1
- package/src/activities/workflow-event-activities.ts +96 -69
- package/src/claimcheck/payload-codec.ts +33 -1
- package/src/client/__tests__/stigmer-client.test.ts +8 -8
- package/src/client/stigmer-client.ts +32 -18
- package/src/encryption/config.ts +91 -0
- package/src/encryption/index.ts +3 -0
- package/src/encryption/payload-codec.ts +152 -0
- package/src/payload-codecs.ts +56 -0
- package/src/preflight.ts +45 -0
- package/src/runner-manager.ts +6 -24
- package/src/runner.ts +6 -25
- package/src/shared/__tests__/approval-policy.test.ts +82 -39
- package/src/shared/__tests__/attachment-naming.test.ts +159 -0
- package/src/shared/__tests__/attachment-vision.test.ts +97 -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__/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 +67 -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 +170 -2
- package/src/shared/__tests__/mcp-resolver.test.ts +146 -3
- package/src/shared/__tests__/model-client.test.ts +154 -0
- package/src/shared/__tests__/model-error.test.ts +289 -1
- package/src/shared/__tests__/model-registry.test.ts +71 -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/attachment-naming.ts +78 -0
- package/src/shared/attachment-vision.ts +92 -9
- 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 +117 -9
- package/src/shared/conversation-attachment.ts +1 -0
- package/src/shared/datastore-attachment.ts +106 -11
- package/src/shared/http-retry.ts +50 -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 +43 -26
- package/src/shared/mcp-resolver.ts +73 -2
- package/src/shared/model-client.ts +161 -19
- package/src/shared/model-error.ts +222 -4
- package/src/shared/model-registry.ts +50 -2
- package/src/shared/plan-mode-permissions.ts +30 -0
- package/src/worker.ts +4 -5
- package/src/workflow-engine/types.ts +18 -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 +7 -0
- package/src/workflows/engine-core.ts +23 -2
- package/src/workflows/execute-from-execution.ts +12 -2
|
@@ -29,9 +29,17 @@ async function serializeForProxy(obj: unknown): Promise<{ type: string; binary:
|
|
|
29
29
|
describe("HttpCheckpointSaver", () => {
|
|
30
30
|
let saver: HttpCheckpointSaver;
|
|
31
31
|
let fetchSpy: ReturnType<typeof vi.fn>;
|
|
32
|
+
let recordedDelays: number[];
|
|
32
33
|
|
|
33
34
|
beforeEach(() => {
|
|
34
|
-
|
|
35
|
+
// delayFn injected so retry-path tests record the backoff schedule
|
|
36
|
+
// instead of sleeping through it (~7.75 s of real delays otherwise).
|
|
37
|
+
recordedDelays = [];
|
|
38
|
+
saver = new HttpCheckpointSaver(PROXY, TOKEN, {
|
|
39
|
+
delayFn: async (ms) => {
|
|
40
|
+
recordedDelays.push(ms);
|
|
41
|
+
},
|
|
42
|
+
});
|
|
35
43
|
fetchSpy = vi.fn();
|
|
36
44
|
vi.stubGlobal("fetch", fetchSpy);
|
|
37
45
|
});
|
|
@@ -163,6 +171,193 @@ describe("HttpCheckpointSaver", () => {
|
|
|
163
171
|
});
|
|
164
172
|
});
|
|
165
173
|
|
|
174
|
+
// The bounded-backoff loop (cloud#188). Classification policy itself is
|
|
175
|
+
// covered by shared/__tests__/http-retry.test.ts; these cases pin the
|
|
176
|
+
// loop's behavior at the saver's call sites: transient failures recover,
|
|
177
|
+
// deterministic failures never retry, budget exhaustion still fails
|
|
178
|
+
// loudly, and 404 semantics survive unchanged.
|
|
179
|
+
describe("retry behavior", () => {
|
|
180
|
+
const ok404 = { status: 404, ok: false, statusText: "Not Found" };
|
|
181
|
+
const err503 = { status: 503, ok: false, statusText: "Service Unavailable" };
|
|
182
|
+
const err400 = { status: 400, ok: false, statusText: "Bad Request" };
|
|
183
|
+
const err401 = { status: 401, ok: false, statusText: "Unauthorized" };
|
|
184
|
+
|
|
185
|
+
function makeCheckpoint(): Checkpoint {
|
|
186
|
+
return {
|
|
187
|
+
v: 4,
|
|
188
|
+
id: "cp-retry",
|
|
189
|
+
ts: new Date().toISOString(),
|
|
190
|
+
channel_values: {},
|
|
191
|
+
channel_versions: {},
|
|
192
|
+
versions_seen: {},
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
const md: CheckpointMetadata = { source: "loop", step: 0, parents: {} };
|
|
196
|
+
|
|
197
|
+
it("recovers a put from a transient 503", async () => {
|
|
198
|
+
fetchSpy
|
|
199
|
+
.mockResolvedValueOnce(err503)
|
|
200
|
+
.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({}) });
|
|
201
|
+
|
|
202
|
+
const result = await saver.put(makeConfig(), makeCheckpoint(), md, {});
|
|
203
|
+
expect(result.configurable?.checkpoint_id).toBe("cp-retry");
|
|
204
|
+
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
|
205
|
+
expect(recordedDelays).toEqual([250]);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("recovers a put from a network-level rejection (undici TypeError)", async () => {
|
|
209
|
+
fetchSpy
|
|
210
|
+
.mockRejectedValueOnce(new TypeError("fetch failed"))
|
|
211
|
+
.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({}) });
|
|
212
|
+
|
|
213
|
+
await expect(saver.put(makeConfig(), makeCheckpoint(), md, {})).resolves.toBeDefined();
|
|
214
|
+
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("backs off exponentially across consecutive transient failures", async () => {
|
|
218
|
+
fetchSpy
|
|
219
|
+
.mockResolvedValueOnce(err503)
|
|
220
|
+
.mockResolvedValueOnce(err503)
|
|
221
|
+
.mockResolvedValueOnce(err503)
|
|
222
|
+
.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({}) });
|
|
223
|
+
|
|
224
|
+
await saver.put(makeConfig(), makeCheckpoint(), md, {});
|
|
225
|
+
expect(recordedDelays).toEqual([250, 500, 1000]);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("fails loudly when the budget is exhausted on a retryable status", async () => {
|
|
229
|
+
fetchSpy.mockResolvedValue(err503);
|
|
230
|
+
|
|
231
|
+
await expect(saver.put(makeConfig(), makeCheckpoint(), md, {}))
|
|
232
|
+
.rejects.toThrow("Checkpoint PUT failed: 503");
|
|
233
|
+
// 1 initial attempt + 5 retries (the default budget).
|
|
234
|
+
expect(fetchSpy).toHaveBeenCalledTimes(6);
|
|
235
|
+
expect(recordedDelays).toEqual([250, 500, 1000, 2000, 4000]);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("rethrows the original error when the budget is exhausted on network rejections", async () => {
|
|
239
|
+
const netErr = new TypeError("fetch failed");
|
|
240
|
+
fetchSpy.mockRejectedValue(netErr);
|
|
241
|
+
|
|
242
|
+
await expect(saver.putWrites(makeConfig(), [["messages", { a: 1 }]], "t1"))
|
|
243
|
+
.rejects.toBe(netErr);
|
|
244
|
+
expect(fetchSpy).toHaveBeenCalledTimes(6);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("never retries a deterministic 400", async () => {
|
|
248
|
+
fetchSpy.mockResolvedValue(err400);
|
|
249
|
+
|
|
250
|
+
await expect(saver.put(makeConfig(), makeCheckpoint(), md, {}))
|
|
251
|
+
.rejects.toThrow("Checkpoint PUT failed: 400");
|
|
252
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
253
|
+
expect(recordedDelays).toEqual([]);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("never retries a rejected credential (401)", async () => {
|
|
257
|
+
fetchSpy.mockResolvedValue(err401);
|
|
258
|
+
|
|
259
|
+
await expect(saver.getTuple(makeConfig())).rejects.toThrow("Checkpoint GET failed: 401");
|
|
260
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("never retries a manual abort (AbortError)", async () => {
|
|
264
|
+
const abortErr = new DOMException("This operation was aborted", "AbortError");
|
|
265
|
+
fetchSpy.mockRejectedValue(abortErr);
|
|
266
|
+
|
|
267
|
+
await expect(saver.getTuple(makeConfig())).rejects.toBe(abortErr);
|
|
268
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("retries a hung request aborted by the per-request timeout (TimeoutError)", async () => {
|
|
272
|
+
fetchSpy
|
|
273
|
+
.mockRejectedValueOnce(
|
|
274
|
+
new DOMException("The operation was aborted due to timeout", "TimeoutError"),
|
|
275
|
+
)
|
|
276
|
+
.mockResolvedValueOnce(ok404);
|
|
277
|
+
|
|
278
|
+
await expect(saver.getTuple(makeConfig())).resolves.toBeUndefined();
|
|
279
|
+
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("preserves getTuple's 404 semantics without retrying", async () => {
|
|
283
|
+
fetchSpy.mockResolvedValue(ok404);
|
|
284
|
+
|
|
285
|
+
await expect(saver.getTuple(makeConfig())).resolves.toBeUndefined();
|
|
286
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("preserves deleteThread's 404 tolerance without retrying", async () => {
|
|
290
|
+
fetchSpy.mockResolvedValue(ok404);
|
|
291
|
+
|
|
292
|
+
await expect(saver.deleteThread("thread-1")).resolves.toBeUndefined();
|
|
293
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("bounds every request with an abort signal", async () => {
|
|
297
|
+
fetchSpy.mockResolvedValue(ok404);
|
|
298
|
+
|
|
299
|
+
await saver.getTuple(makeConfig());
|
|
300
|
+
const [, opts] = fetchSpy.mock.calls[0];
|
|
301
|
+
expect(opts.signal).toBeInstanceOf(AbortSignal);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("surfaces a pending-writes read failure instead of silently degrading", async () => {
|
|
305
|
+
// The checkpoint GET succeeds; the follow-up writes GET fails on every
|
|
306
|
+
// attempt. Pre-#188 code mapped this to "no pending writes" and
|
|
307
|
+
// returned a tuple stripped of resume state.
|
|
308
|
+
const cp = makeCheckpoint();
|
|
309
|
+
const cpSer = await serializeForProxy(cp);
|
|
310
|
+
const mdSer = await serializeForProxy(md);
|
|
311
|
+
const checkpointDoc = {
|
|
312
|
+
thread_id: "thread-1",
|
|
313
|
+
checkpoint_ns: "",
|
|
314
|
+
checkpoint_id: cp.id,
|
|
315
|
+
type: cpSer.type,
|
|
316
|
+
checkpoint: cpSer.binary,
|
|
317
|
+
metadata_type: mdSer.type,
|
|
318
|
+
metadata: mdSer.binary,
|
|
319
|
+
};
|
|
320
|
+
fetchSpy.mockImplementation((url: string) => {
|
|
321
|
+
if (url.includes("/writes")) return Promise.resolve(err503);
|
|
322
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(checkpointDoc) });
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
await expect(saver.getTuple(makeConfig())).rejects.toThrow(
|
|
326
|
+
"Checkpoint writes GET failed: 503",
|
|
327
|
+
);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("recovers the pending-writes read from a transient failure", async () => {
|
|
331
|
+
const cp = makeCheckpoint();
|
|
332
|
+
const cpSer = await serializeForProxy(cp);
|
|
333
|
+
const mdSer = await serializeForProxy(md);
|
|
334
|
+
const checkpointDoc = {
|
|
335
|
+
thread_id: "thread-1",
|
|
336
|
+
checkpoint_ns: "",
|
|
337
|
+
checkpoint_id: cp.id,
|
|
338
|
+
type: cpSer.type,
|
|
339
|
+
checkpoint: cpSer.binary,
|
|
340
|
+
metadata_type: mdSer.type,
|
|
341
|
+
metadata: mdSer.binary,
|
|
342
|
+
};
|
|
343
|
+
let writesCalls = 0;
|
|
344
|
+
fetchSpy.mockImplementation((url: string) => {
|
|
345
|
+
if (url.includes("/writes")) {
|
|
346
|
+
writesCalls++;
|
|
347
|
+
return writesCalls === 1
|
|
348
|
+
? Promise.resolve(err503)
|
|
349
|
+
: Promise.resolve({ ok: true, json: () => Promise.resolve({ writes: [] }) });
|
|
350
|
+
}
|
|
351
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(checkpointDoc) });
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
const tuple = await saver.getTuple(makeConfig());
|
|
355
|
+
expect(tuple?.checkpoint.id).toBe(cp.id);
|
|
356
|
+
expect(tuple?.pendingWrites).toEqual([]);
|
|
357
|
+
expect(writesCalls).toBe(2);
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
166
361
|
describe("URL construction", () => {
|
|
167
362
|
it("strips trailing slashes from proxy endpoint", () => {
|
|
168
363
|
const s = new HttpCheckpointSaver("https://proxy.test///", "tok");
|
|
@@ -13,6 +13,19 @@
|
|
|
13
13
|
*
|
|
14
14
|
* The Java proxy calls Document.parse(json) which handles $binary
|
|
15
15
|
* natively, and doc.toJson() emits the same format on reads.
|
|
16
|
+
*
|
|
17
|
+
* Every request runs through {@link HttpCheckpointSaver.fetchWithRetry}:
|
|
18
|
+
* bounded exponential backoff on transient failures (classified by
|
|
19
|
+
* http-retry.ts) with a per-request abort timeout. This loop is the ONLY
|
|
20
|
+
* retry layer between an agent execution and its checkpoints — the deep
|
|
21
|
+
* agent activity is deliberately non-retryable at the Temporal level
|
|
22
|
+
* (maximumAttempts: 1; replaying a whole agent run is not safe), so before
|
|
23
|
+
* this existed a single dropped request killed the execution
|
|
24
|
+
* (stigmer/stigmer-cloud#188). Retrying puts is safe by server contract:
|
|
25
|
+
* CheckpointStore documents putCheckpoint/putWrite as keyed save-or-replace
|
|
26
|
+
* upserts. Budget exhaustion still fails loudly — checkpoints are not
|
|
27
|
+
* lossy-tolerable (unlike status updates, see status.ts), and silently
|
|
28
|
+
* dropping one would corrupt resume state.
|
|
16
29
|
*/
|
|
17
30
|
|
|
18
31
|
import type { RunnableConfig } from "@langchain/core/runnables";
|
|
@@ -25,6 +38,8 @@ import {
|
|
|
25
38
|
type ChannelVersions,
|
|
26
39
|
} from "@langchain/langgraph-checkpoint";
|
|
27
40
|
import type { CheckpointMetadata, PendingWrite } from "@langchain/langgraph-checkpoint";
|
|
41
|
+
import type { RetryOptions } from "../grpc-retry.js";
|
|
42
|
+
import { isRetryableFetchError, isRetryableHttpStatus } from "../http-retry.js";
|
|
28
43
|
|
|
29
44
|
function encodeB64(data: Uint8Array): string {
|
|
30
45
|
if (typeof Buffer !== "undefined") {
|
|
@@ -77,17 +92,104 @@ function configOrg(config: RunnableConfig): string | undefined {
|
|
|
77
92
|
return config.configurable?.org as string | undefined;
|
|
78
93
|
}
|
|
79
94
|
|
|
95
|
+
/** Retry policy plus the per-request bound; every field is defaulted. */
|
|
96
|
+
export interface HttpCheckpointSaverOptions extends RetryOptions {
|
|
97
|
+
/**
|
|
98
|
+
* Milliseconds before an in-flight request is aborted and the attempt is
|
|
99
|
+
* classified retryable. Without it a hung connection (the realistic
|
|
100
|
+
* unplanned-pod-kill symptom) stalls forever and the retry never engages.
|
|
101
|
+
*/
|
|
102
|
+
readonly requestTimeoutMs?: number;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Default transient-retry policy. Wider than status.ts's DEFAULT_PERSIST_RETRY
|
|
107
|
+
* (3 retries, ~700 ms) on purpose: a dropped status update is lossy-tolerable,
|
|
108
|
+
* a dropped checkpoint kills the execution, so the budget here is sized to
|
|
109
|
+
* span a pod-restart reroute (~7.75 s of delays across 5 retries). Worst case
|
|
110
|
+
* with every attempt hanging — 6 x 30 s + delays, ~3.7 min — stays well inside
|
|
111
|
+
* the deep-agent activity's 1 h startToCloseTimeout (no heartbeatTimeout).
|
|
112
|
+
*/
|
|
113
|
+
const DEFAULT_RETRY = {
|
|
114
|
+
baseDelayMs: 250,
|
|
115
|
+
backoffFactor: 2,
|
|
116
|
+
maxRetries: 5,
|
|
117
|
+
requestTimeoutMs: 30_000,
|
|
118
|
+
} as const;
|
|
119
|
+
|
|
120
|
+
function defaultDelay(ms: number): Promise<void> {
|
|
121
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
122
|
+
}
|
|
123
|
+
|
|
80
124
|
export class HttpCheckpointSaver extends BaseCheckpointSaver {
|
|
81
125
|
private readonly baseUrl: string;
|
|
82
126
|
private readonly headers: Record<string, string>;
|
|
83
|
-
|
|
84
|
-
|
|
127
|
+
private readonly baseDelayMs: number;
|
|
128
|
+
private readonly backoffFactor: number;
|
|
129
|
+
private readonly maxRetries: number;
|
|
130
|
+
private readonly requestTimeoutMs: number;
|
|
131
|
+
private readonly delay: (ms: number) => Promise<void>;
|
|
132
|
+
|
|
133
|
+
constructor(
|
|
134
|
+
proxyEndpoint: string,
|
|
135
|
+
authToken: string,
|
|
136
|
+
options: HttpCheckpointSaverOptions = {},
|
|
137
|
+
) {
|
|
85
138
|
super();
|
|
86
139
|
this.baseUrl = `${proxyEndpoint.replace(/\/+$/, "")}/v1/proxy/checkpoints`;
|
|
87
140
|
this.headers = {
|
|
88
141
|
Authorization: `Bearer ${authToken}`,
|
|
89
142
|
"Content-Type": "application/json",
|
|
90
143
|
};
|
|
144
|
+
this.baseDelayMs = options.baseDelayMs ?? DEFAULT_RETRY.baseDelayMs;
|
|
145
|
+
this.backoffFactor = options.backoffFactor ?? DEFAULT_RETRY.backoffFactor;
|
|
146
|
+
this.maxRetries = options.maxRetries ?? DEFAULT_RETRY.maxRetries;
|
|
147
|
+
this.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_RETRY.requestTimeoutMs;
|
|
148
|
+
this.delay = options.delayFn ?? defaultDelay;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* `fetch` with bounded exponential backoff on transient failures and a
|
|
153
|
+
* per-request abort timeout.
|
|
154
|
+
*
|
|
155
|
+
* Composition contract: on a non-retryable status — or when the budget is
|
|
156
|
+
* exhausted on a retryable one — the last `Response` is RETURNED, so every
|
|
157
|
+
* call site keeps its own `resp.ok` / 404 handling unchanged; this wrapper
|
|
158
|
+
* changes when a response arrives, never what call sites do with it. It
|
|
159
|
+
* throws only what `fetch` itself throws: a network-level rejection that is
|
|
160
|
+
* non-retryable or has exhausted the budget.
|
|
161
|
+
*/
|
|
162
|
+
private async fetchWithRetry(url: string, init?: RequestInit): Promise<Response> {
|
|
163
|
+
for (let attempt = 0; ; attempt++) {
|
|
164
|
+
const canRetry = attempt < this.maxRetries;
|
|
165
|
+
let resp: Response;
|
|
166
|
+
try {
|
|
167
|
+
resp = await fetch(url, {
|
|
168
|
+
...init,
|
|
169
|
+
signal: AbortSignal.timeout(this.requestTimeoutMs),
|
|
170
|
+
});
|
|
171
|
+
} catch (err) {
|
|
172
|
+
if (canRetry && isRetryableFetchError(err)) {
|
|
173
|
+
await this.backOff(attempt, String(err));
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
throw err;
|
|
177
|
+
}
|
|
178
|
+
if (canRetry && isRetryableHttpStatus(resp.status)) {
|
|
179
|
+
await this.backOff(attempt, `HTTP ${resp.status} ${resp.statusText}`);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
return resp;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private async backOff(attempt: number, reason: string): Promise<void> {
|
|
187
|
+
const delayMs = this.baseDelayMs * Math.pow(this.backoffFactor, attempt);
|
|
188
|
+
console.warn(
|
|
189
|
+
`[HttpCheckpointSaver] retryable failure ` +
|
|
190
|
+
`(attempt ${attempt + 1}/${this.maxRetries + 1}, retry in ${delayMs}ms): ${reason}`,
|
|
191
|
+
);
|
|
192
|
+
await this.delay(delayMs);
|
|
91
193
|
}
|
|
92
194
|
|
|
93
195
|
private async serializeTyped(obj: unknown): Promise<[string, BinaryObj]> {
|
|
@@ -113,7 +215,7 @@ export class HttpCheckpointSaver extends BaseCheckpointSaver {
|
|
|
113
215
|
params.set("checkpoint_id", checkpointId);
|
|
114
216
|
}
|
|
115
217
|
|
|
116
|
-
const resp = await
|
|
218
|
+
const resp = await this.fetchWithRetry(`${this.baseUrl}/checkpoint?${params}`, {
|
|
117
219
|
headers: this.headers,
|
|
118
220
|
});
|
|
119
221
|
if (resp.status === 404) return undefined;
|
|
@@ -143,7 +245,7 @@ export class HttpCheckpointSaver extends BaseCheckpointSaver {
|
|
|
143
245
|
params.set("before", beforeId);
|
|
144
246
|
}
|
|
145
247
|
|
|
146
|
-
const resp = await
|
|
248
|
+
const resp = await this.fetchWithRetry(`${this.baseUrl}/checkpoints?${params}`, {
|
|
147
249
|
headers: this.headers,
|
|
148
250
|
});
|
|
149
251
|
if (!resp.ok) {
|
|
@@ -183,7 +285,7 @@ export class HttpCheckpointSaver extends BaseCheckpointSaver {
|
|
|
183
285
|
const orgId = configOrg(config);
|
|
184
286
|
if (orgId) doc.org_id = orgId;
|
|
185
287
|
|
|
186
|
-
const resp = await
|
|
288
|
+
const resp = await this.fetchWithRetry(`${this.baseUrl}/checkpoint`, {
|
|
187
289
|
method: "PUT",
|
|
188
290
|
headers: this.headers,
|
|
189
291
|
body: JSON.stringify(doc),
|
|
@@ -203,7 +305,7 @@ export class HttpCheckpointSaver extends BaseCheckpointSaver {
|
|
|
203
305
|
|
|
204
306
|
async deleteThread(threadId: string): Promise<void> {
|
|
205
307
|
const params = new URLSearchParams({ thread_id: threadId });
|
|
206
|
-
const resp = await
|
|
308
|
+
const resp = await this.fetchWithRetry(`${this.baseUrl}/thread?${params}`, {
|
|
207
309
|
method: "DELETE",
|
|
208
310
|
headers: this.headers,
|
|
209
311
|
});
|
|
@@ -238,7 +340,7 @@ export class HttpCheckpointSaver extends BaseCheckpointSaver {
|
|
|
238
340
|
return doc;
|
|
239
341
|
}));
|
|
240
342
|
|
|
241
|
-
const resp = await
|
|
343
|
+
const resp = await this.fetchWithRetry(`${this.baseUrl}/writes`, {
|
|
242
344
|
method: "PUT",
|
|
243
345
|
headers: this.headers,
|
|
244
346
|
body: JSON.stringify({ writes: docs }),
|
|
@@ -272,7 +374,7 @@ export class HttpCheckpointSaver extends BaseCheckpointSaver {
|
|
|
272
374
|
};
|
|
273
375
|
}
|
|
274
376
|
|
|
275
|
-
const writesResp = await
|
|
377
|
+
const writesResp = await this.fetchWithRetry(
|
|
276
378
|
`${this.baseUrl}/writes?${new URLSearchParams({
|
|
277
379
|
thread_id: threadId,
|
|
278
380
|
checkpoint_ns: checkpointNs,
|
|
@@ -280,8 +382,14 @@ export class HttpCheckpointSaver extends BaseCheckpointSaver {
|
|
|
280
382
|
})}`,
|
|
281
383
|
{ headers: this.headers },
|
|
282
384
|
);
|
|
385
|
+
// Fail loudly, never degrade: this used to map ANY non-ok response to "no
|
|
386
|
+
// pending writes", which silently stripped resume state on a transient
|
|
387
|
+
// 500 — a checkpoint would load without the writes recorded against it.
|
|
388
|
+
if (!writesResp.ok) {
|
|
389
|
+
throw new Error(`Checkpoint writes GET failed: ${writesResp.status} ${writesResp.statusText}`);
|
|
390
|
+
}
|
|
283
391
|
const pendingWrites = await this.parseWrites(
|
|
284
|
-
|
|
392
|
+
(await writesResp.json()) as Record<string, any>,
|
|
285
393
|
);
|
|
286
394
|
|
|
287
395
|
return {
|
|
@@ -105,6 +105,7 @@ export function synthesizeConversationAttachment(
|
|
|
105
105
|
slug: CONVERSATION_ATTACHMENT_SLUG,
|
|
106
106
|
toolApprovals: [],
|
|
107
107
|
pinnedToolApprovals: [],
|
|
108
|
+
toolApprovalOverrides: [],
|
|
108
109
|
discoveredCapabilitiesEmpty: false,
|
|
109
110
|
connectionType: "http",
|
|
110
111
|
url: options.bridgeEndpoint.replace(/\/+$/, "") + CONVERSATION_ROUTE,
|
|
@@ -22,8 +22,12 @@
|
|
|
22
22
|
* OSS callers resolve as the local principal (T05 R2).
|
|
23
23
|
*
|
|
24
24
|
* Approval-free by construction (DD-001 SD-3): empty toolApprovals +
|
|
25
|
-
* pinnedToolApprovals mean mergeApprovalPolicies
|
|
26
|
-
* this server — zero classifier involvement
|
|
25
|
+
* pinnedToolApprovals + toolApprovalOverrides mean mergeApprovalPolicies
|
|
26
|
+
* emits no entries for this server — zero classifier involvement, and no
|
|
27
|
+
* proto surface can target it (the attachment has no McpServerUsage, and
|
|
28
|
+
* since issue #349 an agent's overrides are scoped to their own usage's
|
|
29
|
+
* server, so a same-named override elsewhere cannot reach this one).
|
|
30
|
+
* `discoveredCapabilitiesEmpty`
|
|
27
31
|
* is false and the attachment has no McpServerUsage, so the connect
|
|
28
32
|
* backfill (whose destructiveHint tightener would force-gate
|
|
29
33
|
* delete_record — silently skipped on channels under UNATTENDED mode)
|
|
@@ -63,6 +67,7 @@ export function synthesizeDatastoreAttachment(
|
|
|
63
67
|
slug: DATASTORE_ATTACHMENT_SLUG,
|
|
64
68
|
toolApprovals: [],
|
|
65
69
|
pinnedToolApprovals: [],
|
|
70
|
+
toolApprovalOverrides: [],
|
|
66
71
|
discoveredCapabilitiesEmpty: false,
|
|
67
72
|
};
|
|
68
73
|
|
|
@@ -90,25 +95,115 @@ export function synthesizeDatastoreAttachment(
|
|
|
90
95
|
}
|
|
91
96
|
|
|
92
97
|
/**
|
|
93
|
-
* The
|
|
94
|
-
*
|
|
95
|
-
* the
|
|
96
|
-
*
|
|
98
|
+
* The five record tools the records roster always serves (DD-005; the
|
|
99
|
+
* mcp-server's records roster registers all five unconditionally for the
|
|
100
|
+
* agent audience — only the `org` argument shape varies by audience, see
|
|
101
|
+
* mcp-server/src/domains/records/tools.ts). Because the roster never
|
|
102
|
+
* legitimately narrows, any of these missing from the connected toolset
|
|
103
|
+
* means the store is degraded (issue #325) — the prompt section and the
|
|
104
|
+
* reconciliation both derive from this one list so they cannot drift.
|
|
97
105
|
*/
|
|
98
|
-
export
|
|
99
|
-
|
|
106
|
+
export const EXPECTED_RECORD_TOOLS = [
|
|
107
|
+
"describe_datastore",
|
|
108
|
+
"find_records",
|
|
109
|
+
"insert_record",
|
|
110
|
+
"update_record",
|
|
111
|
+
"delete_record",
|
|
112
|
+
] as const;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Reconcile the record tools actually connected against the roster
|
|
116
|
+
* contract (issue #325). Returns the expected tools absent from
|
|
117
|
+
* `actualToolNames` — empty means healthy. Names are bare tool names
|
|
118
|
+
* exactly as reported by tools/list (the mcp-enabled-tools identity
|
|
119
|
+
* space); extraneous names are ignored.
|
|
120
|
+
*/
|
|
121
|
+
export function missingRecordTools(actualToolNames: Iterable<string>): string[] {
|
|
122
|
+
const present = new Set(actualToolNames);
|
|
123
|
+
return EXPECTED_RECORD_TOOLS.filter((name) => !present.has(name));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The operator-facing degradation notice (issue #325): pushed as a
|
|
128
|
+
* MESSAGE_SYSTEM row on the execution status so "declared N datastores,
|
|
129
|
+
* X/5 record tools connected" is visible without reading a transcript.
|
|
130
|
+
* Lives HERE with the rest of the datastore-attachment wording — the
|
|
131
|
+
* harness only threads it.
|
|
132
|
+
*/
|
|
133
|
+
export function formatDatastoreDegradationNotice(
|
|
134
|
+
declaredCount: number,
|
|
135
|
+
missing: readonly string[],
|
|
136
|
+
): string {
|
|
137
|
+
const connected = EXPECTED_RECORD_TOOLS.length - missing.length;
|
|
138
|
+
return (
|
|
139
|
+
`Datastore record tools unavailable this turn: declared ` +
|
|
140
|
+
`${declaredCount} datastore(s), ${connected}/${EXPECTED_RECORD_TOOLS.length} ` +
|
|
141
|
+
`record tools connected (missing: ${missing.join(", ")}). The agent has ` +
|
|
142
|
+
`been instructed to disclose this instead of answering from memory.`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The datastores prompt section (DD-005 SD-5, the skills-section
|
|
148
|
+
* precedent). Shared by both harnesses so the section text cannot drift
|
|
149
|
+
* (the sender-identity precedent). Two honest renderings (issue #325):
|
|
150
|
+
*
|
|
151
|
+
* - Healthy (`missingToolNames` empty or omitted — the Cursor harness
|
|
152
|
+
* always calls it this way: the Cursor SDK connects MCP itself, so
|
|
153
|
+
* that harness can never observe the live roster):
|
|
154
|
+
* `<available_datastores>` names the attached datastores, points the
|
|
155
|
+
* model at describe_datastore first, and carries a standing
|
|
156
|
+
* failure-disclosure instruction — the only mechanism that covers
|
|
157
|
+
* tools which connected but fail at call time (the WhatsApp-pilot
|
|
158
|
+
* outage shape).
|
|
159
|
+
* - Degraded (deep-agent only, from missingRecordTools against the
|
|
160
|
+
* connected roster): `<unavailable_datastores>` instead — the section
|
|
161
|
+
* must not promise tools the agent does not have. Names the declared
|
|
162
|
+
* datastores, states which record tools are missing, and instructs
|
|
163
|
+
* plain disclosure over improvisation.
|
|
164
|
+
*/
|
|
165
|
+
export function formatDatastoresSection(
|
|
166
|
+
datastoreUsages: DatastoreUsage[],
|
|
167
|
+
missingToolNames: readonly string[] = [],
|
|
168
|
+
): string {
|
|
169
|
+
const entries = datastoreUsages
|
|
100
170
|
.map((u) => u.datastoreRef?.slug)
|
|
101
|
-
.filter((s): s is string => !!s)
|
|
102
|
-
|
|
171
|
+
.filter((s): s is string => !!s)
|
|
172
|
+
.map((slug) => `- ${slug}`);
|
|
173
|
+
|
|
174
|
+
if (missingToolNames.length > 0) {
|
|
175
|
+
return [
|
|
176
|
+
"<unavailable_datastores>",
|
|
177
|
+
"This agent is configured to use the datastores listed below, but the",
|
|
178
|
+
"following record tools failed to connect this turn:",
|
|
179
|
+
missingToolNames.join(", ") + ".",
|
|
180
|
+
"Treat these datastores as unreachable for any operation that needs a",
|
|
181
|
+
"missing tool.",
|
|
182
|
+
"",
|
|
183
|
+
...entries,
|
|
184
|
+
"",
|
|
185
|
+
"When the user's request depends on one of these datastores, tell them",
|
|
186
|
+
"plainly that the datastore cannot be reached right now. Never answer",
|
|
187
|
+
"from memory, guess, or improvise a substitute for what the datastore",
|
|
188
|
+
"would have returned.",
|
|
189
|
+
"</unavailable_datastores>",
|
|
190
|
+
].join("\n");
|
|
191
|
+
}
|
|
192
|
+
|
|
103
193
|
return [
|
|
104
194
|
"<available_datastores>",
|
|
105
195
|
"You have access to the following datastores through the record tools",
|
|
106
|
-
"
|
|
196
|
+
`(${EXPECTED_RECORD_TOOLS.join(", ")}).`,
|
|
107
197
|
"Before your first operation against a datastore, call describe_datastore to",
|
|
108
198
|
"learn its collections, field encodings, and which operations you are allowed",
|
|
109
199
|
"to perform.",
|
|
110
200
|
"",
|
|
111
201
|
...entries,
|
|
202
|
+
"",
|
|
203
|
+
"If any record tool is missing from your toolset or a record tool call",
|
|
204
|
+
"fails, the datastore is unreachable: tell the user plainly that you cannot",
|
|
205
|
+
"reach it right now, and do not answer from memory, guess, or improvise a",
|
|
206
|
+
"substitute for what it would have returned.",
|
|
112
207
|
"</available_datastores>",
|
|
113
208
|
].join("\n");
|
|
114
209
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP error classification for side-channel proxy retries.
|
|
3
|
+
*
|
|
4
|
+
* The runner reaches stigmer-service's Side-Channel Proxy (`/v1/proxy/...`)
|
|
5
|
+
* over plain `fetch` from several clients — the checkpoint saver today;
|
|
6
|
+
* artifact storage, the LLM proxy, and the registry endpoint are candidates
|
|
7
|
+
* (stigmer/stigmer#468). This module is the one place that answers "is this
|
|
8
|
+
* HTTP failure worth retrying?", the `fetch` twin of grpc-retry.ts: it holds
|
|
9
|
+
* the classification policy only, kept small and separately tested. The
|
|
10
|
+
* bounded-backoff loop that consumes it lives with each client (the
|
|
11
|
+
* grpc-retry/status.ts split).
|
|
12
|
+
*
|
|
13
|
+
* The policy is grounded in what the proxy actually emits, not HTTP folklore.
|
|
14
|
+
* CheckpointerProxyController returns exactly four deterministic errors —
|
|
15
|
+
* 400 (malformed document), 403 (FGA deny), 404 (no such checkpoint), and
|
|
16
|
+
* 413 (the 4 MB cap) — and never a transient status. Transient failures
|
|
17
|
+
* reach the runner only as 5xx (Spring's error handler, ingress 502-504),
|
|
18
|
+
* 408/429 (infrastructure between runner and service), or as network-level
|
|
19
|
+
* fetch rejections. 401/403 stay terminal deliberately: retrying cannot fix
|
|
20
|
+
* an expired or rejected credential.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* True for HTTP statuses that signal a transient condition worth retrying:
|
|
25
|
+
* 408 (request timeout), 429 (throttled), and all 5xx. Every other status is
|
|
26
|
+
* deterministic — the proxy's 4xx errors mean the same request will fail the
|
|
27
|
+
* same way forever.
|
|
28
|
+
*/
|
|
29
|
+
export function isRetryableHttpStatus(status: number): boolean {
|
|
30
|
+
return status === 408 || status === 429 || status >= 500;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* True for `fetch` rejections that signal a transient transport condition:
|
|
35
|
+
*
|
|
36
|
+
* - `TypeError` — undici's network-failure shape (`fetch failed`, with the
|
|
37
|
+
* ECONNREFUSED/ECONNRESET/DNS cause attached). A deterministic TypeError
|
|
38
|
+
* (e.g. a malformed URL) also matches, which is accepted: such a bug fails
|
|
39
|
+
* every attempt including the first test run, and the bounded budget caps
|
|
40
|
+
* the wasted retries at seconds.
|
|
41
|
+
* - `TimeoutError` — the rejection shape of `AbortSignal.timeout()`, i.e. a
|
|
42
|
+
* hung connection whose attempt we bounded ourselves.
|
|
43
|
+
*
|
|
44
|
+
* A manual abort (`AbortError`) is deliberately NOT retryable: the caller
|
|
45
|
+
* cancelled on purpose.
|
|
46
|
+
*/
|
|
47
|
+
export function isRetryableFetchError(err: unknown): boolean {
|
|
48
|
+
if (err instanceof TypeError) return true;
|
|
49
|
+
return err instanceof Error && err.name === "TimeoutError";
|
|
50
|
+
}
|