@stigmer/runner 3.0.8-dev.20260613085218 → 3.0.9-dev.20260615145121
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/dist/.build-fingerprint +1 -1
- package/dist/activities/execute-cursor/hook-script.d.ts +23 -12
- package/dist/activities/execute-cursor/hook-script.js +85 -51
- package/dist/activities/execute-cursor/hook-script.js.map +1 -1
- package/dist/activities/execute-cursor/index.js +220 -79
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/message-translator.d.ts +51 -9
- package/dist/activities/execute-cursor/message-translator.js +146 -20
- package/dist/activities/execute-cursor/message-translator.js.map +1 -1
- package/dist/activities/execute-cursor/persist-decision.d.ts +42 -0
- package/dist/activities/execute-cursor/persist-decision.js +30 -0
- package/dist/activities/execute-cursor/persist-decision.js.map +1 -0
- package/dist/activities/execute-cursor/prompt-builder.d.ts +25 -0
- package/dist/activities/execute-cursor/prompt-builder.js +54 -0
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-cursor/workspace-setup.d.ts +8 -2
- package/dist/activities/execute-cursor/workspace-setup.js +62 -30
- package/dist/activities/execute-cursor/workspace-setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/index.js +15 -5
- package/dist/activities/execute-deep-agent/index.js.map +1 -1
- package/dist/activities/execute-deep-agent/status-builder-shared.d.ts +0 -1
- package/dist/activities/execute-deep-agent/status-builder-shared.js +32 -8
- package/dist/activities/execute-deep-agent/status-builder-shared.js.map +1 -1
- package/dist/activities/execute-deep-agent/status-builder.js +4 -5
- package/dist/activities/execute-deep-agent/status-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/streaming-v3.js +4 -5
- package/dist/activities/execute-deep-agent/streaming-v3.js.map +1 -1
- package/dist/activities/execute-deep-agent/streaming.d.ts +9 -1
- package/dist/activities/execute-deep-agent/streaming.js +4 -5
- package/dist/activities/execute-deep-agent/streaming.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-tracker.js +4 -5
- package/dist/activities/execute-deep-agent/subagent-tracker.js.map +1 -1
- package/dist/activities/execute-deep-agent/v3-status-builder.js +6 -5
- package/dist/activities/execute-deep-agent/v3-status-builder.js.map +1 -1
- package/dist/config.d.ts +21 -0
- package/dist/config.js +12 -0
- package/dist/config.js.map +1 -1
- package/dist/in-flight.d.ts +35 -0
- package/dist/in-flight.js +61 -0
- package/dist/in-flight.js.map +1 -0
- package/dist/runner-manager.d.ts +2 -0
- package/dist/runner-manager.js +90 -29
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.d.ts +2 -0
- package/dist/runner.js +2 -0
- package/dist/runner.js.map +1 -1
- package/dist/shared/grpc-retry.d.ts +9 -20
- package/dist/shared/grpc-retry.js +9 -52
- package/dist/shared/grpc-retry.js.map +1 -1
- package/dist/shared/stall-watchdog.d.ts +68 -0
- package/dist/shared/stall-watchdog.js +102 -0
- package/dist/shared/stall-watchdog.js.map +1 -0
- package/dist/shared/status-offload.d.ts +84 -0
- package/dist/shared/status-offload.js +292 -0
- package/dist/shared/status-offload.js.map +1 -0
- package/dist/shared/status.d.ts +34 -3
- package/dist/shared/status.js +102 -9
- package/dist/shared/status.js.map +1 -1
- package/dist/{activities/execute-deep-agent → shared}/streaming-scheduler.d.ts +4 -0
- package/dist/{activities/execute-deep-agent → shared}/streaming-scheduler.js +4 -0
- package/dist/shared/streaming-scheduler.js.map +1 -0
- package/package.json +2 -2
- package/src/__tests__/config.test.ts +8 -0
- package/src/__tests__/in-flight.test.ts +84 -0
- package/src/activities/__tests__/classify-tool-approvals.test.ts +1 -0
- package/src/activities/__tests__/discover-mcp-server.test.ts +1 -0
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +74 -0
- package/src/activities/execute-cursor/__tests__/hook-script.test.ts +90 -15
- package/src/activities/execute-cursor/__tests__/message-translator.test.ts +124 -12
- package/src/activities/execute-cursor/__tests__/persist-decision.test.ts +99 -0
- package/src/activities/execute-cursor/__tests__/tool-result-image.test.ts +244 -0
- package/src/activities/execute-cursor/__tests__/workspace-setup.test.ts +53 -4
- package/src/activities/execute-cursor/hook-script.ts +85 -51
- package/src/activities/execute-cursor/index.ts +187 -38
- package/src/activities/execute-cursor/message-translator.ts +146 -20
- package/src/activities/execute-cursor/persist-decision.ts +54 -0
- package/src/activities/execute-cursor/prompt-builder.ts +59 -0
- package/src/activities/execute-cursor/workspace-setup.ts +76 -44
- package/src/activities/execute-deep-agent/__tests__/index.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/status-builder-shared.test.ts +66 -0
- package/src/activities/execute-deep-agent/__tests__/status-builder.test.ts +6 -3
- package/src/activities/execute-deep-agent/__tests__/streaming-v3.test.ts +70 -0
- package/src/activities/execute-deep-agent/index.ts +17 -5
- package/src/activities/execute-deep-agent/status-builder-shared.ts +27 -5
- package/src/activities/execute-deep-agent/status-builder.ts +3 -5
- package/src/activities/execute-deep-agent/streaming-v3.ts +5 -5
- package/src/activities/execute-deep-agent/streaming.ts +14 -5
- package/src/activities/execute-deep-agent/subagent-tracker.ts +4 -5
- package/src/activities/execute-deep-agent/v3-status-builder.ts +5 -5
- package/src/config.ts +27 -0
- package/src/in-flight.ts +71 -0
- package/src/runner-manager.ts +127 -33
- package/src/runner.ts +6 -0
- package/src/shared/__tests__/artifact-storage.test.ts +1 -0
- package/src/shared/__tests__/grpc-retry-extended.test.ts +6 -144
- package/src/shared/__tests__/grpc-retry.test.ts +5 -134
- package/src/shared/__tests__/stall-watchdog.test.ts +193 -0
- package/src/shared/__tests__/status-offload.test.ts +256 -0
- package/src/shared/__tests__/status.test.ts +199 -0
- package/src/shared/grpc-retry.ts +9 -72
- package/src/shared/stall-watchdog.ts +122 -0
- package/src/shared/status-offload.ts +342 -0
- package/src/shared/status.ts +142 -8
- package/src/{activities/execute-deep-agent → shared}/streaming-scheduler.ts +4 -0
- package/dist/activities/execute-deep-agent/streaming-scheduler.js.map +0 -1
- /package/src/{activities/execute-deep-agent → shared}/__tests__/streaming-scheduler.test.ts +0 -0
package/dist/shared/status.js
CHANGED
|
@@ -10,23 +10,116 @@
|
|
|
10
10
|
import { create, toJson } from "@bufbuild/protobuf";
|
|
11
11
|
import { AgentExecutionStatusSchema, SetupProgressSchema, } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
12
12
|
import { ExecutionControlSignal, ExecutionPhase, } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
13
|
+
import { offloadOversizedToolOutputs, enforceStatusSizeLimit, STATUS_PAYLOAD_HARD_LIMIT_BYTES, } from "./status-offload.js";
|
|
14
|
+
import { isRetryableError, isTerminalError, } from "./grpc-retry.js";
|
|
13
15
|
export function utcTimestamp() {
|
|
14
16
|
return new Date().toISOString().replace("+00:00", "Z");
|
|
15
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* A connect-rpc `resource_exhausted` (gRPC code 8) — the payload exceeded the
|
|
20
|
+
* server's receive cap. Matched by code and, defensively, by the message buf's
|
|
21
|
+
* size check emits ("exceeds maximum size").
|
|
22
|
+
*/
|
|
23
|
+
function isPayloadTooLarge(err) {
|
|
24
|
+
if (typeof err === "object" && err !== null && "code" in err) {
|
|
25
|
+
if (err.code === 8)
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
29
|
+
return /resource_exhausted|exceeds maximum size/i.test(msg);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Default transient-retry policy: three attempts with exponential backoff.
|
|
33
|
+
* Applied to every persist so a transient drop (server restart, brief
|
|
34
|
+
* unavailability) recovers silently instead of dropping a status update.
|
|
35
|
+
*/
|
|
36
|
+
const DEFAULT_PERSIST_RETRY = {
|
|
37
|
+
baseDelayMs: 100,
|
|
38
|
+
backoffFactor: 2,
|
|
39
|
+
maxRetries: 3,
|
|
40
|
+
};
|
|
41
|
+
function defaultDelay(ms) {
|
|
42
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
43
|
+
}
|
|
16
44
|
/**
|
|
17
45
|
* Persist execution status via gRPC. Returns the control signal from the
|
|
18
|
-
* backend (e.g. STOP to abort the execution).
|
|
19
|
-
*
|
|
46
|
+
* backend (e.g. STOP to abort the execution).
|
|
47
|
+
*
|
|
48
|
+
* This is the single, unforgeable chokepoint for status persistence across
|
|
49
|
+
* every harness (streaming and terminal, native and Cursor). Routing all
|
|
50
|
+
* persistence through here guarantees three invariants that must never be
|
|
51
|
+
* skipped by an individual call site:
|
|
52
|
+
*
|
|
53
|
+
* 1. Size bounding — when `offload` is supplied, oversized tool outputs are
|
|
54
|
+
* spilled to artifact storage first (the path that makes screenshots
|
|
55
|
+
* render); an aggregate backstop then runs unconditionally so callers
|
|
56
|
+
* without storage are still protected from the 4 MiB gRPC cap.
|
|
57
|
+
* 2. Transient resilience — `Unavailable`/`DeadlineExceeded` back off and
|
|
58
|
+
* retry (default three attempts); terminal errors never retry.
|
|
59
|
+
* 3. Oversize recovery — a `resource_exhausted` rejection is never silently
|
|
60
|
+
* swallowed: we hard-elide and retry once, so an oversized payload can
|
|
61
|
+
* never freeze persistence invisibly (the bug that stalled the UI).
|
|
20
62
|
*/
|
|
21
|
-
export async function persistStatus(client, executionId, status) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
63
|
+
export async function persistStatus(client, executionId, status, options = {}) {
|
|
64
|
+
const { offload, retry } = options;
|
|
65
|
+
if (offload) {
|
|
66
|
+
try {
|
|
67
|
+
await offloadOversizedToolOutputs(status, offload);
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
console.warn(`[persistStatus] ${executionId}: tool-output offload failed (non-fatal): ${err}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (enforceStatusSizeLimit(status)) {
|
|
74
|
+
console.warn(`[persistStatus] ${executionId}: status exceeded the soft size limit; ` +
|
|
75
|
+
`elided oversized inline fields to fit under the gRPC cap`);
|
|
25
76
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
77
|
+
const baseDelayMs = retry?.baseDelayMs ?? DEFAULT_PERSIST_RETRY.baseDelayMs;
|
|
78
|
+
const backoffFactor = retry?.backoffFactor ?? DEFAULT_PERSIST_RETRY.backoffFactor;
|
|
79
|
+
const maxRetries = retry?.maxRetries ?? DEFAULT_PERSIST_RETRY.maxRetries;
|
|
80
|
+
const delay = retry?.delayFn ?? defaultDelay;
|
|
81
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
82
|
+
try {
|
|
83
|
+
const response = await client.updateStatus(executionId, status);
|
|
84
|
+
return response.signal;
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
// Oversize rejection is recoverable exactly once and out-of-band from the
|
|
88
|
+
// transient budget: hard-elide the largest inline fields and retry now.
|
|
89
|
+
if (isPayloadTooLarge(err)) {
|
|
90
|
+
console.error(`[persistStatus] ${executionId}: payload rejected as too large despite ` +
|
|
91
|
+
`the size guard; hard-eliding and retrying once`);
|
|
92
|
+
enforceStatusSizeLimit(status, STATUS_PAYLOAD_HARD_LIMIT_BYTES);
|
|
93
|
+
try {
|
|
94
|
+
const response = await client.updateStatus(executionId, status);
|
|
95
|
+
return response.signal;
|
|
96
|
+
}
|
|
97
|
+
catch (retryErr) {
|
|
98
|
+
console.error(`[persistStatus] ${executionId}: still failing after hard elide:`, retryErr);
|
|
99
|
+
return ExecutionControlSignal.UNSPECIFIED;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Terminal errors (invalid argument, not found, permission denied) are
|
|
103
|
+
// deterministic — retrying cannot help.
|
|
104
|
+
if (isTerminalError(err)) {
|
|
105
|
+
console.error(`[persistStatus] ${executionId}: terminal error persisting status ` +
|
|
106
|
+
`(attempt ${attempt + 1}/${maxRetries + 1}): ${err}`);
|
|
107
|
+
return ExecutionControlSignal.UNSPECIFIED;
|
|
108
|
+
}
|
|
109
|
+
// Transient errors back off and retry until the budget is exhausted.
|
|
110
|
+
if (isRetryableError(err) && attempt < maxRetries) {
|
|
111
|
+
const delayMs = baseDelayMs * Math.pow(backoffFactor, attempt);
|
|
112
|
+
console.warn(`[persistStatus] ${executionId}: retryable error ` +
|
|
113
|
+
`(attempt ${attempt + 1}/${maxRetries + 1}, retry in ${delayMs}ms): ${err}`);
|
|
114
|
+
await delay(delayMs);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
// Non-retryable, non-terminal (e.g. a bare Error), or retries exhausted.
|
|
118
|
+
console.error(`Failed to persist status for ${executionId}:`, err);
|
|
119
|
+
return ExecutionControlSignal.UNSPECIFIED;
|
|
120
|
+
}
|
|
29
121
|
}
|
|
122
|
+
return ExecutionControlSignal.UNSPECIFIED;
|
|
30
123
|
}
|
|
31
124
|
/**
|
|
32
125
|
* Report a setup progress phase (e.g. "Resolving MCP servers") so the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"status.js","sourceRoot":"","sources":["../../src/shared/status.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,EACL,0BAA0B,EAC1B,mBAAmB,GACpB,MAAM,6DAA6D,CAAC;AAErE,OAAO,EACL,sBAAsB,EACtB,cAAc,GACf,MAAM,8DAA8D,CAAC;
|
|
1
|
+
{"version":3,"file":"status.js","sourceRoot":"","sources":["../../src/shared/status.ts"],"names":[],"mappings":"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"}
|
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
* OR firstUpdate
|
|
14
14
|
*
|
|
15
15
|
* Ported from Python StreamingUpdateScheduler.
|
|
16
|
+
*
|
|
17
|
+
* Shared infrastructure: both the native deep-agent harness and the Cursor
|
|
18
|
+
* harness drive their incremental status persistence through this scheduler so
|
|
19
|
+
* the live stream cadence is identical across harnesses.
|
|
16
20
|
*/
|
|
17
21
|
export declare enum UpdateReason {
|
|
18
22
|
TIME_THRESHOLD = "time_threshold",
|
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
* OR firstUpdate
|
|
14
14
|
*
|
|
15
15
|
* Ported from Python StreamingUpdateScheduler.
|
|
16
|
+
*
|
|
17
|
+
* Shared infrastructure: both the native deep-agent harness and the Cursor
|
|
18
|
+
* harness drive their incremental status persistence through this scheduler so
|
|
19
|
+
* the live stream cadence is identical across harnesses.
|
|
16
20
|
*/
|
|
17
21
|
export var UpdateReason;
|
|
18
22
|
(function (UpdateReason) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"streaming-scheduler.js","sourceRoot":"","sources":["../../src/shared/streaming-scheduler.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stigmer/runner",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.9-dev.20260615145121",
|
|
4
4
|
"description": "Embeddable Temporal worker for the Stigmer AI agent platform — handles agent execution, workflow orchestration, and MCP server management",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"@opentelemetry/resources": "^2.0.0",
|
|
87
87
|
"@opentelemetry/sdk-trace-base": "^2.0.0",
|
|
88
88
|
"@opentelemetry/sdk-trace-node": "^2.0.0",
|
|
89
|
-
"@stigmer/protos": "3.0.
|
|
89
|
+
"@stigmer/protos": "3.0.9-dev.20260615145121",
|
|
90
90
|
"@temporalio/activity": "^1.11.0",
|
|
91
91
|
"@temporalio/client": "^1.11.0",
|
|
92
92
|
"@temporalio/common": "^1.11.0",
|
|
@@ -32,6 +32,14 @@ describe("loadConfig", () => {
|
|
|
32
32
|
expect(config.cursorApiKey).toBe("test-key");
|
|
33
33
|
expect(config.proxyEndpoint).toBeNull();
|
|
34
34
|
expect(config.maxConcurrentActivities).toBe(5);
|
|
35
|
+
expect(config.cursorStreamStallTimeoutMs).toBe(180000);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("respects CURSOR_STREAM_STALL_TIMEOUT_MS", () => {
|
|
39
|
+
process.env.CURSOR_API_KEY = "test-key";
|
|
40
|
+
process.env.CURSOR_STREAM_STALL_TIMEOUT_MS = "90000";
|
|
41
|
+
const config = loadConfig();
|
|
42
|
+
expect(config.cursorStreamStallTimeoutMs).toBe(90000);
|
|
35
43
|
});
|
|
36
44
|
|
|
37
45
|
it("respects STIGMER_TASK_QUEUE", () => {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
activityStartedOnQueue,
|
|
4
|
+
activityFinishedOnQueue,
|
|
5
|
+
inFlightCountForQueue,
|
|
6
|
+
setQueueDrainCallback,
|
|
7
|
+
forgetQueue,
|
|
8
|
+
__resetInFlightRegistryForTests,
|
|
9
|
+
} from "../in-flight.js";
|
|
10
|
+
|
|
11
|
+
const Q = "session:abc";
|
|
12
|
+
|
|
13
|
+
describe("in-flight activity registry", () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
__resetInFlightRegistryForTests();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("counts concurrent activities up and down per queue", () => {
|
|
19
|
+
expect(inFlightCountForQueue(Q)).toBe(0);
|
|
20
|
+
activityStartedOnQueue(Q);
|
|
21
|
+
activityStartedOnQueue(Q);
|
|
22
|
+
expect(inFlightCountForQueue(Q)).toBe(2);
|
|
23
|
+
activityFinishedOnQueue(Q);
|
|
24
|
+
expect(inFlightCountForQueue(Q)).toBe(1);
|
|
25
|
+
activityFinishedOnQueue(Q);
|
|
26
|
+
expect(inFlightCountForQueue(Q)).toBe(0);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("isolates counts across queues", () => {
|
|
30
|
+
activityStartedOnQueue("session:a");
|
|
31
|
+
activityStartedOnQueue("session:b");
|
|
32
|
+
activityStartedOnQueue("session:b");
|
|
33
|
+
expect(inFlightCountForQueue("session:a")).toBe(1);
|
|
34
|
+
expect(inFlightCountForQueue("session:b")).toBe(2);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("never goes negative on an unbalanced finish", () => {
|
|
38
|
+
activityFinishedOnQueue(Q);
|
|
39
|
+
expect(inFlightCountForQueue(Q)).toBe(0);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("fires the drain callback exactly once when the last activity finishes", () => {
|
|
43
|
+
const onDrained = vi.fn();
|
|
44
|
+
activityStartedOnQueue(Q);
|
|
45
|
+
activityStartedOnQueue(Q);
|
|
46
|
+
setQueueDrainCallback(Q, onDrained);
|
|
47
|
+
|
|
48
|
+
activityFinishedOnQueue(Q);
|
|
49
|
+
expect(onDrained).not.toHaveBeenCalled(); // still 1 in flight
|
|
50
|
+
|
|
51
|
+
activityFinishedOnQueue(Q);
|
|
52
|
+
expect(onDrained).toHaveBeenCalledTimes(1);
|
|
53
|
+
|
|
54
|
+
// A subsequent start/finish cycle does not re-fire the consumed callback.
|
|
55
|
+
activityStartedOnQueue(Q);
|
|
56
|
+
activityFinishedOnQueue(Q);
|
|
57
|
+
expect(onDrained).toHaveBeenCalledTimes(1);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("does not fire a drain callback that was cleared (session re-opened)", () => {
|
|
61
|
+
const onDrained = vi.fn();
|
|
62
|
+
activityStartedOnQueue(Q);
|
|
63
|
+
setQueueDrainCallback(Q, onDrained);
|
|
64
|
+
setQueueDrainCallback(Q, undefined); // re-open cancels the deferred teardown
|
|
65
|
+
|
|
66
|
+
activityFinishedOnQueue(Q);
|
|
67
|
+
expect(onDrained).not.toHaveBeenCalled();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("forgetQueue resets tracking for a torn-down worker", () => {
|
|
71
|
+
activityStartedOnQueue(Q);
|
|
72
|
+
forgetQueue(Q);
|
|
73
|
+
expect(inFlightCountForQueue(Q)).toBe(0);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("setQueueDrainCallback is a no-op for an idle (unknown) queue", () => {
|
|
77
|
+
const onDrained = vi.fn();
|
|
78
|
+
setQueueDrainCallback("session:idle", onDrained);
|
|
79
|
+
// No entry exists, so nothing will ever fire it — caller tears down inline.
|
|
80
|
+
activityStartedOnQueue("session:idle");
|
|
81
|
+
activityFinishedOnQueue("session:idle");
|
|
82
|
+
expect(onDrained).not.toHaveBeenCalled();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -14,6 +14,7 @@ import { PendingApprovalSchema } from "@stigmer/protos/ai/stigmer/agentic/agente
|
|
|
14
14
|
|
|
15
15
|
import { buildPrompt } from "../index.js";
|
|
16
16
|
import type { BuildPromptInput } from "../index.js";
|
|
17
|
+
import { buildReinvocationPrompt, formatToolApprovalProtocol } from "../prompt-builder.js";
|
|
17
18
|
import type { AgentResolution, AgentResolutionReason } from "../session-lifecycle.js";
|
|
18
19
|
|
|
19
20
|
const USER_MESSAGE = "What was the secret token I told you?";
|
|
@@ -109,3 +110,76 @@ describe("buildPrompt", () => {
|
|
|
109
110
|
expect(prompt).not.toContain("tool-call-1");
|
|
110
111
|
});
|
|
111
112
|
});
|
|
113
|
+
|
|
114
|
+
describe("tool-approval protocol injection", () => {
|
|
115
|
+
it("includes the tool-approval protocol on the first execution", () => {
|
|
116
|
+
const prompt = buildPrompt(
|
|
117
|
+
input({ resolution: resolution("local", "created_first_execution") }),
|
|
118
|
+
);
|
|
119
|
+
expect(prompt).toContain("<tool_approval_protocol>");
|
|
120
|
+
// The decisive override against a server's "ask first" guidance.
|
|
121
|
+
expect(prompt).toContain("calling the appropriate tool directly");
|
|
122
|
+
expect(prompt).toContain("Invoke the tool and let the platform");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("places the protocol after the agent instructions, before the user request", () => {
|
|
126
|
+
const prompt = buildPrompt(
|
|
127
|
+
input({ resolution: resolution("local", "created_first_execution") }),
|
|
128
|
+
);
|
|
129
|
+
const protocolIdx = prompt.indexOf("<tool_approval_protocol>");
|
|
130
|
+
const requestIdx = prompt.indexOf("<user_request>");
|
|
131
|
+
expect(protocolIdx).toBeGreaterThan(prompt.indexOf("<agent_instructions>"));
|
|
132
|
+
expect(protocolIdx).toBeLessThan(requestIdx);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("also injects the protocol for a fresh agent after a resume failure", () => {
|
|
136
|
+
const prompt = buildPrompt(
|
|
137
|
+
input({ resolution: resolution("local", "created_after_resume_failure") }),
|
|
138
|
+
);
|
|
139
|
+
expect(prompt).toContain("<tool_approval_protocol>");
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe("formatToolApprovalProtocol", () => {
|
|
144
|
+
it("instructs the agent to invoke tools and never ask for permission in prose", () => {
|
|
145
|
+
const section = formatToolApprovalProtocol();
|
|
146
|
+
expect(section).toContain("<tool_approval_protocol>");
|
|
147
|
+
expect(section).toContain("</tool_approval_protocol>");
|
|
148
|
+
expect(section.toLowerCase()).toContain("never ask the");
|
|
149
|
+
// Explicit override of MCP-server "confirm before acting" guidance.
|
|
150
|
+
expect(section.toLowerCase()).toContain("even if a tool or mcp server");
|
|
151
|
+
expect(section).toContain("Invoke the tool and let the platform");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("contains no characters that would break the prompt assembly", () => {
|
|
155
|
+
// Sanity: the protocol is plain prose joined into the user-message prompt.
|
|
156
|
+
expect(formatToolApprovalProtocol()).not.toContain("undefined");
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe("buildReinvocationPrompt", () => {
|
|
161
|
+
function pending(toolCallId: string, message: string) {
|
|
162
|
+
return create(PendingApprovalSchema, { toolCallId, toolName: "Write", message });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
it("tells the agent to carry out approved actions and keep invoking tools", () => {
|
|
166
|
+
const decisions = new Map<string, ApprovalAction>([
|
|
167
|
+
["tc-1", ApprovalAction.APPROVE],
|
|
168
|
+
]);
|
|
169
|
+
const prompt = buildReinvocationPrompt([pending("tc-1", "Write file: a.txt")], decisions);
|
|
170
|
+
expect(prompt).toContain("APPROVED");
|
|
171
|
+
expect(prompt).toContain("Write file: a.txt");
|
|
172
|
+
// The continuation + override directive must be present.
|
|
173
|
+
expect(prompt).toContain("Continue the rest of the task");
|
|
174
|
+
expect(prompt.toLowerCase()).toContain("do not ask the user for permission in prose");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("still carries the continuation/override when every action was skipped", () => {
|
|
178
|
+
const decisions = new Map<string, ApprovalAction>([
|
|
179
|
+
["tc-1", ApprovalAction.SKIP],
|
|
180
|
+
]);
|
|
181
|
+
const prompt = buildReinvocationPrompt([pending("tc-1", "Write file: a.txt")], decisions);
|
|
182
|
+
expect(prompt).toContain("SKIPPED");
|
|
183
|
+
expect(prompt).toContain("Continue the rest of the task");
|
|
184
|
+
});
|
|
185
|
+
});
|
|
@@ -89,13 +89,26 @@ function setup(opts: {
|
|
|
89
89
|
};
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
// Real hook-input shapes (PascalCase name, file_path/command in
|
|
92
|
+
// Real preToolUse hook-input shapes (PascalCase name, file_path/command in
|
|
93
|
+
// tool_input). These omit hook_event_name on purpose: a payload with no event
|
|
94
|
+
// must still take the built-in arm (the script only diverts to the MCP arm on an
|
|
95
|
+
// explicit beforeMCPExecution).
|
|
93
96
|
const hookWrite = (filePath: string) => ({ tool_name: "Write", tool_input: { file_path: filePath, content: "x" } });
|
|
94
97
|
const hookShell = (command: string) => ({ tool_name: "Shell", tool_input: { command, cwd: "/x", timeout: 30000 } });
|
|
95
98
|
const hookDelete = (filePath: string) => ({ tool_name: "Delete", tool_input: { file_path: filePath } });
|
|
96
99
|
const hookRead = (filePath: string) => ({ tool_name: "Read", tool_input: { file_path: filePath } });
|
|
97
100
|
|
|
98
|
-
|
|
101
|
+
// Real beforeMCPExecution shape (captured live): bare tool_name, tool_input as a
|
|
102
|
+
// JSON STRING, server identity, and the hook_event_name discriminator.
|
|
103
|
+
const hookMcp = (name: string, input: Record<string, unknown> = {}) => ({
|
|
104
|
+
tool_name: name,
|
|
105
|
+
tool_input: JSON.stringify(input),
|
|
106
|
+
mcp_server_name: "srv",
|
|
107
|
+
command: "npx -y srv mcp",
|
|
108
|
+
hook_event_name: "beforeMCPExecution",
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
d("generated approval hook (preToolUse + beforeMCPExecution)", () => {
|
|
99
112
|
it("denies gated built-ins (Write/Shell/Delete) and records a category+salient token", () => {
|
|
100
113
|
const h = setup({});
|
|
101
114
|
|
|
@@ -134,24 +147,86 @@ d("generated preToolUse hook", () => {
|
|
|
134
147
|
expect(h.decide(hookWrite("/x/OTHER.txt")).permission).toBe("deny");
|
|
135
148
|
});
|
|
136
149
|
|
|
137
|
-
it("denies require-approval MCP tools and allows them once granted (name-only)", () => {
|
|
138
|
-
const mcpPolicies = { apply_x: { requiresApproval: true, message: "Apply X" } };
|
|
139
|
-
const denyH = setup({ mcpPolicies });
|
|
140
|
-
expect(denyH.decide({ tool_name: "apply_x", tool_input: {} }).permission).toBe("deny");
|
|
141
|
-
expect(denyH.ledger()[0].token).toBe(grantToken("apply_x", ""));
|
|
142
|
-
|
|
143
|
-
const grantH = setup({
|
|
144
|
-
mcpPolicies,
|
|
145
|
-
grants: [{ toolName: "apply_x", mcpServerSlug: "srv", key: "apply_x", salient: "" }],
|
|
146
|
-
});
|
|
147
|
-
expect(grantH.decide({ tool_name: "apply_x", tool_input: {} }).permission).toBe("allow");
|
|
148
|
-
});
|
|
149
|
-
|
|
150
150
|
it("fails closed (deny) when the state file is missing", () => {
|
|
151
151
|
const h = setup({ noStateFile: true });
|
|
152
152
|
expect(h.decide(hookWrite("/x/a.txt")).permission).toBe("deny");
|
|
153
153
|
});
|
|
154
154
|
|
|
155
|
+
// MCP gating runs ONLY on the beforeMCPExecution event (preToolUse does not
|
|
156
|
+
// enforce MCP), so a denial is recorded in exactly one place. The identity is
|
|
157
|
+
// name-only (base64("<tool>\n")) because the bare tool name is identical on the
|
|
158
|
+
// hook input and the runner's stream event.
|
|
159
|
+
describe("MCP tools (beforeMCPExecution event)", () => {
|
|
160
|
+
it("denies a require-approval MCP tool and surfaces its policy message", () => {
|
|
161
|
+
const h = setup({ mcpPolicies: { click: { requiresApproval: true, message: "Approve click?" } } });
|
|
162
|
+
const res = h.decide(hookMcp("click", { app: "Slack", element_index: "59" }));
|
|
163
|
+
expect(res.permission).toBe("deny");
|
|
164
|
+
expect(res.raw).toContain("Approve click?");
|
|
165
|
+
expect(h.ledger()).toHaveLength(1);
|
|
166
|
+
expect(h.ledger()[0].token).toBe(grantToken("click", ""));
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("denial agent_message frames approval as automatic and never trains ask-in-prose", () => {
|
|
170
|
+
const h = setup({ mcpPolicies: { click: { requiresApproval: true, message: "Approve click?" } } });
|
|
171
|
+
const res = h.decide(hookMcp("click"));
|
|
172
|
+
// The agent_message must tell the model approval is handled automatically
|
|
173
|
+
// and that it should continue, NOT stop and wait or ask for permission.
|
|
174
|
+
expect(res.raw).toContain("submitted to the user for approval automatically");
|
|
175
|
+
expect(res.raw).toContain("continue with the rest of the task");
|
|
176
|
+
// The old propose-then-wait framing and internal sentinel must be gone.
|
|
177
|
+
expect(res.raw).not.toContain("STIGMER_APPROVAL_REQUIRED");
|
|
178
|
+
expect(res.raw).not.toContain("Stop and wait");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("allows a require-approval MCP tool once it has been granted (reinvocation)", () => {
|
|
182
|
+
const h = setup({
|
|
183
|
+
mcpPolicies: { click: { requiresApproval: true } },
|
|
184
|
+
grants: [{ toolName: "click", mcpServerSlug: "srv", key: "click", salient: "" }],
|
|
185
|
+
});
|
|
186
|
+
expect(h.decide(hookMcp("click")).permission).toBe("allow");
|
|
187
|
+
expect(h.ledger()).toEqual([]);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("allows an auto-approved MCP tool (absent from mcpToolPolicies)", () => {
|
|
191
|
+
const h = setup({ mcpPolicies: { click: { requiresApproval: true } } });
|
|
192
|
+
expect(h.decide(hookMcp("list_apps")).permission).toBe("allow");
|
|
193
|
+
expect(h.ledger()).toEqual([]);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("allows an MCP tool whose policy is explicitly requiresApproval:false", () => {
|
|
197
|
+
const h = setup({ mcpPolicies: { click: { requiresApproval: false } } });
|
|
198
|
+
expect(h.decide(hookMcp("click")).permission).toBe("allow");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("auto-approve-all allows a require-approval MCP tool", () => {
|
|
202
|
+
const h = setup({ autoApproveAll: true, mcpPolicies: { click: { requiresApproval: true } } });
|
|
203
|
+
expect(h.decide(hookMcp("click")).permission).toBe("allow");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("fails closed (deny) when the state file is missing", () => {
|
|
207
|
+
const h = setup({ noStateFile: true });
|
|
208
|
+
expect(h.decide(hookMcp("click")).permission).toBe("deny");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("does NOT gate the same MCP tool delivered on preToolUse (no double-gating)", () => {
|
|
212
|
+
const h = setup({ mcpPolicies: { click: { requiresApproval: true } } });
|
|
213
|
+
// preToolUse must fall through to allow for MCP — gating belongs to
|
|
214
|
+
// beforeMCPExecution alone, so the denial is never recorded twice.
|
|
215
|
+
const res = h.decide({ tool_name: "click", tool_input: "{}", hook_event_name: "preToolUse" });
|
|
216
|
+
expect(res.permission).toBe("allow");
|
|
217
|
+
expect(h.ledger()).toEqual([]);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("scope guard: a foreign MCP invocation is allowed and never recorded", () => {
|
|
221
|
+
const h = setup({
|
|
222
|
+
mcpPolicies: { click: { requiresApproval: true } },
|
|
223
|
+
runnerPid: 2_147_483_600,
|
|
224
|
+
});
|
|
225
|
+
expect(h.decide(hookMcp("click")).permission).toBe("allow");
|
|
226
|
+
expect(h.ledger()).toEqual([]);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
155
230
|
// Regression: the original grep-based extraction truncated string values at
|
|
156
231
|
// the first JSON-escaped character, so a shell command containing double
|
|
157
232
|
// quotes (e.g. `printf '%s' 'x' > "file"`) produced a ledger token that never
|