pi-better-subagents 0.4.2 → 0.5.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 +1 -1
- package/callback-fields.ts +92 -0
- package/child-incidents.ts +149 -0
- package/docs/failure-observations.md +50 -3
- package/failures.ts +42 -53
- package/finalization.ts +16 -25
- package/incident-model.ts +320 -0
- package/index.ts +31 -26
- package/lifecycle.ts +9 -8
- package/list.mjs +49 -26
- package/list.ts +1 -0
- package/output-payload.ts +842 -0
- package/package.json +3 -3
- package/parse.ts +14 -5
- package/registry.ts +86 -3
- package/shared-callback-batcher.ts +327 -29
- package/shared-failure-observations.ts +501 -22
- package/shared-log-utils.ts +1025 -2
- package/shared-task-sandbox.ts +3 -1
- package/task-guard.ts +15 -3
- package/tools.ts +144 -79
package/shared-task-sandbox.ts
CHANGED
|
@@ -160,6 +160,8 @@ export function installTaskTools(pi: ExtensionAPI, options: {
|
|
|
160
160
|
shellPath?: () => string | undefined;
|
|
161
161
|
trustedSources: readonly string[];
|
|
162
162
|
admitExtensionTool?: (name: string, input: unknown, sourcePath: string | undefined) => boolean;
|
|
163
|
+
/** Build the admitted bash definition from the confined operations (default: the SDK bash tool). */
|
|
164
|
+
bashDefinition?: (cwd: string, operations: BashOperations) => ReturnType<typeof createBashToolDefinition>;
|
|
163
165
|
}) {
|
|
164
166
|
const { controller } = options;
|
|
165
167
|
const sourceKey = (path: string) => path.startsWith("<") ? path : canonicalizePath(path);
|
|
@@ -180,7 +182,7 @@ export function installTaskTools(pi: ExtensionAPI, options: {
|
|
|
180
182
|
pi.registerTool(own(createReadToolDefinition(cwd, { operations: files.read })));
|
|
181
183
|
pi.registerTool(own(createWriteToolDefinition(cwd, { operations: files.write })));
|
|
182
184
|
pi.registerTool(own(createEditToolDefinition(cwd, { operations: files.edit })));
|
|
183
|
-
pi.registerTool(own(createBashToolDefinition(cwd, { operations: bash })));
|
|
185
|
+
pi.registerTool(own(options.bashDefinition ? options.bashDefinition(cwd, bash) : createBashToolDefinition(cwd, { operations: bash })));
|
|
184
186
|
};
|
|
185
187
|
register(options.cwd);
|
|
186
188
|
pi.on("user_bash", () => ({ operations: bash }));
|
package/task-guard.ts
CHANGED
|
@@ -4,6 +4,10 @@ import { describeSandboxSupport } from "./shared-sandbox-core.ts";
|
|
|
4
4
|
import { installTaskTools, TASK_BUILTINS } from "./shared-task-sandbox.ts";
|
|
5
5
|
import { parseTaskPolicy } from "./task-policy.ts";
|
|
6
6
|
import { SANDBOX_POLICY_CHANNEL, SANDBOX_POLICY_REQUEST_CHANNEL } from "./permission-policy.ts";
|
|
7
|
+
import { failureDispositionTool, intentBashDefinition } from "./child-incidents.ts";
|
|
8
|
+
import { DISPOSITION_TOOL } from "./incident-model.ts";
|
|
9
|
+
|
|
10
|
+
const TRUSTED_INLINE_SOURCE = "<inline:task-sandbox>";
|
|
7
11
|
|
|
8
12
|
export default function taskGuard(pi: ExtensionAPI, input: unknown, fatal: (error: unknown) => never): void {
|
|
9
13
|
const policy = parseTaskPolicy(input);
|
|
@@ -16,9 +20,15 @@ export default function taskGuard(pi: ExtensionAPI, input: unknown, fatal: (erro
|
|
|
16
20
|
});
|
|
17
21
|
const controller = Object.freeze({ requireLaunchPlan: () => plan });
|
|
18
22
|
let shellPath: string | undefined;
|
|
23
|
+
let currentCwd = process.cwd();
|
|
19
24
|
const boundary = installTaskTools(pi, { controller, cwd: process.cwd(), shellPath: () => shellPath,
|
|
20
|
-
trustedSources: [
|
|
25
|
+
trustedSources: [TRUSTED_INLINE_SOURCE],
|
|
26
|
+
// Structured command intent (#315): validated before the confined command runs.
|
|
27
|
+
bashDefinition: (cwd, operations) => intentBashDefinition(cwd, operations) as any,
|
|
28
|
+
// The disposition tool performs no file or command I/O; only this inline registration is admitted.
|
|
29
|
+
admitExtensionTool: (name, _input, sourcePath) => name === DISPOSITION_TOOL && sourcePath === TRUSTED_INLINE_SOURCE,
|
|
21
30
|
});
|
|
31
|
+
pi.registerTool(failureDispositionTool(() => currentCwd) as any);
|
|
22
32
|
const profile = Object.freeze({ enabled: true, ...policy.permissions });
|
|
23
33
|
const status = Object.freeze({
|
|
24
34
|
state: "enabled", projectRoot: policy.root, writableRoot: policy.root,
|
|
@@ -31,12 +41,14 @@ export default function taskGuard(pi: ExtensionAPI, input: unknown, fatal: (erro
|
|
|
31
41
|
pi.on("session_start", (_event, ctx) => {
|
|
32
42
|
try {
|
|
33
43
|
shellPath = SettingsManager.create(ctx.cwd, policy.agentDir, { projectTrusted: false }).getShellPath();
|
|
44
|
+
currentCwd = ctx.cwd;
|
|
34
45
|
boundary.register(ctx.cwd);
|
|
35
46
|
const selected = policy.tools.filter((name) => (TASK_BUILTINS as readonly string[]).includes(name));
|
|
36
47
|
boundary.assertInstalled(selected);
|
|
37
|
-
|
|
48
|
+
const activated = selected.length ? [...selected, DISPOSITION_TOOL] : selected;
|
|
49
|
+
pi.setActiveTools(activated);
|
|
38
50
|
const active = new Set(pi.getActiveTools());
|
|
39
|
-
if (active.size !==
|
|
51
|
+
if (active.size !== activated.length || activated.some((name) => !active.has(name))) {
|
|
40
52
|
throw new Error("SDK did not activate the guarded tool set; refusing to start the task.");
|
|
41
53
|
}
|
|
42
54
|
pi.events.emit(SANDBOX_POLICY_CHANNEL, status);
|
package/tools.ts
CHANGED
|
@@ -15,19 +15,33 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
18
|
-
import { readMeta,
|
|
19
|
-
import {
|
|
20
|
-
import { buildSubagentResultText } from "./finalization.ts";
|
|
21
|
-
import { failureSummary, prependFailureSummary } from "./failures.ts";
|
|
22
|
-
import { formatOrphanedResult } from "./lifecycle.ts";
|
|
18
|
+
import { readMeta, listRunRecords, effectiveStatus, isFinalResultStatus, type RunMeta, type RunStatus } from "./registry.ts";
|
|
19
|
+
import { buildSubagentResultPayload } from "./finalization.ts";
|
|
23
20
|
import { stopRun } from "./stop.ts";
|
|
24
|
-
import { fmtElapsed, fmtSpend } from "./widget.ts";
|
|
25
21
|
import {
|
|
26
22
|
SUBAGENT_LIST_DEFAULT_LIMIT,
|
|
27
23
|
SUBAGENT_LIST_MAX_LIMIT,
|
|
28
24
|
SUBAGENT_LIST_STATUSES,
|
|
29
|
-
|
|
25
|
+
collectSubagentList,
|
|
26
|
+
formatSubagentListRow,
|
|
30
27
|
} from "./list.ts";
|
|
28
|
+
import {
|
|
29
|
+
assembleOrphanedResult,
|
|
30
|
+
assembleRunningResult,
|
|
31
|
+
assembleSubagentListPayload,
|
|
32
|
+
assembleSubagentOutput,
|
|
33
|
+
assembleUnreadableMetadata,
|
|
34
|
+
listIncidentCount,
|
|
35
|
+
formatIncidentLabel,
|
|
36
|
+
listRevisions,
|
|
37
|
+
listScopeKey,
|
|
38
|
+
requestScopeKey,
|
|
39
|
+
resolveActiveOrigin,
|
|
40
|
+
resolveRunAccess,
|
|
41
|
+
runInListScope,
|
|
42
|
+
unknownRunError,
|
|
43
|
+
type SubagentToolSession,
|
|
44
|
+
} from "./output-payload.ts";
|
|
31
45
|
import {
|
|
32
46
|
extractChildEventFactsFromLog,
|
|
33
47
|
loadHealthThresholdsFromConfig,
|
|
@@ -35,7 +49,6 @@ import {
|
|
|
35
49
|
type HealthObservation,
|
|
36
50
|
} from "./health-observation.ts";
|
|
37
51
|
import {
|
|
38
|
-
appendHealthDiagnostic,
|
|
39
52
|
formatHealthDiagnosticLine,
|
|
40
53
|
statusThemeColor,
|
|
41
54
|
truncateToVisibleWidth,
|
|
@@ -222,25 +235,49 @@ function displayThemeColor(status: string): string {
|
|
|
222
235
|
type ToolDefinition = Parameters<ExtensionAPI["registerTool"]>[0];
|
|
223
236
|
|
|
224
237
|
// ---- subagent_list --------------------------------------------------------
|
|
225
|
-
export
|
|
238
|
+
export type { SubagentToolSession };
|
|
239
|
+
|
|
240
|
+
/** The session provider bound to this tool call's host context. */
|
|
241
|
+
function forCall(session: SubagentToolSession, ctx: unknown): SubagentToolSession {
|
|
242
|
+
const provider = session.getActiveOrigin;
|
|
243
|
+
if (typeof provider !== "function") return session;
|
|
244
|
+
return { getActiveOrigin: () => provider(ctx) };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function subagentListTool(Type: TypeModule, baseSession: SubagentToolSession = {}): ToolDefinition {
|
|
226
248
|
return {
|
|
227
249
|
name: "subagent_list",
|
|
228
250
|
label: "List Subagents",
|
|
229
251
|
description:
|
|
230
|
-
`List background subagent runs with status
|
|
231
|
-
`Default:
|
|
232
|
-
`Pass all:true for machine-global; limit is clamped to max ${SUBAGENT_LIST_MAX_LIMIT}
|
|
252
|
+
`List background subagent runs with compact status rows. Non-blocking. ` +
|
|
253
|
+
`Default: current session, newest first, limit ${SUBAGENT_LIST_DEFAULT_LIMIT} (1 KiB page). ` +
|
|
254
|
+
`Pass all:true for machine-global; limit is clamped to max ${SUBAGENT_LIST_MAX_LIMIT}. ` +
|
|
255
|
+
`Pass cursor to continue. Incident counts are compact; full failure text lives on subagent_result/output.`,
|
|
233
256
|
promptSnippet: "List background subagent runs and their status",
|
|
234
257
|
parameters: Type.Object({
|
|
235
|
-
all: Type.Optional(Type.Boolean({ description: "If true, list every run on this machine. Default false =
|
|
258
|
+
all: Type.Optional(Type.Boolean({ description: "If true, list every run on this machine. Default false = current session only." })),
|
|
236
259
|
limit: Type.Optional(Type.Number({ description: `Maximum rows to display (default ${SUBAGENT_LIST_DEFAULT_LIMIT}, max ${SUBAGENT_LIST_MAX_LIMIT}; larger values are clamped).` })),
|
|
237
260
|
status: Type.Optional(Type.Array(Type.String(), { description: `Effective statuses to include: ${SUBAGENT_LIST_STATUSES.join(", ")}.` })),
|
|
261
|
+
cursor: Type.Optional(Type.String({ description: "Caller-owned page or status cursor from a previous list response." })),
|
|
262
|
+
maxBytes: Type.Optional(Type.Number({ description: "UTF-8 byte budget for this page (default 1 KiB, max 4 KiB)." })),
|
|
238
263
|
}),
|
|
239
|
-
async execute(_toolCallId: string, params: unknown) {
|
|
240
|
-
const
|
|
264
|
+
async execute(_toolCallId: string, params: unknown, _signal?: unknown, _onUpdate?: unknown, ctx?: unknown) {
|
|
265
|
+
const session = forCall(baseSession, ctx);
|
|
266
|
+
const p = (params ?? {}) as {
|
|
267
|
+
all?: boolean; limit?: number; status?: string[] | string; cursor?: string; maxBytes?: number;
|
|
268
|
+
};
|
|
241
269
|
const now = Date.now();
|
|
242
|
-
const
|
|
243
|
-
|
|
270
|
+
const index = listRunRecords();
|
|
271
|
+
const metas = index.metas;
|
|
272
|
+
const parentPid = process.pid;
|
|
273
|
+
const resolved = resolveActiveOrigin(session);
|
|
274
|
+
const origin = resolved.origin;
|
|
275
|
+
const sessionAvailable = !(resolved.wired && !resolved.available);
|
|
276
|
+
const scopeKey = listScopeKey(p.all === true, origin, parentPid, sessionAvailable);
|
|
277
|
+
let sessionWarning: string | undefined;
|
|
278
|
+
if (p.all !== true && !sessionAvailable) {
|
|
279
|
+
sessionWarning = `Session identity unavailable; ownership of ${metas.length} run(s) cannot be verified, so none are listed. Pass all:true for machine-global.`;
|
|
280
|
+
}
|
|
244
281
|
const healthCache = new Map<string, HealthObservation>();
|
|
245
282
|
const healthById = (id: string) => {
|
|
246
283
|
const hit = healthCache.get(id);
|
|
@@ -251,117 +288,145 @@ export function subagentListTool(Type: TypeModule): ToolDefinition {
|
|
|
251
288
|
healthCache.set(id, obs);
|
|
252
289
|
return obs;
|
|
253
290
|
};
|
|
254
|
-
|
|
291
|
+
const inScope = (meta: RunMeta) => runInListScope(
|
|
292
|
+
meta,
|
|
293
|
+
{ all: p.all === true },
|
|
294
|
+
origin,
|
|
295
|
+
parentPid,
|
|
296
|
+
sessionAvailable,
|
|
297
|
+
);
|
|
298
|
+
const collected = collectSubagentList({
|
|
255
299
|
metas,
|
|
256
300
|
params: p,
|
|
257
|
-
parentPid
|
|
301
|
+
parentPid,
|
|
258
302
|
now,
|
|
259
303
|
statusOf: effectiveStatus,
|
|
260
|
-
usageById: (id: string) => parseRun(id).usage,
|
|
261
304
|
healthById,
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
305
|
+
inScope,
|
|
306
|
+
});
|
|
307
|
+
const items = collected.items as Array<{ meta: RunMeta; status: string }>;
|
|
308
|
+
const incidents = new Map(items.map((row) => [row.meta.id, listIncidentCount(
|
|
309
|
+
row.meta.id,
|
|
310
|
+
row.meta.cwd,
|
|
311
|
+
row.meta.status !== "running" && row.meta.status !== "orphaned",
|
|
312
|
+
)] as const));
|
|
313
|
+
const revisions = listRevisions(
|
|
314
|
+
items.map((row) => ({ id: row.meta.id, status: String(row.status), meta: row.meta })),
|
|
315
|
+
items.map((row) => incidents.get(row.meta.id)?.revision ?? ""),
|
|
316
|
+
);
|
|
317
|
+
const statusesKey = Array.isArray(p.status) ? [...p.status].map(String).sort().join(",") : String(p.status ?? "");
|
|
318
|
+
const incidentRuns = items.filter((row) => (incidents.get(row.meta.id)?.count ?? 0) > 0).length;
|
|
319
|
+
return text(assembleSubagentListPayload({
|
|
320
|
+
warnings: collected.warnings,
|
|
321
|
+
items,
|
|
322
|
+
render: (row) => {
|
|
323
|
+
const incident = incidents.get(row.meta.id);
|
|
324
|
+
return formatSubagentListRow(row.meta, {
|
|
325
|
+
status: row.status,
|
|
326
|
+
now,
|
|
327
|
+
health: healthById(row.meta.id),
|
|
328
|
+
failure: formatIncidentLabel(incident?.count ?? 0, incident?.actionRequired ?? 0),
|
|
329
|
+
});
|
|
265
330
|
},
|
|
331
|
+
limit: Math.max(1, collected.limit),
|
|
332
|
+
cursor: p.cursor,
|
|
333
|
+
maxBytes: p.maxBytes,
|
|
334
|
+
contentRevision: revisions.contentRevision,
|
|
335
|
+
failureRevision: revisions.failureRevision,
|
|
336
|
+
scopeKey,
|
|
337
|
+
statusesKey,
|
|
338
|
+
sessionWarning,
|
|
339
|
+
unreadable: index.unreadable,
|
|
340
|
+
indexError: index.indexError,
|
|
341
|
+
incidentRuns,
|
|
266
342
|
}));
|
|
267
343
|
},
|
|
268
344
|
} as ToolDefinition;
|
|
269
345
|
}
|
|
270
346
|
|
|
271
347
|
// ---- subagent_output ------------------------------------------------------
|
|
272
|
-
export function subagentOutputTool(Type: TypeModule): ToolDefinition {
|
|
348
|
+
export function subagentOutputTool(Type: TypeModule, baseSession: SubagentToolSession = {}): ToolDefinition {
|
|
273
349
|
return {
|
|
274
350
|
name: "subagent_output",
|
|
275
351
|
label: "Subagent Output",
|
|
276
352
|
description:
|
|
277
|
-
"
|
|
278
|
-
"
|
|
353
|
+
"Read a subagent run's current output without waiting. Default is a 1 KiB / 10-line excerpt of the current session. " +
|
|
354
|
+
"Pass all:true to read a foreign-session id. Pass mode=raw for retained-log pages (16 KiB default, 64 KiB cap). " +
|
|
355
|
+
"Pass cursor to continue or to poll for failure-only changes. Missing or unreadable logs are reported as gaps, not as empty healthy output.",
|
|
279
356
|
promptSnippet: "Peek at a subagent's current output without waiting",
|
|
280
357
|
promptGuidelines: [
|
|
281
358
|
"Use subagent_output only when the user explicitly asks how a run is progressing. It never waits — do not call it in a loop.",
|
|
359
|
+
"Do not poll unchanged output. If the response says no new evidence since a cursor, stop.",
|
|
282
360
|
],
|
|
283
361
|
parameters: Type.Object({
|
|
284
362
|
id: Type.String({ description: "Run id from subagent_spawn." }),
|
|
285
|
-
tail_lines: Type.Optional(Type.Number({ description: "
|
|
363
|
+
tail_lines: Type.Optional(Type.Number({ description: "Deprecated alias for lines." })),
|
|
364
|
+
lines: Type.Optional(Type.Number({ description: "Max lines in the default excerpt (default 10)." })),
|
|
365
|
+
cursor: Type.Optional(Type.String({ description: "Caller-owned page or status cursor from a previous response." })),
|
|
366
|
+
maxBytes: Type.Optional(Type.Number({ description: "UTF-8 byte budget for this page (default 1 KiB, max 4 KiB; raw default 16 KiB, max 64 KiB)." })),
|
|
367
|
+
mode: Type.Optional(Type.String({ description: "raw = page retained log bytes. Default is the bounded assembled excerpt." })),
|
|
368
|
+
all: Type.Optional(Type.Boolean({ description: "If true, allow a foreign-session id. Default is current session only." })),
|
|
286
369
|
}),
|
|
287
|
-
async execute(_id: string, params: unknown) {
|
|
288
|
-
const
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
const
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
const raw = tailLog(p.id, p.tail_lines ?? 40);
|
|
298
|
-
const body = formatSubagentOutputBody(
|
|
299
|
-
head,
|
|
300
|
-
tools,
|
|
301
|
-
r.finalText || r.lastActivity || undefined,
|
|
302
|
-
raw,
|
|
303
|
-
r.diagnostics,
|
|
304
|
-
);
|
|
305
|
-
// Health diagnostics for orphaned/lost/degraded only (#67). Healthy/quiet
|
|
306
|
-
// stays on today's body. Independent of meta.callback.
|
|
307
|
-
const healthLine = formatHealthDiagnosticLine(observeMetaHealth(meta));
|
|
308
|
-
return text(prependFailureSummary(appendHealthDiagnostic(body, healthLine), failureSummary(p.id, meta.cwd, st !== "running" && st !== "orphaned")));
|
|
370
|
+
async execute(_id: string, params: unknown, _signal?: unknown, _onUpdate?: unknown, ctx?: unknown) {
|
|
371
|
+
const session = forCall(baseSession, ctx);
|
|
372
|
+
const p = params as { id: string; cursor?: string; maxBytes?: number; mode?: string; all?: boolean; lines?: number; tail_lines?: number };
|
|
373
|
+
const access = resolveRunAccess(p.id, p, session);
|
|
374
|
+
if (access.kind === "missing") throw unknownRunError(p.id);
|
|
375
|
+
if (access.kind === "unreadable") return text(assembleUnreadableMetadata(p.id, access.detail, p));
|
|
376
|
+
if (access.kind === "denied") return text(access.payload);
|
|
377
|
+
const scopeKey = requestScopeKey(p, session);
|
|
378
|
+
const healthLine = formatHealthDiagnosticLine(observeMetaHealth(access.meta));
|
|
379
|
+
return text(assembleSubagentOutput(p.id, access.meta, p, healthLine, scopeKey));
|
|
309
380
|
},
|
|
310
381
|
} as ToolDefinition;
|
|
311
382
|
}
|
|
312
383
|
|
|
313
384
|
// ---- subagent_result ------------------------------------------------------
|
|
314
|
-
export function subagentResultTool(Type: TypeModule): ToolDefinition {
|
|
385
|
+
export function subagentResultTool(Type: TypeModule, baseSession: SubagentToolSession = {}): ToolDefinition {
|
|
315
386
|
return {
|
|
316
387
|
name: "subagent_result",
|
|
317
388
|
label: "Subagent Result",
|
|
318
389
|
description:
|
|
319
|
-
"Read a subagent's final output if it has finished. NEVER waits
|
|
320
|
-
"says so and returns immediately.",
|
|
390
|
+
"Read a subagent's final output if it has finished. NEVER waits. Default current session. Ordinary answers are preserved verbatim in a 2 KiB UTF-8 page (max 8 KiB); pass cursor to reconstruct the rest. Pass all:true for a foreign-session id. mode=raw pages retained log bytes (16 KiB default, 64 KiB cap). Failures and exceptional lifecycle facts come before progress. No tool-name histories or default cost lines.",
|
|
321
391
|
promptSnippet: "Read a finished subagent's final result (never waits)",
|
|
322
392
|
promptGuidelines: [
|
|
323
393
|
"Use subagent_result to collect a finished run's output. If it reports the run is still going, stop — do not poll; you'll be notified when it finishes.",
|
|
394
|
+
"If the response includes nextCursor, call again with that cursor to read the rest of the answer. Do not summarize away unread pages.",
|
|
324
395
|
],
|
|
325
396
|
parameters: Type.Object({
|
|
326
397
|
id: Type.String({ description: "Run id from subagent_spawn." }),
|
|
398
|
+
cursor: Type.Optional(Type.String({ description: "Caller-owned answer page or status cursor from a previous response." })),
|
|
399
|
+
maxBytes: Type.Optional(Type.Number({ description: "UTF-8 byte budget for this page (default 2 KiB, max 8 KiB)." })),
|
|
400
|
+
mode: Type.Optional(Type.String({ description: "raw = page retained log bytes (16 KiB default, 64 KiB cap). Default is the bounded assembled result." })),
|
|
401
|
+
all: Type.Optional(Type.Boolean({ description: "If true, allow a foreign-session id. Default is current session only." })),
|
|
327
402
|
}),
|
|
328
403
|
renderResult(result: unknown, options: unknown, theme: unknown) {
|
|
329
404
|
return renderSubagentResultDisplay(result, options, theme);
|
|
330
405
|
},
|
|
331
|
-
async execute(_id: string, params: unknown) {
|
|
332
|
-
const
|
|
333
|
-
const
|
|
334
|
-
|
|
406
|
+
async execute(_id: string, params: unknown, _signal?: unknown, _onUpdate?: unknown, ctx?: unknown) {
|
|
407
|
+
const session = forCall(baseSession, ctx);
|
|
408
|
+
const p = params as { id: string; cursor?: string; maxBytes?: number; mode?: string; all?: boolean };
|
|
409
|
+
const access = resolveRunAccess(p.id, p, session);
|
|
410
|
+
if (access.kind === "missing") throw unknownRunError(p.id);
|
|
411
|
+
if (access.kind === "unreadable") {
|
|
412
|
+
return subagentResultText(assembleUnreadableMetadata(p.id, access.detail, p));
|
|
413
|
+
}
|
|
414
|
+
if (access.kind === "denied") return subagentResultText(access.payload);
|
|
415
|
+
const meta = access.meta;
|
|
416
|
+
const scopeKey = requestScopeKey(p, session);
|
|
335
417
|
const st = effectiveStatus(meta);
|
|
418
|
+
const healthLine = formatHealthDiagnosticLine(observeMetaHealth(meta));
|
|
336
419
|
if (!isFinalResultStatus(st)) {
|
|
337
420
|
if (st === "orphaned") {
|
|
338
|
-
|
|
339
|
-
// group work may still be alive — never present this as a
|
|
340
|
-
// final result. Surface best-CURRENT artifacts (#65) plus
|
|
341
|
-
// health diagnostic (#67).
|
|
342
|
-
const r = parseRun(p.id);
|
|
343
|
-
const el = fmtElapsed((meta.endedAt ?? Date.now()) - meta.startedAt);
|
|
344
|
-
const spend = fmtSpend(r.usage);
|
|
345
|
-
const tools = r.toolCalls.length ? ` · tools: ${r.toolCalls.join(", ")}` : "";
|
|
346
|
-
const head = `[${p.id} · orphaned · ${el}${spend ? ` · ${spend}` : ""}${tools}]`;
|
|
347
|
-
const rawTail = tailLog(p.id, 40);
|
|
348
|
-
const body = `${head}\n${formatOrphanedResult(r, rawTail)}`;
|
|
349
|
-
const healthLine = formatHealthDiagnosticLine(observeMetaHealth(meta));
|
|
350
|
-
return subagentResultText(prependFailureSummary(appendHealthDiagnostic(body, healthLine), failureSummary(p.id, meta.cwd)));
|
|
421
|
+
return subagentResultText(assembleOrphanedResult(p.id, meta, healthLine, p, scopeKey));
|
|
351
422
|
}
|
|
352
|
-
return subagentResultText(
|
|
423
|
+
return subagentResultText(assembleRunningResult(p.id, meta, p, scopeKey));
|
|
353
424
|
}
|
|
354
|
-
|
|
355
|
-
// Lost runs go through formatLostResult inside formatSubagentResult (#65).
|
|
356
|
-
const body = buildSubagentResultText(p.id);
|
|
425
|
+
const body = buildSubagentResultPayload(p.id, p, healthLine, scopeKey);
|
|
357
426
|
if (body === null) {
|
|
358
|
-
|
|
359
|
-
return subagentResultText(`Run ${p.id} is still running — no result yet. You'll be notified when it finishes; don't poll.`);
|
|
427
|
+
return subagentResultText(assembleRunningResult(p.id, meta, p, scopeKey));
|
|
360
428
|
}
|
|
361
|
-
|
|
362
|
-
// happy paths stay quiet when observation is non-actionable.
|
|
363
|
-
const healthLine = formatHealthDiagnosticLine(observeMetaHealth(meta));
|
|
364
|
-
return subagentResultText(prependFailureSummary(appendHealthDiagnostic(body, healthLine), failureSummary(p.id, meta.cwd, true)));
|
|
429
|
+
return subagentResultText(body);
|
|
365
430
|
},
|
|
366
431
|
} as ToolDefinition;
|
|
367
432
|
}
|