@pi-archimedes/subagent 2.10.4 → 2.11.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/README.md +2 -2
- package/package.json +10 -10
- package/src/agent-panel.mcp.test.ts +76 -0
- package/src/agent-panel.tools.test.ts +162 -0
- package/src/agent-panel.ts +37 -8
- package/src/agent-panel.width-audit.test.ts +258 -0
- package/src/agents-parse.test.ts +252 -0
- package/src/agents.ts +72 -5
- package/src/compact.test.ts +136 -57
- package/src/compact.ts +57 -37
- package/src/dispatch.test.ts +73 -1
- package/src/dispatch.ts +20 -2
- package/src/execute.test.ts +9 -1
- package/src/execute.ts +26 -29
- package/src/expanded.test.ts +472 -0
- package/src/expanded.ts +202 -82
- package/src/format.test.ts +71 -29
- package/src/format.ts +71 -28
- package/src/frontmatter-io.test.ts +62 -0
- package/src/frontmatter-io.ts +16 -2
- package/src/handlers.test.ts +21 -3
- package/src/handlers.ts +22 -3
- package/src/index.output-schema.test.ts +317 -0
- package/src/index.test.ts +566 -0
- package/src/index.ts +169 -16
- package/src/render.ts +46 -36
- package/src/row-block.test.ts +244 -0
- package/src/row-block.ts +209 -0
- package/src/run-outcome.test.ts +71 -0
- package/src/run-outcome.ts +84 -0
- package/src/spawn.test.ts +204 -19
- package/src/spawn.ts +49 -17
- package/src/stream.test.ts +215 -17
- package/src/stream.ts +47 -9
- package/src/thinking-validation.test.ts +96 -0
- package/src/thinking-validation.ts +78 -0
- package/src/tool-declarations.test.ts +58 -0
- package/src/tool-row.width-audit.test.ts +720 -0
- package/src/tool-schema.test.ts +154 -0
- package/src/tool-schema.ts +106 -4
- package/src/tool-validation.test.ts +321 -0
- package/src/tool-validation.ts +262 -0
- package/src/types.ts +86 -0
- package/src/usage.test.ts +95 -0
- package/src/usage.ts +87 -0
- package/src/cost.test.ts +0 -62
- package/src/cost.ts +0 -8
package/src/dispatch.ts
CHANGED
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
|
|
33
33
|
import { dispatch, BridgeTransportError, BridgeCancelledError } from "@pi-archimedes/core/bridge";
|
|
34
34
|
import { resolveModel } from "./spawn.js";
|
|
35
|
+
import { resolveThinkingLevel } from "./thinking-validation.js";
|
|
35
36
|
import type { ExecuteOptions } from "./execute.js";
|
|
36
37
|
import type { SubagentProgress, SubagentResult } from "./types.js";
|
|
37
38
|
|
|
@@ -101,7 +102,9 @@ export function dispatchViaBridge(
|
|
|
101
102
|
task: options.task,
|
|
102
103
|
systemPrompt: options.agentConfig?.systemPrompt?.trim() || null,
|
|
103
104
|
model: model ?? null,
|
|
104
|
-
|
|
105
|
+
// The same rule the fork path applies: a level pi's `--thinking` would warn
|
|
106
|
+
// about and drop is not handed to the runner either (`thinking-validation.ts`).
|
|
107
|
+
thinking: resolveThinkingLevel(options.agentConfig?.thinking).level ?? null,
|
|
105
108
|
tools: options.agentConfig?.tools?.length ? options.agentConfig.tools : null,
|
|
106
109
|
};
|
|
107
110
|
|
|
@@ -114,6 +117,11 @@ export function dispatchViaBridge(
|
|
|
114
117
|
const progress: SubagentProgress = {
|
|
115
118
|
agent: agentName,
|
|
116
119
|
status: "completed",
|
|
120
|
+
// The Client ran the session and reported it finished; that is a
|
|
121
|
+
// completed run in this vocabulary. It has no stopReason to expose and
|
|
122
|
+
// no cancellation of its own to report — a local cancel settles below
|
|
123
|
+
// with BridgeCancelledError, which is a failure by contract.
|
|
124
|
+
outcome: "completed",
|
|
117
125
|
task: options.task,
|
|
118
126
|
currentTool: undefined,
|
|
119
127
|
currentToolArgs: undefined,
|
|
@@ -134,6 +142,7 @@ export function dispatchViaBridge(
|
|
|
134
142
|
agent: agentName,
|
|
135
143
|
task: options.task,
|
|
136
144
|
exitCode: 0,
|
|
145
|
+
outcome: "completed",
|
|
137
146
|
usage: {
|
|
138
147
|
input: res.metrics.inputTokens,
|
|
139
148
|
output: res.metrics.outputTokens,
|
|
@@ -142,6 +151,9 @@ export function dispatchViaBridge(
|
|
|
142
151
|
cost: res.metrics.cost,
|
|
143
152
|
turns: 0,
|
|
144
153
|
},
|
|
154
|
+
// The Client ran this as a session of its own and books its usage there;
|
|
155
|
+
// this session must not also report it. See `SubagentResult`.
|
|
156
|
+
usageAccountedElsewhere: true,
|
|
145
157
|
model,
|
|
146
158
|
finalOutput: res.output,
|
|
147
159
|
error: undefined,
|
|
@@ -169,7 +181,13 @@ export function dispatchViaBridge(
|
|
|
169
181
|
const isLocalCancel = err instanceof BridgeCancelledError;
|
|
170
182
|
const errorMessage = isLocalCancel ? "cancelled" : err instanceof Error ? err.message : String(err);
|
|
171
183
|
const durationMs = Date.now() - startTime;
|
|
172
|
-
|
|
184
|
+
// Same ownership rule as the success path above: this settled from a
|
|
185
|
+
// desktop-side session (or a cancel of one), so its usage is that
|
|
186
|
+
// session's to report even if it ended badly.
|
|
187
|
+
return {
|
|
188
|
+
...buildFailedResult(agentName, options.task, errorMessage, model, durationMs),
|
|
189
|
+
usageAccountedElsewhere: true,
|
|
190
|
+
};
|
|
173
191
|
},
|
|
174
192
|
);
|
|
175
193
|
}
|
package/src/execute.test.ts
CHANGED
|
@@ -43,7 +43,9 @@ function activateBridge(socketPath: string): void {
|
|
|
43
43
|
process.env.PI_ARCHIMEDES_BRIDGE_SESSION = "test-desktop-session";
|
|
44
44
|
process.env.PI_ARCHIMEDES_BRIDGE_SERVER_PID = "12345";
|
|
45
45
|
const onSpy = vi.fn();
|
|
46
|
-
|
|
46
|
+
// The handle the relay subscribes on — inert here (these tests are about fork
|
|
47
|
+
// vs dispatch, not about the relay), but a real session always hands one over.
|
|
48
|
+
registerBridge({ on: onSpy, events: { emit: () => {}, on: () => () => {} } } as unknown as ExtensionAPI);
|
|
47
49
|
const calls = onSpy.mock.calls as Array<[string, (e: unknown, ctx: unknown) => void]>;
|
|
48
50
|
const startCall = calls.find((c) => c[0] === "session_start");
|
|
49
51
|
expect(startCall).toBeTruthy();
|
|
@@ -75,6 +77,9 @@ function makeOptions(overrides: Partial<ExecuteOptions> = {}): ExecuteOptions {
|
|
|
75
77
|
cwd: undefined,
|
|
76
78
|
signal: undefined,
|
|
77
79
|
onUpdate: undefined,
|
|
80
|
+
// These tests never fork, so the handle is inert — present because a real
|
|
81
|
+
// ExecuteOptions always carries one.
|
|
82
|
+
events: { emit: () => {}, on: () => () => {} },
|
|
78
83
|
toolCallId: "tc-1",
|
|
79
84
|
...overrides,
|
|
80
85
|
};
|
|
@@ -168,6 +173,9 @@ describe("executeSubagent (bridge branch)", () => {
|
|
|
168
173
|
expect(result.error).toBe("cancelled");
|
|
169
174
|
expect(result.progress!.status).toBe("failed");
|
|
170
175
|
expect(result.progress!.error).toBe("cancelled");
|
|
176
|
+
// Bridge-mode bookkeeping: whatever the desktop did with the task, its
|
|
177
|
+
// session owns the usage — the parent must not fold it into its own totals.
|
|
178
|
+
expect(result.usageAccountedElsewhere).toBe(true);
|
|
171
179
|
expect(spawnSpy).not.toHaveBeenCalled();
|
|
172
180
|
// Exactly ONE update: the final-failure progress (no "dispatched"
|
|
173
181
|
// placeholder — a cancelled task does not claim it was dispatched).
|
package/src/execute.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { getBridge } from "@pi-archimedes/core/bridge";
|
|
2
|
+
import type { EventBus } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import { spawnSubagent, resolveModel } from "./spawn.js";
|
|
3
4
|
import { dispatchViaBridge, buildFailedResult } from "./dispatch.js";
|
|
4
5
|
import { streamEvents } from "./stream.js";
|
|
5
|
-
import { emitCostUpdate } from "./cost.js";
|
|
6
6
|
import type { AgentConfig } from "./agents.js";
|
|
7
7
|
import type { SubagentProgress, SubagentResult } from "./types.js";
|
|
8
8
|
|
|
@@ -15,6 +15,10 @@ export interface ExecuteOptions {
|
|
|
15
15
|
cwd: string | undefined;
|
|
16
16
|
signal: AbortSignal | undefined;
|
|
17
17
|
onUpdate: ((progress: SubagentProgress) => void) | undefined;
|
|
18
|
+
/** The parent's host handle (`pi.events`) — forwarded to the ask bridge and to
|
|
19
|
+
* the todo announcements in `streamEvents`. Neither is an extension, so
|
|
20
|
+
* neither can reach a handle of its own. */
|
|
21
|
+
events: EventBus;
|
|
18
22
|
/** The subagent tool-call id (the tool's `_id`) — rides the dispatch frame
|
|
19
23
|
* for Client correlation; `undefined` → the frame's `toolCallId` is absent.
|
|
20
24
|
* In `executeParallel` all N tasks share the single subagent tool-call's id. */
|
|
@@ -49,7 +53,11 @@ export async function executeSubagent(options: ExecuteOptions): Promise<Subagent
|
|
|
49
53
|
// so executeParallel's progress slot updates from the pending placeholder
|
|
50
54
|
// to "failed" instead of a stale "Starting...".
|
|
51
55
|
if (options.signal?.aborted) {
|
|
52
|
-
const result =
|
|
56
|
+
const result = {
|
|
57
|
+
...buildFailedResult(agentName, options.task, "cancelled", model, Date.now() - startTime),
|
|
58
|
+
// Bridge mode: any usage this run ever had belongs to a desktop session.
|
|
59
|
+
usageAccountedElsewhere: true,
|
|
60
|
+
};
|
|
53
61
|
options.onUpdate?.(result.progress!);
|
|
54
62
|
return result;
|
|
55
63
|
}
|
|
@@ -81,23 +89,21 @@ export async function executeSubagent(options: ExecuteOptions): Promise<Subagent
|
|
|
81
89
|
if (!("fallback" in outcome)) {
|
|
82
90
|
// Bridge used (success or a response-carrying error, incl. the terminal
|
|
83
91
|
// "cancelled" frame — the desktop is alive, its outcome is authoritative):
|
|
84
|
-
// return it
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
92
|
+
// return it, with no cost report of its own. The PARENT never reports a
|
|
93
|
+
// child's usage for a run it did not perform: the subagent process emits
|
|
94
|
+
// its OWN usage to its OWN bridge (the core self-usage emitter), which is a
|
|
95
|
+
// different session's total. Every result `dispatchViaBridge` settles
|
|
96
|
+
// carries `usageAccountedElsewhere`, which is how the tool knows to leave
|
|
97
|
+
// it off the tool result — pi folds that `usage` into THIS session's
|
|
98
|
+
// totals, so copying it across would bill the parent twice. The fork path
|
|
99
|
+
// below does report, through the tool result's native `usage`.
|
|
91
100
|
return outcome;
|
|
92
101
|
}
|
|
93
102
|
// { fallback: true } — unreachable bridge: fall through to the fork path.
|
|
94
103
|
}
|
|
95
104
|
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
let lastEmittedOutput = 0;
|
|
99
|
-
let lastEmittedCost = 0;
|
|
100
|
-
|
|
105
|
+
// Progress is passed straight through: cost is reported once, on the tool
|
|
106
|
+
// result's native `usage`, so there is no delta bookkeeping left to do here.
|
|
101
107
|
try {
|
|
102
108
|
const child = spawnSubagent({
|
|
103
109
|
task: options.task,
|
|
@@ -106,26 +112,14 @@ export async function executeSubagent(options: ExecuteOptions): Promise<Subagent
|
|
|
106
112
|
cwd: options.cwd,
|
|
107
113
|
signal: options.signal,
|
|
108
114
|
agent: options.agentConfig,
|
|
115
|
+
events: options.events,
|
|
109
116
|
});
|
|
110
117
|
|
|
111
118
|
const result = await streamEvents(child, {
|
|
112
119
|
agent: agentName,
|
|
113
120
|
task: options.task,
|
|
121
|
+
events: options.events,
|
|
114
122
|
onProgress: (progress: SubagentProgress) => {
|
|
115
|
-
// Emit only deltas to avoid double-counting in CostAccumulator
|
|
116
|
-
const deltaInput = progress.inputTokens - lastEmittedInput;
|
|
117
|
-
const deltaOutput = progress.outputTokens - lastEmittedOutput;
|
|
118
|
-
const deltaCost = progress.cost - lastEmittedCost;
|
|
119
|
-
if (deltaInput > 0 || deltaOutput > 0 || deltaCost > 0) {
|
|
120
|
-
emitCostUpdate(agentName, {
|
|
121
|
-
inputTokens: deltaInput,
|
|
122
|
-
outputTokens: deltaOutput,
|
|
123
|
-
cost: deltaCost,
|
|
124
|
-
});
|
|
125
|
-
lastEmittedInput = progress.inputTokens;
|
|
126
|
-
lastEmittedOutput = progress.outputTokens;
|
|
127
|
-
lastEmittedCost = progress.cost;
|
|
128
|
-
}
|
|
129
123
|
options.onUpdate?.(progress);
|
|
130
124
|
},
|
|
131
125
|
});
|
|
@@ -142,7 +136,8 @@ export async function executeSubagent(options: ExecuteOptions): Promise<Subagent
|
|
|
142
136
|
// synthesize one so the parallel renderer stays aligned with results.
|
|
143
137
|
{
|
|
144
138
|
agent: agentName,
|
|
145
|
-
status: result.exitCode === 0 ? "completed" : "failed",
|
|
139
|
+
status: result.outcome || result.exitCode === 0 ? "completed" : "failed",
|
|
140
|
+
...(result.outcome ? { outcome: result.outcome } : {}),
|
|
146
141
|
task: options.task,
|
|
147
142
|
currentTool: undefined,
|
|
148
143
|
currentToolArgs: undefined,
|
|
@@ -187,6 +182,7 @@ export async function executeParallel(options: {
|
|
|
187
182
|
tasks: Array<{ agent: string | undefined; agentConfig: AgentConfig | undefined; task: string; model: string | undefined; activeModel: string | undefined; cwd: string | undefined; toolCallId?: string }>;
|
|
188
183
|
signal: AbortSignal | undefined;
|
|
189
184
|
onUpdate: ((progress: SubagentProgress[]) => void) | undefined;
|
|
185
|
+
events: EventBus;
|
|
190
186
|
}): Promise<SubagentResult[]> {
|
|
191
187
|
// Pre-fill one pending slot per task, keyed by task index (NOT agent name).
|
|
192
188
|
// This keeps all N lines stacked from t=0 in stable task order, with no
|
|
@@ -218,6 +214,7 @@ export async function executeParallel(options: {
|
|
|
218
214
|
executeSubagent({
|
|
219
215
|
...taskDef,
|
|
220
216
|
signal: options.signal,
|
|
217
|
+
events: options.events,
|
|
221
218
|
onUpdate: (progress: SubagentProgress) => {
|
|
222
219
|
// Store latest progress in this task's stable slot (by index).
|
|
223
220
|
latestProgress[index] = progress;
|