pi-pier 0.3.1 → 0.4.2
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 +7 -5
- package/package.json +3 -3
- package/schemas/efficiency-config.schema.json +6 -0
- package/src/ask-dialog.ts +9 -2
- package/src/ask-user.ts +142 -74
- package/src/bootstrap.ts +2 -1
- package/src/compact-coordinator.ts +207 -35
- package/src/compact-economics-core.ts +62 -29
- package/src/config-catalog-core.ts +1 -0
- package/src/cordis-services.ts +64 -0
- package/src/dashboard-command.ts +40 -15
- package/src/delegation-core.ts +115 -0
- package/src/efficiency-config-core.ts +17 -6
- package/src/efficiency-store.ts +43 -6
- package/src/guards.ts +6 -0
- package/src/herdr-client.ts +141 -36
- package/src/history-store.ts +2 -0
- package/src/index-gates.ts +6 -12
- package/src/index-master.ts +12 -16
- package/src/index-pipe.ts +6 -0
- package/src/index-runtime.ts +14 -0
- package/src/index-worker.ts +4 -10
- package/src/index.ts +310 -76
- package/src/jev-client.ts +32 -7
- package/src/jev-core.ts +71 -36
- package/src/observation-core.ts +13 -1
- package/src/panel-open.ts +47 -0
- package/src/pier-options.ts +1 -2
- package/src/pipe-channel.ts +9 -0
- package/src/plugins/heat-plan.ts +18 -3
- package/src/plugins/observation.ts +118 -46
- package/src/plugins/subagent.ts +119 -68
- package/src/plugins/terminal.ts +221 -57
- package/src/plugins/todo.ts +63 -63
- package/src/reducer-core.ts +3 -5
- package/src/reducer-invoker.ts +27 -13
- package/src/renderers.ts +25 -2
- package/src/role-manifest.ts +2 -3
- package/src/roles/worker-default.json +2 -2
- package/src/settle-wake-core.ts +17 -3
- package/src/subagent-core.ts +209 -3
- package/src/subagent-poller.ts +69 -19
- package/src/subagent-session.ts +67 -4
- package/src/subagent-spawn.ts +55 -7
- package/src/terminal-core.ts +178 -50
- package/src/todo-core.ts +47 -2
- package/src/todo-read-hook.ts +54 -17
- package/src/todo-reminder-core.ts +9 -5
- package/src/todo-window.ts +8 -4
- package/src/todos-service.ts +12 -0
- package/src/tree-view.ts +162 -0
- package/src/vocab.ts +7 -1
- package/src/worker-marker.ts +76 -0
- package/src/focus-poller.ts +0 -223
package/src/plugins/subagent.ts
CHANGED
|
@@ -7,12 +7,13 @@
|
|
|
7
7
|
import { Context } from '@deepseek-ai/cordis';
|
|
8
8
|
import { Type } from 'typebox';
|
|
9
9
|
import { basename, dirname, join } from 'node:path';
|
|
10
|
-
import
|
|
10
|
+
import { cordisGet } from '../cordis-services.ts';
|
|
11
11
|
import type { HerdrClientLike } from '../herdr-client.ts';
|
|
12
12
|
import {
|
|
13
13
|
Semaphore,
|
|
14
|
+
SUBAGENT_OUTPUT_SCHEMA,
|
|
14
15
|
agoText,
|
|
15
|
-
|
|
16
|
+
attachSubagentStructuredContent,
|
|
16
17
|
buildAliveNotice,
|
|
17
18
|
idParam,
|
|
18
19
|
flattenToolParams,
|
|
@@ -20,7 +21,9 @@ import {
|
|
|
20
21
|
newestPerTaskId,
|
|
21
22
|
rekeySub,
|
|
22
23
|
resolveTaskIdPrefix,
|
|
24
|
+
taskIdResolutionError,
|
|
23
25
|
tabNameForTask,
|
|
26
|
+
subRef,
|
|
24
27
|
type AliveProbe,
|
|
25
28
|
type SubEntry,
|
|
26
29
|
type SubagentPort,
|
|
@@ -41,8 +44,9 @@ import { toolError, ToolError } from '../tool-error.ts';
|
|
|
41
44
|
import { pipeNameFor, pipeRequestTo, type PipeRequest, type PipeResponse } from '../pipe-channel.ts';
|
|
42
45
|
import type { TerminalStateSlot } from './terminal.ts';
|
|
43
46
|
import { bareSessionId, resolveSessionFileValue } from '../session-tail.ts';
|
|
44
|
-
import {
|
|
47
|
+
import { DELEGATION_ENV, type DelegationContext } from '../delegation-core.ts';
|
|
45
48
|
import type { JevRuntime } from '../jev-client.ts';
|
|
49
|
+
import { createGitIo, createSpawnAction, createSpawner } from '../subagent-spawn.ts';
|
|
46
50
|
import { createPoller } from '../subagent-poller.ts';
|
|
47
51
|
import { createGcController, createSubagentRegistry } from '../subagent-gc.ts';
|
|
48
52
|
import { createSessionIo, executeSubagentOutput, type SubagentOutputCursor } from '../subagent-session.ts';
|
|
@@ -80,24 +84,35 @@ export interface SubagentDeps {
|
|
|
80
84
|
logRouting?: (row: RoutingTelemetryRecord) => void;
|
|
81
85
|
}
|
|
82
86
|
|
|
87
|
+
/** Sent with every request: what a call needs. Details live in SUBAGENT_MANUAL (`action: "help"`). */
|
|
83
88
|
const SUBAGENT_DESCRIPTION = [
|
|
84
|
-
'Delegate a self-contained
|
|
85
|
-
'
|
|
86
|
-
'
|
|
87
|
-
'
|
|
88
|
-
'
|
|
89
|
-
'
|
|
90
|
-
'[spawn] `tab` (optional): name of a task tab to place the subagent into (join if a tab with this name exists, otherwise create it). Overrides the default placement. Default placement groups by git worktree: a subagent working in your checkout shares your tab; one working in a separate worktree (pass its path via `cwd`; create worktrees with `git worktree add`) gets its own tab named after the worktree directory.',
|
|
91
|
-
'[spawn] `role` + `allowed_tools`: when role matches a profile (searched: workspace .pi-herdr/roles/ → user-global ~/.pi/agent/herdr-pi/roles/ → builtin), the worker toolset becomes the composed manifest — baseline ∪ allowed_tools minus role-deny tools (deny always wins). Custom roles: drop a JSON profile into .pi-herdr/roles/ (master/worker-default reserved). Unknown role names remain display labels only.',
|
|
92
|
-
'[spawn] `isolate` (default false): creates a FRESH git worktree for this subagent and runs it there (branch pier/<slug> from your HEAD under ~/.herdr/worktrees/<repo>/). Three-way choice: heavy independent writing in parallel with your own edits or other workers, or work needing its own clean reviewable diff → isolate; read-mostly or sequential helper work → omit (shared checkout, writes guarded by the write-lock); targeting an existing directory/worktree → cwd. In isolate mode the subagent\'s writes cannot conflict with your checkout; its panes group into a tab named after the worktree; its prompt is prefixed with commit discipline (commit to its own branch, NEVER push); when it settles you get a diff summary (commits since base, files changed, uncommitted count). Review with git log/diff HEAD..<branch>, merge with git merge --no-ff <branch>; once merged and clean the worktree auto-removes (branch kept). Mutually exclusive with cwd.',
|
|
93
|
-
'[resume] `taskId`: revive a finished (collected) subagent from the delegation ledger — opens its saved conversation in a new pane (pi --session), then use action send to give it new work. The ledger is an append-only JSONL file, one row per status change (same taskId rows = generations, latest row is current): fields taskId, description, status (running|settled|consumed|closed), outcome (closing text), paneId, sessionFile, launchCommand, createdAt. Ledgers are per-checkout at ~/.pi/agent/herdr-pi/history/<flattened-cwd>/history.jsonl, but resume searches every checkout this master has spawned into, not only the current cwd. A still-running task is not resumed — use action send or output. Use action list for live panes.',
|
|
94
|
-
'[list] no extra parameters: list background subagents with live state (running / idle), pane ids, last activity, role, and descriptions. Foreground one-shot panes are not listed.',
|
|
95
|
-
'[output] `agentId` (required, the subagent pane id): view incremental output of a running or background subagent since the last output call. Text returned to the model is bounded (default 6000 chars) with status metadata (running/idle/blocked/settled), revision, and a truncated flag. When the delta cannot be computed the full text is returned with a "buffer scrolled or reset" note — that is normal for fullscreen TUI panes, not a crash indicator. Use this to observe subagent progress before settlement.',
|
|
96
|
-
'[interrupt] `agentId`: abort the current turn (fire-and-return). The pane stays; you can send again.',
|
|
97
|
-
'[role] `agentId` + `role`: switch that worker\'s role profile mid-session (pi 0.86 transcript tool delta — the new toolset applies on its next request and survives resume). Same role resolution as spawn; the reply reports the tool diff. Use when a task outgrew its delegation (needs more tools) or should be narrowed.',
|
|
98
|
-
'[send] `agentId` + `message`: follow-up to a background subagent. If working, delivered at next tool-call gap (steer, seconds); if idle, wakes a new turn. After settle, send to wake it; do not spawn a duplicate.',
|
|
89
|
+
'Delegate a self-contained task to a subagent: an isolated pi session in its own herdr pane (separate context — it does NOT see this conversation; a human can talk to it in that pane).',
|
|
90
|
+
'Actions (default spawn): spawn | send | output | list | interrupt | resume | role | help.',
|
|
91
|
+
'spawn: `prompt` is the COMPLETE task, since only the prompt reaches it; `description` labels the pane and is the todo-reconcile key — when delegating a todo entry, pass its content without the ` <sub>` marker and the entry auto-completes on settle. Several spawns in one message run in parallel (max 4).',
|
|
92
|
+
'A foreground spawn BLOCKS this call for up to ~5 minutes; pass run_in_background: true for long tasks. A background (or timed-out foreground) worker\'s settlement notice arrives automatically at your next tool boundary — no need to sleep or poll.',
|
|
93
|
+
'send: follow-up to a running or settled worker (steered between its tool calls). output: what it printed since your last check, plus its final report once done. resume: revive a collected task by taskId. role: switch a worker\'s role profile mid-session.',
|
|
94
|
+
'help: the full manual (isolate/cwd/tab placement, role profiles, the delegation ledger).',
|
|
99
95
|
].join(' ');
|
|
100
96
|
|
|
97
|
+
/** Returned by `action: "help"`: the reference text that used to ride every request. */
|
|
98
|
+
const SUBAGENT_MANUAL = [
|
|
99
|
+
'subagent manual',
|
|
100
|
+
'',
|
|
101
|
+
'spawn — the subagent shares this workspace and works independently; its result is its final text answer. Do not delegate the current step itself. Placement: by default panes group by git worktree — a subagent in your checkout shares your tab, one in a separate worktree gets its own tab named after the worktree directory. `tab` joins or creates a named task tab instead. `cwd` targets an existing directory or worktree (create worktrees with git worktree add).',
|
|
102
|
+
'',
|
|
103
|
+
'isolate (default false) — creates a FRESH git worktree (branch pier/<slug> from your HEAD under ~/.herdr/worktrees/<repo>/). Choose it for heavy independent writing in parallel with your own edits or other workers, or work needing its own clean reviewable diff; omit it for read-mostly or sequential helper work (shared checkout, writes guarded by the write-lock); use cwd for an existing directory/worktree. Its writes cannot conflict with your checkout, its prompt is prefixed with commit discipline (commit to its own branch, never push), and on settle you get a diff summary (commits since base, files changed, uncommitted count). Review with git log/diff HEAD..<branch>, merge with git merge --no-ff <branch>; once merged and clean the worktree auto-removes (branch kept). Mutually exclusive with cwd.',
|
|
104
|
+
'',
|
|
105
|
+
'role + allowed_tools — when role matches a profile (searched: workspace .pi-herdr/roles/ → user-global ~/.pi/agent/herdr-pi/roles/ → builtin), the worker toolset becomes baseline ∪ allowed_tools minus role-deny tools (deny always wins). Custom roles: drop a JSON profile into .pi-herdr/roles/ (master/worker-default reserved). Unknown role names stay display labels. action role switches a live worker (pi transcript tool delta: applies on its next request, survives resume); the reply reports the tool diff.',
|
|
106
|
+
'',
|
|
107
|
+
'list — background subagents with live state (running / idle), pane ids, last activity, role and description; foreground one-shot panes are not listed.',
|
|
108
|
+
'',
|
|
109
|
+
'output — `agentId` required; returns the delta since your last output call (default 6000 chars, max_chars 100-16000) with status (running/idle/blocked/settled), revision and a truncated flag. TUI chrome is stripped; once the worker has finished, the complete transcript report is attached. A "buffer scrolled or reset" note means the full text was returned — normal for fullscreen TUI panes.',
|
|
110
|
+
'',
|
|
111
|
+
'send — `agentId` + `message`: if working, delivered at its next tool-call gap (seconds); if idle or settled, wakes a new turn. Do not spawn a duplicate for the same task. interrupt — aborts its current turn; the pane stays and you can send again.',
|
|
112
|
+
'',
|
|
113
|
+
'resume — `taskId`: revives a collected subagent from the delegation ledger in a new pane (pi --session); then use send. The ledger is append-only JSONL, one row per status change (taskId, description, status running|settled|consumed|closed, outcome, paneId, sessionFile, launchCommand, createdAt), per checkout at ~/.pi/agent/herdr-pi/history/<flattened-cwd>/history.jsonl; resume searches every checkout this master spawned into. A still-running task is not resumed — use send or output.',
|
|
114
|
+
].join('\n');
|
|
115
|
+
|
|
101
116
|
const SUBAGENT_CONCURRENCY = 4;
|
|
102
117
|
|
|
103
118
|
function defaultAgentSessionsDir(): string {
|
|
@@ -106,8 +121,8 @@ function defaultAgentSessionsDir(): string {
|
|
|
106
121
|
}
|
|
107
122
|
|
|
108
123
|
export default function subagentPlugin(ctx: Context): void {
|
|
109
|
-
const surface = ctx
|
|
110
|
-
const d = ctx
|
|
124
|
+
const surface = cordisGet(ctx, 'pi-herdr.surface');
|
|
125
|
+
const d = cordisGet(ctx, 'pi-herdr.subagent-deps');
|
|
111
126
|
const { client, env, sessionRoot, port, terminalState } = d;
|
|
112
127
|
const pi = surface.raw as {
|
|
113
128
|
appendEntry?: (customType: string, data: unknown) => void;
|
|
@@ -141,6 +156,7 @@ export default function subagentPlugin(ctx: Context): void {
|
|
|
141
156
|
taskId: e.taskId,
|
|
142
157
|
kind: e.kind,
|
|
143
158
|
paneId: e.paneId,
|
|
159
|
+
...(e.slug ? { slug: e.slug } : {}),
|
|
144
160
|
tabId: e.tabId,
|
|
145
161
|
tabName: e.tabName,
|
|
146
162
|
workspaceId: env?.workspaceId ?? '',
|
|
@@ -187,7 +203,19 @@ export default function subagentPlugin(ctx: Context): void {
|
|
|
187
203
|
listRunningSubs() {
|
|
188
204
|
return [...subs.values()]
|
|
189
205
|
.filter((s) => s.background && s.status === 'running')
|
|
190
|
-
.map((s) => ({ paneId: s.paneId, description: s.description }));
|
|
206
|
+
.map((s) => ({ paneId: s.paneId, slug: s.slug, description: s.description, kind: s.kind }));
|
|
207
|
+
},
|
|
208
|
+
listRows() {
|
|
209
|
+
return [...subs.values()]
|
|
210
|
+
.filter((s) => s.status !== 'closed')
|
|
211
|
+
.map((s) => ({
|
|
212
|
+
paneId: s.paneId,
|
|
213
|
+
slug: s.slug,
|
|
214
|
+
description: s.description,
|
|
215
|
+
kind: s.kind,
|
|
216
|
+
status: s.status,
|
|
217
|
+
settledAt: s.consumedAt ?? null,
|
|
218
|
+
}));
|
|
191
219
|
},
|
|
192
220
|
async settleStatLine(paneId) {
|
|
193
221
|
const entry = subs.get(paneId);
|
|
@@ -207,8 +235,8 @@ export default function subagentPlugin(ctx: Context): void {
|
|
|
207
235
|
if (port.current === boundPort) port.current = null;
|
|
208
236
|
}, 'subagent-port');
|
|
209
237
|
|
|
210
|
-
const injectNotice = (content: string): Promise<void> => {
|
|
211
|
-
if (d.deliverNotice) return d.deliverNotice(content);
|
|
238
|
+
const injectNotice = (content: string, paneId?: string): Promise<void> => {
|
|
239
|
+
if (d.deliverNotice) return d.deliverNotice(content, paneId);
|
|
212
240
|
pi.sendUserMessage?.(content, { deliverAs: 'followUp' });
|
|
213
241
|
return Promise.resolve();
|
|
214
242
|
};
|
|
@@ -331,7 +359,18 @@ export default function subagentPlugin(ctx: Context): void {
|
|
|
331
359
|
const spawned = await spawnPaneInTaskTab(
|
|
332
360
|
{ desiredTab: entry.tabName || latest.tabName || null, description: entry.description },
|
|
333
361
|
entry.cwd,
|
|
334
|
-
{
|
|
362
|
+
{
|
|
363
|
+
PI_HERDR_SUBAGENT: '1',
|
|
364
|
+
[DELEGATION_ENV]: JSON.stringify({
|
|
365
|
+
parentPaneId: env?.paneId ?? '',
|
|
366
|
+
parentSessionId: bareSessionId(d.getSessionId()),
|
|
367
|
+
// todo_sync needs the master's pipe; kind 'task' maps to the composed default.
|
|
368
|
+
parentPipe: pipeNameFor(process.cwd(), env?.paneId ?? ''),
|
|
369
|
+
taskId: entry.taskId,
|
|
370
|
+
description: entry.description,
|
|
371
|
+
role: entry.kind === 'task' ? 'worker-default' : entry.kind,
|
|
372
|
+
} satisfies DelegationContext),
|
|
373
|
+
},
|
|
335
374
|
launchLine(resumeFile, null, approve),
|
|
336
375
|
);
|
|
337
376
|
const ready = await waitSubReady(entry.cwd, spawned.paneId);
|
|
@@ -358,6 +397,7 @@ export default function subagentPlugin(ctx: Context): void {
|
|
|
358
397
|
taskId: h.taskId,
|
|
359
398
|
kind: h.kind,
|
|
360
399
|
paneId: h.paneId,
|
|
400
|
+
...(h.slug ? { slug: h.slug } : {}),
|
|
361
401
|
tabId: h.tabId,
|
|
362
402
|
tabName: h.tabName ?? '',
|
|
363
403
|
cwd: h.cwd,
|
|
@@ -377,12 +417,23 @@ export default function subagentPlugin(ctx: Context): void {
|
|
|
377
417
|
if (!rawId) return { error: 'Error: unknown subagent id "" (see action list)' };
|
|
378
418
|
const direct = subs.get(rawId);
|
|
379
419
|
if (direct) return { entry: direct };
|
|
420
|
+
// Phase 2 slug reference: exact match against live rows (paneId stays first for back-compat).
|
|
421
|
+
// `slug@paneId` accepts both parts; a revived row changed its paneId, so the slug part wins then.
|
|
422
|
+
const at = rawId.indexOf('@');
|
|
423
|
+
const slugPart = at > 0 ? rawId.slice(0, at) : rawId;
|
|
424
|
+
const panePart = at > 0 ? rawId.slice(at + 1) : null;
|
|
425
|
+
const combined = panePart !== null
|
|
426
|
+
? (subs.get(panePart) ?? [...subs.values()].find((s) => s.slug === slugPart && s.paneId === panePart) ?? null)
|
|
427
|
+
: null;
|
|
428
|
+
if (combined) return { entry: combined };
|
|
429
|
+
const bySlug = [...subs.values()].find((s) => s.slug === slugPart);
|
|
430
|
+
if (bySlug) return { entry: bySlug };
|
|
380
431
|
|
|
381
432
|
const byTaskId = newestPerTaskId(subs.values());
|
|
382
433
|
const res = resolveTaskIdPrefix(rawId, byTaskId.keys());
|
|
383
434
|
if (res.kind === 'resolved') return { entry: byTaskId.get(res.taskId)! };
|
|
384
|
-
|
|
385
|
-
if (
|
|
435
|
+
const resError = taskIdResolutionError(res, 'subagent id', rawId);
|
|
436
|
+
if (resError) return { error: resError };
|
|
386
437
|
|
|
387
438
|
if (cwd) {
|
|
388
439
|
const hist = readHistory(histFile(cwd));
|
|
@@ -395,7 +446,8 @@ export default function subagentPlugin(ctx: Context): void {
|
|
|
395
446
|
return { entry };
|
|
396
447
|
}
|
|
397
448
|
}
|
|
398
|
-
|
|
449
|
+
const histError = taskIdResolutionError(histRes, 'subagent id', rawId);
|
|
450
|
+
if (histError) return { error: histError };
|
|
399
451
|
}
|
|
400
452
|
return { error: `Error: unknown subagent id "${rawId}" (see action list)` };
|
|
401
453
|
}
|
|
@@ -415,16 +467,14 @@ export default function subagentPlugin(ctx: Context): void {
|
|
|
415
467
|
persistSubs();
|
|
416
468
|
}
|
|
417
469
|
|
|
418
|
-
async function executeSubagentResume(params: ToolParams
|
|
470
|
+
async function executeSubagentResume(params: ToolParams) {
|
|
419
471
|
if (!client.available) return toolError('requires a herdr-managed pane.');
|
|
420
472
|
const rawTaskId = idParam(params, 'taskId', 'agentId');
|
|
421
473
|
if (!rawTaskId) return toolError('missing taskId for resume (see action list or delegation ledger).');
|
|
422
474
|
const running = newestPerTaskId([...subs.values()].filter((s) => s.background && s.status === 'running'));
|
|
423
475
|
const live = resolveTaskIdPrefix(rawTaskId, running.keys());
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
}
|
|
427
|
-
if (live.kind === 'ambiguous') return toolError(ambiguousIdError('task id', rawTaskId, live.candidates));
|
|
476
|
+
const liveError = taskIdResolutionError(live, 'task id', rawTaskId);
|
|
477
|
+
if (liveError) return toolError(liveError);
|
|
428
478
|
if (live.kind === 'resolved') {
|
|
429
479
|
const entry = running.get(live.taskId)!;
|
|
430
480
|
return {
|
|
@@ -434,12 +484,9 @@ export default function subagentPlugin(ctx: Context): void {
|
|
|
434
484
|
}
|
|
435
485
|
const history = readHistoryTree(dirname(defaultAgentSessionsDir()));
|
|
436
486
|
const resolution = resolveTaskIdPrefix(rawTaskId, history.map((e) => e.taskId));
|
|
437
|
-
if (resolution.kind
|
|
438
|
-
return toolError(
|
|
439
|
-
|
|
440
|
-
if (resolution.kind === 'ambiguous') return toolError(ambiguousIdError('task id', rawTaskId, resolution.candidates));
|
|
441
|
-
if (resolution.kind === 'not_found') {
|
|
442
|
-
return toolError(`Error: no history for task "${rawTaskId}" in this workspace or its spawned checkouts.`);
|
|
487
|
+
if (resolution.kind !== 'resolved') {
|
|
488
|
+
return toolError(taskIdResolutionError(resolution, 'task id', rawTaskId)
|
|
489
|
+
?? `Error: no history for task "${rawTaskId}" in this workspace or its spawned checkouts.`);
|
|
443
490
|
}
|
|
444
491
|
const taskId = resolution.taskId;
|
|
445
492
|
const latest = latestGeneration(history, taskId);
|
|
@@ -477,7 +524,7 @@ export default function subagentPlugin(ctx: Context): void {
|
|
|
477
524
|
? 'reused existing pane with same session; pi still running there'
|
|
478
525
|
: entry.sessionFile ? 'session restored' : 'session file missing; fresh conversation';
|
|
479
526
|
return {
|
|
480
|
-
content: [{ type: 'text', text: `resumed subagent ${entry
|
|
527
|
+
content: [{ type: 'text', text: `resumed subagent ${subRef(entry)} from task ${taskId} (${how}).` }],
|
|
481
528
|
details: { paneId: entry.paneId, taskId },
|
|
482
529
|
};
|
|
483
530
|
} catch (err) {
|
|
@@ -548,7 +595,7 @@ export default function subagentPlugin(ctx: Context): void {
|
|
|
548
595
|
persistSubs();
|
|
549
596
|
throw inner;
|
|
550
597
|
}
|
|
551
|
-
return { content: [{ type: 'text', text: `Message sent to subagent ${entry
|
|
598
|
+
return { content: [{ type: 'text', text: `Message sent to subagent ${subRef(entry)}.` }], details: { paneId: entry.paneId } };
|
|
552
599
|
},
|
|
553
600
|
});
|
|
554
601
|
}
|
|
@@ -556,7 +603,7 @@ export default function subagentPlugin(ctx: Context): void {
|
|
|
556
603
|
async function executeSubagentInterrupt(params: ToolParams, toolCtx: unknown) {
|
|
557
604
|
const { entry } = targetOf(params, toolCtx);
|
|
558
605
|
if (entry.status === 'closed') {
|
|
559
|
-
return { content: [{ type: 'text', text: `Interrupt accepted for subagent ${entry
|
|
606
|
+
return { content: [{ type: 'text', text: `Interrupt accepted for subagent ${subRef(entry)} (already idle/closed; no-op).` }], details: {} };
|
|
560
607
|
}
|
|
561
608
|
return pipeAction(entry, {
|
|
562
609
|
ready: false,
|
|
@@ -565,7 +612,7 @@ export default function subagentPlugin(ctx: Context): void {
|
|
|
565
612
|
// No settlement notice for an interrupted turn: the requester already knows.
|
|
566
613
|
const lastId = lastRequestIdByPane.get(entry.paneId);
|
|
567
614
|
if (lastId) d.claimSettleNotice(`${entry.paneId}:${lastId}`);
|
|
568
|
-
return { content: [{ type: 'text', text: `Interrupt accepted for subagent ${entry
|
|
615
|
+
return { content: [{ type: 'text', text: `Interrupt accepted for subagent ${subRef(entry)} (fire-and-return).` }], details: { paneId: entry.paneId } };
|
|
569
616
|
},
|
|
570
617
|
});
|
|
571
618
|
}
|
|
@@ -602,13 +649,15 @@ export default function subagentPlugin(ctx: Context): void {
|
|
|
602
649
|
const lines: string[] = [];
|
|
603
650
|
for (const sub of byTask.values()) {
|
|
604
651
|
const tabTag = sub.tabName ? ` [tab: ${sub.tabName}]` : '';
|
|
605
|
-
|
|
652
|
+
// The branch (`pier/<slug>`) is not a directory; printing it made masters cd into
|
|
653
|
+
// `<repo>/pier/<slug>`. Always print the absolute worktree path they can actually enter.
|
|
654
|
+
const wtTag = sub.isolate ? ` [wt: ${sub.isolate.worktreePath}]` : '';
|
|
606
655
|
if (sub.status === 'closed') {
|
|
607
656
|
lines.push(`${sub.taskId.slice(0, 8)} [idle] (${sub.kind}, closed; action send revives)${tabTag}${wtTag} ${sub.description}`);
|
|
608
657
|
continue;
|
|
609
658
|
}
|
|
610
659
|
if (sub.status === 'consumed' || sub.status === 'settled') {
|
|
611
|
-
lines.push(`${sub
|
|
660
|
+
lines.push(`${subRef(sub)} [idle] (${sub.kind}, settled; action send revives)${tabTag}${wtTag} ${sub.description}`);
|
|
612
661
|
continue;
|
|
613
662
|
}
|
|
614
663
|
const state = 'running';
|
|
@@ -624,7 +673,7 @@ export default function subagentPlugin(ctx: Context): void {
|
|
|
624
673
|
const cwdTag = probe?.foregroundCwd && probe.foregroundCwd !== sub.cwd
|
|
625
674
|
? ` [cwd: ${basename(probe.foregroundCwd) || probe.foregroundCwd}]`
|
|
626
675
|
: '';
|
|
627
|
-
lines.push(`${sub
|
|
676
|
+
lines.push(`${subRef(sub)} [${state}${takeoverMark}${statusTag}${activityTag}${gateTag}] (${sub.kind})${tabTag}${wtTag}${cwdTag} ${sub.description}`);
|
|
628
677
|
}
|
|
629
678
|
return { content: [{ type: 'text', text: lines.join('\n') }], details: {} };
|
|
630
679
|
}
|
|
@@ -643,6 +692,7 @@ export default function subagentPlugin(ctx: Context): void {
|
|
|
643
692
|
session,
|
|
644
693
|
spawn,
|
|
645
694
|
poller,
|
|
695
|
+
getSessionId: d.getSessionId,
|
|
646
696
|
...(d.logRouting ? { logRouting: d.logRouting } : {}),
|
|
647
697
|
});
|
|
648
698
|
|
|
@@ -655,16 +705,18 @@ export default function subagentPlugin(ctx: Context): void {
|
|
|
655
705
|
// carry the final report. No preferred file — start from herdr's per-pane report, not a stale registry value.
|
|
656
706
|
readFinalReport: async (paneId: string, entryCwd: string) =>
|
|
657
707
|
(await session.subSessionState(paneId, entryCwd, 0)).text,
|
|
708
|
+
persistSubs,
|
|
658
709
|
};
|
|
659
710
|
|
|
660
711
|
const ACTIONS: Record<string, (params: ToolParams, toolCtx: unknown, onUpdate?: (u: unknown) => void) => Promise<unknown>> = {
|
|
661
712
|
spawn: (params, toolCtx, onUpdate) => executeSubagentSpawn(params, toolCtx, onUpdate),
|
|
662
|
-
resume: (params
|
|
713
|
+
resume: (params) => executeSubagentResume(params),
|
|
663
714
|
list: () => executeSubagentList(),
|
|
664
715
|
send: (params, toolCtx) => executeSubagentSend(params, toolCtx),
|
|
665
716
|
interrupt: (params, toolCtx) => executeSubagentInterrupt(params, toolCtx),
|
|
666
717
|
output: (params, toolCtx) => executeSubagentOutput(params, toolCtx, outputActionDeps),
|
|
667
718
|
role: (params, toolCtx) => executeSubagentRole(params, toolCtx),
|
|
719
|
+
help: async () => ({ content: [{ type: 'text', text: SUBAGENT_MANUAL }], details: {} }),
|
|
668
720
|
};
|
|
669
721
|
|
|
670
722
|
scoped.registerTool({
|
|
@@ -672,15 +724,12 @@ export default function subagentPlugin(ctx: Context): void {
|
|
|
672
724
|
label: 'Subagent',
|
|
673
725
|
// B4: the model picks tools from the system prompt's snippet/guideline surface; delegation
|
|
674
726
|
// decisions are made here, so state them rather than hoping for recall.
|
|
675
|
-
promptSnippet: 'subagent: delegate a self-contained task to a pi worker pane (spawn/isolate/background/send/output/interrupt/resume/list/role).',
|
|
727
|
+
promptSnippet: 'subagent: delegate a self-contained task to a pi worker pane (spawn/isolate/background/send/output/interrupt/resume/list/role/help).',
|
|
676
728
|
promptGuidelines: [
|
|
677
|
-
'Use subagent
|
|
678
|
-
'
|
|
679
|
-
'Use isolate: true when the worker
|
|
680
|
-
'
|
|
681
|
-
'Do not poll with action: "output" in a tight loop: the pane pushes a settlement notice when it finishes. Check once, then continue other work.',
|
|
682
|
-
'Send follow-up instructions with action: "send" (delivered as a steer between tool calls) instead of spawning a second worker for the same task.',
|
|
683
|
-
'Pass agentId, taskId, message, and max_chars as top-level arguments. Do not wrap them in a parameters object.',
|
|
729
|
+
'Use subagent for self-contained work whose result is what matters: output that would flood this context, long builds/test runs, or independent parts of a bigger change that can run in parallel.',
|
|
730
|
+
'The worker does not see this conversation: put paths, acceptance criteria, the test command and what to report back into the prompt.',
|
|
731
|
+
'Use isolate: true when the worker writes files you or another worker edit at the same time, or when its work needs its own reviewable branch.',
|
|
732
|
+
'Pass agentId, taskId, message and max_chars as top-level arguments, not inside a parameters object.',
|
|
684
733
|
],
|
|
685
734
|
description: SUBAGENT_DESCRIPTION,
|
|
686
735
|
parameters: Type.Object({
|
|
@@ -692,20 +741,22 @@ export default function subagentPlugin(ctx: Context): void {
|
|
|
692
741
|
Type.Literal('interrupt'),
|
|
693
742
|
Type.Literal('output'),
|
|
694
743
|
Type.Literal('role'),
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
744
|
+
Type.Literal('help'),
|
|
745
|
+
], { description: 'Default spawn' })),
|
|
746
|
+
description: Type.Optional(Type.String({ description: '[spawn] Pane label; also the todo-reconcile key' })),
|
|
747
|
+
prompt: Type.Optional(Type.String({ description: '[spawn] The complete self-contained task' })),
|
|
748
|
+
run_in_background: Type.Optional(Type.Boolean({ description: '[spawn] Return at once; the settlement notice arrives later (default false)' })),
|
|
749
|
+
cwd: Type.Optional(Type.String({ description: '[spawn] Existing directory or worktree to run in (absolute or workspace-relative); exclusive with isolate' })),
|
|
750
|
+
isolate: Type.Optional(Type.Boolean({ description: '[spawn] Run in a fresh git worktree (branch pier/<slug>) for heavy parallel writes or a reviewable diff; settle reports a diff summary (see help)' })),
|
|
751
|
+
role: Type.Optional(Type.String({ description: '[spawn|role] Role profile name; unknown names are display labels only' })),
|
|
752
|
+
tab: Type.Optional(Type.String({ description: '[spawn] Task tab to join or create (default: grouped by worktree)' })),
|
|
753
|
+
allowed_tools: Type.Optional(Type.Array(Type.String(), { description: '[spawn] Extra tools on top of the role baseline' })),
|
|
754
|
+
taskId: Type.Optional(Type.String({ description: '[resume] Task id from the delegation ledger' })),
|
|
755
|
+
agentId: Type.Optional(Type.String({ description: '[send|interrupt|output|role] Slug, pane id, or slug@paneId as printed by spawn/list' })),
|
|
756
|
+
message: Type.Optional(Type.String({ description: '[send] Follow-up message' })),
|
|
757
|
+
max_chars: Type.Optional(Type.Integer({ description: '[output] Output cap (default 6000, 100-16000)' })),
|
|
708
758
|
}),
|
|
759
|
+
outputSchema: SUBAGENT_OUTPUT_SCHEMA,
|
|
709
760
|
// B2: `action` is optional in the schema; normalize it so the validated arguments and the
|
|
710
761
|
// code path never disagree and `spawn` is explicit in logs.
|
|
711
762
|
prepareArguments: (args: unknown) => {
|
|
@@ -722,7 +773,7 @@ export default function subagentPlugin(ctx: Context): void {
|
|
|
722
773
|
if (!handler) {
|
|
723
774
|
return toolError(`Error: unknown action "${action}" (valid: ${Object.keys(ACTIONS).join(', ')})`);
|
|
724
775
|
}
|
|
725
|
-
return handler(flat, toolCtx, onUpdate);
|
|
776
|
+
return attachSubagentStructuredContent(action, await handler(flat, toolCtx, onUpdate));
|
|
726
777
|
},
|
|
727
778
|
});
|
|
728
779
|
}
|