@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
|
@@ -1,130 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ExecutionBudgetMiddleware: the round count, the stop it makes, and the
|
|
3
|
+
* advisories before it.
|
|
4
|
+
*
|
|
5
|
+
* Pins what `max_tool_rounds = N` means (#1113): exactly N model responses
|
|
6
|
+
* that propose tools, whatever the middleware stack; parallel calls in one
|
|
7
|
+
* response are one round; a response without tool calls, or a `Command`, is
|
|
8
|
+
* not a round; the stop replaces round N+1's model call (the handler is never
|
|
9
|
+
* called for it) and is the typed `ToolRoundLimitError` the native turn reads
|
|
10
|
+
* as `tool_call_limit`. The threshold advisory counts the same rounds, so it
|
|
11
|
+
* lands at its percentage of the budget the stop enforces. Periodic mode (the
|
|
12
|
+
* sub-agent stacks) keeps counting model calls and never stops.
|
|
13
|
+
*/
|
|
14
|
+
|
|
1
15
|
import { describe, it, expect, vi } from "vitest";
|
|
2
16
|
import { AIMessage } from "@langchain/core/messages";
|
|
17
|
+
import { Command } from "@langchain/langgraph";
|
|
3
18
|
import { createExecutionBudgetMiddleware } from "../execution-budget.js";
|
|
19
|
+
import type { ModelCallRequest, StigmerMiddleware } from "../types.js";
|
|
20
|
+
import { ToolRoundLimitError, UNBOUNDED_ADVISORY_TOOL_ROUNDS } from "../../shared/tool-rounds.js";
|
|
21
|
+
|
|
22
|
+
type ModelResponse = AIMessage | Command;
|
|
4
23
|
|
|
5
|
-
|
|
24
|
+
const REQUEST: ModelCallRequest = { model: {}, messages: [], state: {}, runtime: {} };
|
|
25
|
+
|
|
26
|
+
function textResponse(content = "hello"): AIMessage {
|
|
6
27
|
return new AIMessage({ content });
|
|
7
28
|
}
|
|
8
29
|
|
|
30
|
+
/** A response proposing `calls` tool calls: one round, however many calls. */
|
|
31
|
+
function toolResponse(round: number, calls = 1): AIMessage {
|
|
32
|
+
return new AIMessage({
|
|
33
|
+
content: `round ${round}`,
|
|
34
|
+
tool_calls: Array.from({ length: calls }, (_, i) => ({ id: `call-${round}-${i}`, name: "read_file", args: { file_path: `/f${round}-${i}.md` } })),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function handlerOf(respond: (call: number) => ModelResponse) {
|
|
39
|
+
let calls = 0;
|
|
40
|
+
return vi.fn(async (_request: ModelCallRequest): Promise<ModelResponse> => respond(calls++));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function callModel(mw: StigmerMiddleware, handler: ReturnType<typeof handlerOf>): Promise<ModelResponse> {
|
|
44
|
+
return mw.wrapModelCall!(REQUEST, handler);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The requests the handler saw that carried an advisory (the base request has no messages). */
|
|
48
|
+
function advisedCalls(handler: ReturnType<typeof handlerOf>): number[] {
|
|
49
|
+
return handler.mock.calls.flatMap((call, index) => (call[0].messages.length > 0 ? [index] : []));
|
|
50
|
+
}
|
|
51
|
+
|
|
9
52
|
describe("ExecutionBudgetMiddleware", () => {
|
|
10
|
-
describe("
|
|
11
|
-
it("
|
|
12
|
-
const mw = createExecutionBudgetMiddleware({
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
await mw.wrapModelCall!(request, handler);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// All calls should pass through without modification
|
|
25
|
-
expect(handler).toHaveBeenCalledTimes(7);
|
|
26
|
-
for (const call of handler.mock.calls) {
|
|
27
|
-
expect(call[0]).toBe(request);
|
|
28
|
-
}
|
|
53
|
+
describe("the round limit", () => {
|
|
54
|
+
it("runs exactly N tool rounds, then throws ToolRoundLimitError in place of the next model call", async () => {
|
|
55
|
+
const mw = createExecutionBudgetMiddleware({ maxToolRounds: 10, warningPct: 80 });
|
|
56
|
+
const handler = handlerOf((call) => toolResponse(call));
|
|
57
|
+
|
|
58
|
+
for (let i = 0; i < 10; i++) await callModel(mw, handler);
|
|
59
|
+
const stop = callModel(mw, handler);
|
|
60
|
+
|
|
61
|
+
await expect(stop).rejects.toBeInstanceOf(ToolRoundLimitError);
|
|
62
|
+
await expect(stop).rejects.toMatchObject({ toolRoundLimit: 10 });
|
|
63
|
+
expect(handler, "round 11's model call never happens").toHaveBeenCalledTimes(10);
|
|
29
64
|
});
|
|
30
65
|
|
|
31
|
-
it("
|
|
32
|
-
const mw = createExecutionBudgetMiddleware({
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
66
|
+
it("counts parallel tool calls in one response as one round", async () => {
|
|
67
|
+
const mw = createExecutionBudgetMiddleware({ maxToolRounds: 10, warningPct: 80 });
|
|
68
|
+
const handler = handlerOf((call) => toolResponse(call, 3));
|
|
69
|
+
|
|
70
|
+
for (let i = 0; i < 10; i++) await callModel(mw, handler);
|
|
71
|
+
|
|
72
|
+
await expect(callModel(mw, handler)).rejects.toBeInstanceOf(ToolRoundLimitError);
|
|
73
|
+
expect(handler).toHaveBeenCalledTimes(10);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("does not count a response without tool calls, nor a Command, as a round", async () => {
|
|
77
|
+
const mw = createExecutionBudgetMiddleware({ maxToolRounds: 10, warningPct: 80 });
|
|
78
|
+
const handler = handlerOf((call) => (call % 3 === 0 ? toolResponse(call) : call % 3 === 1 ? textResponse() : new Command({ goto: "tools" })));
|
|
79
|
+
|
|
80
|
+
// Calls 0, 3, …, 27 are the 10 tool rounds; the 18 between them are text
|
|
81
|
+
// responses and Commands. Call 28 is the first after round 10.
|
|
82
|
+
for (let i = 0; i < 28; i++) await callModel(mw, handler);
|
|
83
|
+
|
|
84
|
+
await expect(callModel(mw, handler)).rejects.toBeInstanceOf(ToolRoundLimitError);
|
|
85
|
+
expect(handler).toHaveBeenCalledTimes(28);
|
|
48
86
|
});
|
|
49
87
|
|
|
50
|
-
it("
|
|
51
|
-
const mw = createExecutionBudgetMiddleware({
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
88
|
+
it("never stops an unlimited turn (maxToolRounds null, the proto's 0/unset)", async () => {
|
|
89
|
+
const mw = createExecutionBudgetMiddleware({ maxToolRounds: null, warningPct: 80 });
|
|
90
|
+
const handler = handlerOf((call) => toolResponse(call));
|
|
91
|
+
|
|
92
|
+
for (let i = 0; i < UNBOUNDED_ADVISORY_TOOL_ROUNDS + 50; i++) await callModel(mw, handler);
|
|
93
|
+
|
|
94
|
+
expect(handler).toHaveBeenCalledTimes(UNBOUNDED_ADVISORY_TOOL_ROUNDS + 50);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("gives a new message a fresh budget: beforeAgent resets the count", async () => {
|
|
98
|
+
const mw = createExecutionBudgetMiddleware({ maxToolRounds: 10, warningPct: 80 });
|
|
99
|
+
const handler = handlerOf((call) => toolResponse(call));
|
|
100
|
+
|
|
101
|
+
for (let i = 0; i < 10; i++) await callModel(mw, handler);
|
|
102
|
+
await expect(callModel(mw, handler)).rejects.toBeInstanceOf(ToolRoundLimitError);
|
|
103
|
+
|
|
104
|
+
mw.beforeAgent!({}, {});
|
|
105
|
+
await callModel(mw, handler);
|
|
106
|
+
expect(handler).toHaveBeenCalledTimes(11);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("rejects a non-positive limit (unlimited is null, never 0)", () => {
|
|
110
|
+
expect(() => createExecutionBudgetMiddleware({ maxToolRounds: 0 })).toThrow("maxToolRounds");
|
|
67
111
|
});
|
|
68
112
|
});
|
|
69
113
|
|
|
70
|
-
describe("
|
|
71
|
-
it("
|
|
72
|
-
const mw = createExecutionBudgetMiddleware({
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
for (let i = 0; i < 12; i++) {
|
|
80
|
-
await mw.wrapModelCall!(request, handler);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Advisories queued at rounds 3, 6, 9 (delivered at rounds 4, 7, 10)
|
|
84
|
-
const callsWithAdvisory = handler.mock.calls.filter(
|
|
85
|
-
(call: unknown[]) => (call[0] as { messages: unknown[] }).messages.length > 0,
|
|
86
|
-
);
|
|
87
|
-
expect(callsWithAdvisory.length).toBeGreaterThanOrEqual(3);
|
|
114
|
+
describe("threshold mode", () => {
|
|
115
|
+
it("does not warn before the threshold", async () => {
|
|
116
|
+
const mw = createExecutionBudgetMiddleware({ maxToolRounds: 10, warningPct: 80 });
|
|
117
|
+
const handler = handlerOf((call) => toolResponse(call));
|
|
118
|
+
|
|
119
|
+
for (let i = 0; i < 8; i++) await callModel(mw, handler);
|
|
120
|
+
|
|
121
|
+
expect(advisedCalls(handler)).toEqual([]);
|
|
122
|
+
for (const call of handler.mock.calls) expect(call[0]).toBe(REQUEST);
|
|
88
123
|
});
|
|
89
124
|
|
|
90
|
-
it("
|
|
91
|
-
const mw = createExecutionBudgetMiddleware({
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
);
|
|
105
|
-
|
|
125
|
+
it("warns on the model call after the threshold's tool round, with the exact rounds left", async () => {
|
|
126
|
+
const mw = createExecutionBudgetMiddleware({ maxToolRounds: 10, warningPct: 80 });
|
|
127
|
+
const handler = handlerOf((call) => toolResponse(call));
|
|
128
|
+
|
|
129
|
+
for (let i = 0; i < 9; i++) await callModel(mw, handler);
|
|
130
|
+
|
|
131
|
+
// 80% of 10 = round 8, so the 9th call (index 8) carries the advisory.
|
|
132
|
+
expect(advisedCalls(handler)).toEqual([8]);
|
|
133
|
+
const advisory = handler.mock.calls[8][0].messages[0];
|
|
134
|
+
expect(advisory).toMatchObject({ content: expect.stringContaining("8 of 10 tool rounds used, 2 remaining") });
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("counts tool rounds, not model calls, toward the threshold", async () => {
|
|
138
|
+
const mw = createExecutionBudgetMiddleware({ maxToolRounds: 10, warningPct: 80 });
|
|
139
|
+
const handler = handlerOf(() => textResponse());
|
|
140
|
+
|
|
141
|
+
for (let i = 0; i < 12; i++) await callModel(mw, handler);
|
|
142
|
+
|
|
143
|
+
expect(advisedCalls(handler)).toEqual([]);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("fires the warning only once", async () => {
|
|
147
|
+
const mw = createExecutionBudgetMiddleware({ maxToolRounds: 20, warningPct: 80 });
|
|
148
|
+
const handler = handlerOf((call) => toolResponse(call));
|
|
149
|
+
|
|
150
|
+
for (let i = 0; i < 20; i++) await callModel(mw, handler);
|
|
151
|
+
|
|
152
|
+
expect(advisedCalls(handler)).toEqual([16]);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("warns toward the unbounded advisory figure when the turn has no limit", async () => {
|
|
156
|
+
const mw = createExecutionBudgetMiddleware({ maxToolRounds: null, warningPct: 80 });
|
|
157
|
+
const handler = handlerOf((call) => toolResponse(call));
|
|
158
|
+
const threshold = Math.floor(UNBOUNDED_ADVISORY_TOOL_ROUNDS * 0.8);
|
|
159
|
+
|
|
160
|
+
for (let i = 0; i < threshold + 1; i++) await callModel(mw, handler);
|
|
161
|
+
|
|
162
|
+
expect(advisedCalls(handler)).toEqual([threshold]);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("resets on beforeAgent", async () => {
|
|
166
|
+
const mw = createExecutionBudgetMiddleware({ maxToolRounds: 10, warningPct: 50 });
|
|
167
|
+
const handler = handlerOf((call) => toolResponse(call));
|
|
168
|
+
|
|
169
|
+
for (let i = 0; i < 6; i++) await callModel(mw, handler);
|
|
170
|
+
mw.beforeAgent!({}, {});
|
|
171
|
+
handler.mockClear();
|
|
172
|
+
|
|
173
|
+
await callModel(mw, handler);
|
|
174
|
+
expect(handler.mock.calls[0][0]).toBe(REQUEST);
|
|
106
175
|
});
|
|
107
176
|
});
|
|
108
177
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
178
|
+
describe("periodic mode", () => {
|
|
179
|
+
it("injects advisory every N model calls", async () => {
|
|
180
|
+
const mw = createExecutionBudgetMiddleware({ warningInterval: 3, maxWarnings: 4 });
|
|
181
|
+
const handler = handlerOf(() => textResponse());
|
|
182
|
+
|
|
183
|
+
for (let i = 0; i < 12; i++) await callModel(mw, handler);
|
|
184
|
+
|
|
185
|
+
// Advisories queued at calls 3, 6, 9, 12 (delivered at calls 4, 7, 10).
|
|
186
|
+
expect(advisedCalls(handler)).toEqual([3, 6, 9]);
|
|
113
187
|
});
|
|
114
|
-
// 18/6 = 3 rounds, 50% = round 1 (min 3)
|
|
115
|
-
const handler = vi.fn().mockResolvedValue(makeAIMessage());
|
|
116
|
-
const request = { model: {}, messages: [], state: {}, runtime: {} } as any;
|
|
117
188
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
189
|
+
it("stops advising after maxWarnings", async () => {
|
|
190
|
+
const mw = createExecutionBudgetMiddleware({ warningInterval: 2, maxWarnings: 2 });
|
|
191
|
+
const handler = handlerOf(() => textResponse());
|
|
121
192
|
|
|
122
|
-
|
|
123
|
-
|
|
193
|
+
for (let i = 0; i < 20; i++) await callModel(mw, handler);
|
|
194
|
+
|
|
195
|
+
expect(advisedCalls(handler)).toHaveLength(2);
|
|
196
|
+
});
|
|
124
197
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
198
|
+
it("never stops a turn when no limit is configured (the sub-agent stacks)", async () => {
|
|
199
|
+
const mw = createExecutionBudgetMiddleware({ warningInterval: 30, maxWarnings: 4 });
|
|
200
|
+
const handler = handlerOf((call) => toolResponse(call));
|
|
201
|
+
|
|
202
|
+
for (let i = 0; i < 200; i++) await callModel(mw, handler);
|
|
203
|
+
|
|
204
|
+
expect(handler).toHaveBeenCalledTimes(200);
|
|
205
|
+
});
|
|
128
206
|
});
|
|
129
207
|
|
|
130
208
|
it("rejects invalid warningPct", () => {
|
|
@@ -7,9 +7,12 @@
|
|
|
7
7
|
* usage delta the adapter reports moves the runtime's estimate, and when the
|
|
8
8
|
* estimate crosses the cap the runtime stops the turn and settles TERMINATED
|
|
9
9
|
* with the cost-limit copy. The graph learns of the stop through its abort
|
|
10
|
-
* signal, the same way it learns of a platform STOP or a stall.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* signal, the same way it learns of a platform STOP or a stall. The
|
|
11
|
+
* middleware warns inside the graph; the runtime enforces, because the
|
|
12
|
+
* running estimate advances only in its usage accumulator. The round budget
|
|
13
|
+
* is the one budget enforced where it is counted (`execution-budget.ts`): a
|
|
14
|
+
* round is visible only at the model call, and stopping there is what makes
|
|
15
|
+
* it exact.
|
|
13
16
|
*
|
|
14
17
|
* Until #1096 this module was `cost-cap.ts` and did both: it also
|
|
15
18
|
* blocked every tool call once the running cost passed the cap and gave the
|
|
@@ -1,26 +1,41 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Execution budget middleware for autonomous agents
|
|
2
|
+
* Execution budget middleware for autonomous agents: `max_tool_rounds`' own
|
|
3
|
+
* enforcement, and the advisories that warn the model before it.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
+
* The stop (whenever `maxToolRounds` is set):
|
|
6
|
+
* A round is one model response that proposes one or more tool calls
|
|
7
|
+
* (parallel calls are one round), counted here as each response returns.
|
|
8
|
+
* Once N rounds have been proposed, the next model call does not happen:
|
|
9
|
+
* `wrapModelCall` throws `ToolRoundLimitError` in its place, the graph
|
|
10
|
+
* ends on round N's tool results, and the native turn reads the error as
|
|
11
|
+
* the `tool_call_limit` outcome (`shared/tool-rounds.ts`
|
|
12
|
+
* `isToolCallBudgetStop`). Stopping before a model call, never between a
|
|
13
|
+
* model call and its tools, is what makes N exact and leaves no proposed
|
|
14
|
+
* call unexecuted in the checkpoint. It is the one stop a middleware makes
|
|
15
|
+
* (`middleware/index.ts`): the runtime still sees it, as an outcome.
|
|
16
|
+
*
|
|
17
|
+
* Two advisory modes:
|
|
5
18
|
*
|
|
6
19
|
* Threshold mode (default, warningInterval=null):
|
|
7
|
-
* Fires a single SystemMessage at a
|
|
8
|
-
*
|
|
20
|
+
* Fires a single SystemMessage at a percentage of the round budget
|
|
21
|
+
* (`maxToolRounds`, or the unbounded advisory figure when unset).
|
|
9
22
|
*
|
|
10
23
|
* Periodic mode (warningInterval set):
|
|
11
|
-
* Fires a SystemMessage every N model
|
|
12
|
-
* up to maxWarnings times.
|
|
24
|
+
* Fires a SystemMessage every N model calls with escalating urgency,
|
|
25
|
+
* up to maxWarnings times. Sub-agent stacks use it, with no stop.
|
|
13
26
|
*
|
|
14
27
|
* Uses wrapModelCall (not afterModel) to safely prepend advisory
|
|
15
28
|
* messages to the model's input, avoiding the AIMessage/ToolMessage
|
|
16
29
|
* ordering violation that would occur if we injected between a
|
|
17
|
-
* tool_use AIMessage and its corresponding ToolMessage
|
|
30
|
+
* tool_use AIMessage and its corresponding ToolMessage, and so that the
|
|
31
|
+
* count and the stop add no graph node of their own: a node per round is
|
|
32
|
+
* exactly the cost that made the old super-step budget drift.
|
|
18
33
|
*/
|
|
19
34
|
|
|
20
|
-
import { SystemMessage } from "@langchain/core/messages";
|
|
35
|
+
import { AIMessage, SystemMessage } from "@langchain/core/messages";
|
|
21
36
|
import type { StigmerMiddleware, ExecutionBudgetConfig } from "./types.js";
|
|
37
|
+
import { ToolRoundLimitError, UNBOUNDED_ADVISORY_TOOL_ROUNDS } from "../shared/tool-rounds.js";
|
|
22
38
|
|
|
23
|
-
const DEFAULT_RECURSION_LIMIT = 6000;
|
|
24
39
|
const DEFAULT_WARNING_PCT = 80;
|
|
25
40
|
const MIN_WARNING_PCT = 50;
|
|
26
41
|
const MAX_WARNING_PCT = 95;
|
|
@@ -46,46 +61,51 @@ const PERIODIC_MESSAGES: readonly string[] = [
|
|
|
46
61
|
"unless absolutely essential to your conclusion.",
|
|
47
62
|
];
|
|
48
63
|
|
|
49
|
-
function computeWarningRound(
|
|
50
|
-
const
|
|
51
|
-
const threshold = Math.floor(estimatedTotalRounds * warningPct / 100);
|
|
64
|
+
function computeWarningRound(roundBudget: number, warningPct: number): number {
|
|
65
|
+
const threshold = Math.floor(roundBudget * warningPct / 100);
|
|
52
66
|
return Math.max(threshold, MIN_ROUNDS_BEFORE_WARNING);
|
|
53
67
|
}
|
|
54
68
|
|
|
69
|
+
/** Whether a model call's response proposed tools: the round definition (a `Command` response is not one). */
|
|
70
|
+
function isToolRound(response: unknown): boolean {
|
|
71
|
+
return AIMessage.isInstance(response) && (response.tool_calls?.length ?? 0) > 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
55
74
|
export function createExecutionBudgetMiddleware(
|
|
56
75
|
config: Partial<ExecutionBudgetConfig> = {},
|
|
57
76
|
): StigmerMiddleware {
|
|
58
|
-
const
|
|
77
|
+
const maxToolRounds = config.maxToolRounds ?? null;
|
|
78
|
+
const roundBudget = maxToolRounds ?? UNBOUNDED_ADVISORY_TOOL_ROUNDS;
|
|
59
79
|
const warningPct = config.warningPct ?? DEFAULT_WARNING_PCT;
|
|
60
80
|
const warningInterval = config.warningInterval ?? null;
|
|
61
81
|
const maxWarnings = config.maxWarnings ?? 4;
|
|
62
82
|
|
|
63
83
|
const isPeriodic = warningInterval !== null;
|
|
64
84
|
|
|
85
|
+
if (maxToolRounds !== null && maxToolRounds <= 0) {
|
|
86
|
+
throw new Error(`maxToolRounds must be positive or null, got ${maxToolRounds}`);
|
|
87
|
+
}
|
|
65
88
|
if (isPeriodic) {
|
|
66
89
|
if (warningInterval <= 0) throw new Error(`warningInterval must be positive, got ${warningInterval}`);
|
|
67
90
|
if (maxWarnings <= 0) throw new Error(`maxWarnings must be positive, got ${maxWarnings}`);
|
|
68
|
-
} else {
|
|
69
|
-
|
|
70
|
-
throw new Error(`warningPct must be between ${MIN_WARNING_PCT} and ${MAX_WARNING_PCT}, got ${warningPct}`);
|
|
71
|
-
}
|
|
72
|
-
if (recursionLimit <= 0) throw new Error(`recursionLimit must be positive, got ${recursionLimit}`);
|
|
91
|
+
} else if (warningPct < MIN_WARNING_PCT || warningPct > MAX_WARNING_PCT) {
|
|
92
|
+
throw new Error(`warningPct must be between ${MIN_WARNING_PCT} and ${MAX_WARNING_PCT}, got ${warningPct}`);
|
|
73
93
|
}
|
|
74
94
|
|
|
75
95
|
let modelRoundCount = 0;
|
|
96
|
+
let toolRoundCount = 0;
|
|
76
97
|
let warningCount = 0;
|
|
77
98
|
let nextWarningRound = isPeriodic
|
|
78
99
|
? warningInterval!
|
|
79
|
-
: computeWarningRound(
|
|
100
|
+
: computeWarningRound(roundBudget, warningPct);
|
|
80
101
|
let pendingAdvisory: SystemMessage | null = null;
|
|
81
102
|
|
|
82
103
|
function createThresholdWarning(): SystemMessage {
|
|
83
|
-
const
|
|
84
|
-
const remaining = Math.max(estimatedTotal - modelRoundCount, 0);
|
|
104
|
+
const remaining = Math.max(roundBudget - toolRoundCount, 0);
|
|
85
105
|
return new SystemMessage({
|
|
86
106
|
content:
|
|
87
|
-
`You are approaching the
|
|
88
|
-
`(
|
|
107
|
+
`You are approaching the tool-round limit for this message ` +
|
|
108
|
+
`(${toolRoundCount} of ${roundBudget} tool rounds used, ${remaining} remaining). ` +
|
|
89
109
|
`Prioritize completing your current task. Summarize results ` +
|
|
90
110
|
`and any remaining work so the user can continue in the next message.`,
|
|
91
111
|
});
|
|
@@ -113,12 +133,11 @@ export function createExecutionBudgetMiddleware(
|
|
|
113
133
|
}
|
|
114
134
|
} else {
|
|
115
135
|
if (warningCount > 0) return;
|
|
116
|
-
if (
|
|
136
|
+
if (toolRoundCount >= nextWarningRound) {
|
|
117
137
|
warningCount = 1;
|
|
118
|
-
const estimatedTotal = Math.floor(recursionLimit / 6);
|
|
119
138
|
console.warn(
|
|
120
|
-
`[ExecutionBudget] WARNING: round ${
|
|
121
|
-
`(
|
|
139
|
+
`[ExecutionBudget] WARNING: tool round ${toolRoundCount} of ${roundBudget} ` +
|
|
140
|
+
`(${warningPct}% threshold)`,
|
|
122
141
|
);
|
|
123
142
|
pendingAdvisory = createThresholdWarning();
|
|
124
143
|
}
|
|
@@ -130,14 +149,23 @@ export function createExecutionBudgetMiddleware(
|
|
|
130
149
|
|
|
131
150
|
beforeAgent() {
|
|
132
151
|
modelRoundCount = 0;
|
|
152
|
+
toolRoundCount = 0;
|
|
133
153
|
warningCount = 0;
|
|
134
154
|
pendingAdvisory = null;
|
|
135
155
|
nextWarningRound = isPeriodic
|
|
136
156
|
? warningInterval!
|
|
137
|
-
: computeWarningRound(
|
|
157
|
+
: computeWarningRound(roundBudget, warningPct);
|
|
138
158
|
},
|
|
139
159
|
|
|
140
160
|
async wrapModelCall(request, handler) {
|
|
161
|
+
if (maxToolRounds !== null && toolRoundCount >= maxToolRounds) {
|
|
162
|
+
console.log(
|
|
163
|
+
`[ExecutionBudget] Tool-round limit reached: ${toolRoundCount}/${maxToolRounds}; ` +
|
|
164
|
+
`ending the turn before the next model call`,
|
|
165
|
+
);
|
|
166
|
+
throw new ToolRoundLimitError(maxToolRounds);
|
|
167
|
+
}
|
|
168
|
+
|
|
141
169
|
let effectiveRequest = request;
|
|
142
170
|
|
|
143
171
|
if (pendingAdvisory !== null) {
|
|
@@ -150,6 +178,7 @@ export function createExecutionBudgetMiddleware(
|
|
|
150
178
|
const response = await handler(effectiveRequest);
|
|
151
179
|
|
|
152
180
|
modelRoundCount++;
|
|
181
|
+
if (isToolRound(response)) toolRoundCount++;
|
|
153
182
|
evaluateBudget();
|
|
154
183
|
|
|
155
184
|
return response;
|
|
@@ -162,13 +191,10 @@ export function createExecutionBudgetMiddleware(
|
|
|
162
191
|
`${warningCount}/${maxWarnings} advisories (interval=${warningInterval})`,
|
|
163
192
|
);
|
|
164
193
|
} else {
|
|
165
|
-
const
|
|
166
|
-
const pctUsed = estimatedTotal > 0
|
|
167
|
-
? Math.floor(modelRoundCount * 100 / estimatedTotal)
|
|
168
|
-
: 0;
|
|
194
|
+
const pctUsed = Math.floor(toolRoundCount * 100 / roundBudget);
|
|
169
195
|
console.log(
|
|
170
|
-
`[ExecutionBudget] Summary (threshold): ${
|
|
171
|
-
`of
|
|
196
|
+
`[ExecutionBudget] Summary (threshold): ${toolRoundCount} tool rounds ` +
|
|
197
|
+
`of ${roundBudget} (${pctUsed}% used), warnings=${warningCount}`,
|
|
172
198
|
);
|
|
173
199
|
}
|
|
174
200
|
},
|
package/src/middleware/index.ts
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* since issue #754 — FIRST, so every downstream middleware observes
|
|
9
9
|
* canonical workspace-absolute paths)
|
|
10
10
|
* 1. Loop detection (always)
|
|
11
|
-
* 2. Execution budget (always — advises at ~80% of
|
|
12
|
-
*
|
|
11
|
+
* 2. Execution budget (always — counts tool rounds, advises at ~80% of
|
|
12
|
+
* `max_tool_rounds` and ends the turn at it)
|
|
13
13
|
* 3. Tool intent (always — bind-time shell schema extension, issue #276)
|
|
14
14
|
* 4. Tool truncation (always)
|
|
15
15
|
* 5. Approval gate (conditional: absent under the global bypass)
|
|
@@ -19,11 +19,14 @@
|
|
|
19
19
|
* 8. OTel spans (always, no-op when OTel not configured)
|
|
20
20
|
*
|
|
21
21
|
* Every middleware here either shapes the graph's tool surface or ADVISES
|
|
22
|
-
* the model; none of them stops the run
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
22
|
+
* the model; none of them stops the run, except the execution budget, which
|
|
23
|
+
* is `max_tool_rounds`' own enforcement: it ends the turn as the adapter's
|
|
24
|
+
* `tool_call_limit` outcome, the job LangGraph's recursion limit held until
|
|
25
|
+
* #1113, so the runtime sees it as an outcome and never as a completion.
|
|
26
|
+
* Every other stop — the platform's, the cost cap's, a stall's — is the turn
|
|
27
|
+
* runtime's, delivered to the graph through its abort signal (since #1096;
|
|
28
|
+
* until then a graceful-stop middleware and the cost cap's tool block were
|
|
29
|
+
* second stops inside the graph that the runtime could not see).
|
|
27
30
|
*
|
|
28
31
|
* Ahead of this stack the parent graph adds the framework's to-do list
|
|
29
32
|
* middleware (`execute-deep-agent/turn-setup.ts`): it is langchain's, not
|
package/src/middleware/types.ts
CHANGED
|
@@ -71,7 +71,12 @@ export interface LoopDetectionConfig {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
export interface ExecutionBudgetConfig {
|
|
74
|
-
|
|
74
|
+
/**
|
|
75
|
+
* The turn's round budget, already clamped (`shared/tool-rounds.ts`
|
|
76
|
+
* `resolveToolRoundLimit`); the middleware ends the turn when it is spent.
|
|
77
|
+
* `null` is unlimited: the advisory still fires, nothing is stopped.
|
|
78
|
+
*/
|
|
79
|
+
readonly maxToolRounds: number | null;
|
|
75
80
|
readonly warningPct: number;
|
|
76
81
|
readonly warningInterval: number | null;
|
|
77
82
|
readonly maxWarnings: number;
|
package/src/runner-manager.ts
CHANGED
|
@@ -722,9 +722,10 @@ export function mapManagerOptionsToConfig(
|
|
|
722
722
|
temporalConnection:
|
|
723
723
|
options.temporalConnection ?? loadTemporalConnectionConfig(getRunnerSecret),
|
|
724
724
|
stigmerBackendEndpoint: normalizeEndpoint(options.stigmerEndpoint),
|
|
725
|
-
// Env-only like the env-loaded path (loadConfig): the bridge
|
|
726
|
-
//
|
|
725
|
+
// Env-only like the env-loaded path (loadConfig): the bridge and public
|
|
726
|
+
// endpoints are deployment topology, not per-session state.
|
|
727
727
|
mcpBridgeEndpoint: process.env.STIGMER_MCP_BRIDGE_ENDPOINT ?? null,
|
|
728
|
+
mcpPublicEndpoint: process.env.STIGMER_MCP_PUBLIC_ENDPOINT || null,
|
|
728
729
|
stigmerTokenRef: tokenRef,
|
|
729
730
|
stigmerRunnerTokenRef: runnerTokenRef,
|
|
730
731
|
// The manager's proxy credential is the minted runner token once adopted
|