@stigmer/runner 3.1.3 → 3.1.4
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/capture-flow.d.ts +26 -15
- package/dist/activities/execute-cursor/capture-flow.js +56 -18
- package/dist/activities/execute-cursor/capture-flow.js.map +1 -1
- package/dist/activities/execute-cursor/command-provenance.d.ts +11 -25
- package/dist/activities/execute-cursor/command-provenance.js +25 -115
- package/dist/activities/execute-cursor/command-provenance.js.map +1 -1
- package/dist/activities/execute-cursor/index.js +296 -477
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/todo-tracker.d.ts +6 -1
- package/dist/activities/execute-cursor/todo-tracker.js +15 -43
- package/dist/activities/execute-cursor/todo-tracker.js.map +1 -1
- package/dist/activities/execute-cursor/turn-stream.d.ts +141 -0
- package/dist/activities/execute-cursor/turn-stream.js +249 -0
- package/dist/activities/execute-cursor/turn-stream.js.map +1 -0
- package/dist/activities/execute-deep-agent/command-provenance.d.ts +61 -0
- package/dist/activities/execute-deep-agent/command-provenance.js +72 -0
- package/dist/activities/execute-deep-agent/command-provenance.js.map +1 -0
- package/dist/activities/execute-deep-agent/index.js +60 -18
- package/dist/activities/execute-deep-agent/index.js.map +1 -1
- package/dist/activities/execute-deep-agent/status-builder.js +8 -1
- package/dist/activities/execute-deep-agent/status-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/v3-status-builder.js +12 -1
- package/dist/activities/execute-deep-agent/v3-status-builder.js.map +1 -1
- package/dist/otel.js +10 -0
- package/dist/otel.js.map +1 -1
- package/dist/shared/filereview/cas-progress.d.ts +63 -0
- package/dist/shared/filereview/cas-progress.js +128 -0
- package/dist/shared/filereview/cas-progress.js.map +1 -0
- package/dist/shared/filereview/cas-substrate.d.ts +29 -0
- package/dist/shared/filereview/cas-substrate.js +46 -21
- package/dist/shared/filereview/cas-substrate.js.map +1 -1
- package/dist/shared/filereview/command-provenance.d.ts +93 -0
- package/dist/shared/filereview/command-provenance.js +132 -0
- package/dist/shared/filereview/command-provenance.js.map +1 -0
- package/dist/shared/filereview/git-substrate.d.ts +9 -3
- package/dist/shared/filereview/git-substrate.js.map +1 -1
- package/dist/shared/filereview/index.d.ts +4 -3
- package/dist/shared/filereview/index.js +3 -2
- package/dist/shared/filereview/index.js.map +1 -1
- package/dist/shared/filereview/progress.d.ts +105 -34
- package/dist/shared/filereview/progress.js +96 -34
- package/dist/shared/filereview/progress.js.map +1 -1
- package/dist/shared/plan-mode-prompt.d.ts +9 -0
- package/dist/shared/plan-mode-prompt.js +18 -0
- package/dist/shared/plan-mode-prompt.js.map +1 -1
- package/dist/shared/todos.d.ts +56 -0
- package/dist/shared/todos.js +98 -0
- package/dist/shared/todos.js.map +1 -0
- package/dist/shared/tool-row.d.ts +16 -0
- package/dist/shared/tool-row.js +31 -0
- package/dist/shared/tool-row.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/otel-turn-span.test.ts +61 -0
- package/src/activities/execute-cursor/__tests__/progress-substrate.test.ts +169 -0
- package/src/activities/execute-cursor/__tests__/turn-stream.test.ts +349 -0
- package/src/activities/execute-cursor/capture-flow.ts +71 -25
- package/src/activities/execute-cursor/command-provenance.ts +25 -120
- package/src/activities/execute-cursor/index.ts +344 -504
- package/src/activities/execute-cursor/todo-tracker.ts +17 -59
- package/src/activities/execute-cursor/turn-stream.ts +418 -0
- package/src/activities/execute-deep-agent/__tests__/command-provenance.test.ts +252 -0
- package/src/activities/execute-deep-agent/__tests__/status-builder.test.ts +78 -0
- package/src/activities/execute-deep-agent/__tests__/v3-status-builder.test.ts +105 -1
- package/src/activities/execute-deep-agent/command-provenance.ts +102 -0
- package/src/activities/execute-deep-agent/index.ts +74 -18
- package/src/activities/execute-deep-agent/status-builder.ts +9 -0
- package/src/activities/execute-deep-agent/v3-status-builder.ts +13 -0
- package/src/otel.ts +8 -0
- package/src/shared/__tests__/todos.test.ts +216 -0
- package/src/shared/filereview/__tests__/cas-progress.test.ts +228 -0
- package/src/shared/filereview/__tests__/cas-substrate.test.ts +66 -0
- package/src/shared/filereview/__tests__/command-provenance.test.ts +252 -0
- package/src/shared/filereview/__tests__/progress.test.ts +112 -10
- package/src/shared/filereview/cas-progress.ts +170 -0
- package/src/shared/filereview/cas-substrate.ts +69 -24
- package/src/shared/filereview/command-provenance.ts +180 -0
- package/src/shared/filereview/git-substrate.ts +9 -3
- package/src/shared/filereview/index.ts +15 -1
- package/src/shared/filereview/progress.ts +171 -47
- package/src/shared/plan-mode-prompt.ts +18 -0
- package/src/shared/todos.ts +126 -0
- package/src/shared/tool-row.ts +34 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the DEEP-AGENT approved-command provenance adapter (DD-28).
|
|
3
|
+
*
|
|
4
|
+
* These pin the two things that differ from the Cursor harness: identity turn
|
|
5
|
+
* scoping (a call whose id is absent from the pre-stream settled snapshot is
|
|
6
|
+
* this-turn's) and same-row consent (the executed shell row carries its own
|
|
7
|
+
* server-authored approval_action). The two regressions the plan calls out are
|
|
8
|
+
* asserted explicitly: a seeded WAITING_APPROVAL -> COMPLETED approved shell IS
|
|
9
|
+
* counted, and a prior turn's settled call is NOT re-evaluated. The DD-28 rule
|
|
10
|
+
* itself is covered by shared/filereview/__tests__/command-provenance.test.ts.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, it, expect } from "vitest";
|
|
14
|
+
import { create, type MessageInitShape } from "@bufbuild/protobuf";
|
|
15
|
+
import { AgentExecutionStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
16
|
+
import {
|
|
17
|
+
AgentMessageSchema,
|
|
18
|
+
ToolCallSchema,
|
|
19
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
20
|
+
import type { AgentMessage, ToolCall } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
21
|
+
import { SubAgentExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/subagent_pb";
|
|
22
|
+
import {
|
|
23
|
+
ApprovalAction,
|
|
24
|
+
MessageType,
|
|
25
|
+
ToolCallStatus,
|
|
26
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
27
|
+
|
|
28
|
+
import { deriveTurnCommandProvenance } from "../command-provenance.js";
|
|
29
|
+
|
|
30
|
+
function toolCall(overrides: MessageInitShape<typeof ToolCallSchema>): ToolCall {
|
|
31
|
+
return create(ToolCallSchema, {
|
|
32
|
+
id: "call-1",
|
|
33
|
+
status: ToolCallStatus.TOOL_CALL_COMPLETED,
|
|
34
|
+
...overrides,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function aiMessage(toolCalls: ToolCall[]): AgentMessage {
|
|
39
|
+
return create(AgentMessageSchema, { type: MessageType.MESSAGE_AI, toolCalls });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** A COMPLETED shell row (the deep-agent shell tool is `execute`). */
|
|
43
|
+
function executedShell(id: string, overrides: MessageInitShape<typeof ToolCallSchema> = {}): ToolCall {
|
|
44
|
+
return toolCall({ id, name: "execute", args: { command: "seq 1 5 > out.txt" }, ...overrides });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function status(messages: AgentMessage[], subAgents: MessageInitShape<typeof SubAgentExecutionSchema>[] = []) {
|
|
48
|
+
return create(AgentExecutionStatusSchema, {
|
|
49
|
+
messages,
|
|
50
|
+
subAgentExecutions: subAgents.map((sa) => create(SubAgentExecutionSchema, sa)),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const NO_SETTLED = new Set<string>();
|
|
55
|
+
const NO_SUBAGENTS = new Set<string>();
|
|
56
|
+
|
|
57
|
+
describe("deep-agent deriveTurnCommandProvenance", () => {
|
|
58
|
+
it("cites the executed shell's OWN id for a per-command approval (same-row consent)", () => {
|
|
59
|
+
const shell = executedShell("execute-1", { approvalAction: ApprovalAction.APPROVE });
|
|
60
|
+
const provenance = deriveTurnCommandProvenance({
|
|
61
|
+
status: status([aiMessage([shell])]),
|
|
62
|
+
priorSettledToolCallIds: NO_SETTLED,
|
|
63
|
+
priorSubAgentToolCallIds: NO_SUBAGENTS,
|
|
64
|
+
globalBypass: false,
|
|
65
|
+
});
|
|
66
|
+
expect(provenance).toBeDefined();
|
|
67
|
+
expect(provenance!.consentToolCallIds).toEqual(["execute-1"]);
|
|
68
|
+
expect(provenance!.authorizedByAutoApproveAll).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("cites the executed shell's own id for an APPROVE_ALL on the same row", () => {
|
|
72
|
+
const shell = executedShell("execute-1", { approvalAction: ApprovalAction.APPROVE_ALL });
|
|
73
|
+
const provenance = deriveTurnCommandProvenance({
|
|
74
|
+
status: status([aiMessage([shell])]),
|
|
75
|
+
priorSettledToolCallIds: NO_SETTLED,
|
|
76
|
+
priorSubAgentToolCallIds: NO_SUBAGENTS,
|
|
77
|
+
globalBypass: false,
|
|
78
|
+
});
|
|
79
|
+
expect(provenance!.consentToolCallIds).toEqual(["execute-1"]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("cites a prior APPROVE_ALL lease row for a command that ran ungated under the lease", () => {
|
|
83
|
+
// Prior turn: a gated command the user approved-all (settled before this turn).
|
|
84
|
+
const leaseRow = executedShell("lease-row", {
|
|
85
|
+
status: ToolCallStatus.TOOL_CALL_COMPLETED,
|
|
86
|
+
approvalAction: ApprovalAction.APPROVE_ALL,
|
|
87
|
+
});
|
|
88
|
+
// This turn: a fresh command runs ungated under the lease — no approval_action.
|
|
89
|
+
const leased = executedShell("execute-leased");
|
|
90
|
+
const provenance = deriveTurnCommandProvenance({
|
|
91
|
+
status: status([aiMessage([leaseRow]), aiMessage([leased])]),
|
|
92
|
+
priorSettledToolCallIds: new Set(["lease-row"]), // the lease row is a prior-turn settled call
|
|
93
|
+
priorSubAgentToolCallIds: NO_SUBAGENTS,
|
|
94
|
+
globalBypass: false,
|
|
95
|
+
});
|
|
96
|
+
expect(provenance).toBeDefined();
|
|
97
|
+
expect(provenance!.consentToolCallIds).toEqual(["lease-row"]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("flags auto_approve_all for an ungated command under the global bypass", () => {
|
|
101
|
+
const shell = executedShell("execute-1"); // no approval_action, no lease
|
|
102
|
+
const provenance = deriveTurnCommandProvenance({
|
|
103
|
+
status: status([aiMessage([shell])]),
|
|
104
|
+
priorSettledToolCallIds: NO_SETTLED,
|
|
105
|
+
priorSubAgentToolCallIds: NO_SUBAGENTS,
|
|
106
|
+
globalBypass: true,
|
|
107
|
+
});
|
|
108
|
+
expect(provenance).toBeDefined();
|
|
109
|
+
expect(provenance!.consentToolCallIds).toEqual([]);
|
|
110
|
+
expect(provenance!.authorizedByAutoApproveAll).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("fails closed on a file-tool call this turn (even under auto_approve_all)", () => {
|
|
114
|
+
const shell = executedShell("execute-1", { approvalAction: ApprovalAction.APPROVE });
|
|
115
|
+
const write = toolCall({ id: "write-1", name: "write_file", args: { path: "a.txt" } });
|
|
116
|
+
const provenance = deriveTurnCommandProvenance({
|
|
117
|
+
status: status([aiMessage([shell, write])]),
|
|
118
|
+
priorSettledToolCallIds: NO_SETTLED,
|
|
119
|
+
priorSubAgentToolCallIds: NO_SUBAGENTS,
|
|
120
|
+
globalBypass: true,
|
|
121
|
+
});
|
|
122
|
+
expect(provenance).toBeUndefined();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("fails closed on an MCP tool this turn", () => {
|
|
126
|
+
const shell = executedShell("execute-1", { approvalAction: ApprovalAction.APPROVE });
|
|
127
|
+
const mcp = toolCall({ id: "mcp-1", name: "apply_resource", mcpServerSlug: "planton" });
|
|
128
|
+
const provenance = deriveTurnCommandProvenance({
|
|
129
|
+
status: status([aiMessage([shell, mcp])]),
|
|
130
|
+
priorSettledToolCallIds: NO_SETTLED,
|
|
131
|
+
priorSubAgentToolCallIds: NO_SUBAGENTS,
|
|
132
|
+
globalBypass: false,
|
|
133
|
+
});
|
|
134
|
+
expect(provenance).toBeUndefined();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("fails closed on an unrecognized tool name this turn", () => {
|
|
138
|
+
const shell = executedShell("execute-1", { approvalAction: ApprovalAction.APPROVE });
|
|
139
|
+
const unknown = toolCall({ id: "u-1", name: "mystery_tool" });
|
|
140
|
+
const provenance = deriveTurnCommandProvenance({
|
|
141
|
+
status: status([aiMessage([shell, unknown])]),
|
|
142
|
+
priorSettledToolCallIds: NO_SETTLED,
|
|
143
|
+
priorSubAgentToolCallIds: NO_SUBAGENTS,
|
|
144
|
+
globalBypass: false,
|
|
145
|
+
});
|
|
146
|
+
expect(provenance).toBeUndefined();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("fails closed on a top-level sub-agent delegation (task row this turn)", () => {
|
|
150
|
+
const shell = executedShell("execute-1", { approvalAction: ApprovalAction.APPROVE });
|
|
151
|
+
const task = toolCall({ id: "task-1", name: "task", args: { prompt: "go" } });
|
|
152
|
+
const provenance = deriveTurnCommandProvenance({
|
|
153
|
+
status: status([aiMessage([shell, task])]),
|
|
154
|
+
priorSettledToolCallIds: NO_SETTLED,
|
|
155
|
+
priorSubAgentToolCallIds: NO_SUBAGENTS,
|
|
156
|
+
globalBypass: false,
|
|
157
|
+
});
|
|
158
|
+
expect(provenance).toBeUndefined();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("fails closed when a sub-agent produced NEW tool-call rows this turn", () => {
|
|
162
|
+
const shell = executedShell("execute-1", { approvalAction: ApprovalAction.APPROVE });
|
|
163
|
+
const subAgentShell = executedShell("sub-execute-1", { approvalAction: ApprovalAction.APPROVE });
|
|
164
|
+
const provenance = deriveTurnCommandProvenance({
|
|
165
|
+
status: status([aiMessage([shell])], [
|
|
166
|
+
{ name: "worker", messages: [aiMessage([subAgentShell])] },
|
|
167
|
+
]),
|
|
168
|
+
priorSettledToolCallIds: NO_SETTLED,
|
|
169
|
+
priorSubAgentToolCallIds: NO_SUBAGENTS, // the sub-agent row is new this turn
|
|
170
|
+
globalBypass: false,
|
|
171
|
+
});
|
|
172
|
+
expect(provenance).toBeUndefined();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("does NOT fail closed for a sub-agent whose rows all pre-existed this turn", () => {
|
|
176
|
+
const shell = executedShell("execute-1", { approvalAction: ApprovalAction.APPROVE });
|
|
177
|
+
const priorSubShell = executedShell("sub-prior-1", { approvalAction: ApprovalAction.APPROVE });
|
|
178
|
+
const provenance = deriveTurnCommandProvenance({
|
|
179
|
+
status: status([aiMessage([shell])], [
|
|
180
|
+
{ name: "worker", messages: [aiMessage([priorSubShell])] },
|
|
181
|
+
]),
|
|
182
|
+
priorSettledToolCallIds: NO_SETTLED,
|
|
183
|
+
priorSubAgentToolCallIds: new Set(["sub-prior-1"]), // seeded from a prior turn
|
|
184
|
+
globalBypass: false,
|
|
185
|
+
});
|
|
186
|
+
expect(provenance).toBeDefined();
|
|
187
|
+
expect(provenance!.consentToolCallIds).toEqual(["execute-1"]);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("returns undefined when no command executed this turn", () => {
|
|
191
|
+
const pending = executedShell("execute-pending", {
|
|
192
|
+
status: ToolCallStatus.TOOL_CALL_WAITING_APPROVAL,
|
|
193
|
+
});
|
|
194
|
+
const provenance = deriveTurnCommandProvenance({
|
|
195
|
+
status: status([aiMessage([pending])]),
|
|
196
|
+
priorSettledToolCallIds: NO_SETTLED,
|
|
197
|
+
priorSubAgentToolCallIds: NO_SUBAGENTS,
|
|
198
|
+
globalBypass: false,
|
|
199
|
+
});
|
|
200
|
+
expect(provenance).toBeUndefined();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// ── Regression: identity scoping (the reason positional scoping can't port) ──
|
|
204
|
+
|
|
205
|
+
it("REGRESSION counts a seeded WAITING_APPROVAL -> COMPLETED approved shell (its id was not settled pre-stream)", () => {
|
|
206
|
+
// The shell was proposed + approved in a prior segment (WAITING_APPROVAL, so
|
|
207
|
+
// NOT in the pre-stream settled snapshot) and executes in place this segment.
|
|
208
|
+
const executedThisTurn = executedShell("execute-approved", {
|
|
209
|
+
approvalAction: ApprovalAction.APPROVE,
|
|
210
|
+
});
|
|
211
|
+
const provenance = deriveTurnCommandProvenance({
|
|
212
|
+
status: status([aiMessage([executedThisTurn])]),
|
|
213
|
+
priorSettledToolCallIds: NO_SETTLED, // it was WAITING, not settled, before the stream
|
|
214
|
+
priorSubAgentToolCallIds: NO_SUBAGENTS,
|
|
215
|
+
globalBypass: false,
|
|
216
|
+
});
|
|
217
|
+
expect(provenance).toBeDefined();
|
|
218
|
+
expect(provenance!.consentToolCallIds).toEqual(["execute-approved"]);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("REGRESSION does NOT re-count a prior turn's already-settled shell", () => {
|
|
222
|
+
const priorShell = executedShell("execute-prior", { approvalAction: ApprovalAction.APPROVE });
|
|
223
|
+
const provenance = deriveTurnCommandProvenance({
|
|
224
|
+
status: status([aiMessage([priorShell])]),
|
|
225
|
+
priorSettledToolCallIds: new Set(["execute-prior"]), // settled in a prior turn
|
|
226
|
+
priorSubAgentToolCallIds: NO_SUBAGENTS,
|
|
227
|
+
globalBypass: false,
|
|
228
|
+
});
|
|
229
|
+
// Nothing executed THIS turn -> nothing to attribute the change set to.
|
|
230
|
+
expect(provenance).toBeUndefined();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("REGRESSION a prior turn's settled non-shell call never disqualifies this turn", () => {
|
|
234
|
+
// A prior FAILED edit must be excluded by the settled snapshot — otherwise it
|
|
235
|
+
// would trip the !== SHELL fail-closed on a clean shell-only turn.
|
|
236
|
+
const priorEdit = toolCall({
|
|
237
|
+
id: "edit-prior",
|
|
238
|
+
name: "edit_file",
|
|
239
|
+
status: ToolCallStatus.TOOL_CALL_FAILED,
|
|
240
|
+
args: { path: "a.txt" },
|
|
241
|
+
});
|
|
242
|
+
const shellThisTurn = executedShell("execute-1", { approvalAction: ApprovalAction.APPROVE });
|
|
243
|
+
const provenance = deriveTurnCommandProvenance({
|
|
244
|
+
status: status([aiMessage([priorEdit]), aiMessage([shellThisTurn])]),
|
|
245
|
+
priorSettledToolCallIds: new Set(["edit-prior"]),
|
|
246
|
+
priorSubAgentToolCallIds: NO_SUBAGENTS,
|
|
247
|
+
globalBypass: false,
|
|
248
|
+
});
|
|
249
|
+
expect(provenance).toBeDefined();
|
|
250
|
+
expect(provenance!.consentToolCallIds).toEqual(["execute-1"]);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
ExecutionPhase,
|
|
11
11
|
MessageType,
|
|
12
12
|
ToolCallStatus,
|
|
13
|
+
ToolKind,
|
|
14
|
+
TodoStatus,
|
|
13
15
|
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
14
16
|
import { StatusBuilder, type StreamEvent, type ApprovalPolicyProvider } from "../status-builder.js";
|
|
15
17
|
import type { MergedToolPolicy } from "../../../shared/approval-policy.js";
|
|
@@ -1852,4 +1854,80 @@ describe("StatusBuilder", () => {
|
|
|
1852
1854
|
expect(status.phase).not.toBe(ExecutionPhase.EXECUTION_WAITING_FOR_APPROVAL);
|
|
1853
1855
|
});
|
|
1854
1856
|
});
|
|
1857
|
+
|
|
1858
|
+
// ── Todo extraction (write_todos → status.todos) ─────────────────
|
|
1859
|
+
//
|
|
1860
|
+
// v2 parity with V3StatusBuilder: a completed write_todos projects into
|
|
1861
|
+
// status.todos through the same shared mapper; a failed one does not.
|
|
1862
|
+
|
|
1863
|
+
describe("todo extraction", () => {
|
|
1864
|
+
it("projects a completed write_todos into status.todos", () => {
|
|
1865
|
+
const sb = makeBuilder();
|
|
1866
|
+
sb.processEvent(
|
|
1867
|
+
toolStartEvent("run-1", "write_todos", {
|
|
1868
|
+
todos: [
|
|
1869
|
+
{ content: "Step one", status: "in_progress" },
|
|
1870
|
+
{ content: "Step two", status: "pending" },
|
|
1871
|
+
],
|
|
1872
|
+
}),
|
|
1873
|
+
);
|
|
1874
|
+
sb.processEvent(toolEndEvent("run-1", "Todos updated"));
|
|
1875
|
+
|
|
1876
|
+
const todos = sb.currentStatus.todos;
|
|
1877
|
+
expect(Object.keys(todos)).toEqual(["todo-0", "todo-1"]);
|
|
1878
|
+
expect(todos["todo-0"].content).toBe("Step one");
|
|
1879
|
+
expect(todos["todo-0"].status).toBe(TodoStatus.TODO_IN_PROGRESS);
|
|
1880
|
+
expect(todos["todo-1"].status).toBe(TodoStatus.TODO_PENDING);
|
|
1881
|
+
});
|
|
1882
|
+
|
|
1883
|
+
it("keeps the write_todos ToolCall in messages stamped ToolKind.TODO", () => {
|
|
1884
|
+
const sb = makeBuilder();
|
|
1885
|
+
sb.processEvent(
|
|
1886
|
+
toolStartEvent("run-1", "write_todos", {
|
|
1887
|
+
todos: [{ content: "A", status: "pending" }],
|
|
1888
|
+
}),
|
|
1889
|
+
);
|
|
1890
|
+
sb.processEvent(toolEndEvent("run-1", "ok"));
|
|
1891
|
+
|
|
1892
|
+
const tc = sb.currentStatus.messages
|
|
1893
|
+
.flatMap((m) => m.toolCalls)
|
|
1894
|
+
.find((t) => t.name === "write_todos");
|
|
1895
|
+
expect(tc).toBeDefined();
|
|
1896
|
+
expect(tc!.toolKind).toBe(ToolKind.TODO);
|
|
1897
|
+
});
|
|
1898
|
+
|
|
1899
|
+
it("does not project on a failed write_todos (Command never ran)", () => {
|
|
1900
|
+
const sb = makeBuilder();
|
|
1901
|
+
sb.processEvent(
|
|
1902
|
+
toolStartEvent("run-1", "write_todos", {
|
|
1903
|
+
todos: [{ content: "A", status: "pending" }],
|
|
1904
|
+
}),
|
|
1905
|
+
);
|
|
1906
|
+
sb.processEvent(toolEndEvent("run-1", { error: "boom" }));
|
|
1907
|
+
expect(Object.keys(sb.currentStatus.todos)).toHaveLength(0);
|
|
1908
|
+
});
|
|
1909
|
+
|
|
1910
|
+
it("full-replaces the map on a subsequent write_todos", () => {
|
|
1911
|
+
const sb = makeBuilder();
|
|
1912
|
+
sb.processEvent(
|
|
1913
|
+
toolStartEvent("run-1", "write_todos", {
|
|
1914
|
+
todos: [
|
|
1915
|
+
{ content: "one", status: "completed" },
|
|
1916
|
+
{ content: "two", status: "in_progress" },
|
|
1917
|
+
],
|
|
1918
|
+
}),
|
|
1919
|
+
);
|
|
1920
|
+
sb.processEvent(toolEndEvent("run-1", "ok"));
|
|
1921
|
+
sb.processEvent(
|
|
1922
|
+
toolStartEvent("run-2", "write_todos", {
|
|
1923
|
+
todos: [{ content: "two", status: "completed" }],
|
|
1924
|
+
}),
|
|
1925
|
+
);
|
|
1926
|
+
sb.processEvent(toolEndEvent("run-2", "ok"));
|
|
1927
|
+
|
|
1928
|
+
const todos = sb.currentStatus.todos;
|
|
1929
|
+
expect(Object.keys(todos)).toEqual(["todo-0"]);
|
|
1930
|
+
expect(todos["todo-0"].status).toBe(TodoStatus.TODO_COMPLETED);
|
|
1931
|
+
});
|
|
1932
|
+
});
|
|
1855
1933
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
2
|
import { create } from "@bufbuild/protobuf";
|
|
3
3
|
import { AgentExecutionStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
4
|
-
import { ExecutionPhase, MessageType, ToolCallStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
4
|
+
import { ExecutionPhase, MessageType, ToolCallStatus, ToolKind, TodoStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
5
5
|
import { V3StatusBuilder } from "../v3-status-builder.js";
|
|
6
6
|
import { normalize } from "../v3-protocol-normalizer.js";
|
|
7
7
|
import type { ApprovalPolicyProvider } from "../status-builder.js";
|
|
@@ -504,4 +504,108 @@ describe("V3StatusBuilder", () => {
|
|
|
504
504
|
expect(sb.currentStatus.messages).toHaveLength(0);
|
|
505
505
|
});
|
|
506
506
|
});
|
|
507
|
+
|
|
508
|
+
// ── Todo extraction (write_todos → status.todos) ─────────────────
|
|
509
|
+
//
|
|
510
|
+
// The native harness emits write_todos; projecting it into status.todos is
|
|
511
|
+
// what lights up the client TodoCard at parity with the Cursor harness.
|
|
512
|
+
|
|
513
|
+
describe("todo extraction", () => {
|
|
514
|
+
it("projects a completed write_todos into status.todos", () => {
|
|
515
|
+
const sb = makeBuilder();
|
|
516
|
+
feedAll(sb, [
|
|
517
|
+
makeMessageStart("run-1"),
|
|
518
|
+
makeTextDelta("run-1", "Planning the build."),
|
|
519
|
+
makeMessageFinish("run-1", { usage: { input_tokens: 10, output_tokens: 5 } }),
|
|
520
|
+
makeToolStarted("todo-1", "write_todos", {
|
|
521
|
+
todos: [
|
|
522
|
+
{ content: "Step one", status: "in_progress" },
|
|
523
|
+
{ content: "Step two", status: "pending" },
|
|
524
|
+
],
|
|
525
|
+
}),
|
|
526
|
+
makeToolFinished("todo-1", "Todos updated"),
|
|
527
|
+
]);
|
|
528
|
+
|
|
529
|
+
const todos = sb.currentStatus.todos;
|
|
530
|
+
expect(Object.keys(todos)).toEqual(["todo-0", "todo-1"]);
|
|
531
|
+
expect(todos["todo-0"].content).toBe("Step one");
|
|
532
|
+
expect(todos["todo-0"].status).toBe(TodoStatus.TODO_IN_PROGRESS);
|
|
533
|
+
expect(todos["todo-1"].content).toBe("Step two");
|
|
534
|
+
expect(todos["todo-1"].status).toBe(TodoStatus.TODO_PENDING);
|
|
535
|
+
expect(sb.forceNextUpdate).toBe(true);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it("keeps the write_todos ToolCall in messages stamped ToolKind.TODO", () => {
|
|
539
|
+
const sb = makeBuilder();
|
|
540
|
+
feedAll(sb, [
|
|
541
|
+
makeToolStarted("todo-1", "write_todos", {
|
|
542
|
+
todos: [{ content: "Step one", status: "pending" }],
|
|
543
|
+
}),
|
|
544
|
+
makeToolFinished("todo-1", "Todos updated"),
|
|
545
|
+
]);
|
|
546
|
+
|
|
547
|
+
const toolCalls = sb.currentStatus.messages.flatMap((m) => m.toolCalls);
|
|
548
|
+
const tc = toolCalls.find((t) => t.name === "write_todos");
|
|
549
|
+
expect(tc).toBeDefined();
|
|
550
|
+
expect(tc!.toolKind).toBe(ToolKind.TODO);
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
it("does not project until the call completes", () => {
|
|
554
|
+
const sb = makeBuilder();
|
|
555
|
+
feedAll(sb, [
|
|
556
|
+
makeToolStarted("todo-1", "write_todos", {
|
|
557
|
+
todos: [{ content: "Step one", status: "pending" }],
|
|
558
|
+
}),
|
|
559
|
+
]);
|
|
560
|
+
// Tool started but not finished — the state Command has not run yet.
|
|
561
|
+
expect(Object.keys(sb.currentStatus.todos)).toHaveLength(0);
|
|
562
|
+
|
|
563
|
+
feedAll(sb, [makeToolFinished("todo-1", "Todos updated")]);
|
|
564
|
+
expect(Object.keys(sb.currentStatus.todos)).toHaveLength(1);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
it("full-replaces the map on a subsequent write_todos", () => {
|
|
568
|
+
const sb = makeBuilder();
|
|
569
|
+
feedAll(sb, [
|
|
570
|
+
makeToolStarted("todo-1", "write_todos", {
|
|
571
|
+
todos: [
|
|
572
|
+
{ content: "Step one", status: "completed" },
|
|
573
|
+
{ content: "Step two", status: "in_progress" },
|
|
574
|
+
],
|
|
575
|
+
}),
|
|
576
|
+
makeToolFinished("todo-1", "ok"),
|
|
577
|
+
makeToolStarted("todo-2", "write_todos", {
|
|
578
|
+
todos: [{ content: "Step two", status: "completed" }],
|
|
579
|
+
}),
|
|
580
|
+
makeToolFinished("todo-2", "ok"),
|
|
581
|
+
]);
|
|
582
|
+
|
|
583
|
+
const todos = sb.currentStatus.todos;
|
|
584
|
+
expect(Object.keys(todos)).toEqual(["todo-0"]);
|
|
585
|
+
expect(todos["todo-0"].content).toBe("Step two");
|
|
586
|
+
expect(todos["todo-0"].status).toBe(TodoStatus.TODO_COMPLETED);
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
it("does not project a sub-agent's write_todos into parent status.todos", () => {
|
|
590
|
+
const sb = makeBuilder();
|
|
591
|
+
const subNs = ["tools:task-1", "tools:sub-todo"];
|
|
592
|
+
feedAll(sb, [
|
|
593
|
+
// Register a sub-agent so its namespace routes to the SubAgentTracker.
|
|
594
|
+
makeToolStarted("task-1", "task", {
|
|
595
|
+
subagent_type: "worker",
|
|
596
|
+
description: "delegate",
|
|
597
|
+
}),
|
|
598
|
+
// The sub-agent writes todos — must not leak into the parent map.
|
|
599
|
+
makeToolStarted(
|
|
600
|
+
"sub-todo",
|
|
601
|
+
"write_todos",
|
|
602
|
+
{ todos: [{ content: "sub step", status: "pending" }] },
|
|
603
|
+
{ namespace: subNs },
|
|
604
|
+
),
|
|
605
|
+
makeToolFinished("sub-todo", "ok", { namespace: subNs }),
|
|
606
|
+
]);
|
|
607
|
+
|
|
608
|
+
expect(Object.keys(sb.currentStatus.todos)).toHaveLength(0);
|
|
609
|
+
});
|
|
610
|
+
});
|
|
507
611
|
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Approved-command turn provenance (DD-28) — the DEEP-AGENT (native) harness
|
|
3
|
+
* adapter over the shared qualification rule ({@link qualifyTurnCommandProvenance}).
|
|
4
|
+
*
|
|
5
|
+
* The deep-agent's consent model differs from Cursor's in exactly two ways, and
|
|
6
|
+
* this adapter is precisely those two differences:
|
|
7
|
+
*
|
|
8
|
+
* 1. IDENTITY turn scoping (not positional). Cursor scopes a turn by the message
|
|
9
|
+
* index its stream started at. The deep-agent cannot: an approved command
|
|
10
|
+
* executes IN PLACE at its seeded transcript position (it was proposed in a
|
|
11
|
+
* prior segment, seeded into this segment, and updated to COMPLETED by
|
|
12
|
+
* `StatusBuilder.findResumableSeededToolCall`). So "this turn's calls" are
|
|
13
|
+
* those whose id is absent from a pre-stream {@link collectSettledToolCallIds}
|
|
14
|
+
* snapshot, and "executed" is a COMPLETED status.
|
|
15
|
+
*
|
|
16
|
+
* 2. SAME-ROW direct consent (not a grant token). A gated deep-agent tool pauses
|
|
17
|
+
* on a LangGraph interrupt; SubmitApproval writes `approval_action` onto that
|
|
18
|
+
* same row, and the row keeps its id + `approval_action` when it executes on
|
|
19
|
+
* resume. So the executed command IS its own consent row — cite its own id.
|
|
20
|
+
* (The server's PreserveApprovalFields keeps that `approval_action` on the row
|
|
21
|
+
* across the status write regardless of its new status, so the backend's
|
|
22
|
+
* verification finds it.)
|
|
23
|
+
*
|
|
24
|
+
* A turn that delegated ANY sub-agent fails closed (DD-28 D1: "delegated zero
|
|
25
|
+
* sub-agents"). A sub-agent's writes fold into the parent turn's change set
|
|
26
|
+
* (DD-19) but are not attributable to a consented top-level command. The
|
|
27
|
+
* top-level `task` call (a SUBAGENT kind) already trips the shared rule's
|
|
28
|
+
* `!== SHELL` fail-closed; the explicit sub-agent-novelty guard below also covers
|
|
29
|
+
* the (currently-impossible under the replace-per-turn model) case of a sub-agent
|
|
30
|
+
* that mutates across a turn boundary without a fresh top-level `task` row.
|
|
31
|
+
*
|
|
32
|
+
* (Trust boundary + fail-closed contract: see the shared module.)
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import {
|
|
36
|
+
ApprovalAction,
|
|
37
|
+
ToolCallStatus,
|
|
38
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
39
|
+
import type { AgentExecutionStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
40
|
+
import type { TurnCommandProvenance } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/filereview_pb";
|
|
41
|
+
import { qualifyTurnCommandProvenance } from "../../shared/filereview/command-provenance.js";
|
|
42
|
+
import { collectSubAgentToolCallIds } from "../../shared/tool-row.js";
|
|
43
|
+
|
|
44
|
+
export interface DeepAgentCommandProvenanceInputs {
|
|
45
|
+
/** The status at capture time — top-level messages + sub-agent executions. */
|
|
46
|
+
readonly status: AgentExecutionStatus;
|
|
47
|
+
/**
|
|
48
|
+
* The top-level tool-call ids that had already SETTLED before this turn's
|
|
49
|
+
* stream (from {@link collectSettledToolCallIds}). A top-level call whose id is
|
|
50
|
+
* absent is this-turn's; the shared rule then keeps only the COMPLETED ones.
|
|
51
|
+
*/
|
|
52
|
+
readonly priorSettledToolCallIds: ReadonlySet<string>;
|
|
53
|
+
/**
|
|
54
|
+
* The sub-agent tool-call ids that existed before this turn's stream (from
|
|
55
|
+
* {@link collectSubAgentToolCallIds}). Any growth means a sub-agent acted this
|
|
56
|
+
* turn → fail closed.
|
|
57
|
+
*/
|
|
58
|
+
readonly priorSubAgentToolCallIds: ReadonlySet<string>;
|
|
59
|
+
/** True when the pre-armed spec.auto_approve_all bypassed the gate. */
|
|
60
|
+
readonly globalBypass: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Derive the {@link TurnCommandProvenance} for a deep-agent turn, or undefined
|
|
65
|
+
* when it does not qualify. Scopes this turn's top-level calls by id-novelty,
|
|
66
|
+
* resolves consent from the row's own server-authored `approval_action`, fails
|
|
67
|
+
* closed on any sub-agent activity, and delegates the DD-28 rule to
|
|
68
|
+
* {@link qualifyTurnCommandProvenance}.
|
|
69
|
+
*/
|
|
70
|
+
export function deriveTurnCommandProvenance(
|
|
71
|
+
inputs: DeepAgentCommandProvenanceInputs,
|
|
72
|
+
): TurnCommandProvenance | undefined {
|
|
73
|
+
const { status, priorSettledToolCallIds, priorSubAgentToolCallIds, globalBypass } = inputs;
|
|
74
|
+
|
|
75
|
+
// DD-28 D1 cond 2: any sub-agent activity this turn disqualifies. A sub-agent
|
|
76
|
+
// that ran this turn contributes at least one tool-call id absent from the
|
|
77
|
+
// pre-stream snapshot.
|
|
78
|
+
for (const id of collectSubAgentToolCallIds(status.subAgentExecutions)) {
|
|
79
|
+
if (!priorSubAgentToolCallIds.has(id)) return undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const messages = status.messages;
|
|
83
|
+
// This turn's top-level calls: those not already settled before the stream.
|
|
84
|
+
const turnToolCalls = messages
|
|
85
|
+
.flatMap((m) => m.toolCalls)
|
|
86
|
+
.filter((tc) => !priorSettledToolCallIds.has(tc.id));
|
|
87
|
+
|
|
88
|
+
return qualifyTurnCommandProvenance({
|
|
89
|
+
turnToolCalls,
|
|
90
|
+
messages,
|
|
91
|
+
// A deep-agent command executed iff its row reached COMPLETED this turn.
|
|
92
|
+
isExecutedCommand: (tc) => tc.status === ToolCallStatus.TOOL_CALL_COMPLETED,
|
|
93
|
+
// The gated command carries its own server-authored approval_action, written
|
|
94
|
+
// in place by SubmitApproval and preserved when the row executes on resume.
|
|
95
|
+
resolveDirectConsent: (tc) =>
|
|
96
|
+
tc.approvalAction === ApprovalAction.APPROVE ||
|
|
97
|
+
tc.approvalAction === ApprovalAction.APPROVE_ALL
|
|
98
|
+
? tc.id
|
|
99
|
+
: undefined,
|
|
100
|
+
globalBypass,
|
|
101
|
+
});
|
|
102
|
+
}
|