pi-pier 0.3.1 → 0.4.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 +8 -5
- package/package.json +3 -3
- package/src/bootstrap.ts +2 -1
- package/src/compact-coordinator.ts +3 -2
- 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 +1 -4
- package/src/efficiency-store.ts +1 -1
- package/src/guards.ts +6 -0
- package/src/herdr-client.ts +126 -31
- 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 +14 -0
- package/src/index-runtime.ts +14 -0
- package/src/index-worker.ts +4 -10
- package/src/index.ts +298 -48
- package/src/jev-core.ts +1 -3
- package/src/panel-open.ts +47 -0
- package/src/pier-options.ts +1 -2
- package/src/pipe-channel.ts +17 -0
- package/src/plugins/subagent.ts +68 -30
- package/src/plugins/terminal.ts +34 -15
- package/src/plugins/todo.ts +20 -42
- package/src/reducer-core.ts +3 -5
- package/src/renderers.ts +37 -2
- package/src/role-manifest.ts +2 -3
- package/src/roles/worker-default.json +3 -3
- package/src/settle-wake-core.ts +9 -1
- package/src/subagent-core.ts +170 -2
- package/src/subagent-poller.ts +17 -1
- package/src/subagent-spawn.ts +51 -4
- package/src/terminal-core.ts +68 -34
- package/src/todo-read-hook.ts +23 -0
- package/src/tree-view.ts +162 -0
- package/src/worker-marker.ts +76 -0
- package/src/focus-poller.ts +0 -223
package/README.md
CHANGED
|
@@ -43,8 +43,8 @@ The installer also verifies node / pi / herdr versions.
|
|
|
43
43
|
| Tool | Purpose |
|
|
44
44
|
|---|---|
|
|
45
45
|
| `todo_write` | Full-replacement todo list. Session JSONL is the single source of truth; correct rollback on branch switches. Projected live onto the pane title (`▶i ○p ■b ✓c (N/M) · current task`). `/todos` command to view/edit/unblock |
|
|
46
|
-
| `subagent` | Delegate self-contained subtasks to an isolated pi session in its own herdr pane. Actions: `spawn` (default; foreground / parallel / background), `list`, `send`, `interrupt`, `resume`, `output` (incremental preview of a running subagent's pane output), `role` (switch that worker's role profile mid-session — pi
|
|
47
|
-
| `
|
|
46
|
+
| `subagent` | Delegate self-contained subtasks to an isolated pi session in its own herdr pane. Actions: `spawn` (default; foreground / parallel / background), `list`, `send`, `interrupt`, `resume`, `output` (incremental preview of a running subagent's pane output), `role` (switch that worker's role profile mid-session — pi records the toolset change as a transcript delta, so it applies on the worker's next request and survives resume). Every spawned task gets a stable slug: outputs print `slug@paneId`, and `agentId` accepts the slug, the pane id, or the combined form |
|
|
47
|
+
| `report_progress` | Worker panes only: send the delegating master a short checkpoint update without ending the task. Arrives in the master's context as a display-less custom message (no turn, no chat flood), rendered as a `⤴ progress` card |
|
|
48
48
|
| `terminal` | Persistent interactive shells in dedicated herdr panes. Actions: `open`, `send`, `read`, `signal`, `close`, `list` |
|
|
49
49
|
|
|
50
50
|
### Commands
|
|
@@ -52,13 +52,16 @@ The installer also verifies node / pi / herdr versions.
|
|
|
52
52
|
| Command | Purpose |
|
|
53
53
|
|---|---|
|
|
54
54
|
| `/todos` | Show the todo list, or edit it: `/todos done|drop|rm|unblock <fuzzy match>` |
|
|
55
|
+
| `/dashboard [overlay]` | Ops dashboard. Default opens the shared Pier **popup** on its `Dashboard` tab (90% × 80%, singleton); `overlay` opens the same view as an independent full-window pane. Non-Herdr falls back to an in-pane text view. |
|
|
56
|
+
| `/pier-tree [overlay]` | Master only: the full delegation tree — your complete todo list plus every running subagent's full list (live-pushed over the pipe) and settled nodes with ages. Default opens the same shared popup on its `Tree` tab (the snapshot refreshes on every todo/worker push; `1`/`2`/`Tab` switch views inside the popup); `overlay` opens an independent full-window pane; bare pi / a missing plugin falls back to the in-pane notify view. The resident widget stays one line per agent and hides entirely once the self list is fully completed (D113/D114) |
|
|
55
57
|
| `/locks` | Show write locks held by this pane and by live panes — the **human/operator view** of the write-lock beacons (herdr only). Agents see the holders inside their own write warnings; the raw tokens are readable via `herdr agent list` |
|
|
56
|
-
| `/pier-role` | Show the current role + available role names, or switch mid-session: `/pier-role <name>` (
|
|
58
|
+
| `/pier-role` | Show the current role + available role names, or switch mid-session: `/pier-role <name>` (widening beyond the current toolset asks for confirmation; the switch lands as a transcript tool delta) |
|
|
57
59
|
| `/pier-config` | Read-only configuration guide over four planes: bare call = index + hands a guided change to the agent; `show [plane\|all]` = effective value + source (`env > workspace > user > default`); `check` = validate all planes; `doc [path]` = write a report |
|
|
58
60
|
|
|
59
61
|
### Behaviors
|
|
60
62
|
|
|
61
63
|
- **Human-in-the-loop**: every subagent is a visible, interactive TUI pane — step into it anytime to talk directly (fix bugs, take over, answer its `ask_user_question`). Blocked gates raise sidebar markers + notifications
|
|
64
|
+
- **Delegation identity**: the worker knows who delegated it — spawn env (`PI_HERDR_DELEGATION`) replays through the worker marker and the session's own first-frame entry; its system prompt carries a `pier-delegation` section (identity + delivery contract), the transcript opens with an "⟵ delegated by master …" card, the pane title gets an `↳` prefix, and `pi-parent`/`pi-task` tokens feed the workbench dashboard tree and blocked-alert attribution
|
|
62
65
|
- **Blocking dialogs block the pane**: pi's `ui_prompt_start` / `ui_prompt_end` events cover the inherently blocking `ctx.ui` dialogs (`select` / `confirm` / `input` / `editor`), so a dialog from any extension (not only pier's own ask tool) reports the pane as blocked and emits the `herdr:blocked` edge; nested gates are coalesced. `custom` is excluded because pi also routes resident overlays through it (pier's slim-frame overlay never calls `done()`, and a resident overlay keeps pi's prompt span open anyway)
|
|
63
66
|
- **Role gate stops early**: a tool call denied by the role manifest returns `terminate`, so a batch whose results are all terminating ends without another model round trip
|
|
64
67
|
- **Readable transcript**: pier's own session entries (todo edits, subagent/terminal registries, role manifests, soft approvals) and its reminder messages render as compact cards instead of raw JSON
|
|
@@ -105,8 +108,8 @@ pi overwrites tools/commands by name; event listeners stack.
|
|
|
105
108
|
## Requirements
|
|
106
109
|
|
|
107
110
|
- Node ≥ 22
|
|
108
|
-
- pi ≥
|
|
109
|
-
- **herdr ≥ 0.9.
|
|
111
|
+
- pi ≥ 1.3.0 (`@earendil-works/pi-coding-agent`) — `before_agent_start.messageType` and resumed-session active tools
|
|
112
|
+
- **herdr ≥ 0.9.5** — required for subagents / pane integration / notifications (see the IMPORTANT note above); native `pane.focused`, self-reported resume, and shutdown-safe session saves restore subagent panes after a server restart
|
|
110
113
|
|
|
111
114
|
## Development
|
|
112
115
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-pier",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "pier: pi extension that fuses pi agent sessions with the herdr pane/tab orchestrator — todo_write loop closure + interactive subagents in isolated panes",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -49,11 +49,11 @@
|
|
|
49
49
|
"@deepseek-ai/cordis-plugin-loader": "^1.0.2"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
|
-
"@earendil-works/pi-coding-agent": "
|
|
52
|
+
"@earendil-works/pi-coding-agent": ">=1.3.0",
|
|
53
53
|
"typebox": "*"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
|
-
"@earendil-works/pi-coding-agent": "
|
|
56
|
+
"@earendil-works/pi-coding-agent": "1.3.0",
|
|
57
57
|
"typebox": "~1.3.7",
|
|
58
58
|
"typescript": "~5.9.3"
|
|
59
59
|
}
|
package/src/bootstrap.ts
CHANGED
|
@@ -12,6 +12,7 @@ import Group from '@deepseek-ai/cordis-plugin-group';
|
|
|
12
12
|
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
13
13
|
import * as path from 'node:path';
|
|
14
14
|
import { DisposeLedger } from './ledger.ts';
|
|
15
|
+
import { cordisProvide } from './cordis-services.ts';
|
|
15
16
|
import { pierOption } from './pier-options.ts';
|
|
16
17
|
|
|
17
18
|
export interface BootstrapHooks {
|
|
@@ -37,7 +38,7 @@ export async function createCordisApp(hooks: BootstrapHooks = {}): Promise<Cordi
|
|
|
37
38
|
const root = new Context();
|
|
38
39
|
const ledger = new DisposeLedger();
|
|
39
40
|
// Inject the ledger through Cordis DI because loader transforms entry config and cannot carry live instances.
|
|
40
|
-
root
|
|
41
|
+
cordisProvide(root, 'pi-herdr.ledger', ledger);
|
|
41
42
|
if (hooks.onDispose) {
|
|
42
43
|
root.effect(() => () => { hooks.onDispose?.(); }, 'session-root');
|
|
43
44
|
}
|
|
@@ -227,8 +227,9 @@ export class CompactCoordinator {
|
|
|
227
227
|
return;
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
-
// P1-3 backoff: after a failed summarization
|
|
231
|
-
//
|
|
230
|
+
// P1-3 backoff: after a failed summarization, skip compact decisions for a bounded window —
|
|
231
|
+
// each attempt aborts an in-flight turn, so a doomed retry burns turns. (The token-cap failure
|
|
232
|
+
// class is gone since pi 1.4.0 uncapped summary output; transient timeouts/aborts remain.)
|
|
232
233
|
if (this.state.compactBackoffTurnEnds > 0) {
|
|
233
234
|
this.state.compactBackoffTurnEnds--;
|
|
234
235
|
if (opts.config.logEnabled) {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Closed set of cordis services this process provides on the root.
|
|
3
|
+
*
|
|
4
|
+
* `ctx.get`/`ctx.provide` also accept an arbitrary string and return `any`, so a raw call plus `as`
|
|
5
|
+
* neither rejects a mistyped key nor a dropped bag field (fe99252). Mounts and plugins go through
|
|
6
|
+
* the helpers below. Instances stay on the root: a plugin-fiber Service would unload them on HMR.
|
|
7
|
+
*/
|
|
8
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
9
|
+
import type { DisposeLedger } from './ledger.ts';
|
|
10
|
+
import type { PiSurface } from './pi-surface.ts';
|
|
11
|
+
import type { TerminalDeps } from './plugins/terminal.ts';
|
|
12
|
+
import type { TodoDeps, TodoUiSlot } from './plugins/todo.ts';
|
|
13
|
+
import type { TodosService } from './todos-service.ts';
|
|
14
|
+
import type { SubagentDeps } from './plugins/subagent.ts';
|
|
15
|
+
|
|
16
|
+
export interface PierCordisServices {
|
|
17
|
+
'pi-herdr.ledger': DisposeLedger;
|
|
18
|
+
'pi-herdr.surface': PiSurface<object>;
|
|
19
|
+
'pi-herdr.terminal-deps': TerminalDeps;
|
|
20
|
+
'pi-herdr.todo-deps': TodoDeps;
|
|
21
|
+
'pi-herdr.subagent-deps': SubagentDeps;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
declare module '@deepseek-ai/cordis' {
|
|
25
|
+
interface Context extends PierCordisServices {}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function cordisProvide<K extends keyof PierCordisServices>(
|
|
29
|
+
ctx: Context,
|
|
30
|
+
key: K,
|
|
31
|
+
value: PierCordisServices[K],
|
|
32
|
+
): void {
|
|
33
|
+
ctx.provide(key, value);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Throws when the root has not provided `key`. `ctx.get` itself returns `T | undefined`. */
|
|
37
|
+
export function cordisGet<K extends keyof PierCordisServices>(ctx: Context, key: K): PierCordisServices[K] {
|
|
38
|
+
const value = ctx.get(key) as PierCordisServices[K] | undefined;
|
|
39
|
+
if (value === undefined) throw new Error(`[pi-herdr] cordis service missing: ${key}`);
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The six required 'pi-herdr.todo-deps' fields both mounts assemble identically (maxItems mirrors
|
|
44
|
+
* the tool prompt's "at most 15 entries"); masters and workers spread their own optional seams —
|
|
45
|
+
* getWorkers/getBlockedDepth/stopReminder — on top of this base. */
|
|
46
|
+
export function baseTodoDeps(m: {
|
|
47
|
+
pi: unknown;
|
|
48
|
+
todos: TodosService;
|
|
49
|
+
todoUi: TodoUiSlot;
|
|
50
|
+
mirrorTodos: () => void;
|
|
51
|
+
}): Pick<TodoDeps, 'todos' | 'allowParallelInProgress' | 'maxItems' | 'mirrorTodos' | 'appendEntry' | 'state'> {
|
|
52
|
+
return {
|
|
53
|
+
todos: m.todos,
|
|
54
|
+
allowParallelInProgress: m.todos.config.allowParallelInProgress,
|
|
55
|
+
maxItems: 15,
|
|
56
|
+
mirrorTodos: m.mirrorTodos,
|
|
57
|
+
appendEntry: (customType: string, data: unknown) => {
|
|
58
|
+
// Unchecked cast: pi's ExtensionAPI type omits this optional runtime entry-append seam.
|
|
59
|
+
const pi = m.pi as { appendEntry?: (t: string, d: unknown) => void };
|
|
60
|
+
pi.appendEntry?.(customType, data);
|
|
61
|
+
},
|
|
62
|
+
state: m.todoUi,
|
|
63
|
+
};
|
|
64
|
+
}
|
package/src/dashboard-command.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { HerdrClientLike, HerdrEnv } from './herdr-client.ts';
|
|
2
|
+
import { openPierPanel } from './panel-open.ts';
|
|
2
3
|
import { isTodoStatus, type TodoItem, type TodoStatus } from './vocab.ts';
|
|
3
4
|
|
|
4
5
|
export interface DashboardCommandDeps {
|
|
@@ -9,6 +10,8 @@ export interface DashboardCommandDeps {
|
|
|
9
10
|
env: HerdrEnv | null;
|
|
10
11
|
getTodoItems?: () => readonly TodoItem[];
|
|
11
12
|
getHeldLocks?: () => readonly string[];
|
|
13
|
+
/** Publishes the current delegation tree and returns its snapshot path (panel Tree tab). */
|
|
14
|
+
prepareTree?: () => string | null;
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
/** SUMMARY_ORDER fixes the summary-line order; LIVE_ROWS adds the detail line below it.
|
|
@@ -74,26 +77,48 @@ export function formatStandaloneDashboard(opts: {
|
|
|
74
77
|
|
|
75
78
|
export function installDashboardCommand(deps: DashboardCommandDeps): void {
|
|
76
79
|
deps.pi.registerCommand('dashboard', {
|
|
77
|
-
description: 'Open the Pier Ops Dashboard (modal popup
|
|
78
|
-
handler: async (
|
|
80
|
+
description: 'Open the Pier Ops Dashboard (modal popup by default; `/dashboard overlay` = independent full-window pane)',
|
|
81
|
+
handler: async (args, ctx) => {
|
|
79
82
|
const ui = (ctx as { ui?: { notify?: (text: string, level?: string) => void } }).ui;
|
|
83
|
+
const overlay = typeof args === 'string' && args.trim().toLowerCase() === 'overlay';
|
|
80
84
|
|
|
81
85
|
if (deps.client.available && deps.env) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
86
|
+
if (overlay) {
|
|
87
|
+
// Escape hatch: a real full-window pane instead of the singleton popup.
|
|
88
|
+
const treeFile = deps.prepareTree?.() ?? null;
|
|
89
|
+
try {
|
|
90
|
+
const res = await deps.client.openPluginPane({
|
|
91
|
+
pluginId: 'pier.workbench',
|
|
92
|
+
entrypoint: 'dashboard',
|
|
93
|
+
placement: 'overlay',
|
|
94
|
+
focus: true,
|
|
95
|
+
env: {
|
|
96
|
+
PIER_PANEL: 'dashboard',
|
|
97
|
+
...(treeFile ? { PIER_TREE_FILE: treeFile } : {}),
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
if (res.mode === 'fallback_tab') {
|
|
101
|
+
ui?.notify?.('Herdr < 0.9.1: Pier Dashboard opened in new tab', 'info');
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
} catch {
|
|
105
|
+
// Fall through to the local view.
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
const res = await openPierPanel(
|
|
109
|
+
{ client: deps.client, env: deps.env },
|
|
110
|
+
{ panel: 'dashboard', treeFile: deps.prepareTree?.() ?? null },
|
|
111
|
+
);
|
|
112
|
+
if (res === 'popup') return;
|
|
113
|
+
if (res === 'tab') {
|
|
92
114
|
ui?.notify?.('Herdr < 0.9.1: Pier Dashboard opened in new tab', 'info');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (res === 'busy') {
|
|
118
|
+
ui?.notify?.('Another popup is already open — close it and run /dashboard again.', 'warning');
|
|
119
|
+
return;
|
|
93
120
|
}
|
|
94
|
-
|
|
95
|
-
} catch {
|
|
96
|
-
// Plugin missing or socket error: fall through to the Level 3 local view.
|
|
121
|
+
// 'unavailable' → Level 3 local view.
|
|
97
122
|
}
|
|
98
123
|
}
|
|
99
124
|
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delegation identity (RFC docs/rfc-delegation-identity.md Phase 1): a worker pane knows who
|
|
3
|
+
* delegated it, for what task, and shows that on every surface it already touches — system prompt
|
|
4
|
+
* section, session custom entry (first-frame card), herdr sidebar tokens, and the pane title.
|
|
5
|
+
* Pure builders only; the composition root owns the side effects.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Contract env handed to the worker process by the master (PI_HERDR_ prefix per naming rule). */
|
|
9
|
+
export const DELEGATION_ENV = 'PI_HERDR_DELEGATION';
|
|
10
|
+
/** Worker-session custom entry written once at first boot; replayed on resume. */
|
|
11
|
+
export const DELEGATION_CUSTOM_TYPE = 'pi-herdr.delegation';
|
|
12
|
+
/** herdr sidebar tokens (pane.report_metadata): parent pane + delegated task. */
|
|
13
|
+
export const DELEGATION_PARENT_TOKEN = 'pi-parent';
|
|
14
|
+
export const DELEGATION_TASK_TOKEN = 'pi-task';
|
|
15
|
+
/** herdr truncates title/token values at 80 chars; clip locally to keep glyphs whole. */
|
|
16
|
+
const TOKEN_MAX = 80;
|
|
17
|
+
|
|
18
|
+
export interface DelegationContext {
|
|
19
|
+
/** Master pane that spawned this worker; '' when the master pane is unknown (tests, RPC). */
|
|
20
|
+
readonly parentPaneId: string;
|
|
21
|
+
/** Master pi session id (cross-locate the parent transcript via /resume). */
|
|
22
|
+
readonly parentSessionId: string;
|
|
23
|
+
/** SubEntry.taskId — stable across revives. */
|
|
24
|
+
readonly taskId: string;
|
|
25
|
+
/** Task short label (SubEntry.description). */
|
|
26
|
+
readonly description: string;
|
|
27
|
+
/** Role manifest name active at spawn. */
|
|
28
|
+
readonly role: string;
|
|
29
|
+
/** Phase 2: the master's pipe name — report_progress dials it directly (D49 reply-address
|
|
30
|
+
* semantics, precomputed because the worker cannot derive the master's session cwd). */
|
|
31
|
+
readonly parentPipe?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function str(v: unknown): string | null {
|
|
35
|
+
return typeof v === 'string' ? v : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Validate a parsed delegation record (env JSON, marker JSON or custom-entry data); null when malformed. */
|
|
39
|
+
export function delegationFromRecord(parsed: unknown): DelegationContext | null {
|
|
40
|
+
if (!parsed || typeof parsed !== 'object') return null;
|
|
41
|
+
const rec = parsed as Record<string, unknown>; // shape-checked field by field below
|
|
42
|
+
const parentPipe = str(rec.parentPipe);
|
|
43
|
+
const taskId = str(rec.taskId);
|
|
44
|
+
const description = str(rec.description);
|
|
45
|
+
if (!taskId || !description) return null;
|
|
46
|
+
return {
|
|
47
|
+
parentPaneId: str(rec.parentPaneId) ?? '',
|
|
48
|
+
parentSessionId: str(rec.parentSessionId) ?? '',
|
|
49
|
+
taskId,
|
|
50
|
+
description,
|
|
51
|
+
role: str(rec.role) ?? 'worker-default',
|
|
52
|
+
...(parentPipe ? { parentPipe } : {}),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Parse the spawn env; null when absent or structurally invalid (taskId/description mandatory). */
|
|
57
|
+
export function parseDelegationContext(raw: string | undefined | null): DelegationContext | null {
|
|
58
|
+
if (!raw || typeof raw !== 'string') return null;
|
|
59
|
+
try {
|
|
60
|
+
return delegationFromRecord(JSON.parse(raw));
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** env map for spawnPaneInTaskTab/createTab; JSON like the role manifest next to it. */
|
|
67
|
+
export function delegationEnv(ctx: DelegationContext): Record<string, string> {
|
|
68
|
+
return { [DELEGATION_ENV]: JSON.stringify(ctx) };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Fold `pi-herdr.delegation` branch entries (last one wins) — the /resume replay source. */
|
|
72
|
+
export function foldDelegation(
|
|
73
|
+
entries: ReadonlyArray<{ type?: string; customType?: string; data?: unknown }>,
|
|
74
|
+
): DelegationContext | null {
|
|
75
|
+
let found: DelegationContext | null = null;
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
if (entry.type !== 'custom' || entry.customType !== DELEGATION_CUSTOM_TYPE) continue;
|
|
78
|
+
const next = delegationFromRecord(entry.data);
|
|
79
|
+
if (next) found = next;
|
|
80
|
+
}
|
|
81
|
+
return found;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* System-prompt section text: identity + delivery contract in one stable wording, so the master
|
|
86
|
+
* model does not have to restate it in every spawn prompt (pi-subagent-manager idea A+F).
|
|
87
|
+
*/
|
|
88
|
+
export function buildDelegationSection(ctx: DelegationContext, ownPaneId: string): string {
|
|
89
|
+
const parent = ctx.parentPaneId || 'unknown pane';
|
|
90
|
+
const session = ctx.parentSessionId || 'unknown session';
|
|
91
|
+
const self = ownPaneId ? ` You are pane ${ownPaneId} (agentId "${ownPaneId}") — cite it when asking questions or reporting errors.`
|
|
92
|
+
: '';
|
|
93
|
+
return [
|
|
94
|
+
`You are a delegated subagent of the master pane ${parent} (session ${session}); you were not started by a human.`,
|
|
95
|
+
`Task: ${ctx.description}. Role: ${ctx.role}.${self}`,
|
|
96
|
+
'Contract: when finished, state your final answer plainly — the master collects it from this pane\'s settlement; do not ask who requested the work. If you cannot finish, say exactly what blocks you instead of stopping silently.',
|
|
97
|
+
].join('\n');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Sidebar token patch; empty (no request) when there is no parent pane to attribute. */
|
|
101
|
+
export function delegationSidebarTokens(ctx: DelegationContext): Record<string, string> {
|
|
102
|
+
if (!ctx.parentPaneId) return {};
|
|
103
|
+
return {
|
|
104
|
+
[DELEGATION_PARENT_TOKEN]: ctx.parentPaneId.slice(0, TOKEN_MAX),
|
|
105
|
+
[DELEGATION_TASK_TOKEN]: ctx.description.slice(0, TOKEN_MAX),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Worker pane title marker: `↳ <todo title>`, or `↳ <task>` alone before the first todo_write. */
|
|
110
|
+
export function applyWorkerTag(title: string | null, delegatedTask: string | null): string | null {
|
|
111
|
+
if (!delegatedTask) return title;
|
|
112
|
+
const base = title ?? delegatedTask;
|
|
113
|
+
return `↳ ${base.slice(0, TOKEN_MAX - 2)}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* top-level key disables every mechanism).
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import { isPlainObject } from './guards.ts';
|
|
12
13
|
import { existsSync, readFileSync } from 'node:fs';
|
|
13
14
|
import { homedir } from 'node:os';
|
|
14
15
|
import { join } from 'node:path';
|
|
@@ -86,10 +87,6 @@ export const DEFAULT_EFFICIENCY_CONFIG: EfficiencyConfig = Object.freeze({
|
|
|
86
87
|
jev: Object.freeze({ enabled: false, logEnabled: false, model: 'jev-1.13.0', timeoutMs: 2000, minConfidence: 0.6 }),
|
|
87
88
|
});
|
|
88
89
|
|
|
89
|
-
function isPlainObject(v: unknown): v is Record<string, unknown> {
|
|
90
|
-
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
90
|
type FieldKind = 'bool' | 'int' | 'number' | 'ratio' | 'string' | 'optionalString';
|
|
94
91
|
|
|
95
92
|
interface FieldSpec {
|
package/src/efficiency-store.ts
CHANGED
|
@@ -58,7 +58,7 @@ export function reducerObjectPath(root: string, sha256: string): string {
|
|
|
58
58
|
|
|
59
59
|
export function efficiencyLogPath(
|
|
60
60
|
root: string,
|
|
61
|
-
mechanism: 'compact' | 'observation' | 'reducer' | 'jev' | 'routing',
|
|
61
|
+
mechanism: 'compact' | 'observation' | 'reducer' | 'jev' | 'routing' | 'tools',
|
|
62
62
|
customPath?: string,
|
|
63
63
|
): string {
|
|
64
64
|
if (customPath && typeof customPath === 'string') return resolve(customPath);
|
package/src/guards.ts
ADDED