@yagni-app/code-staging 1.0.1-staging.1204.1 → 1.0.2-staging.1211.1
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/extension/askAdvisorTool.d.ts +7 -0
- package/dist/extension/askAdvisorTool.js +5 -1
- package/dist/extension/childUsage.d.ts +40 -0
- package/dist/extension/childUsage.js +43 -0
- package/dist/extension/footer.d.ts +13 -1
- package/dist/extension/footer.js +14 -7
- package/dist/extension/index.js +10 -3
- package/dist/extension/permission/gate.d.ts +6 -0
- package/dist/extension/permission/gate.js +8 -0
- package/dist/extension/pipeline/goCommand.d.ts +8 -0
- package/dist/extension/pipeline/goCommand.js +8 -0
- package/dist/extension/subagents.d.ts +11 -1
- package/dist/extension/subagents.js +16 -1
- package/package.json +2 -2
|
@@ -29,6 +29,7 @@ import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-age
|
|
|
29
29
|
import { type Component } from "@earendil-works/pi-tui";
|
|
30
30
|
import { Type } from "typebox";
|
|
31
31
|
import { type AdvisorLimits, type AdvisorStateHandle } from "./advisor.js";
|
|
32
|
+
import type { ChildUsageHandle } from "./childUsage.js";
|
|
32
33
|
import type { WorkingLineHandle } from "./workingLine.js";
|
|
33
34
|
import { runStage as defaultRunStage } from "./pipeline/runner.js";
|
|
34
35
|
import { type PipelineStage } from "./pipeline/types.js";
|
|
@@ -56,6 +57,12 @@ export interface MakeAskAdvisorToolOptions {
|
|
|
56
57
|
* absent, the tool falls back to ui.setWorkingMessage directly.
|
|
57
58
|
*/
|
|
58
59
|
workingLine?: WorkingLineHandle;
|
|
60
|
+
/**
|
|
61
|
+
* Session child-usage accumulator (childUsage.ts), so the footer's session
|
|
62
|
+
* totals include advisor consults — the spend the consult-cost line already
|
|
63
|
+
* prints. Absent = not wired (tests/harnesses): no recording.
|
|
64
|
+
*/
|
|
65
|
+
childUsage?: ChildUsageHandle;
|
|
59
66
|
}
|
|
60
67
|
/**
|
|
61
68
|
* Assemble the consult brief. The advisor's persona already tells it not to take
|
|
@@ -129,7 +129,7 @@ export function makeAskAdvisorTool(opts) {
|
|
|
129
129
|
renderShell: "self",
|
|
130
130
|
renderCall: renderAdvisorCall,
|
|
131
131
|
renderResult: renderSubagentResult,
|
|
132
|
-
async execute(
|
|
132
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
133
133
|
// Read the LIVE session model: pi's picker can change it after this tool
|
|
134
134
|
// was registered, in both directions.
|
|
135
135
|
const decision = decideConsult({
|
|
@@ -194,6 +194,10 @@ export function makeAskAdvisorTool(opts) {
|
|
|
194
194
|
const cost = result.usage?.cost ?? 0;
|
|
195
195
|
const state = opts.state.record(cost);
|
|
196
196
|
finalizeTask(progress, result, Date.now());
|
|
197
|
+
// Footer session totals include the consult. Keyed by toolCallId — one
|
|
198
|
+
// consult per call, unique even for parallel consults minted in the same
|
|
199
|
+
// millisecond — so a re-fired finalize for the same call is a no-op.
|
|
200
|
+
opts.childUsage?.record("advisor", toolCallId, result.usage);
|
|
197
201
|
if (result.exitCode !== 0 && !result.finalOutput.trim()) {
|
|
198
202
|
// Fail honestly rather than returning an empty recommendation. The
|
|
199
203
|
// consult still counts: it spawned, and it may well have spent.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-scoped child-process usage accounting (YAG-383's honest-local
|
|
3
|
+
* counterpart for the FOOTER).
|
|
4
|
+
*
|
|
5
|
+
* `collectUsage` (footer.ts) reads pi's session entries, which only ever carry
|
|
6
|
+
* the DRIVER's own turns: subagents, advisor consults, Guardian reviews, and
|
|
7
|
+
* every /go stage run in a child process whose usage never lands in those
|
|
8
|
+
* entries. The per-child surfaces (subagent receipts, /go's run-cost note,
|
|
9
|
+
* advisor's consult-cost line) already fold each child's usage from its NDJSON
|
|
10
|
+
* stream — this module is the session-level SUM of those already-held figures,
|
|
11
|
+
* so the footer's totals can include what the receipts already printed.
|
|
12
|
+
*
|
|
13
|
+
* Same shape as `makeAdvisorState` (advisor.ts): a closure-held handle, `read()`
|
|
14
|
+
* snapshots, and an additive `record()`. Tokens ride along with cost so the
|
|
15
|
+
* footer's `↑in ↓out` figures stay honest too. `record()` is idempotent per
|
|
16
|
+
* (source, key): a re-fired completion for the same child must never
|
|
17
|
+
* double-count its spend, so each seam keys by the child's identity (one key
|
|
18
|
+
* per subagent task / consult / review / run attempt).
|
|
19
|
+
*/
|
|
20
|
+
/** Usage fields the footer's line-2 stats render. */
|
|
21
|
+
export interface ChildUsageDelta {
|
|
22
|
+
input: number;
|
|
23
|
+
output: number;
|
|
24
|
+
cacheRead: number;
|
|
25
|
+
cacheWrite: number;
|
|
26
|
+
cost: number;
|
|
27
|
+
}
|
|
28
|
+
export interface ChildUsageHandle {
|
|
29
|
+
read(): ChildUsageDelta;
|
|
30
|
+
/**
|
|
31
|
+
* Add one child's usage, deduped by `(source, key)`. A repeated record for
|
|
32
|
+
* the same key is a no-op (re-fire safety), not an overwrite: child spend is
|
|
33
|
+
* cumulative and never revised downward. `delta` may be absent (a degraded
|
|
34
|
+
* or stubbed runner that reported no usage): the key is still consumed and
|
|
35
|
+
* nothing is added — never a throw.
|
|
36
|
+
*/
|
|
37
|
+
record(source: string, key: string, delta: Partial<ChildUsageDelta> | undefined): ChildUsageDelta;
|
|
38
|
+
}
|
|
39
|
+
export declare function makeChildUsageState(): ChildUsageHandle;
|
|
40
|
+
//# sourceMappingURL=childUsage.d.ts.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-scoped child-process usage accounting (YAG-383's honest-local
|
|
3
|
+
* counterpart for the FOOTER).
|
|
4
|
+
*
|
|
5
|
+
* `collectUsage` (footer.ts) reads pi's session entries, which only ever carry
|
|
6
|
+
* the DRIVER's own turns: subagents, advisor consults, Guardian reviews, and
|
|
7
|
+
* every /go stage run in a child process whose usage never lands in those
|
|
8
|
+
* entries. The per-child surfaces (subagent receipts, /go's run-cost note,
|
|
9
|
+
* advisor's consult-cost line) already fold each child's usage from its NDJSON
|
|
10
|
+
* stream — this module is the session-level SUM of those already-held figures,
|
|
11
|
+
* so the footer's totals can include what the receipts already printed.
|
|
12
|
+
*
|
|
13
|
+
* Same shape as `makeAdvisorState` (advisor.ts): a closure-held handle, `read()`
|
|
14
|
+
* snapshots, and an additive `record()`. Tokens ride along with cost so the
|
|
15
|
+
* footer's `↑in ↓out` figures stay honest too. `record()` is idempotent per
|
|
16
|
+
* (source, key): a re-fired completion for the same child must never
|
|
17
|
+
* double-count its spend, so each seam keys by the child's identity (one key
|
|
18
|
+
* per subagent task / consult / review / run attempt).
|
|
19
|
+
*/
|
|
20
|
+
const num = (n) => {
|
|
21
|
+
const v = n ?? 0;
|
|
22
|
+
return Number.isFinite(v) && v > 0 ? v : 0;
|
|
23
|
+
};
|
|
24
|
+
export function makeChildUsageState() {
|
|
25
|
+
const seen = new Set();
|
|
26
|
+
const totals = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
|
|
27
|
+
return {
|
|
28
|
+
read: () => ({ ...totals }),
|
|
29
|
+
record(source, key, delta) {
|
|
30
|
+
const dedupeKey = `${source}\u0000${key}`;
|
|
31
|
+
if (seen.has(dedupeKey))
|
|
32
|
+
return { ...totals };
|
|
33
|
+
seen.add(dedupeKey);
|
|
34
|
+
totals.input += num(delta?.input);
|
|
35
|
+
totals.output += num(delta?.output);
|
|
36
|
+
totals.cacheRead += num(delta?.cacheRead);
|
|
37
|
+
totals.cacheWrite += num(delta?.cacheWrite);
|
|
38
|
+
totals.cost += num(delta?.cost);
|
|
39
|
+
return { ...totals };
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=childUsage.js.map
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
* — useful reference for what data to replicate and how to format it.
|
|
40
40
|
*/
|
|
41
41
|
import type { ExtensionContext, ReadonlyFooterDataProvider, Theme } from "@earendil-works/pi-coding-agent";
|
|
42
|
+
import type { ChildUsageHandle } from "./childUsage.js";
|
|
42
43
|
import type { ModeHolder, PermissionMode } from "./permission/gate.js";
|
|
43
44
|
export declare const BRANCH_MAX_WIDTH = 60;
|
|
44
45
|
export declare function cyclePermissionMode(current: PermissionMode): PermissionMode;
|
|
@@ -99,6 +100,17 @@ export declare function renderFooterLines(input: {
|
|
|
99
100
|
/** Formatted `+N -M` / `±`, or null when clean — appended to the branch as `[ … ]`. */
|
|
100
101
|
diff?: string | null;
|
|
101
102
|
usage: UsageTotals;
|
|
103
|
+
/**
|
|
104
|
+
* Session child-process spend (subagents, advisor consults, Guardian
|
|
105
|
+
* reviews, /go runs) — childUsage.ts's session accumulator. Added into
|
|
106
|
+
* the line-2 stats so the footer's totals include the spend the per-child
|
|
107
|
+
* receipts already print; absent/zero changes nothing.
|
|
108
|
+
*/
|
|
109
|
+
childUsage?: {
|
|
110
|
+
input: number;
|
|
111
|
+
output: number;
|
|
112
|
+
cost: number;
|
|
113
|
+
};
|
|
102
114
|
contextPercent: number | null;
|
|
103
115
|
statuses: string[];
|
|
104
116
|
}, theme: Pick<Theme, "fg">, width: number, padX?: number): string[];
|
|
@@ -111,7 +123,7 @@ export interface FooterInvalidateHandle {
|
|
|
111
123
|
invalidateGit(): void;
|
|
112
124
|
requestRender(): void;
|
|
113
125
|
}
|
|
114
|
-
export declare function createYagniFooterFactory(ctx: ExtensionContext, modeHolder?: ModeHolder, invalidateHandle?: FooterInvalidateHandle): (_tui: unknown, theme: Theme, footerData: ReadonlyFooterDataProvider) => {
|
|
126
|
+
export declare function createYagniFooterFactory(ctx: ExtensionContext, modeHolder?: ModeHolder, invalidateHandle?: FooterInvalidateHandle, childUsage?: ChildUsageHandle): (_tui: unknown, theme: Theme, footerData: ReadonlyFooterDataProvider) => {
|
|
115
127
|
render(width: number): string[];
|
|
116
128
|
invalidate(): void;
|
|
117
129
|
dispose(): void;
|
package/dist/extension/footer.js
CHANGED
|
@@ -245,13 +245,19 @@ export function renderFooterLines(input, theme, width, padX = 0) {
|
|
|
245
245
|
}
|
|
246
246
|
const line1 = pad + truncateToWidth(line1Parts.join(sep), contentWidth, dim("…"));
|
|
247
247
|
// Line 2: [mode ·] model · ↑in ↓out $cost · ctx%
|
|
248
|
+
// Child-process spend (subagents, advisor, /go) joins the driver totals so
|
|
249
|
+
// the footer matches the per-child receipts and /cost's session scope.
|
|
250
|
+
const child = input.childUsage;
|
|
251
|
+
const totalIn = input.usage.input + (child?.input ?? 0);
|
|
252
|
+
const totalOut = input.usage.output + (child?.output ?? 0);
|
|
253
|
+
const totalCost = input.usage.cost + (child?.cost ?? 0);
|
|
248
254
|
const statParts = [];
|
|
249
|
-
if (
|
|
250
|
-
statParts.push(`↑${formatTokens(
|
|
251
|
-
if (
|
|
252
|
-
statParts.push(`↓${formatTokens(
|
|
253
|
-
if (
|
|
254
|
-
statParts.push(`$${
|
|
255
|
+
if (totalIn)
|
|
256
|
+
statParts.push(`↑${formatTokens(totalIn)}`);
|
|
257
|
+
if (totalOut)
|
|
258
|
+
statParts.push(`↓${formatTokens(totalOut)}`);
|
|
259
|
+
if (totalCost)
|
|
260
|
+
statParts.push(`$${totalCost.toFixed(3)}`);
|
|
255
261
|
const stats = statParts.join(" ");
|
|
256
262
|
const percentText = input.contextPercent === null ? "?" : `${Math.round(input.contextPercent)}%`;
|
|
257
263
|
const line2Parts = [];
|
|
@@ -272,7 +278,7 @@ export function renderFooterLines(input, theme, width, padX = 0) {
|
|
|
272
278
|
}
|
|
273
279
|
return lines;
|
|
274
280
|
}
|
|
275
|
-
export function createYagniFooterFactory(ctx, modeHolder, invalidateHandle) {
|
|
281
|
+
export function createYagniFooterFactory(ctx, modeHolder, invalidateHandle, childUsage) {
|
|
276
282
|
return (_tui, theme, footerData) => {
|
|
277
283
|
let gitCache;
|
|
278
284
|
let diffStatCache;
|
|
@@ -305,6 +311,7 @@ export function createYagniFooterFactory(ctx, modeHolder, invalidateHandle) {
|
|
|
305
311
|
mode: modeHolder?.get() ?? null,
|
|
306
312
|
diff: formatDiffStat(diffStatCache?.get() ?? null),
|
|
307
313
|
usage: collectUsage(ctx.sessionManager),
|
|
314
|
+
childUsage: childUsage?.read(),
|
|
308
315
|
contextPercent: ctx.getContextUsage()?.percent ?? null,
|
|
309
316
|
statuses,
|
|
310
317
|
}, theme, width, resolveFooterPadX());
|
package/dist/extension/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { Text } from "@earendil-works/pi-tui";
|
|
3
3
|
import { DEFAULT_ADVISOR_LIMITS, formatAdvisorSubtotal, makeAdvisorState } from "./advisor.js";
|
|
4
|
+
import { makeChildUsageState } from "./childUsage.js";
|
|
4
5
|
import { appendGrant, loadGrants, resolveRepoKey, storagePrefix } from "./permission/approvedPrefixes.js";
|
|
5
6
|
import { redactCommand } from "./redact.js";
|
|
6
7
|
import { formatGuardianSubtotal, GUARDIAN_MODEL_TIER, makeGuardianState, resolveGuardianLimits, reviewCommand, deriveGuardianTimeoutMs } from "./permission/guardian.js";
|
|
@@ -187,7 +188,11 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
187
188
|
// hide it forever from a session that switched TO Advanced. One state handle
|
|
188
189
|
// per session, shared with /advise so they draw on the same cap.
|
|
189
190
|
const advisorState = makeAdvisorState();
|
|
190
|
-
|
|
191
|
+
// Session child-usage accumulator: every child process's spend (subagent
|
|
192
|
+
// tasks, advisor consults, Guardian reviews, /go runs) lands here so the
|
|
193
|
+
// footer's session totals include what the per-child receipts print.
|
|
194
|
+
const childUsage = makeChildUsageState();
|
|
195
|
+
const askAdvisorTool = makeAskAdvisorTool({ state: advisorState, workingLine, childUsage });
|
|
191
196
|
pi.registerTool(askAdvisorTool);
|
|
192
197
|
// /advise runs the SAME tool, sharing the state handle, so a manual consult
|
|
193
198
|
// draws on the same cap rather than opening a side channel around it.
|
|
@@ -232,7 +237,7 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
232
237
|
// driver's delegation identity for the diamond directive (see the
|
|
233
238
|
// before_agent_start handler below) and widens the tool's fan-out ceiling.
|
|
234
239
|
const ultraHolder = createUltraHolder();
|
|
235
|
-
registerSubagents(pi, { isUltra: () => ultraHolder.get(), workingLine });
|
|
240
|
+
registerSubagents(pi, { isUltra: () => ultraHolder.get(), workingLine, childUsage });
|
|
236
241
|
registerUltraCommand(pi, ultraHolder);
|
|
237
242
|
// Shared fetch timeout for the small, interactive display-path reads below
|
|
238
243
|
// (/cost's spend + headroom, and Task 8's per-run spend for /go's summary):
|
|
@@ -242,6 +247,7 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
242
247
|
// The grounded multi-agent pipeline entry point: /go <ticket> runs
|
|
243
248
|
// map → plan → implement → review → fix, each child grounded by inheritance.
|
|
244
249
|
registerGoCommand(pi, {
|
|
250
|
+
childUsage,
|
|
245
251
|
// /ultra is one dial for the whole session: the same holder the subagent
|
|
246
252
|
// tool reads widens the implement diamond's parallel ceiling (4 -> 8) for
|
|
247
253
|
// the fan and its fix turns. Read per run, so a toggle lands on the next /go.
|
|
@@ -359,6 +365,7 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
359
365
|
guardianLimits,
|
|
360
366
|
guardianTier,
|
|
361
367
|
guardianDisabled,
|
|
368
|
+
childUsage,
|
|
362
369
|
guardianReview: (command, deps) => reviewCommand(command, { ...deps, modelTier: guardianTier }),
|
|
363
370
|
onGuardianReview: (ev) => {
|
|
364
371
|
guardianLogSink(ev);
|
|
@@ -900,7 +907,7 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
900
907
|
// context % on line 2, and extension statuses (brand, todos, mode) on
|
|
901
908
|
// line 3. The factory captures ctx so the footer can read session data
|
|
902
909
|
// (token stats, context usage) that isn't on the footerData provider.
|
|
903
|
-
ctx.ui?.setFooter?.((tui, theme, footerData) => createYagniFooterFactory(ctx, modeHolder, footerInvalidateHandle)(tui, theme, footerData));
|
|
910
|
+
ctx.ui?.setFooter?.((tui, theme, footerData) => createYagniFooterFactory(ctx, modeHolder, footerInvalidateHandle, childUsage)(tui, theme, footerData));
|
|
904
911
|
ctx.ui?.onTerminalInput?.((data) => {
|
|
905
912
|
if (isShiftTab(data)) {
|
|
906
913
|
modeHolder.set(cyclePermissionMode(modeHolder.get()));
|
|
@@ -151,6 +151,12 @@ export interface RegisterPermissionDeps {
|
|
|
151
151
|
guardianState?: import("./guardian.js").GuardianStateHandle;
|
|
152
152
|
/** Guardian limits (timeouts, circuit breaker). Defaults to DEFAULT_GUARDIAN_LIMITS. */
|
|
153
153
|
guardianLimits?: import("./guardian.js").GuardianLimits;
|
|
154
|
+
/**
|
|
155
|
+
* Session child-usage accumulator (childUsage.ts): each completed Guardian
|
|
156
|
+
* review's spend is recorded so the footer's session totals include it.
|
|
157
|
+
* Absent = not wired: no recording, nothing else changes.
|
|
158
|
+
*/
|
|
159
|
+
childUsage?: import("../childUsage.js").ChildUsageHandle;
|
|
154
160
|
/** Override the Guardian model tier (default: efficient). */
|
|
155
161
|
guardianTier?: string;
|
|
156
162
|
/** Injectable Guardian review function (tests pass a stub). */
|
|
@@ -366,6 +366,10 @@ export function registerPermissionGate(pi, deps = {}) {
|
|
|
366
366
|
const ERROR_ASK_CAP = 3;
|
|
367
367
|
let errorFallbackAsks = 0;
|
|
368
368
|
let breakerEscalationOffered = false;
|
|
369
|
+
// Uniquifies each Guardian review's childUsage key: parallel gated tool
|
|
370
|
+
// calls can start their reviews in the same millisecond, and a timestamp
|
|
371
|
+
// alone would silently drop the second review's spend as a "duplicate".
|
|
372
|
+
let reviewSpendSeq = 0;
|
|
369
373
|
const emitGateEvent = (event) => {
|
|
370
374
|
if (!deps.onGuardianEvent)
|
|
371
375
|
return;
|
|
@@ -561,6 +565,10 @@ export function registerPermissionGate(pi, deps = {}) {
|
|
|
561
565
|
ctx.ui.setStatus?.("yagni-guardian", undefined);
|
|
562
566
|
}
|
|
563
567
|
const durationMs = Date.now() - startMs;
|
|
568
|
+
// Footer session totals include the review's spend. Each review
|
|
569
|
+
// records exactly once, right here, so the key only has to be unique
|
|
570
|
+
// per review — the seq keeps same-millisecond parallel reviews apart.
|
|
571
|
+
deps.childUsage?.record("guardian", `${startMs}#${reviewSpendSeq++}`, { cost: reviewResult.cost });
|
|
564
572
|
const emitDiag = (outcome, rationale, rawOutput) => {
|
|
565
573
|
if (!deps.onGuardianReview)
|
|
566
574
|
return;
|
|
@@ -110,6 +110,14 @@ export interface RegisterGoDeps {
|
|
|
110
110
|
* cares injects its own fake.
|
|
111
111
|
*/
|
|
112
112
|
fetchRunSpend?: (runId: string, signal?: AbortSignal) => Promise<SpendResponse | null>;
|
|
113
|
+
/**
|
|
114
|
+
* Session child-usage accumulator (childUsage.ts). Each attempt's newly-run
|
|
115
|
+
* stage usage is recorded (keyed by runId + attempt start, because a resumed
|
|
116
|
+
* attempt's stages are new spend, never a replay) so the footer's session
|
|
117
|
+
* totals include /go spend — the same figure `runCostNote` already prints.
|
|
118
|
+
* Absent = not wired.
|
|
119
|
+
*/
|
|
120
|
+
childUsage?: import("../childUsage.js").ChildUsageHandle;
|
|
113
121
|
/** Injectable fs existence check (worktree adoption on resume). */
|
|
114
122
|
exists?: (path: string) => boolean;
|
|
115
123
|
/** Clock seam for registry rows + staleness. */
|
|
@@ -969,6 +969,14 @@ export function registerGoCommand(pi, deps = {}) {
|
|
|
969
969
|
if (result.stopReason === "clean" && runSignal.aborted) {
|
|
970
970
|
result = { ...result, stopReason: "aborted" };
|
|
971
971
|
}
|
|
972
|
+
// Footer session totals include this run's spend. Keyed per ATTEMPT
|
|
973
|
+
// (runId + this invocation's start), not per runId: `result.stages`
|
|
974
|
+
// only ever holds the stages THIS attempt actually ran (a resume's
|
|
975
|
+
// replayed spend lives in the checkpoint's priorUsage, never here),
|
|
976
|
+
// so an abort → same-session resume must record both attempts — a
|
|
977
|
+
// runId-only key would silently drop the resume's new spend. Partial
|
|
978
|
+
// spend on an aborted attempt is still real spend worth counting.
|
|
979
|
+
deps.childUsage?.record("go", `${runId}@${startedAt}`, aggregateRunUsage(result.stages, result.rounds));
|
|
972
980
|
// FINISH stage (spec §3c): ONLY on a clean stop. Commit the run's work
|
|
973
981
|
// (worktree always; --here only over a clean pre-run baseline) with the
|
|
974
982
|
// provenance trailer, and push + PR when --pr asked for it. Iron
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
21
21
|
import { Type } from "typebox";
|
|
22
|
+
import type { ChildUsageHandle } from "./childUsage.js";
|
|
22
23
|
import type { WorkingLineHandle } from "./workingLine.js";
|
|
23
24
|
import { runStage } from "./pipeline/runner.js";
|
|
24
25
|
import { type ModelTier, type PipelineStage } from "./pipeline/types.js";
|
|
@@ -103,6 +104,13 @@ export interface MakeSubagentToolDeps {
|
|
|
103
104
|
* the tool falls back to setting ui.setWorkingMessage directly.
|
|
104
105
|
*/
|
|
105
106
|
workingLine?: WorkingLineHandle;
|
|
107
|
+
/**
|
|
108
|
+
* Session child-usage accumulator (childUsage.ts). Each completed task's
|
|
109
|
+
* folded usage is recorded so the footer's session totals include the
|
|
110
|
+
* child spend the per-task receipt already prints. Absent = not wired
|
|
111
|
+
* (headless/eval harnesses): no recording, nothing else changes.
|
|
112
|
+
*/
|
|
113
|
+
childUsage?: ChildUsageHandle;
|
|
106
114
|
}
|
|
107
115
|
export declare function makeSubagentTool(deps?: MakeSubagentToolDeps): {
|
|
108
116
|
name: string;
|
|
@@ -120,7 +128,7 @@ export declare function makeSubagentTool(deps?: MakeSubagentToolDeps): {
|
|
|
120
128
|
renderShell: "self";
|
|
121
129
|
renderCall: typeof renderSubagentCall;
|
|
122
130
|
renderResult: typeof renderSubagentResult;
|
|
123
|
-
execute(
|
|
131
|
+
execute(toolCallId: string, params: SubagentParams, signal?: AbortSignal, onUpdate?: (update: {
|
|
124
132
|
content: Array<{
|
|
125
133
|
type: "text";
|
|
126
134
|
text: string;
|
|
@@ -151,6 +159,8 @@ export interface RegisterSubagentsDeps {
|
|
|
151
159
|
isUltra?: () => boolean;
|
|
152
160
|
/** Session working-line manager; see MakeSubagentToolDeps.workingLine. */
|
|
153
161
|
workingLine?: WorkingLineHandle;
|
|
162
|
+
/** Session child-usage accumulator; see MakeSubagentToolDeps.childUsage. */
|
|
163
|
+
childUsage?: ChildUsageHandle;
|
|
154
164
|
}
|
|
155
165
|
/** Wire the subagent tool and the /agents listing command. */
|
|
156
166
|
export declare function registerSubagents(pi: ExtensionAPI, deps?: RegisterSubagentsDeps): void;
|
|
@@ -33,6 +33,17 @@ import { applyChildEvent, finalizeTask, formatWorkingMessage, newTaskProgress, p
|
|
|
33
33
|
* within the model proxy's 64-char caller-label limit.
|
|
34
34
|
*/
|
|
35
35
|
const SUBAGENT_CALLER_PREFIX = "subagent:";
|
|
36
|
+
/**
|
|
37
|
+
* Dedupe key for one task's childUsage record. Neither `startedAt` (parallel
|
|
38
|
+
* tasks on the same agent are minted in the same `resolved.map` pass and can
|
|
39
|
+
* share a millisecond) nor the per-invocation index (every invocation starts
|
|
40
|
+
* at 0) is unique alone; the pair is, because one tool call's task set is
|
|
41
|
+
* minted exactly once. A re-fired finalize for the same record lands on the
|
|
42
|
+
* same key, so the accumulator's (source, key) dedupe makes it a no-op.
|
|
43
|
+
*/
|
|
44
|
+
function progressKey(toolCallId, p, index) {
|
|
45
|
+
return `${toolCallId}/${index}/${p.agent}@${p.startedAt}`;
|
|
46
|
+
}
|
|
36
47
|
export const SUBAGENT_TOOL_NAME = "subagent";
|
|
37
48
|
export const GENERAL_AGENT_NAME = "general";
|
|
38
49
|
export const MAX_PARALLEL_SUBAGENTS = 4;
|
|
@@ -354,7 +365,7 @@ export function makeSubagentTool(deps = {}) {
|
|
|
354
365
|
renderShell: "self",
|
|
355
366
|
renderCall: renderSubagentCall,
|
|
356
367
|
renderResult: renderSubagentResult,
|
|
357
|
-
async execute(
|
|
368
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
358
369
|
const fail = (text) => ({
|
|
359
370
|
content: [{ type: "text", text }],
|
|
360
371
|
details: {},
|
|
@@ -428,6 +439,10 @@ export function makeSubagentTool(deps = {}) {
|
|
|
428
439
|
},
|
|
429
440
|
});
|
|
430
441
|
finalizeTask(progress, result, Date.now());
|
|
442
|
+
// Record the child's spend into the session accumulator so the
|
|
443
|
+
// footer totals include it (dedupe key: this progress record's
|
|
444
|
+
// identity — per invocation, so parallel tasks never collide).
|
|
445
|
+
deps.childUsage?.record("subagent", progressKey(toolCallId, progress, index), result.usage);
|
|
431
446
|
emit();
|
|
432
447
|
return { agent: def.name, task, result };
|
|
433
448
|
}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yagni-app/code-staging",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2-staging.1211.1",
|
|
4
4
|
"description": "YAGNI Code: a terminal coding agent that already knows your company. One YAGNI login routes the model and grounds the agent in your team's context.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"author": "YAGNI, Inc. <jack@yagni.app> (https://yagni.app)",
|
|
@@ -41,5 +41,5 @@
|
|
|
41
41
|
"turndown": "^7.2.4",
|
|
42
42
|
"typebox": "^1.3.15"
|
|
43
43
|
},
|
|
44
|
-
"yagniSourceSha": "
|
|
44
|
+
"yagniSourceSha": "2feaf4da115b5f9322ccc20e175837fdb69d3e6c"
|
|
45
45
|
}
|