@stigmer/runner 3.2.2 → 3.3.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/dist/.build-fingerprint +1 -1
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.d.ts +5 -0
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js +1 -1
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js.map +1 -1
- package/dist/activities/execute-cursor/approval-state.d.ts +27 -2
- package/dist/activities/execute-cursor/approval-state.js +13 -1
- package/dist/activities/execute-cursor/approval-state.js.map +1 -1
- package/dist/activities/execute-cursor/blueprint-resolver.d.ts +9 -1
- package/dist/activities/execute-cursor/blueprint-resolver.js +1 -0
- package/dist/activities/execute-cursor/blueprint-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/hook-script.js +38 -0
- package/dist/activities/execute-cursor/hook-script.js.map +1 -1
- package/dist/activities/execute-cursor/index.d.ts +2 -0
- package/dist/activities/execute-cursor/index.js +56 -13
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/message-translator.d.ts +26 -0
- package/dist/activities/execute-cursor/message-translator.js +78 -2
- package/dist/activities/execute-cursor/message-translator.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.d.ts +7 -1
- package/dist/activities/execute-cursor/prompt-builder.js +4 -0
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-cursor/session-lifecycle.d.ts +45 -0
- package/dist/activities/execute-cursor/session-lifecycle.js +41 -0
- package/dist/activities/execute-cursor/session-lifecycle.js.map +1 -1
- package/dist/activities/execute-cursor/turn-boundary.js +15 -2
- package/dist/activities/execute-cursor/turn-boundary.js.map +1 -1
- package/dist/activities/execute-deep-agent/hitl.d.ts +25 -0
- package/dist/activities/execute-deep-agent/hitl.js +48 -1
- package/dist/activities/execute-deep-agent/hitl.js.map +1 -1
- package/dist/activities/execute-deep-agent/index.js +10 -1
- package/dist/activities/execute-deep-agent/index.js.map +1 -1
- package/dist/activities/execute-deep-agent/prompt-builder.d.ts +5 -0
- package/dist/activities/execute-deep-agent/prompt-builder.js +3 -0
- package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.d.ts +14 -0
- package/dist/activities/execute-deep-agent/setup.js +36 -3
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/status-builder-shared.d.ts +8 -0
- package/dist/activities/execute-deep-agent/status-builder-shared.js.map +1 -1
- package/dist/activities/execute-deep-agent/status-builder.js +6 -0
- package/dist/activities/execute-deep-agent/status-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/v3-status-builder.js +6 -0
- package/dist/activities/execute-deep-agent/v3-status-builder.js.map +1 -1
- package/dist/activities/workflow-event-activities.js +9 -0
- package/dist/activities/workflow-event-activities.js.map +1 -1
- package/dist/config.d.ts +8 -0
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/middleware/approval-gate.d.ts +20 -0
- package/dist/middleware/approval-gate.js +16 -1
- package/dist/middleware/approval-gate.js.map +1 -1
- package/dist/runner-manager.js +3 -0
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.js +3 -0
- package/dist/runner.js.map +1 -1
- package/dist/shared/approval-policy.d.ts +24 -1
- package/dist/shared/approval-policy.js +35 -1
- package/dist/shared/approval-policy.js.map +1 -1
- package/dist/shared/datastore-attachment.d.ts +79 -0
- package/dist/shared/datastore-attachment.js +129 -0
- package/dist/shared/datastore-attachment.js.map +1 -0
- package/dist/shared/with-timeout.d.ts +10 -0
- package/dist/shared/with-timeout.js +14 -1
- package/dist/shared/with-timeout.js.map +1 -1
- package/dist/workflow-engine/tasks/human-input.js +1 -0
- package/dist/workflow-engine/tasks/human-input.js.map +1 -1
- package/dist/workflow-engine/types.d.ts +22 -0
- package/dist/workflow-engine/types.js.map +1 -1
- package/dist/workflows/human-input-orchestrator.d.ts +2 -1
- package/dist/workflows/human-input-orchestrator.js +2 -1
- package/dist/workflows/human-input-orchestrator.js.map +1 -1
- package/package.json +2 -2
- 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/__tests__/workflow-event-activities.test.ts +29 -0
- package/src/activities/execute-cursor/__test-utils__/cursor-hook-harness.ts +6 -0
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +25 -0
- package/src/activities/execute-cursor/__tests__/hitl-ledger.test.ts +105 -0
- package/src/activities/execute-cursor/__tests__/hook-script.test.ts +54 -0
- package/src/activities/execute-cursor/__tests__/session-lifecycle.test.ts +122 -1
- package/src/activities/execute-cursor/approval-state.ts +32 -1
- package/src/activities/execute-cursor/blueprint-resolver.ts +10 -1
- package/src/activities/execute-cursor/hook-script.ts +40 -0
- package/src/activities/execute-cursor/index.ts +64 -15
- package/src/activities/execute-cursor/message-translator.ts +82 -1
- package/src/activities/execute-cursor/prompt-builder.ts +12 -1
- package/src/activities/execute-cursor/session-lifecycle.ts +76 -0
- package/src/activities/execute-cursor/turn-boundary.ts +23 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/hitl.test.ts +103 -2
- package/src/activities/execute-deep-agent/__tests__/index.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +1 -0
- package/src/activities/execute-deep-agent/hitl.ts +51 -0
- package/src/activities/execute-deep-agent/index.ts +11 -1
- package/src/activities/execute-deep-agent/prompt-builder.ts +9 -0
- package/src/activities/execute-deep-agent/setup.ts +57 -2
- package/src/activities/execute-deep-agent/status-builder-shared.ts +8 -0
- package/src/activities/execute-deep-agent/status-builder.ts +7 -0
- package/src/activities/execute-deep-agent/v3-status-builder.ts +7 -0
- package/src/activities/workflow-event-activities.ts +9 -0
- package/src/config.ts +11 -0
- package/src/middleware/__tests__/approval-gate.test.ts +95 -0
- package/src/middleware/approval-gate.ts +37 -0
- package/src/runner-manager.ts +3 -0
- package/src/runner.ts +3 -0
- package/src/shared/__tests__/artifact-storage.test.ts +1 -0
- package/src/shared/__tests__/datastore-attachment.test.ts +165 -0
- package/src/shared/__tests__/with-timeout.test.ts +16 -1
- package/src/shared/approval-policy.ts +41 -2
- package/src/shared/datastore-attachment.ts +166 -0
- package/src/shared/with-timeout.ts +15 -1
- package/src/workflow-engine/__tests__/tasks/human-input.test.ts +40 -0
- package/src/workflow-engine/tasks/human-input.ts +1 -0
- package/src/workflow-engine/types.ts +23 -0
- package/src/workflows/human-input-orchestrator.ts +2 -1
|
@@ -39,7 +39,7 @@ import type { Run, ConversationTurn } from "@cursor/sdk";
|
|
|
39
39
|
|
|
40
40
|
import type { Config } from "../../config.js";
|
|
41
41
|
import { StigmerClient } from "../../client/stigmer-client.js";
|
|
42
|
-
import {
|
|
42
|
+
import { resolveAgentWithTransportRecovery } from "./session-lifecycle.js";
|
|
43
43
|
import type { AgentResolution, CreateAgentOptions, CreateCloudAgentOptions } from "./session-lifecycle.js";
|
|
44
44
|
import { CursorMode } from "@stigmer/protos/ai/stigmer/agentic/session/v1/enum_pb";
|
|
45
45
|
import { determineCursorMode, isCloudMode } from "./cursor-mode.js";
|
|
@@ -55,9 +55,13 @@ import { DeltaEnricher } from "./delta-enricher.js";
|
|
|
55
55
|
import { TodoTracker } from "./todo-tracker.js";
|
|
56
56
|
import { StreamingUpdateScheduler, loadStreamingConfig } from "../../shared/streaming-scheduler.js";
|
|
57
57
|
import { createCursorEventRecorder } from "./cursor-event-recorder.js";
|
|
58
|
-
import { resolveMcpServers, validateMcpServerEnv } from "./mcp-resolver.js";
|
|
58
|
+
import { resolveMcpServers, toCursorMcpConfig, validateMcpServerEnv } from "./mcp-resolver.js";
|
|
59
|
+
import {
|
|
60
|
+
injectDatastoreAttachment,
|
|
61
|
+
synthesizeDatastoreAttachment,
|
|
62
|
+
} from "../../shared/datastore-attachment.js";
|
|
59
63
|
import { mergeApprovalPolicies } from "./approval-policy.js";
|
|
60
|
-
import { deriveActiveLeases } from "../../shared/approval-policy.js";
|
|
64
|
+
import { deriveActiveLeases, isUnattendedApprovalMode } from "../../shared/approval-policy.js";
|
|
61
65
|
import { backfillMcpServersIfNeeded } from "./connect-backfill.js";
|
|
62
66
|
import { resolveExecutionEnv } from "./env-resolver.js";
|
|
63
67
|
import { resolveBlueprint } from "./blueprint-resolver.js";
|
|
@@ -117,7 +121,6 @@ import type { ClassifiedError } from "./error-classifier.js";
|
|
|
117
121
|
import { createAgent, createCloudAgent } from "./session-lifecycle.js";
|
|
118
122
|
import { setMaxListeners } from "node:events";
|
|
119
123
|
import { startHeartbeat } from "../../shared/heartbeat.js";
|
|
120
|
-
import { withTimeout } from "../../shared/with-timeout.js";
|
|
121
124
|
import { getShutdownSignalForQueue } from "../../runner-manager.js";
|
|
122
125
|
|
|
123
126
|
/**
|
|
@@ -584,6 +587,33 @@ async function executeCursorInner(
|
|
|
584
587
|
client, mcpResolution, blueprint.mergedMcpServerUsages, envVars, sessionOrg,
|
|
585
588
|
heartbeat, secretKeys,
|
|
586
589
|
);
|
|
590
|
+
|
|
591
|
+
// Phase 4a2: Synthesize the datastore records attachment (T05).
|
|
592
|
+
// Deliberately AFTER resolve + backfill: the attachment has no
|
|
593
|
+
// McpServerUsage and reports discovered capabilities, so the
|
|
594
|
+
// backfill's destructiveHint tightener can never force-gate
|
|
595
|
+
// delete_record (which on channels would be silently skipped).
|
|
596
|
+
// Empty approval maps keep it approval-free by construction.
|
|
597
|
+
if (blueprint.datastoreUsages.length > 0) {
|
|
598
|
+
const scopedCredential =
|
|
599
|
+
(await client.acquireScopedRunnerToken({ agentExecutionId: executionId }))
|
|
600
|
+
?? config.stigmerTokenRef?.current
|
|
601
|
+
?? config.stigmerToken;
|
|
602
|
+
const attachment = synthesizeDatastoreAttachment(blueprint.datastoreUsages, {
|
|
603
|
+
bridgeEndpoint: config.mcpBridgeEndpoint,
|
|
604
|
+
credential: scopedCredential,
|
|
605
|
+
backendEndpoint: config.stigmerBackendEndpoint,
|
|
606
|
+
});
|
|
607
|
+
if (attachment) {
|
|
608
|
+
const resolvedServers = injectDatastoreAttachment(
|
|
609
|
+
mcpResolution.resolvedServers, attachment,
|
|
610
|
+
);
|
|
611
|
+
mcpResolution = {
|
|
612
|
+
resolvedServers,
|
|
613
|
+
cursorConfig: toCursorMcpConfig(resolvedServers),
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
}
|
|
587
617
|
const mcpConfig = mcpResolution.cursorConfig;
|
|
588
618
|
|
|
589
619
|
// Phase 4b: Merge approval policies from all layers.
|
|
@@ -722,6 +752,11 @@ async function executeCursorInner(
|
|
|
722
752
|
// absent a git tree keeps gating gitignored writes and a non-git workspace
|
|
723
753
|
// falls back to the deny-gate entirely (no regression).
|
|
724
754
|
const captureIgnored = captureMode && !!artifactStorage;
|
|
755
|
+
// Unattended approval mode (DD-014): approver-less surfaces (channels,
|
|
756
|
+
// guest shares) stamp APPROVAL_MODE_UNATTENDED; the hook then records
|
|
757
|
+
// approval denials with the non-pausing "unattended" kind, so the
|
|
758
|
+
// first-denial stop never fires and the turn boundary settles the denied
|
|
759
|
+
// calls as SKIPPED instead of pausing a turn nobody can approve.
|
|
725
760
|
const approvalState = buildApprovalState(
|
|
726
761
|
mergedPolicies,
|
|
727
762
|
globalBypass,
|
|
@@ -730,6 +765,7 @@ async function executeCursorInner(
|
|
|
730
765
|
captureMode,
|
|
731
766
|
captureIgnored,
|
|
732
767
|
gitWorkspace,
|
|
768
|
+
isUnattendedApprovalMode(execution),
|
|
733
769
|
);
|
|
734
770
|
const hitlGate = await installHitlGate({
|
|
735
771
|
workspaceRoot: primaryWorkspaceDir,
|
|
@@ -842,21 +878,29 @@ async function executeCursorInner(
|
|
|
842
878
|
|
|
843
879
|
// Agent.create/Agent.resume have no timeout of their own — a degraded
|
|
844
880
|
// transport (dead proxy connection, stale HTTP/2 session) hangs them
|
|
845
|
-
// forever, which the periodic heartbeat would happily keep alive.
|
|
846
|
-
//
|
|
847
|
-
//
|
|
848
|
-
//
|
|
881
|
+
// forever, which the periodic heartbeat would happily keep alive. Each
|
|
882
|
+
// attempt is bounded; on expiry the wrapper resets the proxy transport
|
|
883
|
+
// and retries once, so a stale-session hang recovers without failing the
|
|
884
|
+
// execution. A second expiry propagates a plain Error to the generic
|
|
885
|
+
// catch below, which persists EXECUTION_FAILED (no Temporal retry —
|
|
886
|
+
// the activity returns rather than throws, and maximumAttempts is 1).
|
|
849
887
|
heartbeatPhase = "resolving_agent";
|
|
850
888
|
const resolveTimeoutSeconds = Math.round(config.agentResolveTimeoutMs / 1000);
|
|
851
|
-
let resolution: AgentResolution = await
|
|
852
|
-
|
|
853
|
-
|
|
889
|
+
let resolution: AgentResolution = await resolveAgentWithTransportRecovery({
|
|
890
|
+
harnessStateId: threadId,
|
|
891
|
+
createOptions,
|
|
892
|
+
mode: agentMode,
|
|
893
|
+
timeoutMs: config.agentResolveTimeoutMs,
|
|
894
|
+
buildTimeoutMessage: (finalAttempt) =>
|
|
854
895
|
`Cursor agent ${threadId ? "resume" : "create"} timed out after ${resolveTimeoutSeconds}s ` +
|
|
855
896
|
`(${config.proxyEndpoint ? `via proxy ${config.proxyEndpoint}` : "direct Cursor API connection"}). ` +
|
|
856
|
-
`The transport connection is likely dead.
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
897
|
+
`The transport connection is likely dead. ` +
|
|
898
|
+
(finalAttempt
|
|
899
|
+
? `An automatic retry on a fresh transport connection also timed out. ` +
|
|
900
|
+
`Retry the message later; if this persists, check proxy and network health.`
|
|
901
|
+
: `Resetting the transport and retrying automatically.`),
|
|
902
|
+
resetTransport: closeProxySessions,
|
|
903
|
+
});
|
|
860
904
|
|
|
861
905
|
console.log(
|
|
862
906
|
`ExecuteCursor agent resolved: execution=${executionId}, ` +
|
|
@@ -907,6 +951,7 @@ async function executeCursorInner(
|
|
|
907
951
|
instructions: blueprint.instructions,
|
|
908
952
|
userMessage: spec.message,
|
|
909
953
|
skills: skillMetadata,
|
|
954
|
+
datastoreUsages: blueprint.datastoreUsages,
|
|
910
955
|
subAgents: blueprint.subAgents,
|
|
911
956
|
workspaceDirs: blueprint.workspaceDirs,
|
|
912
957
|
workspaceFileRefs: spec.workspaceFileRefs ?? [],
|
|
@@ -1487,6 +1532,7 @@ async function executeCursorInner(
|
|
|
1487
1532
|
instructions: blueprint.instructions,
|
|
1488
1533
|
userMessage: spec.message,
|
|
1489
1534
|
skills: skillMetadata,
|
|
1535
|
+
datastoreUsages: blueprint.datastoreUsages,
|
|
1490
1536
|
subAgents: blueprint.subAgents,
|
|
1491
1537
|
workspaceDirs: blueprint.workspaceDirs,
|
|
1492
1538
|
workspaceFileRefs: spec.workspaceFileRefs ?? [],
|
|
@@ -2043,6 +2089,8 @@ export interface BuildPromptInput {
|
|
|
2043
2089
|
instructions: string;
|
|
2044
2090
|
userMessage: string;
|
|
2045
2091
|
skills: import("./prompt-builder.js").SkillMetadata[];
|
|
2092
|
+
/** Datastores attached via `datastore_usages` — the `<available_datastores>` section. */
|
|
2093
|
+
datastoreUsages?: import("@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb").DatastoreUsage[];
|
|
2046
2094
|
subAgents: import("@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb").SubAgent[];
|
|
2047
2095
|
workspaceDirs: string[];
|
|
2048
2096
|
workspaceFileRefs: string[];
|
|
@@ -2142,6 +2190,7 @@ export function buildPrompt(input: BuildPromptInput): string {
|
|
|
2142
2190
|
instructions,
|
|
2143
2191
|
userMessage,
|
|
2144
2192
|
skills,
|
|
2193
|
+
datastoreUsages: input.datastoreUsages ?? [],
|
|
2145
2194
|
subAgents,
|
|
2146
2195
|
workspaceDirs,
|
|
2147
2196
|
workspaceFileRefs,
|
|
@@ -36,7 +36,7 @@ import { AgentMessageSchema, ToolCallSchema } from "@stigmer/protos/ai/stigmer/a
|
|
|
36
36
|
import type { AgentMessage, ToolCall } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
37
37
|
import { SubAgentExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/subagent_pb";
|
|
38
38
|
import type { SubAgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/subagent_pb";
|
|
39
|
-
import { MessageType, ToolCallStatus, SubAgentStatus, ToolKind } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
39
|
+
import { ApprovalPolicySource, MessageType, ToolCallStatus, SubAgentStatus, ToolKind } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
40
40
|
import type { SDKMessage } from "@cursor/sdk";
|
|
41
41
|
import type { MergedToolPolicy } from "./approval-policy.js";
|
|
42
42
|
import { lookupMcpToolPolicy, resolveApprovalMessage, builtInRequiresApproval, getBuiltInApprovalMessage, SALIENT_ARG_FIELDS } from "./approval-policy.js";
|
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
POLICY_ENGINE_VERSION,
|
|
45
45
|
resolveApprovalProvenance,
|
|
46
46
|
toProtoPolicySource,
|
|
47
|
+
unattendedSkipMessage,
|
|
47
48
|
} from "../../shared/approval-policy.js";
|
|
48
49
|
import {
|
|
49
50
|
approvalDenials,
|
|
@@ -1916,6 +1917,86 @@ export function detectUnattributedHookBlocks(
|
|
|
1916
1917
|
return blocks;
|
|
1917
1918
|
}
|
|
1918
1919
|
|
|
1920
|
+
/**
|
|
1921
|
+
* Stamp the tool calls the hook denied under UNATTENDED approval mode
|
|
1922
|
+
* (DD-014) as terminal TOOL_CALL_SKIPPED rows with UNATTENDED_SKIP
|
|
1923
|
+
* provenance — the Cursor twin of the native harness's
|
|
1924
|
+
* `reconcileUnattendedSkips`, so both harnesses persist the same honest
|
|
1925
|
+
* shape for a platform-resolved skip.
|
|
1926
|
+
*
|
|
1927
|
+
* An unattended denial never pauses the run (its ledger kind is excluded
|
|
1928
|
+
* from {@link approvalDenials}), so the stream leaves the denied call as a
|
|
1929
|
+
* FAILED row carrying Cursor's generic hook-block error — dishonest ("the
|
|
1930
|
+
* tool broke") and alarming in a transcript an org admin reviews. This pass
|
|
1931
|
+
* correlates the unattended ledger entries to their streamed calls with the
|
|
1932
|
+
* SAME two identities the rest of the file uses — exact token, then
|
|
1933
|
+
* (category, workspace-normalized path) for the abs-vs-rel drift — and
|
|
1934
|
+
* settles each match to SKIPPED with a result explaining the skip.
|
|
1935
|
+
*
|
|
1936
|
+
* Scope: hook-blocked FAILED rows and still-non-terminal (PENDING/RUNNING)
|
|
1937
|
+
* rows only — a COMPLETED row is a real execution and is never rewritten
|
|
1938
|
+
* (an unattended deny cannot produce one). `approval_action`/`approved_by`
|
|
1939
|
+
* stay untouched: server-owned, human-decision-only fields (DD-014 D-e).
|
|
1940
|
+
* Unmatched ledger entries need no synthesis — there is no pause to
|
|
1941
|
+
* surface; the model already saw the deny and adapted in-turn.
|
|
1942
|
+
*
|
|
1943
|
+
* Returns how many tool calls were stamped.
|
|
1944
|
+
*/
|
|
1945
|
+
export function stampUnattendedSkippedToolCalls(
|
|
1946
|
+
messages: readonly AgentMessage[],
|
|
1947
|
+
subAgentExecutions: readonly SubAgentExecution[],
|
|
1948
|
+
unattendedLedger: readonly DeniedLedgerEntry[],
|
|
1949
|
+
workspaceRoot?: string,
|
|
1950
|
+
): number {
|
|
1951
|
+
if (unattendedLedger.length === 0) return 0;
|
|
1952
|
+
|
|
1953
|
+
const ledgerTokens = new Set(unattendedLedger.map((e) => e.token));
|
|
1954
|
+
const ledgerNormalizedSalients = new Set<string>();
|
|
1955
|
+
if (workspaceRoot) {
|
|
1956
|
+
for (const entry of unattendedLedger) {
|
|
1957
|
+
const decoded = decodeIdentityToken(entry.token);
|
|
1958
|
+
if (!decoded) continue;
|
|
1959
|
+
const normalized = normalizedFileSalient(decoded.key, decoded.salient, workspaceRoot);
|
|
1960
|
+
if (normalized) ledgerNormalizedSalients.add(normalized);
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
const matchesLedger = (tc: ToolCall): boolean => {
|
|
1965
|
+
if (ledgerTokens.has(toolCallIdentityToken(tc))) return true;
|
|
1966
|
+
if (!workspaceRoot) return false;
|
|
1967
|
+
const id = toolIdentity(tc.name, tc.mcpServerSlug, toolCallArgs(tc));
|
|
1968
|
+
const normalized = normalizedFileSalient(id.key, id.salient, workspaceRoot);
|
|
1969
|
+
return !!normalized && ledgerNormalizedSalients.has(normalized);
|
|
1970
|
+
};
|
|
1971
|
+
|
|
1972
|
+
let stamped = 0;
|
|
1973
|
+
const apply = (msgs: readonly AgentMessage[]): void => {
|
|
1974
|
+
for (const msg of msgs) {
|
|
1975
|
+
for (const tc of msg.toolCalls) {
|
|
1976
|
+
const deniedShape =
|
|
1977
|
+
(tc.status === ToolCallStatus.TOOL_CALL_FAILED && isHookBlockError(tc.error)) ||
|
|
1978
|
+
tc.status === ToolCallStatus.TOOL_CALL_PENDING ||
|
|
1979
|
+
tc.status === ToolCallStatus.TOOL_CALL_RUNNING;
|
|
1980
|
+
if (!deniedShape || !matchesLedger(tc)) continue;
|
|
1981
|
+
tc.status = ToolCallStatus.TOOL_CALL_SKIPPED;
|
|
1982
|
+
tc.approvalPolicySource = ApprovalPolicySource.UNATTENDED_SKIP;
|
|
1983
|
+
tc.policyEngineVersion = POLICY_ENGINE_VERSION;
|
|
1984
|
+
tc.error = "";
|
|
1985
|
+
tc.result = unattendedSkipMessage(tc.name);
|
|
1986
|
+
tc.isStreaming = false;
|
|
1987
|
+
if (!tc.completedAt) tc.completedAt = utcTimestamp();
|
|
1988
|
+
stamped++;
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
};
|
|
1992
|
+
|
|
1993
|
+
apply(messages);
|
|
1994
|
+
for (const subAgent of subAgentExecutions) {
|
|
1995
|
+
apply(subAgent.messages);
|
|
1996
|
+
}
|
|
1997
|
+
return stamped;
|
|
1998
|
+
}
|
|
1999
|
+
|
|
1919
2000
|
/**
|
|
1920
2001
|
* Compute a streamed tool call's identity token in the same canonical space the
|
|
1921
2002
|
* preToolUse hook records denials in (see {@link toolIdentity} / primaryToken).
|
|
@@ -17,10 +17,11 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import { resolve } from "node:path";
|
|
20
|
-
import type { SubAgent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
|
|
20
|
+
import type { DatastoreUsage, SubAgent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
|
|
21
21
|
import type { PendingApproval } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/approval_pb";
|
|
22
22
|
import { ApprovalAction, InteractionMode } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
23
23
|
import { formatContextBridgeText } from "../../shared/context-bridge.js";
|
|
24
|
+
import { formatDatastoresSection } from "../../shared/datastore-attachment.js";
|
|
24
25
|
import {
|
|
25
26
|
formatSenderIdentityText,
|
|
26
27
|
type SenderIdentity,
|
|
@@ -53,6 +54,12 @@ export interface EnhancedPromptOptions {
|
|
|
53
54
|
instructions: string;
|
|
54
55
|
userMessage: string;
|
|
55
56
|
skills: SkillMetadata[];
|
|
57
|
+
/**
|
|
58
|
+
* Datastores attached via `datastore_usages` — rendered as the
|
|
59
|
+
* `<available_datastores>` section (DD-005 SD-5, skills precedent)
|
|
60
|
+
* pointing the model at the synthesized record tools.
|
|
61
|
+
*/
|
|
62
|
+
datastoreUsages?: DatastoreUsage[];
|
|
56
63
|
subAgents: SubAgent[];
|
|
57
64
|
workspaceDirs: string[];
|
|
58
65
|
workspaceFileRefs: string[];
|
|
@@ -113,6 +120,10 @@ export function buildEnhancedPrompt(options: EnhancedPromptOptions): string {
|
|
|
113
120
|
sections.push(formatSkillsSection(options.skills));
|
|
114
121
|
}
|
|
115
122
|
|
|
123
|
+
if (options.datastoreUsages !== undefined && options.datastoreUsages.length > 0) {
|
|
124
|
+
sections.push(formatDatastoresSection(options.datastoreUsages));
|
|
125
|
+
}
|
|
126
|
+
|
|
116
127
|
if (options.subAgents.length > 0) {
|
|
117
128
|
sections.push(formatSubAgentsSection(options.subAgents));
|
|
118
129
|
}
|
|
@@ -41,6 +41,7 @@ import { join } from "node:path";
|
|
|
41
41
|
|
|
42
42
|
import { Agent } from "@cursor/sdk";
|
|
43
43
|
import type { SDKAgent, CursorAgentPlatformOptions, AgentDefinition } from "@cursor/sdk";
|
|
44
|
+
import { withTimeout, TimeoutError } from "../../shared/with-timeout.js";
|
|
44
45
|
import type { CursorMcpServerConfig } from "./mcp-resolver.js";
|
|
45
46
|
|
|
46
47
|
// ---------------------------------------------------------------------------
|
|
@@ -437,6 +438,81 @@ export async function resolveAgent(
|
|
|
437
438
|
};
|
|
438
439
|
}
|
|
439
440
|
|
|
441
|
+
// ---------------------------------------------------------------------------
|
|
442
|
+
// Transport-recovery wrapper
|
|
443
|
+
// ---------------------------------------------------------------------------
|
|
444
|
+
|
|
445
|
+
export interface ResolveWithTransportRecoveryOptions {
|
|
446
|
+
harnessStateId: string;
|
|
447
|
+
createOptions: CreateAgentOptions | CreateCloudAgentOptions;
|
|
448
|
+
mode: "local" | "cloud";
|
|
449
|
+
/** Bound applied to each attempt independently. */
|
|
450
|
+
timeoutMs: number;
|
|
451
|
+
/**
|
|
452
|
+
* Builds the timeout rejection message. `finalAttempt` is true when no
|
|
453
|
+
* further automatic retry will follow, so the message can be honest about
|
|
454
|
+
* whether "retry" means the system or the user.
|
|
455
|
+
*/
|
|
456
|
+
buildTimeoutMessage: (finalAttempt: boolean) => string;
|
|
457
|
+
/**
|
|
458
|
+
* Closes the transport the hung attempt is riding on so the next attempt
|
|
459
|
+
* dials fresh. Injected (rather than imported from the http2 interceptor)
|
|
460
|
+
* to keep this module transport-agnostic and the recovery unit-testable.
|
|
461
|
+
*/
|
|
462
|
+
resetTransport: () => void;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Resolve a Cursor Agent with one automatic recovery from a transport hang.
|
|
467
|
+
*
|
|
468
|
+
* Agent.create/Agent.resume have no timeout of their own — a degraded
|
|
469
|
+
* transport (dead proxy connection, stale HTTP/2 session) hangs them forever.
|
|
470
|
+
* Each attempt is bounded by `timeoutMs`; when the first attempt expires,
|
|
471
|
+
* the transport is reset and the full resolveAgent() is retried once. Only
|
|
472
|
+
* a TimeoutError triggers recovery: deterministic failures (auth, validation)
|
|
473
|
+
* propagate immediately — resetting the transport cannot fix them.
|
|
474
|
+
*
|
|
475
|
+
* The retry re-invokes resolveAgent(), not createAgent() directly: at
|
|
476
|
+
* resolve time only the transport is suspect, not the agent handle, so a
|
|
477
|
+
* timed-out resume is retried resume-first and conversation context is
|
|
478
|
+
* preserved. (This deliberately diverges from the stream-phase recovery in
|
|
479
|
+
* index.ts, which jumps to a fresh create because there the handle itself
|
|
480
|
+
* has already failed a run.)
|
|
481
|
+
*
|
|
482
|
+
* Orphan semantics: withTimeout bounds the wait, not the work, so the first
|
|
483
|
+
* attempt's promise survives its expiry. resetTransport() closes the HTTP/2
|
|
484
|
+
* session that attempt is riding on, so the orphan rejects promptly and
|
|
485
|
+
* withTimeout's attached catch absorbs the late rejection (settled promise —
|
|
486
|
+
* no unhandledRejection). In the residual case where the orphan completes
|
|
487
|
+
* before the reset, it leaves an agent that never receives a prompt: no LLM
|
|
488
|
+
* cost, no state impact, cleaned up with the sandbox.
|
|
489
|
+
*/
|
|
490
|
+
export async function resolveAgentWithTransportRecovery(
|
|
491
|
+
opts: ResolveWithTransportRecoveryOptions,
|
|
492
|
+
): Promise<AgentResolution> {
|
|
493
|
+
const attempt = (finalAttempt: boolean) =>
|
|
494
|
+
withTimeout(
|
|
495
|
+
opts.timeoutMs,
|
|
496
|
+
() => opts.buildTimeoutMessage(finalAttempt),
|
|
497
|
+
() => resolveAgent(opts.harnessStateId, opts.createOptions, opts.mode),
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
try {
|
|
501
|
+
return await attempt(false);
|
|
502
|
+
} catch (err) {
|
|
503
|
+
if (!(err instanceof TimeoutError)) throw err;
|
|
504
|
+
|
|
505
|
+
console.warn(
|
|
506
|
+
`resolveAgentWithTransportRecovery: agent resolution timed out after ` +
|
|
507
|
+
`${opts.timeoutMs}ms (sessionId=${opts.createOptions.sessionId}, mode=${opts.mode}, ` +
|
|
508
|
+
`resume=${!!opts.harnessStateId}) — resetting transport and retrying once`,
|
|
509
|
+
);
|
|
510
|
+
opts.resetTransport();
|
|
511
|
+
|
|
512
|
+
return attempt(true);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
440
516
|
/**
|
|
441
517
|
* Dispose a Cursor Agent when a session is deleted.
|
|
442
518
|
* Best-effort: logs and swallows errors.
|
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
denialKindOf,
|
|
43
43
|
primaryToken,
|
|
44
44
|
readDenialLedger,
|
|
45
|
+
unattendedDenials,
|
|
45
46
|
type ApprovalGrant,
|
|
46
47
|
} from "./approval-state.js";
|
|
47
48
|
import { deriveTurnCommandProvenance } from "./command-provenance.js";
|
|
@@ -50,6 +51,7 @@ import {
|
|
|
50
51
|
clearProvisionalPostDenialNarration,
|
|
51
52
|
detectUnattributedHookBlocks,
|
|
52
53
|
reconcileDeniedToolCalls,
|
|
54
|
+
stampUnattendedSkippedToolCalls,
|
|
53
55
|
type UnattributedHookBlock,
|
|
54
56
|
} from "./message-translator.js";
|
|
55
57
|
|
|
@@ -302,6 +304,27 @@ export async function runTurnBoundary(opts: TurnBoundaryOptions): Promise<TurnBo
|
|
|
302
304
|
}
|
|
303
305
|
}
|
|
304
306
|
|
|
307
|
+
// Unattended approval mode (DD-014): denials the hook resolved with the
|
|
308
|
+
// non-pausing "unattended" kind never became gates above; settle their
|
|
309
|
+
// streamed rows (FAILED-with-hook-error or interrupted non-terminal) to
|
|
310
|
+
// honest TOOL_CALL_SKIPPED + UNATTENDED_SKIP provenance — the same shape
|
|
311
|
+
// the native harness's reconcileUnattendedSkips persists. Runs before the
|
|
312
|
+
// #205 attribution pass, which then never sees them as hook-block FAILED
|
|
313
|
+
// rows (they are in the full ledger regardless, so attribution was safe
|
|
314
|
+
// either way).
|
|
315
|
+
const stampedUnattended = stampUnattendedSkippedToolCalls(
|
|
316
|
+
status.messages,
|
|
317
|
+
status.subAgentExecutions,
|
|
318
|
+
unattendedDenials(deniedLedger),
|
|
319
|
+
primaryWorkspaceDir,
|
|
320
|
+
);
|
|
321
|
+
if (stampedUnattended > 0) {
|
|
322
|
+
console.log(
|
|
323
|
+
`ExecuteCursor turn boundary: ${stampedUnattended} unattended-mode tool call(s) ` +
|
|
324
|
+
`settled as SKIPPED (no approver on this surface; execution=${executionId})`,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
305
328
|
// Issue #205 invariant: a blocked tool must never silently complete. Match
|
|
306
329
|
// this turn's hook-blocked FAILED rows against the FULL ledger (all kinds) —
|
|
307
330
|
// anything left over was denied by a hook that is not ours (or by our hook
|
|
@@ -239,6 +239,7 @@ const httpConfig: Config = {
|
|
|
239
239
|
temporalAddress: "localhost:7233",
|
|
240
240
|
temporalNamespace: "default",
|
|
241
241
|
stigmerBackendEndpoint: "http://localhost:7234",
|
|
242
|
+
mcpBridgeEndpoint: null,
|
|
242
243
|
stigmerToken: null,
|
|
243
244
|
cursorApiKey: "",
|
|
244
245
|
workspaceRootDir: "/tmp/test",
|
|
@@ -284,6 +284,7 @@ const baseConfig: Config = {
|
|
|
284
284
|
temporalAddress: "localhost:7233",
|
|
285
285
|
temporalNamespace: "default",
|
|
286
286
|
stigmerBackendEndpoint: "http://localhost:7234",
|
|
287
|
+
mcpBridgeEndpoint: null,
|
|
287
288
|
stigmerToken: null,
|
|
288
289
|
cursorApiKey: "",
|
|
289
290
|
workspaceRootDir: "/tmp/test",
|
|
@@ -238,6 +238,7 @@ const baseConfig: Config = {
|
|
|
238
238
|
temporalAddress: "localhost:7233",
|
|
239
239
|
temporalNamespace: "default",
|
|
240
240
|
stigmerBackendEndpoint: "http://localhost:7234",
|
|
241
|
+
mcpBridgeEndpoint: null,
|
|
241
242
|
stigmerToken: null,
|
|
242
243
|
cursorApiKey: "",
|
|
243
244
|
workspaceRootDir: "/tmp/test",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import { Command } from "@langchain/langgraph";
|
|
3
|
-
import { resolveResumeInput, reconcileNonExecutingDecisions } from "../hitl.js";
|
|
4
|
-
import { ApprovalAction, ToolCallStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
3
|
+
import { resolveResumeInput, reconcileNonExecutingDecisions, reconcileUnattendedSkips } from "../hitl.js";
|
|
4
|
+
import { ApprovalAction, ApprovalPolicySource, ToolCallStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
5
5
|
import { create } from "@bufbuild/protobuf";
|
|
6
6
|
import {
|
|
7
7
|
AgentMessageSchema,
|
|
@@ -238,3 +238,104 @@ describe("reconcileNonExecutingDecisions", () => {
|
|
|
238
238
|
expect(status.messages[0].toolCalls[0].status).toBe(ToolCallStatus.TOOL_CALL_WAITING_APPROVAL);
|
|
239
239
|
});
|
|
240
240
|
});
|
|
241
|
+
|
|
242
|
+
describe("reconcileUnattendedSkips (DD-014)", () => {
|
|
243
|
+
function statusWithCalls(
|
|
244
|
+
toolCalls: Array<{ id: string; status: ToolCallStatus; result?: string }>,
|
|
245
|
+
) {
|
|
246
|
+
return create(AgentExecutionStatusSchema, {
|
|
247
|
+
messages: [
|
|
248
|
+
create(AgentMessageSchema, {
|
|
249
|
+
toolCalls: toolCalls.map(tc =>
|
|
250
|
+
create(ToolCallSchema, {
|
|
251
|
+
id: tc.id,
|
|
252
|
+
name: "gated_tool",
|
|
253
|
+
status: tc.status,
|
|
254
|
+
result: tc.result ?? "",
|
|
255
|
+
}),
|
|
256
|
+
),
|
|
257
|
+
}),
|
|
258
|
+
],
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
it("terminalizes a registry-recorded call to SKIPPED with UNATTENDED_SKIP provenance", () => {
|
|
263
|
+
// The stream saw the gate's skip ToolMessage as a normal tool result and
|
|
264
|
+
// marked the call COMPLETED — the reconciler owns the honest terminal shape.
|
|
265
|
+
const status = statusWithCalls([
|
|
266
|
+
{ id: "call-1", status: ToolCallStatus.TOOL_CALL_COMPLETED, result: "was skipped" },
|
|
267
|
+
]);
|
|
268
|
+
|
|
269
|
+
reconcileUnattendedSkips(status, new Set(["call-1"]));
|
|
270
|
+
|
|
271
|
+
const tc = status.messages[0].toolCalls[0];
|
|
272
|
+
expect(tc.status).toBe(ToolCallStatus.TOOL_CALL_SKIPPED);
|
|
273
|
+
expect(tc.approvalPolicySource).toBe(ApprovalPolicySource.UNATTENDED_SKIP);
|
|
274
|
+
expect(tc.policyEngineVersion).not.toBe("");
|
|
275
|
+
// Server-owned human-decision fields stay untouched (DD-014 D-e).
|
|
276
|
+
expect(tc.approvalAction).toBe(ApprovalAction.UNSPECIFIED);
|
|
277
|
+
expect(tc.approvedBy).toBe("");
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("backfills the skip result on a row whose tool events never fired", () => {
|
|
281
|
+
const status = statusWithCalls([
|
|
282
|
+
{ id: "call-1", status: ToolCallStatus.TOOL_CALL_RUNNING },
|
|
283
|
+
]);
|
|
284
|
+
|
|
285
|
+
reconcileUnattendedSkips(status, new Set(["call-1"]));
|
|
286
|
+
|
|
287
|
+
const tc = status.messages[0].toolCalls[0];
|
|
288
|
+
expect(tc.status).toBe(ToolCallStatus.TOOL_CALL_SKIPPED);
|
|
289
|
+
expect(tc.result).toContain("skipped automatically");
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("touches only registry entries and no-ops on an empty/absent registry", () => {
|
|
293
|
+
const status = statusWithCalls([
|
|
294
|
+
{ id: "call-1", status: ToolCallStatus.TOOL_CALL_COMPLETED, result: "real result" },
|
|
295
|
+
]);
|
|
296
|
+
|
|
297
|
+
reconcileUnattendedSkips(status, new Set(["other-call"]));
|
|
298
|
+
reconcileUnattendedSkips(status, new Set());
|
|
299
|
+
reconcileUnattendedSkips(status, undefined);
|
|
300
|
+
|
|
301
|
+
const tc = status.messages[0].toolCalls[0];
|
|
302
|
+
expect(tc.status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
|
|
303
|
+
expect(tc.result).toBe("real result");
|
|
304
|
+
expect(tc.approvalPolicySource).toBe(ApprovalPolicySource.UNSPECIFIED);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("covers sub-agent transcripts (sub-agent gates share the parent registry)", () => {
|
|
308
|
+
const status = create(AgentExecutionStatusSchema, {
|
|
309
|
+
messages: [],
|
|
310
|
+
subAgentExecutions: [{
|
|
311
|
+
messages: [
|
|
312
|
+
create(AgentMessageSchema, {
|
|
313
|
+
toolCalls: [create(ToolCallSchema, {
|
|
314
|
+
id: "sub-call-1",
|
|
315
|
+
name: "gated_tool",
|
|
316
|
+
status: ToolCallStatus.TOOL_CALL_COMPLETED,
|
|
317
|
+
})],
|
|
318
|
+
}),
|
|
319
|
+
],
|
|
320
|
+
}],
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
reconcileUnattendedSkips(status, new Set(["sub-call-1"]));
|
|
324
|
+
|
|
325
|
+
const tc = status.subAgentExecutions[0].messages[0].toolCalls[0];
|
|
326
|
+
expect(tc.status).toBe(ToolCallStatus.TOOL_CALL_SKIPPED);
|
|
327
|
+
expect(tc.approvalPolicySource).toBe(ApprovalPolicySource.UNATTENDED_SKIP);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("is idempotent", () => {
|
|
331
|
+
const status = statusWithCalls([
|
|
332
|
+
{ id: "call-1", status: ToolCallStatus.TOOL_CALL_COMPLETED },
|
|
333
|
+
]);
|
|
334
|
+
|
|
335
|
+
reconcileUnattendedSkips(status, new Set(["call-1"]));
|
|
336
|
+
const after = JSON.stringify(status.messages[0].toolCalls[0]);
|
|
337
|
+
reconcileUnattendedSkips(status, new Set(["call-1"]));
|
|
338
|
+
|
|
339
|
+
expect(JSON.stringify(status.messages[0].toolCalls[0])).toBe(after);
|
|
340
|
+
});
|
|
341
|
+
});
|
|
@@ -35,6 +35,7 @@ describe("ExecuteDeepAgent activity", () => {
|
|
|
35
35
|
temporalAddress: "localhost:7233",
|
|
36
36
|
temporalNamespace: "default",
|
|
37
37
|
stigmerBackendEndpoint: "http://localhost:7234",
|
|
38
|
+
mcpBridgeEndpoint: null,
|
|
38
39
|
stigmerToken: null,
|
|
39
40
|
cursorApiKey: "",
|
|
40
41
|
workspaceRootDir: "/tmp/test",
|
|
@@ -244,6 +244,7 @@ const memoryConfig: Config = {
|
|
|
244
244
|
temporalAddress: "localhost:7233",
|
|
245
245
|
temporalNamespace: "default",
|
|
246
246
|
stigmerBackendEndpoint: "http://localhost:7234",
|
|
247
|
+
mcpBridgeEndpoint: null,
|
|
247
248
|
stigmerToken: null,
|
|
248
249
|
cursorApiKey: "",
|
|
249
250
|
workspaceRootDir: "/tmp/test",
|
|
@@ -17,9 +17,11 @@ import { Command } from "@langchain/langgraph";
|
|
|
17
17
|
import type { AgentExecution, AgentExecutionStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
18
18
|
import {
|
|
19
19
|
ApprovalAction,
|
|
20
|
+
ApprovalPolicySource,
|
|
20
21
|
ToolCallStatus,
|
|
21
22
|
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
22
23
|
import type { AgentMessage } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
24
|
+
import { POLICY_ENGINE_VERSION, unattendedSkipMessage } from "../../shared/approval-policy.js";
|
|
23
25
|
|
|
24
26
|
// APPROVE_ALL resumes the interrupted tool exactly like APPROVE. Its
|
|
25
27
|
// "auto-approve the rest of the run" effect is realized in setup.ts (the
|
|
@@ -233,3 +235,52 @@ export function reconcileNonExecutingDecisions(status: AgentExecutionStatus): vo
|
|
|
233
235
|
apply(subAgent.messages);
|
|
234
236
|
}
|
|
235
237
|
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Terminalize every tool call the approval gate auto-skipped under UNATTENDED
|
|
241
|
+
* approval mode — the sibling of {@link reconcileNonExecutingDecisions} for
|
|
242
|
+
* skips that have no human decision behind them.
|
|
243
|
+
*
|
|
244
|
+
* The gate (the single writer of the registry) records each auto-skipped
|
|
245
|
+
* tool-call id at the moment it returns the skip ToolMessage; this reconciler
|
|
246
|
+
* (the single writer of the terminal row) folds each id into:
|
|
247
|
+
* - `status = TOOL_CALL_SKIPPED` — the tool did not run, whatever transient
|
|
248
|
+
* status the stream left behind (COMPLETED from a tool_finished that carried
|
|
249
|
+
* the skip message, or RUNNING when no tool events fired);
|
|
250
|
+
* - `approval_policy_source = UNATTENDED_SKIP` — the resolution layer,
|
|
251
|
+
* overriding the gating-layer source stamped at tool-start (the
|
|
252
|
+
* AUTO_APPROVE_ALL precedent: layer-4 resolutions own the resolved call);
|
|
253
|
+
* - a result backfilled from {@link unattendedSkipMessage} when the stream
|
|
254
|
+
* delivered none, so the transcript row is never blank.
|
|
255
|
+
*
|
|
256
|
+
* `approval_action` / `approved_by` are deliberately NOT touched — those are
|
|
257
|
+
* server-owned fields recording HUMAN decisions only (DD-014 D-e). No
|
|
258
|
+
* approval-request event exists for these calls, so the pending-approvals
|
|
259
|
+
* projection stays empty by construction. Idempotent: re-running re-resolves
|
|
260
|
+
* identically. Covers sub-agent transcripts because sub-agent gates inherit
|
|
261
|
+
* the parent's registry instance.
|
|
262
|
+
*/
|
|
263
|
+
export function reconcileUnattendedSkips(
|
|
264
|
+
status: AgentExecutionStatus,
|
|
265
|
+
unattendedSkips: ReadonlySet<string> | undefined,
|
|
266
|
+
): void {
|
|
267
|
+
if (!unattendedSkips || unattendedSkips.size === 0) return;
|
|
268
|
+
|
|
269
|
+
const apply = (messages: readonly AgentMessage[]): void => {
|
|
270
|
+
for (const msg of messages) {
|
|
271
|
+
for (const tc of msg.toolCalls) {
|
|
272
|
+
if (!unattendedSkips.has(tc.id)) continue;
|
|
273
|
+
tc.status = ToolCallStatus.TOOL_CALL_SKIPPED;
|
|
274
|
+
tc.approvalPolicySource = ApprovalPolicySource.UNATTENDED_SKIP;
|
|
275
|
+
tc.policyEngineVersion = POLICY_ENGINE_VERSION;
|
|
276
|
+
tc.isStreaming = false;
|
|
277
|
+
if (!tc.result) tc.result = unattendedSkipMessage(tc.name);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
apply(status.messages);
|
|
283
|
+
for (const subAgent of status.subAgentExecutions) {
|
|
284
|
+
apply(subAgent.messages);
|
|
285
|
+
}
|
|
286
|
+
}
|