@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
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Mid-run live capture — the harness-agnostic glue that turns a
|
|
3
|
-
* delta
|
|
4
|
-
*
|
|
5
|
-
* far" strip renders (DD-32).
|
|
2
|
+
* Mid-run live capture — the harness-agnostic glue that turns a per-turn
|
|
3
|
+
* workspace delta into the transient `AgentExecutionStatus.file_change_progress`
|
|
4
|
+
* snapshot the "N files changed so far" strip renders (DD-32 / DD-33).
|
|
6
5
|
*
|
|
7
6
|
* THE MODEL
|
|
8
7
|
* ---------
|
|
@@ -13,24 +12,33 @@
|
|
|
13
12
|
* `file_change_sets`) remains the single authoritative, reviewable diff; a mid-run
|
|
14
13
|
* snapshot is no more authoritative than a streamed tool-call arg.
|
|
15
14
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
15
|
+
* THE SUBSTRATE ABSTRACTION (DD-33)
|
|
16
|
+
* ---------------------------------
|
|
17
|
+
* Where the delta comes from differs by workspace: a git tree diffs cheaply with
|
|
18
|
+
* `git --numstat` (the git substrate), a non-git / gitignored workspace reads the
|
|
19
|
+
* CAS observer (the cas substrate, {@link ./cas-progress.js}), and a git tree
|
|
20
|
+
* with gitignored writes composes both (the hybrid substrate). A
|
|
21
|
+
* {@link ProgressSubstrate} hides that behind one `capture()` so the floor + attach
|
|
22
|
+
* logic below is written once. Each substrate is a per-turn object owning its own
|
|
23
|
+
* short-circuit cache; `capture()` always returns the FULL cumulative turn delta
|
|
24
|
+
* plus a `changed` flag, so the hybrid can merge both slices even when only one
|
|
25
|
+
* moved (a `ProgressDelta | undefined` would drop the unchanged slice).
|
|
21
26
|
*
|
|
22
27
|
* SECRET SAFETY
|
|
23
28
|
* -------------
|
|
24
29
|
* No file bodies are ever carried. A secret-like path ({@link isSecretLikePath})
|
|
25
30
|
* is still surfaced (path visible) but with its line counts ZEROED — the same
|
|
26
|
-
* "path visible, content withheld" rule the ledger uses (DD-12).
|
|
27
|
-
*
|
|
31
|
+
* "path visible, content withheld" rule the ledger uses (DD-12). The cas
|
|
32
|
+
* substrate additionally excludes gate-blocked secrets up front
|
|
33
|
+
* ({@link partitionIgnoredPathsBySecret}); this zeroing is the belt-and-suspenders
|
|
34
|
+
* backstop. Nothing new can leak through this field.
|
|
28
35
|
*
|
|
29
|
-
* @since File-Change HITL Redesign (mid-run live capture / DD-32)
|
|
36
|
+
* @since File-Change HITL Redesign (mid-run live capture / DD-32; non-git + hybrid / DD-33)
|
|
30
37
|
*/
|
|
31
38
|
|
|
32
39
|
import { create } from "@bufbuild/protobuf";
|
|
33
40
|
import type { AgentExecutionStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
41
|
+
import type { FileChangeKind } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
34
42
|
import type {
|
|
35
43
|
FileChangeProgress,
|
|
36
44
|
FileChangeProgressEntry,
|
|
@@ -41,22 +49,74 @@ import {
|
|
|
41
49
|
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/filereview_pb";
|
|
42
50
|
import { utcTimestamp } from "../status.js";
|
|
43
51
|
import { toFileChangeKind } from "./capture.js";
|
|
44
|
-
import {
|
|
52
|
+
import {
|
|
53
|
+
captureProgressDelta,
|
|
54
|
+
type GitProgressEntry,
|
|
55
|
+
} from "./git-substrate.js";
|
|
45
56
|
import { isSecretLikePath } from "./secret-paths.js";
|
|
46
57
|
|
|
58
|
+
/**
|
|
59
|
+
* One file's slim, content-free progress row — the substrate-neutral input to
|
|
60
|
+
* {@link buildFileChangeProgress}. Carries the proto {@link FileChangeKind}
|
|
61
|
+
* directly (git and cas substrates both normalize to it), so no side re-maps.
|
|
62
|
+
* `linesAdded`/`linesRemoved` are 0 when uncountable (binary / oversized /
|
|
63
|
+
* secret-withheld).
|
|
64
|
+
*/
|
|
65
|
+
export interface ProgressEntry {
|
|
66
|
+
/** Path before the change (workspace-relative). Empty for ADD. */
|
|
67
|
+
readonly pathBefore: string;
|
|
68
|
+
/** Path after the change (workspace-relative). Empty for DELETE. */
|
|
69
|
+
readonly pathAfter: string;
|
|
70
|
+
readonly kind: FileChangeKind;
|
|
71
|
+
readonly linesAdded: number;
|
|
72
|
+
readonly linesRemoved: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The substrate-neutral progress delta the git/cas/hybrid substrates emit.
|
|
77
|
+
*
|
|
78
|
+
* `totalFilesChanged` is the honest count of changed files this turn, which may
|
|
79
|
+
* EXCEED `entries.length` when a substrate caps how many files it reads (the cas
|
|
80
|
+
* substrate reads only a bounded prefix — DD-33). Undefined means "the entries
|
|
81
|
+
* ARE every changed file" (the git substrate, whose numstat is free), so
|
|
82
|
+
* {@link buildFileChangeProgress} falls back to `entries.length`.
|
|
83
|
+
*/
|
|
84
|
+
export interface ProgressDelta {
|
|
85
|
+
readonly entries: readonly ProgressEntry[];
|
|
86
|
+
readonly totalFilesChanged?: number;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** One mid-run capture: the FULL cumulative turn delta + whether it changed. */
|
|
90
|
+
export interface ProgressCapture {
|
|
91
|
+
readonly delta: ProgressDelta;
|
|
92
|
+
/** False ⇒ nothing moved since the last capture; the caller skips re-attach. */
|
|
93
|
+
readonly changed: boolean;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* A per-turn source of the mid-run delta. Implementations own their own
|
|
98
|
+
* short-circuit cache and ALWAYS return the full cumulative delta (never a
|
|
99
|
+
* bare "unchanged" sentinel), so {@link createHybridProgressSubstrate} can merge
|
|
100
|
+
* a changed slice with an unchanged one without losing the latter.
|
|
101
|
+
*/
|
|
102
|
+
export interface ProgressSubstrate {
|
|
103
|
+
capture(): Promise<ProgressCapture>;
|
|
104
|
+
}
|
|
105
|
+
|
|
47
106
|
/**
|
|
48
107
|
* Cap the per-file entry list so a turn touching thousands of files does not
|
|
49
108
|
* bloat the persisted status (which rides Temporal / the store). `files_changed`
|
|
50
109
|
* and the aggregate counts stay honest over ALL files; the strip shows "… and K
|
|
51
|
-
* more" when the list is capped.
|
|
110
|
+
* more" when the list is capped. Also the cas substrate's read budget — it reads
|
|
111
|
+
* after-bytes for at most this many files per capture (DD-33).
|
|
52
112
|
*/
|
|
53
113
|
export const PROGRESS_MAX_ENTRIES = 200;
|
|
54
114
|
|
|
55
115
|
/**
|
|
56
116
|
* Minimum wall-clock gap between two mid-run captures, in ms. A floor on cost:
|
|
57
|
-
* every capture stages the working tree (
|
|
58
|
-
* that runs regardless of persist frequency. Env-tunable for
|
|
59
|
-
* deployments; defaults to 2s (feels live without hammering
|
|
117
|
+
* every capture stages the working tree (git) or reads the touched set (cas), so
|
|
118
|
+
* we bound how often that runs regardless of persist frequency. Env-tunable for
|
|
119
|
+
* large-repo deployments; defaults to 2s (feels live without hammering the disk).
|
|
60
120
|
*/
|
|
61
121
|
export const PROGRESS_CAPTURE_MIN_INTERVAL_MS = readMinIntervalMs();
|
|
62
122
|
|
|
@@ -81,23 +141,22 @@ export function shouldCaptureProgress(
|
|
|
81
141
|
}
|
|
82
142
|
|
|
83
143
|
/**
|
|
84
|
-
* Build the transient {@link FileChangeProgress} message from a
|
|
144
|
+
* Build the transient {@link FileChangeProgress} message from a substrate-neutral
|
|
85
145
|
* delta. Zeroes counts for secret-like paths (path visible, magnitude withheld),
|
|
86
|
-
* caps the entry list at {@link PROGRESS_MAX_ENTRIES}
|
|
87
|
-
* `files_changed
|
|
88
|
-
*
|
|
146
|
+
* caps the entry list at {@link PROGRESS_MAX_ENTRIES}, and reports the honest
|
|
147
|
+
* `files_changed` (`delta.totalFilesChanged` when a substrate capped its reads,
|
|
148
|
+
* else the entry count). Aggregate counts sum over the emitted entries. Pure and
|
|
149
|
+
* exported for direct testing.
|
|
89
150
|
*/
|
|
90
151
|
export function buildFileChangeProgress(
|
|
91
152
|
delta: ProgressDelta,
|
|
92
153
|
changeSetId: string,
|
|
93
154
|
): FileChangeProgress {
|
|
94
|
-
let filesChanged = 0;
|
|
95
155
|
let totalAdded = 0;
|
|
96
156
|
let totalRemoved = 0;
|
|
97
157
|
const entries: FileChangeProgressEntry[] = [];
|
|
98
158
|
|
|
99
159
|
for (const entry of delta.entries) {
|
|
100
|
-
filesChanged += 1;
|
|
101
160
|
// A secret-like path is surfaced (path visible) but its magnitude is withheld
|
|
102
161
|
// — counts zeroed, mirroring the ledger's "path visible, content withheld".
|
|
103
162
|
const secret = isSecretLikePath(entry.pathAfter || entry.pathBefore);
|
|
@@ -111,7 +170,7 @@ export function buildFileChangeProgress(
|
|
|
111
170
|
create(FileChangeProgressEntrySchema, {
|
|
112
171
|
pathBefore: entry.pathBefore,
|
|
113
172
|
pathAfter: entry.pathAfter,
|
|
114
|
-
kind:
|
|
173
|
+
kind: entry.kind,
|
|
115
174
|
linesAdded,
|
|
116
175
|
linesRemoved,
|
|
117
176
|
}),
|
|
@@ -121,7 +180,7 @@ export function buildFileChangeProgress(
|
|
|
121
180
|
|
|
122
181
|
return create(FileChangeProgressSchema, {
|
|
123
182
|
changeSetId,
|
|
124
|
-
filesChanged,
|
|
183
|
+
filesChanged: delta.totalFilesChanged ?? delta.entries.length,
|
|
125
184
|
linesAdded: totalAdded,
|
|
126
185
|
linesRemoved: totalRemoved,
|
|
127
186
|
entries,
|
|
@@ -130,12 +189,12 @@ export function buildFileChangeProgress(
|
|
|
130
189
|
}
|
|
131
190
|
|
|
132
191
|
/**
|
|
133
|
-
* Mutable per-turn state the caller threads across persists: the last
|
|
134
|
-
*
|
|
135
|
-
*
|
|
192
|
+
* Mutable per-turn state the caller threads across persists: the last capture
|
|
193
|
+
* time (the floor). Substrate-specific short-circuit state (git tree sha, cas
|
|
194
|
+
* signature) lives inside the {@link ProgressSubstrate}, not here. A fresh turn
|
|
195
|
+
* starts a fresh state (`{ lastAtMs: 0 }`).
|
|
136
196
|
*/
|
|
137
197
|
export interface ProgressCaptureState {
|
|
138
|
-
lastTreeSha?: string;
|
|
139
198
|
lastAtMs: number;
|
|
140
199
|
}
|
|
141
200
|
|
|
@@ -146,11 +205,10 @@ export function newProgressCaptureState(): ProgressCaptureState {
|
|
|
146
205
|
|
|
147
206
|
/**
|
|
148
207
|
* Capture-and-attach the live progress snapshot onto `status.file_change_progress`,
|
|
149
|
-
* throttled by the floor
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
* loop; each passes its own `excludePaths`.
|
|
208
|
+
* throttled by the floor. Mutates `status` and `state` in place; a no-op when the
|
|
209
|
+
* floor has not elapsed or the substrate reports nothing changed. Content-free and
|
|
210
|
+
* secret-safe (see {@link buildFileChangeProgress}). Called by both harnesses from
|
|
211
|
+
* their per-persist loop, each passing a substrate built for its workspace shape.
|
|
154
212
|
*
|
|
155
213
|
* The field is set even when the delta is now EMPTY (the agent reverted its own
|
|
156
214
|
* edits) so the strip reflects the reversion (it hides at zero) rather than
|
|
@@ -159,11 +217,8 @@ export function newProgressCaptureState(): ProgressCaptureState {
|
|
|
159
217
|
*/
|
|
160
218
|
export async function captureFileChangeProgress(opts: {
|
|
161
219
|
readonly status: AgentExecutionStatus;
|
|
162
|
-
readonly gitRoot: string;
|
|
163
|
-
readonly executionId: string;
|
|
164
220
|
readonly changeSetId: string;
|
|
165
|
-
readonly
|
|
166
|
-
readonly excludePaths?: readonly string[];
|
|
221
|
+
readonly substrate: ProgressSubstrate;
|
|
167
222
|
readonly state: ProgressCaptureState;
|
|
168
223
|
/** Injectable clock for tests; defaults to `Date.now()`. */
|
|
169
224
|
readonly nowMs?: number;
|
|
@@ -172,15 +227,84 @@ export async function captureFileChangeProgress(opts: {
|
|
|
172
227
|
if (!shouldCaptureProgress(opts.state.lastAtMs, now)) return;
|
|
173
228
|
opts.state.lastAtMs = now;
|
|
174
229
|
|
|
175
|
-
const delta = await
|
|
176
|
-
|
|
177
|
-
opts.executionId,
|
|
178
|
-
opts.baselineTree,
|
|
179
|
-
opts.excludePaths,
|
|
180
|
-
opts.state.lastTreeSha,
|
|
181
|
-
);
|
|
182
|
-
if (!delta) return; // working tree unchanged since the last capture
|
|
230
|
+
const { delta, changed } = await opts.substrate.capture();
|
|
231
|
+
if (!changed) return;
|
|
183
232
|
|
|
184
|
-
opts.state.lastTreeSha = delta.afterTree;
|
|
185
233
|
opts.status.fileChangeProgress = buildFileChangeProgress(delta, opts.changeSetId);
|
|
186
234
|
}
|
|
235
|
+
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
// Substrates
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
|
|
240
|
+
function gitEntryToProgressEntry(e: GitProgressEntry): ProgressEntry {
|
|
241
|
+
return {
|
|
242
|
+
pathBefore: e.pathBefore,
|
|
243
|
+
pathAfter: e.pathAfter,
|
|
244
|
+
kind: toFileChangeKind(e.changeType),
|
|
245
|
+
linesAdded: e.linesAdded,
|
|
246
|
+
linesRemoved: e.linesRemoved,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* The git substrate: the working-tree `--numstat` delta against the pinned
|
|
252
|
+
* baseline. `git add -A` honors `.gitignore`, so this covers exactly the
|
|
253
|
+
* git-TRACKED changes (from any source — tool or shell), disjoint from the cas
|
|
254
|
+
* substrate's gitignored set. Preserves the tree-sha short-circuit: on an
|
|
255
|
+
* unchanged tree it returns the cached full delta with `changed:false` (the
|
|
256
|
+
* caller skips the re-attach), byte-identical to the pre-DD-33 behavior.
|
|
257
|
+
*/
|
|
258
|
+
export function createGitProgressSubstrate(opts: {
|
|
259
|
+
readonly workspaceRoot: string;
|
|
260
|
+
readonly executionId: string;
|
|
261
|
+
readonly baselineTree: string;
|
|
262
|
+
readonly excludePaths?: readonly string[];
|
|
263
|
+
}): ProgressSubstrate {
|
|
264
|
+
let lastTreeSha: string | undefined;
|
|
265
|
+
let cachedFull: ProgressDelta = { entries: [] };
|
|
266
|
+
return {
|
|
267
|
+
async capture(): Promise<ProgressCapture> {
|
|
268
|
+
const gitDelta = await captureProgressDelta(
|
|
269
|
+
opts.workspaceRoot,
|
|
270
|
+
opts.executionId,
|
|
271
|
+
opts.baselineTree,
|
|
272
|
+
opts.excludePaths,
|
|
273
|
+
lastTreeSha,
|
|
274
|
+
);
|
|
275
|
+
if (gitDelta === undefined) {
|
|
276
|
+
// Working tree unchanged since the last capture — reuse the cache so the
|
|
277
|
+
// hybrid can still merge this (unchanged) slice with a changed cas slice.
|
|
278
|
+
return { delta: cachedFull, changed: false };
|
|
279
|
+
}
|
|
280
|
+
lastTreeSha = gitDelta.afterTree;
|
|
281
|
+
cachedFull = { entries: gitDelta.entries.map(gitEntryToProgressEntry) };
|
|
282
|
+
return { delta: cachedFull, changed: true };
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* The hybrid substrate: a git tree whose gitignored writes are captured via CAS.
|
|
289
|
+
* Concatenates both slices (disjoint by the `.gitignore` boundary — numstat sees
|
|
290
|
+
* only tracked paths, the observer only gitignored ones) and sums the honest
|
|
291
|
+
* totals. `changed` is true when EITHER slice moved; because each child returns
|
|
292
|
+
* its full cumulative delta, the merged delta always carries both slices.
|
|
293
|
+
*/
|
|
294
|
+
export function createHybridProgressSubstrate(
|
|
295
|
+
git: ProgressSubstrate,
|
|
296
|
+
cas: ProgressSubstrate,
|
|
297
|
+
): ProgressSubstrate {
|
|
298
|
+
return {
|
|
299
|
+
async capture(): Promise<ProgressCapture> {
|
|
300
|
+
const [g, c] = await Promise.all([git.capture(), cas.capture()]);
|
|
301
|
+
const delta: ProgressDelta = {
|
|
302
|
+
entries: [...g.delta.entries, ...c.delta.entries],
|
|
303
|
+
totalFilesChanged:
|
|
304
|
+
(g.delta.totalFilesChanged ?? g.delta.entries.length) +
|
|
305
|
+
(c.delta.totalFilesChanged ?? c.delta.entries.length),
|
|
306
|
+
};
|
|
307
|
+
return { delta, changed: g.changed || c.changed };
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
}
|
|
@@ -29,6 +29,15 @@
|
|
|
29
29
|
* message is published verbatim as a plan document (whose filename is derived
|
|
30
30
|
* from the leading `#` title), so a fenced or chat-suffixed plan degrades the
|
|
31
31
|
* reviewable document the user sees.
|
|
32
|
+
*
|
|
33
|
+
* Fence hygiene inside the document is part of the same contract. A plan
|
|
34
|
+
* frequently quotes file content that itself contains fenced code blocks
|
|
35
|
+
* ("insert this section into README.md"); with a same-length outer fence the
|
|
36
|
+
* inner block's closer terminates the outer fence early and corrupts the
|
|
37
|
+
* rendered document — on every client (the react SDK and the terminal
|
|
38
|
+
* renderer alike). And because the plan viewers render top-level ```mermaid
|
|
39
|
+
* fences as diagrams, the directive steers diagrams into the plan body proper
|
|
40
|
+
* rather than leaving them buried, unrendered, inside quoted file content.
|
|
32
41
|
*/
|
|
33
42
|
export const PLAN_MODE_DIRECTIVE = [
|
|
34
43
|
"IMPORTANT: You are in Plan mode — a read-only analysis turn whose " +
|
|
@@ -50,6 +59,15 @@ export const PLAN_MODE_DIRECTIVE = [
|
|
|
50
59
|
"- Reference concrete file paths and describe the specific changes " +
|
|
51
60
|
"planned for each.",
|
|
52
61
|
"- Do NOT wrap the document in a code fence.",
|
|
62
|
+
"- When quoting content that itself contains fenced code blocks (e.g. a " +
|
|
63
|
+
"proposed file section with a code sample inside), open the outer fence " +
|
|
64
|
+
"with MORE backticks than any inner fence (four or more) — a same-length " +
|
|
65
|
+
"inner closer would terminate the outer fence early and corrupt the " +
|
|
66
|
+
"rendered document.",
|
|
67
|
+
"- Fenced ```mermaid blocks at the top level of the document render as " +
|
|
68
|
+
"diagrams in the plan viewer. When a diagram helps communicate the " +
|
|
69
|
+
"design (architecture, flows), include it directly in the plan body — " +
|
|
70
|
+
"not only inside quoted file content, where it stays unrendered source.",
|
|
53
71
|
'- Do NOT end with conversational closers ("Let me know...", "Shall I ' +
|
|
54
72
|
'proceed?") — the next step is the user\'s Build action, and trailing ' +
|
|
55
73
|
"chat would be published as part of the document.",
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Todo-list projection shared by every harness's status writer.
|
|
3
|
+
*
|
|
4
|
+
* All harnesses expose an agent to-do tool — the Cursor SDK emits `TodoWrite`
|
|
5
|
+
* (legacy) / `updateTodos` (current), the native deepagents harness emits
|
|
6
|
+
* `write_todos` — and each writes the SAME `AgentExecutionStatus.todos` proto
|
|
7
|
+
* map that the clients (React `TodoCard`, CLI) render. This module is the single
|
|
8
|
+
* place that maps a raw tool payload into that map, so the Cursor `TodoTracker`
|
|
9
|
+
* and the native v2/v3 status builders cannot drift in how they build todos.
|
|
10
|
+
*
|
|
11
|
+
* The mapping is a superset of what any one harness emits: the Cursor SDK sends
|
|
12
|
+
* per-item `id`/`created_at` and can send a `merge` flag and camelCase statuses;
|
|
13
|
+
* deepagents sends only `{ content, status }` (three statuses, no id, no merge).
|
|
14
|
+
* One helper handling the superset serves all three writers.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { create } from "@bufbuild/protobuf";
|
|
18
|
+
import { TodoItemSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/todo_pb";
|
|
19
|
+
import type { TodoItem } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/todo_pb";
|
|
20
|
+
import { TodoStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
21
|
+
import { utcTimestamp } from "./status.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A single raw todo as it arrives in a to-do tool call's arguments. Every field
|
|
25
|
+
* is optional and defensively typed: the Cursor `updateTodos` schema omits
|
|
26
|
+
* per-item `id`, deepagents omits `id`/`created_at`, and the values reach us as
|
|
27
|
+
* untyped JSON (a Cursor `args` string or a protobuf `JsonObject` value).
|
|
28
|
+
*/
|
|
29
|
+
export interface RawTodoItem {
|
|
30
|
+
id?: string;
|
|
31
|
+
content?: string;
|
|
32
|
+
status?: string;
|
|
33
|
+
created_at?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Raw status string -> proto `TodoStatus`. Keyed on the lowercased status so
|
|
38
|
+
* the current SDK's camelCase `inProgress` and the legacy snake_case
|
|
39
|
+
* `in_progress` both resolve. `cancelled` is only ever emitted by the Cursor
|
|
40
|
+
* SDK — deepagents' three-status schema never sends it — but it is kept here so
|
|
41
|
+
* the one shared mapping is faithful to every producer. Unknown values fall back
|
|
42
|
+
* to `TODO_PENDING` at the call site.
|
|
43
|
+
*/
|
|
44
|
+
const STATUS_MAP: Record<string, TodoStatus> = {
|
|
45
|
+
pending: TodoStatus.TODO_PENDING,
|
|
46
|
+
in_progress: TodoStatus.TODO_IN_PROGRESS,
|
|
47
|
+
inprogress: TodoStatus.TODO_IN_PROGRESS,
|
|
48
|
+
completed: TodoStatus.TODO_COMPLETED,
|
|
49
|
+
cancelled: TodoStatus.TODO_CANCELLED,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Project a to-do tool call's payload into a `status.todos` map, in place.
|
|
54
|
+
* Returns whether the map changed — the caller's "dirty" / force-persist signal.
|
|
55
|
+
*
|
|
56
|
+
* Semantics (identical across all three harness writers):
|
|
57
|
+
* - Empty or non-array `rawTodos`: a full-replace (`!merge`) clears the map and
|
|
58
|
+
* is a change; a merge is a no-op (nothing to merge in) and is not.
|
|
59
|
+
* - Otherwise a full-replace clears first, then every item is (re)written.
|
|
60
|
+
* - `id` is the provided id or a stable index fallback (`todo-<i>`); order is
|
|
61
|
+
* the array order, so index ids are stable across full-replace updates.
|
|
62
|
+
* - `created_at` is preserved from the prior same-id entry ONLY on merge; a
|
|
63
|
+
* full-replace clears first, so it becomes `raw.created_at || now`. (The
|
|
64
|
+
* native harness is always full-replace and sends no `created_at`, so a
|
|
65
|
+
* native todo's `created_at` is `now` on each write — deliberately identical
|
|
66
|
+
* to the Cursor non-merge path.)
|
|
67
|
+
* - `updated_at` is always `now`.
|
|
68
|
+
*
|
|
69
|
+
* Note on clearing: an empty full-replace clears the in-memory map, but the Go
|
|
70
|
+
* `update_status` activity replaces persisted todos only when the incoming map
|
|
71
|
+
* is non-empty, so a settled list survives in history — the clear never reaches
|
|
72
|
+
* the record. This is intentional and relied upon by both harnesses.
|
|
73
|
+
*/
|
|
74
|
+
export function applyTodoUpdate(
|
|
75
|
+
target: { [key: string]: TodoItem },
|
|
76
|
+
rawTodos: unknown,
|
|
77
|
+
opts: { merge: boolean; now?: string },
|
|
78
|
+
): boolean {
|
|
79
|
+
const { merge } = opts;
|
|
80
|
+
const now = opts.now ?? utcTimestamp();
|
|
81
|
+
|
|
82
|
+
if (!Array.isArray(rawTodos) || rawTodos.length === 0) {
|
|
83
|
+
if (!merge) {
|
|
84
|
+
clearMap(target);
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!merge) {
|
|
91
|
+
clearMap(target);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
for (let i = 0; i < rawTodos.length; i++) {
|
|
95
|
+
const raw = coerceRawTodo(rawTodos[i]);
|
|
96
|
+
const id = raw.id || `todo-${i}`;
|
|
97
|
+
const statusStr = (raw.status ?? "pending").toLowerCase();
|
|
98
|
+
const status = STATUS_MAP[statusStr] ?? TodoStatus.TODO_PENDING;
|
|
99
|
+
|
|
100
|
+
// Preserve the original creation time only when merging into an existing
|
|
101
|
+
// entry; a full-replace cleared the map above, so `existing` is undefined.
|
|
102
|
+
const existing = merge ? target[id] : undefined;
|
|
103
|
+
|
|
104
|
+
target[id] = create(TodoItemSchema, {
|
|
105
|
+
id,
|
|
106
|
+
content: raw.content ?? "",
|
|
107
|
+
status,
|
|
108
|
+
createdAt: existing?.createdAt || raw.created_at || now,
|
|
109
|
+
updatedAt: now,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Narrow an untyped array element to a {@link RawTodoItem}; non-objects become empty. */
|
|
117
|
+
function coerceRawTodo(value: unknown): RawTodoItem {
|
|
118
|
+
return typeof value === "object" && value !== null ? (value as RawTodoItem) : {};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Delete every key from the map in place (proto map fields have no `.clear()`). */
|
|
122
|
+
function clearMap(target: { [key: string]: TodoItem }): void {
|
|
123
|
+
for (const key of Object.keys(target)) {
|
|
124
|
+
delete target[key];
|
|
125
|
+
}
|
|
126
|
+
}
|
package/src/shared/tool-row.ts
CHANGED
|
@@ -186,3 +186,37 @@ export function collectSubAgentToolCallIds(
|
|
|
186
186
|
}
|
|
187
187
|
return ids;
|
|
188
188
|
}
|
|
189
|
+
|
|
190
|
+
/** Terminal tool-call statuses — a row that has finished (however it finished). */
|
|
191
|
+
const TERMINAL_TOOL_CALL_STATUSES: ReadonlySet<ToolCallStatus> = new Set([
|
|
192
|
+
ToolCallStatus.TOOL_CALL_COMPLETED,
|
|
193
|
+
ToolCallStatus.TOOL_CALL_FAILED,
|
|
194
|
+
ToolCallStatus.TOOL_CALL_SKIPPED,
|
|
195
|
+
]);
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Collect the ids of tool-call rows that have already SETTLED (reached a terminal
|
|
199
|
+
* state: completed, failed, or skipped) in a transcript.
|
|
200
|
+
*
|
|
201
|
+
* The deep-agent turn-boundary provenance derivation (DD-28) snapshots this
|
|
202
|
+
* BEFORE a turn's stream to scope "this turn's tool calls" by identity: a call
|
|
203
|
+
* whose id is absent from the snapshot is this-turn's — either freshly streamed,
|
|
204
|
+
* or a prior gate that was WAITING_APPROVAL before the stream and executes now on
|
|
205
|
+
* resume (StatusBuilder updates the seeded row in place, so it keeps its id).
|
|
206
|
+
* Unlike the Cursor harness, the deep-agent cannot scope positionally — an
|
|
207
|
+
* approved command executes at its SEEDED position — so identity is the only
|
|
208
|
+
* correct scope, mirroring {@link collectSubAgentToolCallIds}. Terminal (not just
|
|
209
|
+
* completed) so a PRIOR turn's failed/skipped non-shell call is excluded and
|
|
210
|
+
* cannot spuriously disqualify this turn.
|
|
211
|
+
*/
|
|
212
|
+
export function collectSettledToolCallIds(
|
|
213
|
+
messages: readonly AgentMessage[],
|
|
214
|
+
): Set<string> {
|
|
215
|
+
const ids = new Set<string>();
|
|
216
|
+
for (const msg of messages) {
|
|
217
|
+
for (const tc of msg.toolCalls) {
|
|
218
|
+
if (TERMINAL_TOOL_CALL_STATUSES.has(tc.status)) ids.add(tc.id);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return ids;
|
|
222
|
+
}
|