@stigmer/runner 3.40.1 → 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/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.js +7 -7
- 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/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/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__/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__/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/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 +7 -7
- 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__/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/synthesized-attachment.ts +0 -12
- package/src/shared/tool-rounds.ts +70 -24
- package/src/shared/workspace/__tests__/stigmer-link.test.ts +107 -10
- package/src/shared/workspace/platform-dir.ts +19 -1
- 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
|
@@ -50,15 +50,17 @@
|
|
|
50
50
|
* mid-stream is WAITING until the boundary runs), so they are indexed once
|
|
51
51
|
* at construction and consumed on match (a declined row is a
|
|
52
52
|
* closed gate and never matched).
|
|
53
|
-
* - The
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
53
|
+
* - The PROVENANCE, and never the hold. A row's provenance is the shared
|
|
54
|
+
* read-side `resolveApprovalProvenance` over the merged policies and the
|
|
55
|
+
* run's leases: which layer governs the call. Whether the call was HELD
|
|
56
|
+
* for a person's decision is not answered here, because at tool start
|
|
57
|
+
* nobody knows yet: the preToolUse hook decides, with arms this side
|
|
58
|
+
* cannot evaluate (capture mode, grants, bypass, secret blocks), and a
|
|
59
|
+
* call the hook denies is parked by the turn boundary through
|
|
60
|
+
* `approval_proposed` — the one writer of a row's approval fields on both
|
|
61
|
+
* harnesses (#1117). Until then this translator stamped the policy's
|
|
62
|
+
* category verdict at start, lease- and bypass-blind, so a leased,
|
|
63
|
+
* bypassed or capture-mode call that ran read "requires approval".
|
|
62
64
|
* - The RESULT string (`tool-result.ts`): a failure's text is its `error`
|
|
63
65
|
* alone.
|
|
64
66
|
* - The SUB-AGENT: a `task` tool call opens a sub-agent keyed by
|
|
@@ -81,12 +83,11 @@
|
|
|
81
83
|
import type { InteractionUpdate, SDKMessage } from "@cursor/sdk";
|
|
82
84
|
import type { AgentMessage } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
83
85
|
import { ToolCallStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
84
|
-
import type {
|
|
86
|
+
import type { ToolStartedEvent, TranscriptEvent } from "../../harness/transcript/events.js";
|
|
85
87
|
import { resolveApprovalProvenance, type MergedToolPolicy, type PolicySource } from "../../shared/approval-policy.js";
|
|
86
88
|
import { utcTimestamp } from "../../shared/status.js";
|
|
87
89
|
import { isDeclinedRow } from "../../shared/tool-row.js";
|
|
88
90
|
import type { ToolApprovalCategory } from "../../shared/tool-kind.js";
|
|
89
|
-
import { builtInRequiresApproval, getBuiltInApprovalMessage, lookupMcpToolPolicy, resolveApprovalMessage } from "./approval-policy.js";
|
|
90
91
|
import { toolCallIdentityToken, toolIdentity, primaryToken } from "./approval-state.js";
|
|
91
92
|
import { contentDigest } from "../../shared/file-tools.js";
|
|
92
93
|
import { subAgentStepEvents } from "./sub-agent-steps.js";
|
|
@@ -97,7 +98,7 @@ type ToolCallEvent = Extract<SDKMessage, { type: "tool_call" }>;
|
|
|
97
98
|
/** The delegation tool: the one tool name this translator knows, because its start opens a sub-agent. */
|
|
98
99
|
const TASK_TOOL = "task";
|
|
99
100
|
|
|
100
|
-
/** The run's approval posture the translator answers `
|
|
101
|
+
/** The run's approval posture the translator answers `provenance` from. */
|
|
101
102
|
export interface CursorTranslatorOptions {
|
|
102
103
|
/** The merged four-level MCP policy map (`mergeApprovalPolicies`). */
|
|
103
104
|
readonly policies: ReadonlyMap<string, MergedToolPolicy>;
|
|
@@ -303,7 +304,7 @@ export class CursorTranslator {
|
|
|
303
304
|
}
|
|
304
305
|
this.announced.add(rowId);
|
|
305
306
|
}
|
|
306
|
-
out.push(this.toolStarted(rowId, name,
|
|
307
|
+
out.push(this.toolStarted(rowId, name, mcpServerSlug, input));
|
|
307
308
|
// Deltas that arrived before the stream announced this call follow its
|
|
308
309
|
// start, targeted at the row it resolved to.
|
|
309
310
|
const heldMakers = this.held.get(event.call_id);
|
|
@@ -359,7 +360,7 @@ export class CursorTranslator {
|
|
|
359
360
|
return seededId;
|
|
360
361
|
}
|
|
361
362
|
|
|
362
|
-
private toolStarted(callId: string, name: string,
|
|
363
|
+
private toolStarted(callId: string, name: string, mcpServerSlug: string, input: Record<string, unknown>): ToolStartedEvent {
|
|
363
364
|
const { policies, leases } = this.options;
|
|
364
365
|
const provenance: PolicySource | undefined = resolveApprovalProvenance(
|
|
365
366
|
name,
|
|
@@ -368,7 +369,6 @@ export class CursorTranslator {
|
|
|
368
369
|
leases.categories,
|
|
369
370
|
leases.global,
|
|
370
371
|
);
|
|
371
|
-
const gate = this.gateOf(name, isMcp, mcpServerSlug, input);
|
|
372
372
|
return {
|
|
373
373
|
kind: "tool_started",
|
|
374
374
|
callId,
|
|
@@ -376,26 +376,9 @@ export class CursorTranslator {
|
|
|
376
376
|
input,
|
|
377
377
|
mcpServerSlug,
|
|
378
378
|
...(provenance !== undefined ? { provenance } : {}),
|
|
379
|
-
...(gate !== undefined ? { gate } : {}),
|
|
380
379
|
};
|
|
381
380
|
}
|
|
382
381
|
|
|
383
|
-
/**
|
|
384
|
-
* The policy's word that this call requires approval, and the card's
|
|
385
|
-
* message — or nothing for an ungated call. An MCP call (the `mcp` envelope,
|
|
386
|
-
* whatever its provider field says) is governed by the merged policy map
|
|
387
|
-
* alone; a built-in by its category.
|
|
388
|
-
*/
|
|
389
|
-
private gateOf(name: string, isMcp: boolean, mcpServerSlug: string, input: Record<string, unknown>): GateAnswer | undefined {
|
|
390
|
-
if (isMcp) {
|
|
391
|
-
const policy = lookupMcpToolPolicy(name, mcpServerSlug, this.options.policies);
|
|
392
|
-
return policy ? { message: resolveApprovalMessage(policy.approvalMessage, name, input) } : undefined;
|
|
393
|
-
}
|
|
394
|
-
if (!builtInRequiresApproval(name)) return undefined;
|
|
395
|
-
const template = getBuiltInApprovalMessage(name);
|
|
396
|
-
return template ? { message: resolveApprovalMessage(template, name, input) } : undefined;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
382
|
// ── The delta queue ─────────────────────────────────────────────
|
|
400
383
|
|
|
401
384
|
private rowIdOf(callId: string): string {
|
|
@@ -220,7 +220,10 @@ export async function consumeCursorTurnStream(
|
|
|
220
220
|
// The stream event, then the deltas that arrived since the last one —
|
|
221
221
|
// the enricher's order, kept so a completion the delta channel reported
|
|
222
222
|
// in the same window as the stream's own defers to the stream's instant.
|
|
223
|
-
|
|
223
|
+
// The stream event is one observation, folded at one instant (a
|
|
224
|
+
// completion with no running before it starts and finishes together);
|
|
225
|
+
// each drained delta is its own, and carries `observedAt` where it stamps.
|
|
226
|
+
transcript.applyObservation(translator.translate(event));
|
|
224
227
|
for (const fact of translator.drainDeltas()) transcript.apply(fact);
|
|
225
228
|
|
|
226
229
|
// First-denial stop (HITL clean pause). In CAPTURE mode this fires only for
|
|
@@ -146,6 +146,7 @@ export function hermeticDeepAgentConfig(env: HermeticEnvironment, checkpointerTy
|
|
|
146
146
|
temporalConnection: {},
|
|
147
147
|
stigmerBackendEndpoint: "http://localhost:7234",
|
|
148
148
|
mcpBridgeEndpoint: null,
|
|
149
|
+
mcpPublicEndpoint: null,
|
|
149
150
|
stigmerTokenRef: { current: null },
|
|
150
151
|
cursorApiKey: "",
|
|
151
152
|
workspaceRootDir: env.workspaceRootDir,
|
|
@@ -228,6 +229,8 @@ export class DeepAgentScenario {
|
|
|
228
229
|
readonly config: Config,
|
|
229
230
|
script: ScriptSelector,
|
|
230
231
|
onTurn: DeepAgentScenarioOptions["onTurn"],
|
|
232
|
+
/** Tool calls an EARLIER message of the session settled, in its own record ({@link continueDeepAgentScenario}). */
|
|
233
|
+
settledEarlier: ReadonlySet<string> = new Set(),
|
|
231
234
|
) {
|
|
232
235
|
this.model = new ScriptedModel(script, [], {
|
|
233
236
|
onTurn: async (info) => {
|
|
@@ -236,8 +239,10 @@ export class DeepAgentScenario {
|
|
|
236
239
|
// lets the producer run a turn ahead of the consumer. Wait for that
|
|
237
240
|
// ground before ticking, so every stamp of the previous turn precedes
|
|
238
241
|
// this turn's instant, whatever the persist's I/O took (the barrier's
|
|
239
|
-
// own header on `ExecutionRecord` carries the whole reason).
|
|
240
|
-
|
|
242
|
+
// own header on `ExecutionRecord` carries the whole reason). A
|
|
243
|
+
// follow-up message's first turn answers the previous message's last
|
|
244
|
+
// results, which that message's record settled, never this one.
|
|
245
|
+
await this.record.whenToolCallsSettled(info.priorToolCallIds.filter((id) => !settledEarlier.has(id)));
|
|
241
246
|
this.clock.tick();
|
|
242
247
|
if (onTurn && this.controls) await onTurn(info, this.controls);
|
|
243
248
|
},
|
|
@@ -285,6 +290,32 @@ export function beginDeepAgentScenario(options: DeepAgentScenarioOptions): DeepA
|
|
|
285
290
|
return scenario;
|
|
286
291
|
}
|
|
287
292
|
|
|
293
|
+
/**
|
|
294
|
+
* The session's NEXT message on the scenario's thread: a new record and a new
|
|
295
|
+
* script over the same environment, runner config and checkpointer, WITHOUT
|
|
296
|
+
* the reset {@link beginDeepAgentScenario} does. On the `sqlite` checkpointer
|
|
297
|
+
* the new message therefore runs on the checkpoint the previous one left: the
|
|
298
|
+
* production shape of "send another message to continue".
|
|
299
|
+
*/
|
|
300
|
+
export function continueDeepAgentScenario(
|
|
301
|
+
previous: DeepAgentScenario,
|
|
302
|
+
options: Pick<DeepAgentScenarioOptions, "record" | "script" | "onTurn" | "clientOverrides">,
|
|
303
|
+
): DeepAgentScenario {
|
|
304
|
+
const settledEarlier = new Set(previous.record.toolCalls().map((tc) => tc.id));
|
|
305
|
+
const scenario = new DeepAgentScenario(
|
|
306
|
+
previous.env,
|
|
307
|
+
previous.clock,
|
|
308
|
+
options.record,
|
|
309
|
+
previous.config,
|
|
310
|
+
options.script,
|
|
311
|
+
options.onTurn,
|
|
312
|
+
settledEarlier,
|
|
313
|
+
);
|
|
314
|
+
bindScriptedModel(scenario.model);
|
|
315
|
+
bindHermeticClient(options.record.client(options.clientOverrides));
|
|
316
|
+
return scenario;
|
|
317
|
+
}
|
|
318
|
+
|
|
288
319
|
export interface DeepAgentTurnOptions {
|
|
289
320
|
/** The HITL cycle index (0 on the first turn); mints the change-set id. */
|
|
290
321
|
readonly turnSeq?: number;
|
|
@@ -26,6 +26,7 @@ const CONFIG: Config = {
|
|
|
26
26
|
temporalConnection: {},
|
|
27
27
|
stigmerBackendEndpoint: "http://localhost:7234",
|
|
28
28
|
mcpBridgeEndpoint: null,
|
|
29
|
+
mcpPublicEndpoint: null,
|
|
29
30
|
stigmerTokenRef: { current: "tok" },
|
|
30
31
|
cursorApiKey: "",
|
|
31
32
|
workspaceRootDir: "/tmp/adapter-test",
|
|
@@ -27,6 +27,10 @@
|
|
|
27
27
|
* a status the host could reach), and `goldens/file-review-capture.status.json`
|
|
28
28
|
* pins the whole ledger beside Cursor's.
|
|
29
29
|
*
|
|
30
|
+
* Golden hunk since #1443: the inline artifact's `createdAt` is t=1, the
|
|
31
|
+
* edit's finish, where it was t=2, the moment the publisher's upload
|
|
32
|
+
* returned (it reads the clock before its first await now).
|
|
33
|
+
*
|
|
30
34
|
* Parent phase rows exercised beyond the plain arms: a `local_path` workspace
|
|
31
35
|
* entry (mounted as-is in local mode, the agent rooted at the entry per
|
|
32
36
|
* `provisionWorkspace`); capture-mode derivation on a git tree; the baseline
|
package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/file-review-capture.status.json
CHANGED
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"kind": "EXECUTION_ARTIFACT_KIND_FILE",
|
|
41
41
|
"sizeBytes": "22",
|
|
42
42
|
"storageKey": "artifacts/aex_hermetic_0001/notes.md",
|
|
43
|
-
"createdAt": "2026-01-01T00:00:
|
|
43
|
+
"createdAt": "2026-01-01T00:00:01.000Z",
|
|
44
44
|
"contentHash": "e4cdd09e8ca79e376a6ec5766d190754bc8ee4a2d6c5c7f4d86a9dc7fdf25a0f"
|
|
45
45
|
}
|
|
46
46
|
],
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"kind": "EXECUTION_ARTIFACT_KIND_FILE",
|
|
58
58
|
"sizeBytes": "32",
|
|
59
59
|
"storageKey": "artifacts/aex_hermetic_0001/report.md",
|
|
60
|
-
"createdAt": "2026-01-01T00:00:
|
|
60
|
+
"createdAt": "2026-01-01T00:00:01.000Z",
|
|
61
61
|
"contentHash": "62eae4d09b2b051cf4ac5aa1fd70847b34c47994a1732ff999f47749e71b0441"
|
|
62
62
|
}
|
|
63
63
|
],
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"kind": "EXECUTION_ARTIFACT_KIND_FILE",
|
|
68
68
|
"sizeBytes": "32",
|
|
69
69
|
"storageKey": "artifacts/aex_hermetic_0001/report.md",
|
|
70
|
-
"createdAt": "2026-01-01T00:00:
|
|
70
|
+
"createdAt": "2026-01-01T00:00:01.000Z",
|
|
71
71
|
"contentHash": "62eae4d09b2b051cf4ac5aa1fd70847b34c47994a1732ff999f47749e71b0441"
|
|
72
72
|
}
|
|
73
73
|
],
|
|
@@ -200,89 +200,24 @@
|
|
|
200
200
|
}
|
|
201
201
|
]
|
|
202
202
|
},
|
|
203
|
-
{
|
|
204
|
-
"type": "MESSAGE_AI",
|
|
205
|
-
"content": "Reading file 10.",
|
|
206
|
-
"timestamp": "2026-01-01T00:00:11.000Z",
|
|
207
|
-
"toolCalls": [
|
|
208
|
-
{
|
|
209
|
-
"id": "call-hermetic-loop-0010",
|
|
210
|
-
"name": "read_file",
|
|
211
|
-
"args": {
|
|
212
|
-
"file_path": "/f10.md"
|
|
213
|
-
},
|
|
214
|
-
"result": "[{\"type\":\"text\",\"text\":\"Error: File '/f10.md' not found\"}]",
|
|
215
|
-
"status": "TOOL_CALL_COMPLETED",
|
|
216
|
-
"startedAt": "2026-01-01T00:00:11.000Z",
|
|
217
|
-
"completedAt": "2026-01-01T00:00:11.000Z",
|
|
218
|
-
"argsPreview": "{\"file_path\":\"/f10.md\"}",
|
|
219
|
-
"toolKind": "TOOL_KIND_FILE_READ"
|
|
220
|
-
}
|
|
221
|
-
]
|
|
222
|
-
},
|
|
223
|
-
{
|
|
224
|
-
"type": "MESSAGE_AI",
|
|
225
|
-
"content": "Reading file 11.",
|
|
226
|
-
"timestamp": "2026-01-01T00:00:12.000Z",
|
|
227
|
-
"toolCalls": [
|
|
228
|
-
{
|
|
229
|
-
"id": "call-hermetic-loop-0011",
|
|
230
|
-
"name": "read_file",
|
|
231
|
-
"args": {
|
|
232
|
-
"file_path": "/f11.md"
|
|
233
|
-
},
|
|
234
|
-
"result": "[{\"type\":\"text\",\"text\":\"Error: File '/f11.md' not found\"}]",
|
|
235
|
-
"status": "TOOL_CALL_COMPLETED",
|
|
236
|
-
"startedAt": "2026-01-01T00:00:12.000Z",
|
|
237
|
-
"completedAt": "2026-01-01T00:00:12.000Z",
|
|
238
|
-
"argsPreview": "{\"file_path\":\"/f11.md\"}",
|
|
239
|
-
"toolKind": "TOOL_KIND_FILE_READ"
|
|
240
|
-
}
|
|
241
|
-
]
|
|
242
|
-
},
|
|
243
|
-
{
|
|
244
|
-
"type": "MESSAGE_AI",
|
|
245
|
-
"content": "Reading file 12.",
|
|
246
|
-
"timestamp": "2026-01-01T00:00:13.000Z",
|
|
247
|
-
"toolCalls": [
|
|
248
|
-
{
|
|
249
|
-
"id": "call-hermetic-loop-0012",
|
|
250
|
-
"name": "read_file",
|
|
251
|
-
"args": {
|
|
252
|
-
"file_path": "/f12.md"
|
|
253
|
-
},
|
|
254
|
-
"result": "[{\"type\":\"text\",\"text\":\"Error: File '/f12.md' not found\"}]",
|
|
255
|
-
"status": "TOOL_CALL_COMPLETED",
|
|
256
|
-
"startedAt": "2026-01-01T00:00:13.000Z",
|
|
257
|
-
"completedAt": "2026-01-01T00:00:13.000Z",
|
|
258
|
-
"argsPreview": "{\"file_path\":\"/f12.md\"}",
|
|
259
|
-
"toolKind": "TOOL_KIND_FILE_READ"
|
|
260
|
-
}
|
|
261
|
-
]
|
|
262
|
-
},
|
|
263
|
-
{
|
|
264
|
-
"type": "MESSAGE_AI",
|
|
265
|
-
"content": "Reading file 13.",
|
|
266
|
-
"timestamp": "2026-01-01T00:00:14.000Z"
|
|
267
|
-
},
|
|
268
203
|
{
|
|
269
204
|
"type": "MESSAGE_SYSTEM",
|
|
270
205
|
"content": "The agent reached the tool-call limit for this message. Work completed so far has been saved. Send another message to continue where the agent left off.",
|
|
271
|
-
"timestamp": "2026-01-01T00:00:
|
|
206
|
+
"timestamp": "2026-01-01T00:00:10.000Z"
|
|
272
207
|
}
|
|
273
208
|
],
|
|
274
209
|
"phase": "EXECUTION_TERMINATED",
|
|
275
210
|
"error": "Agent reached the tool-call limit for this message. Send another message to continue.",
|
|
276
211
|
"startedAt": "2026-01-01T00:00:00.000Z",
|
|
277
|
-
"completedAt": "2026-01-01T00:00:
|
|
212
|
+
"completedAt": "2026-01-01T00:00:10.000Z",
|
|
278
213
|
"streamingUsage": {
|
|
279
|
-
"inputTokens": "
|
|
280
|
-
"outputTokens": "
|
|
281
|
-
"totalTokens": "
|
|
282
|
-
"turnCount":
|
|
283
|
-
"estimatedCostUsd": 0.
|
|
214
|
+
"inputTokens": "10000",
|
|
215
|
+
"outputTokens": "200",
|
|
216
|
+
"totalTokens": "10200",
|
|
217
|
+
"turnCount": 10,
|
|
218
|
+
"estimatedCostUsd": 0.011000000000000001,
|
|
284
219
|
"model": "claude-haiku-4.5",
|
|
285
|
-
"observedAt": "2026-01-01T00:00:
|
|
220
|
+
"observedAt": "2026-01-01T00:00:10.000Z",
|
|
286
221
|
"requestedServiceTier": "SERVICE_TIER_STANDARD",
|
|
287
222
|
"requestedThinkingMode": "THINKING_MODE_DISABLED"
|
|
288
223
|
},
|
package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/resolution-error.status.json
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
}
|
|
13
13
|
],
|
|
14
14
|
"phase": "EXECUTION_FAILED",
|
|
15
|
-
"error": "
|
|
15
|
+
"error": "[Error] hermetic fault injected at getAgent",
|
|
16
16
|
"startedAt": "2026-01-01T00:00:00.000Z",
|
|
17
17
|
"completedAt": "2026-01-01T00:00:00.000Z"
|
|
18
18
|
}
|
package/src/activities/execute-deep-agent/__tests__/hermetic/inline-artifact-across-gate.test.ts
CHANGED
|
@@ -24,6 +24,11 @@
|
|
|
24
24
|
* this pair of goldens (`inline-artifact.turn1`, `inline-artifact.turn2`) is
|
|
25
25
|
* what proves it did.
|
|
26
26
|
*
|
|
27
|
+
* Turn 2 completes, so its post-stream safety net walks the seeded history
|
|
28
|
+
* and asks for `report.md` again. The publisher re-reads it, finds the bytes
|
|
29
|
+
* the seeded row lists, and uploads nothing: the store sees turn 1's write
|
|
30
|
+
* only (stigmer/stigmer#1448).
|
|
31
|
+
*
|
|
27
32
|
* Also recorded (a shape fact, as found): the write's tool row
|
|
28
33
|
* shows `requiresApproval: true` with `approvalPolicySource` BUILTIN_CATEGORY
|
|
29
34
|
* — the category policy's verdict — while the row is COMPLETED because
|
|
@@ -50,6 +55,12 @@
|
|
|
50
55
|
* to persist "0 files" beside a one-file change set. The runtime now
|
|
51
56
|
* refreshes the strip on every write, the terminal one included.
|
|
52
57
|
*
|
|
58
|
+
* Golden hunk since #1443, in both goldens: the artifact's `createdAt` is
|
|
59
|
+
* t=1, the write's finish, where it was t=2. The publisher read the clock
|
|
60
|
+
* after its upload, which runs beside the stream, so the stamp landed on
|
|
61
|
+
* either side of the next model turn's tick and flaked under the full
|
|
62
|
+
* suite; it reads the clock before its first await now.
|
|
63
|
+
*
|
|
53
64
|
* Regenerate ONLY after a deliberate behavior change:
|
|
54
65
|
* npx vitest run src/activities/execute-deep-agent/__tests__/hermetic -u
|
|
55
66
|
*/
|
|
@@ -88,6 +99,7 @@ import {
|
|
|
88
99
|
runDeepAgentTurn,
|
|
89
100
|
} from "../../__test-utils__/hermetic-deep-agent.js";
|
|
90
101
|
import { CLOSING_TURN, EXECUTE_CALL_A } from "../../__test-utils__/hitl-script.js";
|
|
102
|
+
import { LocalArtifactStorage } from "../../../../shared/artifact-storage.js";
|
|
91
103
|
|
|
92
104
|
const REPORT = "report.md";
|
|
93
105
|
const REPORT_BODY = "# Report\n\nAll fixtures nominal.\n";
|
|
@@ -148,6 +160,7 @@ describe("ExecuteDeepAgent hermetic — inline artifact across a gate (sqlite)",
|
|
|
148
160
|
expect(existsSync(join(env.artifactPath, run1.artifacts[0].storageKey)), "the bytes reached the store").toBe(true);
|
|
149
161
|
const writeRow = record.toolCalls().find((tc) => tc.id === WRITE_CALL_ID)!;
|
|
150
162
|
expect(writeRow.status, "capture mode let the write flow").toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
|
|
163
|
+
expect(run1.artifacts[0].createdAt, "the artifact is stamped at its write's finish").toBe(writeRow.completedAt);
|
|
151
164
|
expect(record.waitingToolCalls().map((tc) => tc.id)).toEqual([EXECUTE_CALL_A.id]);
|
|
152
165
|
await expect(statusJson(run1)).toMatchFileSnapshot("./goldens/inline-artifact.turn1.status.json");
|
|
153
166
|
|
|
@@ -155,13 +168,17 @@ describe("ExecuteDeepAgent hermetic — inline artifact across a gate (sqlite)",
|
|
|
155
168
|
expect(record.decideWaitingToolCalls(ApprovalAction.APPROVE, DECIDED_AT)).toBe(1);
|
|
156
169
|
|
|
157
170
|
// ── Turn 2 ───────────────────────────────────────────────────────────────
|
|
171
|
+
const uploads = vi.spyOn(LocalArtifactStorage.prototype, "upload");
|
|
158
172
|
const turn2 = await runDeepAgentTurn(scenario, { turnSeq: 1 });
|
|
173
|
+
const turn2Keys = uploads.mock.calls.map(([key]) => key);
|
|
174
|
+
uploads.mockRestore();
|
|
159
175
|
expect(turn2.outcome.kind).toBe("returned");
|
|
160
176
|
expect((turn2.outcome as { value: Record<string, unknown> }).value.phase).toBe("EXECUTION_COMPLETED");
|
|
161
177
|
const final = record.lastFullStatus!;
|
|
162
178
|
expect(final.artifacts.map((a) => [a.name, a.storageKey, a.contentHash]), "the seed carried the artifact").toEqual(
|
|
163
179
|
run1.artifacts.map((a) => [a.name, a.storageKey, a.contentHash]),
|
|
164
180
|
);
|
|
181
|
+
expect(turn2Keys, "turn 2 does not upload the unchanged report again").not.toContain(run1.artifacts[0].storageKey);
|
|
165
182
|
expect(final.messages.flatMap((m) => m.toolCalls).map((tc) => [tc.id, tc.status])).toEqual([
|
|
166
183
|
[WRITE_CALL_ID, ToolCallStatus.TOOL_CALL_COMPLETED],
|
|
167
184
|
[EXECUTE_CALL_A.id, ToolCallStatus.TOOL_CALL_COMPLETED],
|
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
* model that keeps calling tools.
|
|
4
4
|
*
|
|
5
5
|
* What the arm does: `max_tool_rounds = 10` (the clamp floor,
|
|
6
|
-
* `shared/tool-rounds.ts` MIN_TOOL_ROUNDS)
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* `
|
|
6
|
+
* `shared/tool-rounds.ts` MIN_TOOL_ROUNDS) is the execution budget
|
|
7
|
+
* middleware's round limit; the script proposes a DISTINCT `read_file` every
|
|
8
|
+
* round (distinct ids and paths, so the loop-detection middleware's
|
|
9
|
+
* "repetitive pattern" warning does not end the run first); after round 10's
|
|
10
|
+
* results the middleware throws `ToolRoundLimitError` in place of round 11's
|
|
11
|
+
* model call; the adapter's turn classifies it, through LangChain's
|
|
12
|
+
* `MiddlewareError` wrapping, as the `tool_call_limit` outcome
|
|
13
|
+
* (`shared/tool-rounds.ts` `isToolCallBudgetStop`), and the runtime's
|
|
14
|
+
* `toolCallLimitArm` writes
|
|
13
15
|
* TERMINATED with the cross-repo prefix `TOOL_CALL_LIMIT_ERROR_PREFIX`
|
|
14
16
|
* (matched with `startsWith` by stigmer-cloud's `reply-extractor.ts`) and
|
|
15
17
|
* its row, PERSISTS it, and returns the slim.
|
|
@@ -29,16 +31,26 @@
|
|
|
29
31
|
* empty message once and its `currentAiMessage[""]` then caught every later
|
|
30
32
|
* row.
|
|
31
33
|
*
|
|
32
|
-
* How many rounds
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
34
|
+
* How many rounds run is the knob's, exactly (#1113): TEN tool rows, on any
|
|
35
|
+
* middleware stack. Until #1113 the knob was LangGraph's recursion limit at
|
|
36
|
+
* 10 × 6 super-steps, and since LangChain makes every middleware hook its own
|
|
37
|
+
* graph node, the rows moved with the stack: 11 until #1096 deleted
|
|
38
|
+
* `graceful-stop.ts` (an `afterModel` node), 13 after it. #1113 moved this
|
|
39
|
+
* golden from 13 rows to 10, the copy unchanged. It also dropped the golden's
|
|
40
|
+
* last AI message, "Reading file 13.": the step limit had cut that fourteenth
|
|
41
|
+
* model call's proposal before its tool ran, so the transcript ended on a
|
|
42
|
+
* call that never executed. The round stop replaces the model call instead,
|
|
43
|
+
* so the transcript ends on round 10's results, and usage counts ten model
|
|
44
|
+
* calls, not fourteen.
|
|
45
|
+
*
|
|
46
|
+
* The second arm is the copy's promise, "send another message to continue":
|
|
47
|
+
* on the durable (`sqlite`) checkpointer, a follow-up message on the same
|
|
48
|
+
* thread starts from the checkpoint the stop left. The stop is a throw inside
|
|
49
|
+
* the model node rather than a limit between super-steps, so this is proven,
|
|
50
|
+
* not assumed: the follow-up's first model turn sees all ten rounds in its
|
|
51
|
+
* transcript (the scripted model indexes its script by the rounds the
|
|
52
|
+
* transcript holds), the run completes, and the budget is the new message's
|
|
53
|
+
* own.
|
|
42
54
|
*
|
|
43
55
|
* Regenerate ONLY after a deliberate behavior change:
|
|
44
56
|
* npx vitest run src/activities/execute-deep-agent/__tests__/hermetic -u
|
|
@@ -65,14 +77,25 @@ import { stubRegistryFetch } from "../../../../__test-utils__/model-registry-fix
|
|
|
65
77
|
import { MIN_TOOL_ROUNDS } from "../../../../shared/tool-rounds.js";
|
|
66
78
|
import {
|
|
67
79
|
beginDeepAgentScenario,
|
|
80
|
+
continueDeepAgentScenario,
|
|
68
81
|
deepAgentExecutionRecord,
|
|
69
82
|
runDeepAgentTurn,
|
|
70
83
|
} from "../../__test-utils__/hermetic-deep-agent.js";
|
|
84
|
+
import type { ScriptedTurn } from "../../__test-utils__/scripted-model.js";
|
|
71
85
|
import { TOOL_CALL_LIMIT_ERROR_PREFIX } from "../../../../shared/tool-rounds.js";
|
|
72
86
|
|
|
73
87
|
/** More rounds than the budget allows; the graph stops the script, not the other way round. */
|
|
74
88
|
const SCRIPTED_ROUNDS = 40;
|
|
75
89
|
|
|
90
|
+
/** The model that keeps calling tools: a distinct `read_file` per round. */
|
|
91
|
+
function readingRounds(): ScriptedTurn[] {
|
|
92
|
+
return Array.from({ length: SCRIPTED_ROUNDS }, (_, i) => ({
|
|
93
|
+
text: `Reading file ${i}.`,
|
|
94
|
+
toolCalls: [{ id: `call-hermetic-loop-${String(i).padStart(4, "0")}`, name: "read_file", args: { file_path: `/f${i}.md` } }],
|
|
95
|
+
usage: { inputTokens: 1_000, outputTokens: 20 },
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
|
|
76
99
|
describe("ExecuteDeepAgent hermetic — tool-call budget exhausted", () => {
|
|
77
100
|
let env: HermeticEnvironment;
|
|
78
101
|
let registry: ReturnType<typeof stubRegistryFetch>;
|
|
@@ -97,13 +120,7 @@ describe("ExecuteDeepAgent hermetic — tool-call budget exhausted", () => {
|
|
|
97
120
|
env,
|
|
98
121
|
clock,
|
|
99
122
|
record,
|
|
100
|
-
script: () => ({
|
|
101
|
-
turns: Array.from({ length: SCRIPTED_ROUNDS }, (_, i) => ({
|
|
102
|
-
text: `Reading file ${i}.`,
|
|
103
|
-
toolCalls: [{ id: `call-hermetic-loop-${String(i).padStart(4, "0")}`, name: "read_file", args: { file_path: `/f${i}.md` } }],
|
|
104
|
-
usage: { inputTokens: 1_000, outputTokens: 20 },
|
|
105
|
-
})),
|
|
106
|
-
}),
|
|
123
|
+
script: () => ({ turns: readingRounds() }),
|
|
107
124
|
});
|
|
108
125
|
|
|
109
126
|
// ── Act ──────────────────────────────────────────────────────────────────
|
|
@@ -125,8 +142,7 @@ describe("ExecuteDeepAgent hermetic — tool-call budget exhausted", () => {
|
|
|
125
142
|
expect(persisted.error, "the event count left the copy").not.toMatch(/\d+ events?/);
|
|
126
143
|
expect(persisted.completedAt).not.toBe("");
|
|
127
144
|
const rows = record.toolCalls();
|
|
128
|
-
expect(rows.length, "
|
|
129
|
-
expect(rows.length).toBeGreaterThanOrEqual(MIN_TOOL_ROUNDS);
|
|
145
|
+
expect(rows.length, "exactly max_tool_rounds rounds ran: the budget counts rounds, not graph steps").toBe(MIN_TOOL_ROUNDS);
|
|
130
146
|
// The message boundary: every row sits on the message whose text proposed it
|
|
131
147
|
// — one "Reading file N." message per row, no empty host (before #1097 all
|
|
132
148
|
// thirteen rows sat on ONE empty message).
|
|
@@ -144,4 +160,48 @@ describe("ExecuteDeepAgent hermetic — tool-call budget exhausted", () => {
|
|
|
144
160
|
const json = JSON.stringify(toJson(AgentExecutionStatusSchema, persisted), null, 2) + "\n";
|
|
145
161
|
await expect(json).toMatchFileSnapshot("./goldens/recursion-limit.persisted.status.json");
|
|
146
162
|
});
|
|
163
|
+
|
|
164
|
+
it("continues on the next message from the checkpoint the stop left (sqlite)", async () => {
|
|
165
|
+
// ── Arrange: the first message exhausts its budget ───────────────────────
|
|
166
|
+
const first = beginDeepAgentScenario({
|
|
167
|
+
env,
|
|
168
|
+
clock,
|
|
169
|
+
record: deepAgentExecutionRecord({ message: "Keep reading files.", maxToolRounds: MIN_TOOL_ROUNDS }),
|
|
170
|
+
checkpointer: "sqlite",
|
|
171
|
+
script: () => ({ turns: readingRounds() }),
|
|
172
|
+
});
|
|
173
|
+
await runDeepAgentTurn(first);
|
|
174
|
+
expect(first.record.lastFullStatus!.phase).toBe(ExecutionPhase.EXECUTION_TERMINATED);
|
|
175
|
+
expect(first.record.toolCalls()).toHaveLength(MIN_TOOL_ROUNDS);
|
|
176
|
+
|
|
177
|
+
// The follow-up's script is indexed by the rounds its transcript holds: a
|
|
178
|
+
// model that saw the ten rounds answers in text; one that lost them would
|
|
179
|
+
// play turn 0 and read a file.
|
|
180
|
+
const LOST_THREAD: ScriptedTurn = {
|
|
181
|
+
text: "Starting over.",
|
|
182
|
+
toolCalls: [{ id: "call-hermetic-lost-thread", name: "read_file", args: { file_path: "/lost.md" } }],
|
|
183
|
+
};
|
|
184
|
+
const PICKED_UP: ScriptedTurn = { text: "Picking up where I left off: all ten files are read." };
|
|
185
|
+
const roundsSeen: number[] = [];
|
|
186
|
+
const record = deepAgentExecutionRecord({ message: "Continue.", maxToolRounds: MIN_TOOL_ROUNDS });
|
|
187
|
+
const next = continueDeepAgentScenario(first, {
|
|
188
|
+
record,
|
|
189
|
+
script: () => ({ turns: [...Array.from({ length: MIN_TOOL_ROUNDS }, () => LOST_THREAD), PICKED_UP] }),
|
|
190
|
+
onTurn: (info) => {
|
|
191
|
+
roundsSeen.push(info.round);
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// ── Act ──────────────────────────────────────────────────────────────────
|
|
196
|
+
const invocation = await runDeepAgentTurn(next);
|
|
197
|
+
|
|
198
|
+
// ── Assert ───────────────────────────────────────────────────────────────
|
|
199
|
+
expect(invocation.outcome.kind).toBe("returned");
|
|
200
|
+
expect(roundsSeen, "the follow-up's one model turn saw the ten rounds the stop checkpointed").toEqual([MIN_TOOL_ROUNDS]);
|
|
201
|
+
expect(record.persistedPhases).toEqual([ExecutionPhase.EXECUTION_IN_PROGRESS, ExecutionPhase.EXECUTION_COMPLETED]);
|
|
202
|
+
const persisted = record.lastFullStatus!;
|
|
203
|
+
expect(persisted.error).toBe("");
|
|
204
|
+
const answer = persisted.messages.filter((m) => m.type === MessageType.MESSAGE_AI).at(-1);
|
|
205
|
+
expect(answer?.content).toBe(PICKED_UP.text);
|
|
206
|
+
});
|
|
147
207
|
});
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Invariant pinned: the runtime's blueprint phase throws, the runtime's
|
|
6
6
|
* catch takes its generic-error arm (`terminal-table.ts` `unexpectedErrorArm`),
|
|
7
7
|
* persists ONE EXECUTION_FAILED status carrying
|
|
8
|
-
* `status.error = "
|
|
8
|
+
* `status.error = "[Error] <message>"` and two system rows
|
|
9
9
|
* — the boilerplate "Internal system error occurred. Please contact support
|
|
10
10
|
* if this issue persists." and `"Error details: [Error] <message>"` — and
|
|
11
11
|
* RETURNS the slim status (the workflow reads the phase; a throw would make
|
|
@@ -14,10 +14,14 @@
|
|
|
14
14
|
*
|
|
15
15
|
* The row shape is one of the alignments #1096 made on purpose: the
|
|
16
16
|
* orchestrator wrote one row `"Error: [Error] <message>"`; the runtime's arm
|
|
17
|
-
* writes the boilerplate plus the details row.
|
|
18
|
-
* not move (the conformance suite pins it). The setup labels are the
|
|
17
|
+
* writes the boilerplate plus the details row. The setup labels are the
|
|
19
18
|
* runtime's resolution phases'.
|
|
20
19
|
*
|
|
20
|
+
* Golden hunk since stigmer#1122: `status.error` is `[Error] <message>`, the
|
|
21
|
+
* details row's text, where it was `Execution failed: [Error] <message>`.
|
|
22
|
+
* This arm alone framed the error; every other FAILED arm, and this
|
|
23
|
+
* harness's own thrown-turn classification, writes the failure unframed.
|
|
24
|
+
*
|
|
21
25
|
* Carried from `index.test.ts` ("returns EXECUTION_FAILED status when setup
|
|
22
26
|
* fails", "includes error message in failed status", "always returns a
|
|
23
27
|
* serializable result").
|
|
@@ -70,7 +74,7 @@ describe("ExecuteDeepAgent hermetic — resolution error", () => {
|
|
|
70
74
|
env.dispose();
|
|
71
75
|
});
|
|
72
76
|
|
|
73
|
-
it("fails with the fault's message in status.error and
|
|
77
|
+
it("fails with the fault's message in status.error and two system rows, and returns", async () => {
|
|
74
78
|
// ── Arrange ──────────────────────────────────────────────────────────────
|
|
75
79
|
const record = deepAgentExecutionRecord({ message: "Never reaches a model." });
|
|
76
80
|
const scenario = beginDeepAgentScenario({
|
|
@@ -101,7 +105,7 @@ describe("ExecuteDeepAgent hermetic — resolution error", () => {
|
|
|
101
105
|
|
|
102
106
|
// ── Assert: the copy ─────────────────────────────────────────────────────
|
|
103
107
|
const final = record.lastFullStatus!;
|
|
104
|
-
expect(final.error).toBe(`
|
|
108
|
+
expect(final.error).toBe(`[Error] ${CONTROL_PLANE_FAULT}`);
|
|
105
109
|
expect(final.messages.filter((m) => m.type === MessageType.MESSAGE_SYSTEM).map((m) => m.content)).toEqual([
|
|
106
110
|
"Internal system error occurred. Please contact support if this issue persists.",
|
|
107
111
|
`Error details: [Error] ${CONTROL_PLANE_FAULT}`,
|