@stigmer/runner 3.13.0 → 3.14.0-dev.20260910084630
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/LICENSE +190 -0
- package/dist/.build-fingerprint +1 -1
- package/dist/activities/execute-cursor/message-translator.d.ts +67 -0
- package/dist/activities/execute-cursor/message-translator.js +103 -0
- package/dist/activities/execute-cursor/message-translator.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.js +13 -1
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-cursor/turn-boundary.d.ts +16 -1
- package/dist/activities/execute-cursor/turn-boundary.js +44 -2
- package/dist/activities/execute-cursor/turn-boundary.js.map +1 -1
- package/dist/ipc-protocol-fixtures.js +3 -4
- package/dist/ipc-protocol-fixtures.js.map +1 -1
- package/dist/ipc-protocol.js +5 -4
- package/dist/ipc-protocol.js.map +1 -1
- package/package.json +15 -5
- package/src/__tests__/ipc-protocol-fixtures.test.ts +3 -4
- package/src/__tests__/ipc-protocol.test.ts +3 -2
- package/src/activities/execute-cursor/__tests__/cursor-generate-image-live.test.ts +147 -0
- package/src/activities/execute-cursor/__tests__/turn-boundary.test.ts +153 -1
- package/src/activities/execute-cursor/message-translator.ts +115 -0
- package/src/activities/execute-cursor/prompt-builder.ts +13 -1
- package/src/activities/execute-cursor/turn-boundary.ts +65 -1
- package/src/ipc-protocol-fixtures.ts +3 -4
- package/src/ipc-protocol.ts +5 -4
|
@@ -1923,6 +1923,121 @@ export function detectUnattributedHookBlocks(
|
|
|
1923
1923
|
return blocks;
|
|
1924
1924
|
}
|
|
1925
1925
|
|
|
1926
|
+
/**
|
|
1927
|
+
* The honest terminal error stamped on a tool call that never resolved (issue
|
|
1928
|
+
* #965). Deliberately states all three negatives — not executed, not approved,
|
|
1929
|
+
* not denied — because the incident's harm was the model claiming an approval
|
|
1930
|
+
* was pending: this text is what the transcript shows INSTEAD of a spinner or
|
|
1931
|
+
* a silent after-the-fact interruption, and it must leave no room for an
|
|
1932
|
+
* approval-is-coming reading.
|
|
1933
|
+
*/
|
|
1934
|
+
export const UNRESOLVED_TOOL_CALL_ERROR =
|
|
1935
|
+
"The tool did not return a result before the turn ended. It was never " +
|
|
1936
|
+
"executed, approved, or denied — no approval is pending for it.";
|
|
1937
|
+
|
|
1938
|
+
/** One never-resolved tool call the turn boundary settled (issue #965). */
|
|
1939
|
+
export interface UnresolvedToolCall {
|
|
1940
|
+
toolCallId: string;
|
|
1941
|
+
toolName: string;
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
/**
|
|
1945
|
+
* Settle this-turn tool calls that are still NON-TERMINAL when a turn
|
|
1946
|
+
* completes without pausing — the issue #965 invariant, the sibling of
|
|
1947
|
+
* {@link detectUnattributedHookBlocks}' #205 invariant ("a blocked tool must
|
|
1948
|
+
* never silently complete" → "an unresolved tool must never silently
|
|
1949
|
+
* complete").
|
|
1950
|
+
*
|
|
1951
|
+
* THE SHAPE THIS CATCHES. A tool that hangs INSIDE the Cursor agent runtime —
|
|
1952
|
+
* the production case is `generateImage`, which rides the SDK's
|
|
1953
|
+
* interaction-query channel rather than the ordinary tool path — streams a
|
|
1954
|
+
* tool_call start, never streams a result, and is invisible to every Stigmer
|
|
1955
|
+
* seam: the hook never denied it (no ledger entry), so the reconcile never
|
|
1956
|
+
* gated it, and the turn completes with the row still PENDING/RUNNING. Before
|
|
1957
|
+
* this sweep, the server's terminal settle (issue #207) stamped such rows
|
|
1958
|
+
* TOOL_CALL_INTERRUPTED silently AFTER the runner reported completion — the
|
|
1959
|
+
* transcript's last word stayed whatever the model claimed, which in
|
|
1960
|
+
* aex_01m1a6ww3nmp4952ar5v0g4g85 was a promise that an approval was pending
|
|
1961
|
+
* when none existed.
|
|
1962
|
+
*
|
|
1963
|
+
* Settling here instead makes the runner the author of the honest record: the
|
|
1964
|
+
* row gets TOOL_CALL_INTERRUPTED with {@link UNRESOLVED_TOOL_CALL_ERROR}, and
|
|
1965
|
+
* the caller (turn-boundary.ts) appends a system disclosure naming what never
|
|
1966
|
+
* ran.
|
|
1967
|
+
*
|
|
1968
|
+
* WHY INTERRUPTED AND NEVER FAILED. TOOL_CALL_INTERRUPTED is deliberately the
|
|
1969
|
+
* one settled status the monotonic merge guard lets live execution evidence
|
|
1970
|
+
* supersede (see the guard's note in this file, ~line 331): if this FAILED
|
|
1971
|
+
* execution is later RECOVERED, the harness checkpoint may re-execute the call
|
|
1972
|
+
* under its original id, and the replayed events must be able to advance the
|
|
1973
|
+
* row to its true outcome. A boundary-stamped FAILED would freeze it forever.
|
|
1974
|
+
*
|
|
1975
|
+
* WHY IT NEVER FAILS THE RUN (unlike #205). A foreign hook block is provably
|
|
1976
|
+
* adversarial — approval semantics are permanently broken, so completing would
|
|
1977
|
+
* always be a lie. An unresolved row can also be benign stream event-loss
|
|
1978
|
+
* where the tool actually ran; failing the run would convert those into
|
|
1979
|
+
* regressions. Disclosure restores honesty at zero regression risk.
|
|
1980
|
+
*
|
|
1981
|
+
* Scope and exclusions, in order:
|
|
1982
|
+
* - THIS turn's parent-transcript rows only (from `turnStartMessageIndex`):
|
|
1983
|
+
* seeded prior-turn rows were adjudicated by their own execution's settle,
|
|
1984
|
+
* and sub-agent inner rows are the server settle's concern — the parent row
|
|
1985
|
+
* (e.g. the Task call) is what the user sees. Mirrors #205's scoping.
|
|
1986
|
+
* - Only PENDING/RUNNING rows: every terminal row was adjudicated, and
|
|
1987
|
+
* WAITING_APPROVAL rows belong to the pause machinery (the caller only runs
|
|
1988
|
+
* this sweep on a NON-pausing turn, so none should exist here anyway).
|
|
1989
|
+
* - Only rows with NO denial-ledger entry of ANY kind (exact token, then the
|
|
1990
|
+
* normalized-path fallback — the same two identities every sweep in this
|
|
1991
|
+
* file uses): a ledger-attributed row is the unattended/secret/fail-closed
|
|
1992
|
+
* machinery's to settle, not ours.
|
|
1993
|
+
*
|
|
1994
|
+
* Returns the settled calls so the boundary can disclose and log them.
|
|
1995
|
+
*/
|
|
1996
|
+
export function settleUnresolvedToolCalls(
|
|
1997
|
+
messages: readonly AgentMessage[],
|
|
1998
|
+
turnStartMessageIndex: number,
|
|
1999
|
+
ledger: readonly DeniedLedgerEntry[],
|
|
2000
|
+
workspaceRoot?: string,
|
|
2001
|
+
): UnresolvedToolCall[] {
|
|
2002
|
+
const ledgerTokens = new Set(ledger.map((e) => e.token));
|
|
2003
|
+
const ledgerNormalizedSalients = new Set<string>();
|
|
2004
|
+
if (workspaceRoot) {
|
|
2005
|
+
for (const entry of ledger) {
|
|
2006
|
+
const decoded = decodeIdentityToken(entry.token);
|
|
2007
|
+
if (!decoded) continue;
|
|
2008
|
+
const normalized = normalizedFileSalient(decoded.key, decoded.salient, workspaceRoot);
|
|
2009
|
+
if (normalized) ledgerNormalizedSalients.add(normalized);
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
const matchesLedger = (tc: ToolCall): boolean => {
|
|
2014
|
+
if (ledgerTokens.has(toolCallIdentityToken(tc))) return true;
|
|
2015
|
+
if (!workspaceRoot) return false;
|
|
2016
|
+
const id = toolIdentity(tc.name, tc.mcpServerSlug, toolCallArgs(tc));
|
|
2017
|
+
const normalized = normalizedFileSalient(id.key, id.salient, workspaceRoot);
|
|
2018
|
+
return !!normalized && ledgerNormalizedSalients.has(normalized);
|
|
2019
|
+
};
|
|
2020
|
+
|
|
2021
|
+
const settled: UnresolvedToolCall[] = [];
|
|
2022
|
+
for (const msg of messages.slice(Math.max(0, turnStartMessageIndex))) {
|
|
2023
|
+
for (const tc of msg.toolCalls) {
|
|
2024
|
+
if (
|
|
2025
|
+
tc.status !== ToolCallStatus.TOOL_CALL_PENDING &&
|
|
2026
|
+
tc.status !== ToolCallStatus.TOOL_CALL_RUNNING
|
|
2027
|
+
) {
|
|
2028
|
+
continue;
|
|
2029
|
+
}
|
|
2030
|
+
if (matchesLedger(tc)) continue;
|
|
2031
|
+
tc.status = ToolCallStatus.TOOL_CALL_INTERRUPTED;
|
|
2032
|
+
tc.error = UNRESOLVED_TOOL_CALL_ERROR;
|
|
2033
|
+
tc.isStreaming = false;
|
|
2034
|
+
if (!tc.completedAt) tc.completedAt = utcTimestamp();
|
|
2035
|
+
settled.push({ toolCallId: tc.id, toolName: tc.name });
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
return settled;
|
|
2039
|
+
}
|
|
2040
|
+
|
|
1926
2041
|
/**
|
|
1927
2042
|
* Stamp the tool calls the hook denied under UNATTENDED approval mode
|
|
1928
2043
|
* (DD-014) as terminal TOOL_CALL_SKIPPED rows with UNATTENDED_SKIP
|
|
@@ -751,12 +751,24 @@ const TOOL_APPROVAL_PROTOCOL_INTRO =
|
|
|
751
751
|
* well-behaved model otherwise concludes the environment is broken and tells the
|
|
752
752
|
* user to "enable hooks in your Cursor settings", contradicting the approval
|
|
753
753
|
* card. This rule reframes that signal as the gate working as designed.
|
|
754
|
+
*
|
|
755
|
+
* The fifth rule is the fourth's honesty boundary (issue #965): the gate
|
|
756
|
+
* recognition must be scoped to the PLATFORM'S OWN message texts ("blocked by
|
|
757
|
+
* a hook"; "submitted to the user for approval"), because a failure inside the
|
|
758
|
+
* harness itself can ALSO speak in permission vocabulary — the production case
|
|
759
|
+
* was Cursor's native generateImage hanging with a write-permission-flavored
|
|
760
|
+
* error, which the old any-"requires approval"-text reading turned into the
|
|
761
|
+
* model promising the user an approval card the platform never held. A failure
|
|
762
|
+
* without the platform's notice is an ordinary tool failure and must be
|
|
763
|
+
* reported as one; the platform's approval surface is never narrated into
|
|
764
|
+
* existence.
|
|
754
765
|
*/
|
|
755
766
|
const TOOL_APPROVAL_PROTOCOL_RULES: readonly string[] = [
|
|
756
767
|
"Carry out every action by calling the appropriate tool directly. Never describe an action you intend to take and then stop, and never ask the user for permission in prose.",
|
|
757
768
|
"When an action needs approval, the platform pauses it, asks the user, and resumes you automatically after they decide. You do not request approval yourself — invoking the tool is how you request it.",
|
|
758
769
|
"Even if a tool or MCP server instructs you to confirm with the user before acting (for example before sending, deleting, or purchasing), do NOT ask in prose. Invoke the tool and let the platform's approval step handle it.",
|
|
759
|
-
"A tool result that says
|
|
770
|
+
"A tool result that says it was \"blocked by a hook\" or that the action was \"submitted to the user for approval\" is the platform's approval gate doing its job — it is NOT an error and NOT a Cursor misconfiguration. Never tell the user to change Cursor settings, enable hooks, or fix their configuration; the gate is intentional, and for THESE results the platform will resume you automatically once the user decides.",
|
|
771
|
+
"Any other tool failure — including one that mentions permissions or approval but does not carry the platform's approval notice above — is an ordinary failure, not the approval gate. Report it to the user honestly as something that did not run. NEVER tell the user an approval is pending or that you will be resumed automatically unless the tool result carried the platform's approval notice; the platform shows its own approval prompts, and you must not invent one.",
|
|
760
772
|
"If an action is declined, do not retry it or attempt a workaround for it; continue with the rest of the task.",
|
|
761
773
|
];
|
|
762
774
|
|
|
@@ -12,7 +12,12 @@
|
|
|
12
12
|
* and redact the model's provisional post-denial narration;
|
|
13
13
|
* 5. detect UNATTRIBUTED hook blocks (issue #205) — a tool blocked by a hook
|
|
14
14
|
* with no ledger entry of any kind was denied by a FOREIGN hook the merge
|
|
15
|
-
* preserved, and the caller fails the run rather than completing silently
|
|
15
|
+
* preserved, and the caller fails the run rather than completing silently;
|
|
16
|
+
* 6. settle UNRESOLVED tool calls (issue #965) — a this-turn row still
|
|
17
|
+
* non-terminal on a completing turn with no ledger attribution hung inside
|
|
18
|
+
* the harness and can never complete; it is settled to an honest
|
|
19
|
+
* TOOL_CALL_INTERRUPTED and disclosed on the transcript instead of being
|
|
20
|
+
* silently stamped by the server's terminal settle after the fact.
|
|
16
21
|
*
|
|
17
22
|
* Extracted from the activity entry point (index.ts Phase 12) so it is directly
|
|
18
23
|
* unit-testable AND re-enterable: the poisoned-handle / transport-timeout
|
|
@@ -51,9 +56,14 @@ import {
|
|
|
51
56
|
clearProvisionalPostDenialNarration,
|
|
52
57
|
detectUnattributedHookBlocks,
|
|
53
58
|
reconcileDeniedToolCalls,
|
|
59
|
+
settleUnresolvedToolCalls,
|
|
54
60
|
stampUnattendedSkippedToolCalls,
|
|
61
|
+
utcTimestamp,
|
|
55
62
|
type UnattributedHookBlock,
|
|
56
63
|
} from "./message-translator.js";
|
|
64
|
+
import { create } from "@bufbuild/protobuf";
|
|
65
|
+
import { AgentMessageSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
66
|
+
import { MessageType } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
57
67
|
|
|
58
68
|
// How long the boundary waits for the first-denial-stop's run.cancel() to
|
|
59
69
|
// settle before reading the final denial ledger and capturing the turn's tree.
|
|
@@ -140,6 +150,16 @@ export interface TurnBoundaryResult {
|
|
|
140
150
|
* undone (a pausing turn is not silent — the caller logs and pauses as usual).
|
|
141
151
|
*/
|
|
142
152
|
readonly unattributedHookBlocks: readonly UnattributedHookBlock[];
|
|
153
|
+
/**
|
|
154
|
+
* This-turn tool calls settled to TOOL_CALL_INTERRUPTED because they were
|
|
155
|
+
* still non-terminal on a completing turn with no ledger attribution (issue
|
|
156
|
+
* #965) — the harness returned no result for them and the platform holds no
|
|
157
|
+
* approval for them. Informational: the boundary already settled the rows
|
|
158
|
+
* and appended the transcript disclosure; the run is NOT failed for these
|
|
159
|
+
* (unlike #205's unattributed blocks, an unresolved row can be benign
|
|
160
|
+
* stream event-loss, so failing would over-punish).
|
|
161
|
+
*/
|
|
162
|
+
readonly settledUnresolvedCount: number;
|
|
143
163
|
}
|
|
144
164
|
|
|
145
165
|
/**
|
|
@@ -338,6 +358,49 @@ export async function runTurnBoundary(opts: TurnBoundaryOptions): Promise<TurnBo
|
|
|
338
358
|
primaryWorkspaceDir,
|
|
339
359
|
);
|
|
340
360
|
const waiting = deniedToolCalls.length > 0 || capturedChangeCount > 0;
|
|
361
|
+
|
|
362
|
+
// Issue #965 invariant: an unresolved tool must never silently complete.
|
|
363
|
+
// On a COMPLETING (non-pausing) turn, any this-turn row still PENDING /
|
|
364
|
+
// RUNNING with no ledger attribution hung inside the harness (the production
|
|
365
|
+
// case: `generateImage`, which rides the SDK's interaction-query channel and
|
|
366
|
+
// is invisible to the hook). Settle it to an honest TOOL_CALL_INTERRUPTED —
|
|
367
|
+
// never FAILED, so a recovery replay can still supersede it (#207) — and
|
|
368
|
+
// disclose it on the transcript, so the model's own narration (which may
|
|
369
|
+
// have promised an approval the platform does not hold) is never the last
|
|
370
|
+
// word. A PAUSING turn is skipped: its non-terminal rows belong to the
|
|
371
|
+
// reconcile/collapse machinery above. Runs AFTER the unattended stamp so a
|
|
372
|
+
// ledger-attributed row is already SKIPPED and cannot double-settle, and
|
|
373
|
+
// AFTER the #205 detection so a hook-block FAILED row keeps its distinct,
|
|
374
|
+
// run-failing treatment. Deliberately does NOT fail the run (unlike #205):
|
|
375
|
+
// an unresolved row can also be benign stream event-loss where the tool
|
|
376
|
+
// actually ran, and converting those into failures would be a regression.
|
|
377
|
+
let settledUnresolved: readonly { toolCallId: string; toolName: string }[] = [];
|
|
378
|
+
if (!waiting) {
|
|
379
|
+
settledUnresolved = settleUnresolvedToolCalls(
|
|
380
|
+
status.messages,
|
|
381
|
+
turnStartMessageIndex,
|
|
382
|
+
deniedLedger,
|
|
383
|
+
primaryWorkspaceDir,
|
|
384
|
+
);
|
|
385
|
+
if (settledUnresolved.length > 0) {
|
|
386
|
+
const names = [...new Set(settledUnresolved.map((s) => s.toolName))].join(", ");
|
|
387
|
+
status.messages.push(create(AgentMessageSchema, {
|
|
388
|
+
type: MessageType.MESSAGE_SYSTEM,
|
|
389
|
+
content:
|
|
390
|
+
`Note: the following tool call(s) never completed and were not executed: ${names}. ` +
|
|
391
|
+
`No approval is pending for them — if the agent said otherwise, disregard that. ` +
|
|
392
|
+
`You can ask the agent to try again.`,
|
|
393
|
+
timestamp: utcTimestamp(),
|
|
394
|
+
}));
|
|
395
|
+
console.warn(
|
|
396
|
+
`ExecuteCursor turn boundary: settled ${settledUnresolved.length} unresolved ` +
|
|
397
|
+
`tool call(s) to INTERRUPTED with disclosure [${names}] — the harness returned ` +
|
|
398
|
+
`no result for them and no ledger entry accounts for them (issue #965; ` +
|
|
399
|
+
`execution=${executionId})`,
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
341
404
|
if (unattributedHookBlocks.length > 0) {
|
|
342
405
|
const culprits = (foreignGatingHooks?.length ?? 0) > 0
|
|
343
406
|
? ` — likely foreign workspace hook(s): ${foreignGatingHooks!.join(", ")}`
|
|
@@ -366,5 +429,6 @@ export async function runTurnBoundary(opts: TurnBoundaryOptions): Promise<TurnBo
|
|
|
366
429
|
capturedChangeCount,
|
|
367
430
|
deniedToolCallCount: deniedToolCalls.length,
|
|
368
431
|
unattributedHookBlocks,
|
|
432
|
+
settledUnresolvedCount: settledUnresolved.length,
|
|
369
433
|
};
|
|
370
434
|
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
// Golden wire-shape examples for the manager-mode IPC contract — one representative
|
|
2
|
-
// instance per message. This is the single source the cross-language
|
|
3
|
-
// against (Rust `protocol.rs
|
|
4
|
-
// `
|
|
5
|
-
// read that artifact. Because every sample is typed against an `Ipc*` interface from
|
|
2
|
+
// instance per message. This is the single source the cross-language mirror asserts
|
|
3
|
+
// against (Rust `protocol.rs`): the generator script serializes `buildFixtures()` to
|
|
4
|
+
// `fixtures/ipc-protocol.generated.json`, and the mirror's tests read that artifact. Because every sample is typed against an `Ipc*` interface from
|
|
6
5
|
// `ipc-protocol.ts`, renaming or retyping a field there fails `tsc` here — that compile
|
|
7
6
|
// error is what binds the fixtures to the contract. Full rules: docs/ipc-protocol.md.
|
|
8
7
|
|
package/src/ipc-protocol.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// Canonical machine-readable definition of the manager-mode IPC contract.
|
|
2
2
|
// The runner emits these messages; the Rust host crate (crates/stigmer-runner-host/src/
|
|
3
|
-
// protocol.rs)
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// protocol.rs) hand-mirrors them, kept honest by golden fixtures generated from this file
|
|
4
|
+
// via ipc-protocol-fixtures.ts (run `make gen-ipc-fixtures`). The conformance harness's
|
|
5
|
+
// manager-mode spawner (test/conformance/src/harness/runner-manager-process.ts) imports
|
|
6
|
+
// these types directly rather than mirroring them. Full spec and the rule for keeping all
|
|
7
|
+
// definitions in sync: docs/ipc-protocol.md.
|
|
7
8
|
|
|
8
9
|
// Integer protocol version advertised in the `ready` handshake. Bump ONLY on a
|
|
9
10
|
// breaking change (removed/renamed message, changed field type, changed lifecycle
|