pi-pier 0.3.0 → 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 +16 -12
- package/package.json +3 -3
- package/schemas/role-manifest.schema.json +7 -1
- package/skills/pi-herdr-workflow/SKILL.md +4 -5
- package/src/ansi-text.ts +62 -11
- package/src/ask-dialog.ts +308 -0
- package/src/ask-user.ts +113 -112
- package/src/bootstrap.ts +10 -13
- package/src/compact-coordinator.ts +279 -261
- package/src/compact-economics-core.ts +77 -145
- package/src/config-catalog-core.ts +77 -197
- package/src/config-command.ts +271 -64
- package/src/cordis-services.ts +64 -0
- package/src/dashboard-command.ts +77 -33
- package/src/delegation-core.ts +115 -0
- package/src/efficiency-config-core.ts +286 -486
- package/src/efficiency-store.ts +16 -29
- package/src/gc-core.ts +10 -36
- package/src/guards.ts +6 -0
- package/src/herdr-client.ts +202 -233
- package/src/history-store.ts +38 -23
- package/src/index-gates.ts +118 -0
- package/src/index-locks.ts +2 -7
- package/src/index-master.ts +31 -29
- package/src/index-notices.ts +28 -15
- package/src/index-pipe.ts +31 -12
- package/src/index-roles.ts +306 -0
- package/src/index-runtime.ts +16 -6
- package/src/index-worker.ts +8 -18
- package/src/index.ts +561 -570
- package/src/jev-client.ts +12 -17
- package/src/jev-core.ts +138 -90
- package/src/ledger.ts +13 -21
- package/src/lock-core.ts +28 -56
- package/src/manifest-compose.ts +35 -20
- package/src/observation-core.ts +52 -100
- package/src/pane-title.ts +19 -38
- package/src/panel-open.ts +47 -0
- package/src/pi-surface.ts +34 -22
- package/src/pier-options.ts +53 -50
- package/src/pipe-channel.ts +44 -59
- package/src/platform-paths.ts +19 -52
- package/src/{core → plugins}/grid-shape.ts +8 -10
- package/src/{core → plugins}/heat-plan.ts +20 -22
- package/src/plugins/observation.ts +475 -0
- package/src/plugins/subagent.ts +766 -0
- package/src/{core → plugins}/terminal.ts +198 -139
- package/src/{core → plugins}/todo.ts +73 -134
- package/src/progress-core.ts +16 -53
- package/src/reconcile-core.ts +24 -33
- package/src/reducer-core.ts +26 -64
- package/src/reducer-invoker.ts +266 -239
- package/src/renderers.ts +108 -84
- package/src/role-loader.ts +33 -28
- package/src/role-manifest.ts +25 -31
- package/src/role-state.ts +119 -0
- package/src/roles/worker-default.json +3 -3
- package/src/routing-telemetry.ts +136 -0
- package/src/runtime-policy.ts +25 -40
- package/src/session-tail.ts +65 -41
- package/src/settle-wake-core.ts +61 -22
- package/src/slim-frame.ts +25 -122
- package/src/stale-core.ts +17 -24
- package/src/storage-layout.ts +14 -43
- package/src/subagent-core.ts +423 -120
- package/src/subagent-gc.ts +109 -67
- package/src/subagent-poller.ts +383 -24
- package/src/subagent-session.ts +516 -0
- package/src/subagent-spawn.ts +577 -52
- package/src/swallow.ts +7 -17
- package/src/terminal-core.ts +187 -134
- package/src/todo-core.ts +57 -109
- package/src/todo-read-hook.ts +46 -39
- package/src/todo-reminder-core.ts +26 -51
- package/src/todo-window.ts +11 -12
- package/src/todos-service.ts +15 -35
- package/src/tool-error.ts +6 -13
- package/src/tool-gate.ts +38 -21
- package/src/tree-view.ts +162 -0
- package/src/vocab.ts +52 -25
- package/src/worker-marker.ts +76 -0
- package/src/ask-multi.ts +0 -260
- package/src/config-guide.ts +0 -361
- package/src/core/observation.ts +0 -555
- package/src/core/subagent.ts +0 -633
- package/src/efficiency-config-io.ts +0 -53
- package/src/focus-poller.ts +0 -244
- package/src/git-adapter.ts +0 -113
- package/src/notice-buffer.ts +0 -23
- package/src/subagent-execute.ts +0 -328
- package/src/subagent-git-io.ts +0 -80
- package/src/subagent-launch.ts +0 -114
- package/src/subagent-list-action.ts +0 -69
- package/src/subagent-output-action.ts +0 -104
- package/src/subagent-output-adapter.ts +0 -27
- package/src/subagent-output-core.ts +0 -263
- package/src/subagent-poll-loop.ts +0 -305
- package/src/subagent-port.ts +0 -27
- package/src/subagent-ready.ts +0 -89
- package/src/subagent-registry.ts +0 -70
- package/src/subagent-resolution.ts +0 -41
- package/src/subagent-scope.ts +0 -41
- package/src/subagent-session-io.ts +0 -230
package/src/history-store.ts
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* <agentRoot>/herdr-pi/history/--<cwd>--/history.jsonl
|
|
6
|
-
* Records are immutable so GC can remove panes without deleting history. Recovery keeps
|
|
7
|
-
* both the sessionFile and launchCommand snapshots as fallbacks.
|
|
2
|
+
* Delegation ledger: append-only JSONL partitioned by cwd, mirroring pi's session partitioning
|
|
3
|
+
* (<agentRoot>/herdr-pi/history/--<cwd>--/history.jsonl). Rows are immutable, so GC can drop panes
|
|
4
|
+
* without losing history; recovery keeps both the sessionFile and launchCommand snapshots.
|
|
8
5
|
*/
|
|
9
6
|
import * as fs from 'node:fs';
|
|
10
7
|
import * as path from 'node:path';
|
|
@@ -12,13 +9,13 @@ import { historyFilePath, preferredHistoryFile } from './storage-layout.ts';
|
|
|
12
9
|
|
|
13
10
|
export { historyFilePath, preferredHistoryFile };
|
|
14
11
|
|
|
15
|
-
/** Legacy short/resident entries normalize to task
|
|
12
|
+
/** Legacy short/resident entries normalize to task, as do missing values; role names pass through. */
|
|
16
13
|
export function normalizeEntryKind(kind: string | null | undefined): string {
|
|
17
14
|
if (!kind || kind === 'short' || kind === 'resident') return 'task';
|
|
18
15
|
return kind;
|
|
19
16
|
}
|
|
20
17
|
|
|
21
|
-
/**
|
|
18
|
+
/** Only a .jsonl path is accepted, so an invalid report cannot overwrite a valid one. */
|
|
22
19
|
export function applyReportedSessionFile(
|
|
23
20
|
current: string | null,
|
|
24
21
|
reported: string | null | undefined,
|
|
@@ -29,11 +26,11 @@ export function applyReportedSessionFile(
|
|
|
29
26
|
|
|
30
27
|
export interface HistoryEntry {
|
|
31
28
|
taskId: string;
|
|
32
|
-
/** 'task' or a role name; disk reads normalize short/resident to task for compatibility. */
|
|
33
29
|
kind: string;
|
|
34
30
|
paneId: string;
|
|
35
31
|
tabId: string;
|
|
36
|
-
/**
|
|
32
|
+
/** Phase 2 task slug — carried so resume-from-history keeps the tool-facing reference stable. */
|
|
33
|
+
slug?: string;
|
|
37
34
|
tabName?: string;
|
|
38
35
|
workspaceId: string;
|
|
39
36
|
cwd: string;
|
|
@@ -45,19 +42,17 @@ export interface HistoryEntry {
|
|
|
45
42
|
status: 'running' | 'settled' | 'consumed' | 'closed';
|
|
46
43
|
outcome?: string | null;
|
|
47
44
|
createdAt: number;
|
|
48
|
-
/** M13c: durable consumption time, which provides the GC TTL basis. */
|
|
49
45
|
consumedAt?: number | null;
|
|
50
46
|
closedAt?: number | null;
|
|
51
|
-
/** Previous-generation pane ID, retained when work is redone or revived. */
|
|
52
47
|
revivedFrom?: string | null;
|
|
53
|
-
/**
|
|
54
|
-
* multiple entries in one second remain auditable (ledger row#23/24 proves both writes). */
|
|
48
|
+
/** Writer marker (spawn / poll-settle / poll-timeout / gc / zombie-sweep …) for auditability. */
|
|
55
49
|
via?: string;
|
|
56
50
|
}
|
|
57
51
|
|
|
58
|
-
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
52
|
+
/**
|
|
53
|
+
* When a patch omits `outcome`, inherit the latest non-empty value for the taskId: GC bookkeeping
|
|
54
|
+
* must not erase a settlement result under the "latest row is state" rule.
|
|
55
|
+
*/
|
|
61
56
|
export function inheritOutcome(lastOutcome: string | null | undefined, patchOutcome: string | null | undefined): string | null {
|
|
62
57
|
if (patchOutcome !== undefined) return patchOutcome;
|
|
63
58
|
return lastOutcome ?? null;
|
|
@@ -82,7 +77,6 @@ function coerceHistoryEntry(obj: unknown): HistoryEntry | null {
|
|
|
82
77
|
return { ...raw, kind: normalizeEntryKind(raw.kind) };
|
|
83
78
|
}
|
|
84
79
|
|
|
85
|
-
/** Parse line by line; skip junk JSON and records missing taskId/status. */
|
|
86
80
|
export function parseHistoryEntries(text: string): HistoryEntry[] {
|
|
87
81
|
const entries: HistoryEntry[] = [];
|
|
88
82
|
for (const line of (text ?? '').split(/\r?\n/)) {
|
|
@@ -107,7 +101,7 @@ export type HistoryReadResult =
|
|
|
107
101
|
| { status: 'missing' }
|
|
108
102
|
| { status: 'unreadable'; error: Error };
|
|
109
103
|
|
|
110
|
-
/** Distinguish missing vs permission/IO failure
|
|
104
|
+
/** Distinguish missing vs permission/IO failure; readHistory still returns [] for both. */
|
|
111
105
|
export function inspectHistory(file: string): HistoryReadResult {
|
|
112
106
|
try {
|
|
113
107
|
if (!fs.existsSync(file)) return { status: 'missing' };
|
|
@@ -118,15 +112,37 @@ export function inspectHistory(file: string): HistoryReadResult {
|
|
|
118
112
|
}
|
|
119
113
|
}
|
|
120
114
|
|
|
121
|
-
/** Read a history file; missing or unreadable yields []. */
|
|
122
115
|
export function readHistory(file: string): HistoryEntry[] {
|
|
123
116
|
const r = inspectHistory(file);
|
|
124
117
|
return r.status === 'ok' ? r.entries : [];
|
|
125
118
|
}
|
|
126
119
|
|
|
120
|
+
/** Every ledger under `<agentRoot>/herdr-pi/history`. Resume must see tasks spawned into another checkout. */
|
|
121
|
+
export function readHistoryTree(agentRoot: string): HistoryEntry[] {
|
|
122
|
+
const root = path.join(agentRoot, 'herdr-pi', 'history');
|
|
123
|
+
let names: string[];
|
|
124
|
+
try {
|
|
125
|
+
names = fs.readdirSync(root);
|
|
126
|
+
} catch {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
const all: HistoryEntry[] = [];
|
|
130
|
+
for (const name of names) {
|
|
131
|
+
let isDir = false;
|
|
132
|
+
try {
|
|
133
|
+
isDir = fs.statSync(path.join(root, name)).isDirectory();
|
|
134
|
+
} catch {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (!isDir) continue;
|
|
138
|
+
all.push(...readHistory(path.join(root, name, 'history.jsonl')));
|
|
139
|
+
}
|
|
140
|
+
return all;
|
|
141
|
+
}
|
|
142
|
+
|
|
127
143
|
export type HistoryWriteResult = { ok: true } | { ok: false; error: Error };
|
|
128
144
|
|
|
129
|
-
/**
|
|
145
|
+
/** Failures are logged and returned so the ledger is not silently lost. */
|
|
130
146
|
export function appendHistory(file: string, entry: HistoryEntry): HistoryWriteResult {
|
|
131
147
|
try {
|
|
132
148
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
@@ -139,7 +155,6 @@ export function appendHistory(file: string, entry: HistoryEntry): HistoryWriteRe
|
|
|
139
155
|
}
|
|
140
156
|
}
|
|
141
157
|
|
|
142
|
-
/** Group all generations of each taskId in ascending createdAt order for review. */
|
|
143
158
|
export function generationsByTask(entries: readonly HistoryEntry[]): Map<string, HistoryEntry[]> {
|
|
144
159
|
const map = new Map<string, HistoryEntry[]>();
|
|
145
160
|
for (const e of entries) {
|
|
@@ -151,7 +166,7 @@ export function generationsByTask(entries: readonly HistoryEntry[]): Map<string,
|
|
|
151
166
|
return map;
|
|
152
167
|
}
|
|
153
168
|
|
|
154
|
-
/**
|
|
169
|
+
/** Newest generation for taskId — the one used for resume. */
|
|
155
170
|
export function latestGeneration(entries: readonly HistoryEntry[], taskId: string): HistoryEntry | null {
|
|
156
171
|
const gens = generationsByTask(entries).get(taskId);
|
|
157
172
|
if (!gens || gens.length === 0) return null;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human-gate reporting: coalesces every blocking prompt (pier's ask tool, pi's own ctx.ui dialogs,
|
|
3
|
+
* external herdr:blocked producers) into ONE blocked edge.
|
|
4
|
+
*
|
|
5
|
+
* Depth semantics: only the 0→1 transition reports blocked (and marks the ask flag); only the 1→0
|
|
6
|
+
* transition restores working/idle. While the depth is positive, working/idle reports are dropped
|
|
7
|
+
* so the pane stays blocked for the whole dialog. */
|
|
8
|
+
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
9
|
+
import type { HerdrClientLike } from './herdr-client.ts';
|
|
10
|
+
|
|
11
|
+
/** ui_prompt events are typed by pi ≥ 1.1 (they postdated the 0.86 pin this gate was written against). */
|
|
12
|
+
|
|
13
|
+
/** Only inherently human-blocking kinds open the gate; `custom` covers resident overlays too (D97). */
|
|
14
|
+
const GATING_PROMPT_KINDS = new Set(['select', 'confirm', 'input', 'editor']);
|
|
15
|
+
|
|
16
|
+
export interface GateRuntimeDeps {
|
|
17
|
+
pi: ExtensionAPI;
|
|
18
|
+
client: HerdrClientLike;
|
|
19
|
+
/** Collapse the todo widget to one line while a gate is open; restore on release. */
|
|
20
|
+
rerenderWidget: () => void;
|
|
21
|
+
/** True while an agent turn is active (drives the state reported on release). */
|
|
22
|
+
isAgentActive: () => boolean;
|
|
23
|
+
/** Idle activity text (idle role badge / current todo) reported with the idle state. */
|
|
24
|
+
idleMessage: () => string | null;
|
|
25
|
+
/** Working activity text (current todo) reported when a gate releases mid-turn. */
|
|
26
|
+
workingMessage: () => string | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface GateRuntime {
|
|
30
|
+
depth(): number;
|
|
31
|
+
/** Gate-aware agent-state report (blocked depth swallows working/idle). */
|
|
32
|
+
report(state: 'working' | 'idle', activity: string | null): void;
|
|
33
|
+
/** Open one gate; true when this call opened the outer one. */
|
|
34
|
+
enter(label: string | null): boolean;
|
|
35
|
+
/** Close one gate; an unmatched release is ignored. True when the outer gate closed. */
|
|
36
|
+
exit(): boolean;
|
|
37
|
+
/** enter/exit + publish the herdr:blocked edge for pier's own gates. */
|
|
38
|
+
publish(active: boolean, label: string | null): void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function createGateRuntime(d: GateRuntimeDeps): GateRuntime {
|
|
42
|
+
const { client } = d;
|
|
43
|
+
let blockedDepth = 0;
|
|
44
|
+
|
|
45
|
+
function report(state: 'working' | 'idle', activity: string | null): void {
|
|
46
|
+
if (!client.available || blockedDepth > 0) return;
|
|
47
|
+
const message = activity ?? (state === 'idle' ? d.idleMessage() : null);
|
|
48
|
+
client.reportAgent(state, message).catch(() => {});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function enter(label: string | null): boolean {
|
|
52
|
+
blockedDepth += 1;
|
|
53
|
+
if (blockedDepth > 1) return false;
|
|
54
|
+
if (client.available) client.reportAgent('blocked', label).catch(() => {});
|
|
55
|
+
// D95: human-gate marker lets the workbench heatmap distinguish ask from block.
|
|
56
|
+
if (label) void client.reportAskFlag(label).catch(() => {});
|
|
57
|
+
d.rerenderWidget();
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function exit(): boolean {
|
|
62
|
+
if (blockedDepth === 0) return false;
|
|
63
|
+
blockedDepth -= 1;
|
|
64
|
+
d.rerenderWidget();
|
|
65
|
+
if (blockedDepth > 0) return false;
|
|
66
|
+
report(d.isAgentActive() ? 'working' : 'idle', d.isAgentActive() ? d.workingMessage() : null);
|
|
67
|
+
void client.reportAskFlag(null).catch(() => {});
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// emit() is synchronous; this flag stops our own listener from double-counting depth.
|
|
72
|
+
let publishing = false;
|
|
73
|
+
function emitHerdrBlocked(active: boolean, label: string | null): void {
|
|
74
|
+
publishing = true;
|
|
75
|
+
try {
|
|
76
|
+
d.pi.events.emit(
|
|
77
|
+
'herdr:blocked',
|
|
78
|
+
active ? { active: true, ...(label ? { label } : {}) } : { active: false },
|
|
79
|
+
);
|
|
80
|
+
} finally {
|
|
81
|
+
publishing = false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
d.pi.events.on('herdr:blocked', (data) => {
|
|
86
|
+
if (publishing) return;
|
|
87
|
+
const active = typeof data === 'object' && data !== null && 'active' in data && data.active === true;
|
|
88
|
+
if (active) {
|
|
89
|
+
enter(typeof data === 'object' && 'label' in data && typeof data.label === 'string' ? data.label : null);
|
|
90
|
+
} else {
|
|
91
|
+
exit();
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
d.pi.on('ui_prompt_start', (event) => {
|
|
96
|
+
const title = typeof event.title === 'string' && event.title.trim() ? event.title.trim() : null;
|
|
97
|
+
if (!GATING_PROMPT_KINDS.has(event.kind)) return;
|
|
98
|
+
const label = title ?? event.kind;
|
|
99
|
+
// The ask tool opens its own gate first, so this is usually a nested no-op;
|
|
100
|
+
// prompts from other extensions or pi core open the gate here.
|
|
101
|
+
if (enter(label)) emitHerdrBlocked(true, label);
|
|
102
|
+
});
|
|
103
|
+
d.pi.on('ui_prompt_end', () => {
|
|
104
|
+
if (exit()) emitHerdrBlocked(false, null);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
depth: () => blockedDepth,
|
|
109
|
+
report,
|
|
110
|
+
enter,
|
|
111
|
+
exit,
|
|
112
|
+
publish(active: boolean, label: string | null): void {
|
|
113
|
+
if (active) enter(label);
|
|
114
|
+
else exit();
|
|
115
|
+
emitHerdrBlocked(active, label);
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
package/src/index-locks.ts
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* Why: lock acquire/release/command lived in the composition root next to
|
|
5
|
-
* unrelated session wiring. The installer owns the three pi hooks and the
|
|
6
|
-
* held-lock set; fail-open on agent.list remains the collaboration policy.
|
|
7
|
-
*/
|
|
1
|
+
/** M18 file write-lock installer (soft veto / hard block): owns the three pi hooks and the
|
|
2
|
+
* held-lock set; fail-open on agent.list is the collaboration policy. */
|
|
8
3
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
9
4
|
import {
|
|
10
5
|
acquireTokensFor,
|
package/src/index-master.ts
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* Why: worker must never load bootstrap/subagent-scope (C3). This module is
|
|
5
|
-
* dynamically imported only from the master branch of index.ts.
|
|
6
|
-
*/
|
|
1
|
+
/** Master-process plugin mount (cordis loader + terminal/todo/subagent), dynamically imported from
|
|
2
|
+
* the master branch of index.ts only: a worker must never load bootstrap/subagent-poller (C3). */
|
|
7
3
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
8
4
|
import type { Context } from '@deepseek-ai/cordis';
|
|
9
5
|
import type { Server } from 'node:net';
|
|
10
6
|
import { PiSurface } from './pi-surface.ts';
|
|
11
7
|
import { createCordisApp } from './bootstrap.ts';
|
|
12
|
-
import {
|
|
13
|
-
import
|
|
14
|
-
import
|
|
15
|
-
import
|
|
8
|
+
import { baseTodoDeps, cordisProvide } from './cordis-services.ts';
|
|
9
|
+
import { disposeSessionRoot } from './subagent-poller.ts';
|
|
10
|
+
import terminalPlugin from './plugins/terminal.ts';
|
|
11
|
+
import todoPlugin from './plugins/todo.ts';
|
|
12
|
+
import subagentPlugin from './plugins/subagent.ts';
|
|
16
13
|
import type { HerdrClientLike, HerdrEnv } from './herdr-client.ts';
|
|
14
|
+
import type { JevRuntime } from './jev-client.ts';
|
|
15
|
+
import type { RoutingTelemetryRecord } from './routing-telemetry.ts';
|
|
17
16
|
import type { TodosService } from './todos-service.ts';
|
|
18
|
-
import type { TodoUiSlot } from './
|
|
19
|
-
import type { SubagentPortBox } from './subagent-
|
|
17
|
+
import type { TodoUiSlot } from './plugins/todo.ts';
|
|
18
|
+
import type { SubagentPortBox } from './subagent-core.ts';
|
|
19
|
+
import type { WorkerWidgetRow } from './tree-view.ts';
|
|
20
20
|
|
|
21
21
|
export interface MasterPluginMount {
|
|
22
22
|
pi: ExtensionAPI;
|
|
@@ -37,6 +37,12 @@ export interface MasterPluginMount {
|
|
|
37
37
|
claimSettleNotice: (key: string) => boolean;
|
|
38
38
|
isCompactionInFlight?: () => boolean;
|
|
39
39
|
isIntentionalAbort?: () => boolean;
|
|
40
|
+
/** Optional jev seam for subagent settle attribution (fail-open when absent). */
|
|
41
|
+
jev?: { ask: JevRuntime['ask']; getMinConfidence: () => number };
|
|
42
|
+
/** Phase 0 routing telemetry append (RFC rfc-jev-role-routing §8); absent in tests. */
|
|
43
|
+
appendRoutingLog?: (row: RoutingTelemetryRecord) => void;
|
|
44
|
+
/** D113: running subagent rows joined with their cached todos — feeds the unified widget. */
|
|
45
|
+
getWorkers?: () => readonly WorkerWidgetRow[];
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
async function loadEntry(
|
|
@@ -62,8 +68,7 @@ export async function mountMasterPlugins(m: MasterPluginMount): Promise<void> {
|
|
|
62
68
|
// strip-types) and misses pi's typebox alias. HMR-only; production uses plugin().
|
|
63
69
|
const useLoader = cordisApp.loaderReady && cordisApp.hmrActive;
|
|
64
70
|
|
|
65
|
-
// Pipe server is created in the common segment; closing it from a core
|
|
66
|
-
// plugin would kill it on hmr reload (d87).
|
|
71
|
+
// Pipe server is created in the common segment; closing it from a core plugin would kill it on hmr reload (d87).
|
|
67
72
|
sessionRoot.effect(() => () => {
|
|
68
73
|
if (m.pipeServerBox.current) {
|
|
69
74
|
try { m.pipeServerBox.current.close(); } catch { /* already closed */ }
|
|
@@ -72,25 +77,19 @@ export async function mountMasterPlugins(m: MasterPluginMount): Promise<void> {
|
|
|
72
77
|
}, 'pipe-server');
|
|
73
78
|
|
|
74
79
|
const surface = new PiSurface(m.pi as unknown as object, cordisApp.ledger);
|
|
75
|
-
sessionRoot
|
|
80
|
+
cordisProvide(sessionRoot, 'pi-herdr.surface', surface);
|
|
76
81
|
const terminalDeps = {
|
|
77
82
|
client: m.client,
|
|
78
83
|
env: m.env,
|
|
79
84
|
state: { activePaneIds: (): Set<string> => new Set() },
|
|
80
85
|
};
|
|
81
|
-
sessionRoot
|
|
86
|
+
cordisProvide(sessionRoot, 'pi-herdr.terminal-deps', terminalDeps);
|
|
82
87
|
|
|
83
|
-
await loadEntry(sessionRoot, useLoader, './
|
|
88
|
+
await loadEntry(sessionRoot, useLoader, './plugins/terminal.ts', terminalPlugin);
|
|
84
89
|
|
|
85
|
-
sessionRoot
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
maxItems: 15,
|
|
89
|
-
mirrorTodos: m.mirrorTodos,
|
|
90
|
-
appendEntry: (customType: string, data: unknown) => {
|
|
91
|
-
(m.pi as { appendEntry?: (t: string, d: unknown) => void }).appendEntry?.(customType, data);
|
|
92
|
-
},
|
|
93
|
-
state: m.todoUi,
|
|
90
|
+
cordisProvide(sessionRoot, 'pi-herdr.todo-deps', {
|
|
91
|
+
...baseTodoDeps(m),
|
|
92
|
+
...(m.getWorkers ? { getWorkers: m.getWorkers } : {}),
|
|
94
93
|
getBlockedDepth: m.getBlockedDepth,
|
|
95
94
|
stopReminder: {
|
|
96
95
|
getBlockedDepth: m.getBlockedDepth,
|
|
@@ -99,9 +98,10 @@ export async function mountMasterPlugins(m: MasterPluginMount): Promise<void> {
|
|
|
99
98
|
isIntentionalAbort: m.isIntentionalAbort,
|
|
100
99
|
},
|
|
101
100
|
});
|
|
102
|
-
await loadEntry(sessionRoot, useLoader, './
|
|
101
|
+
await loadEntry(sessionRoot, useLoader, './plugins/todo.ts', todoPlugin);
|
|
103
102
|
|
|
104
|
-
|
|
103
|
+
// Dropped keys fail here: cordisProvide checks the bag against PierCordisServices (fe99252).
|
|
104
|
+
cordisProvide(sessionRoot, 'pi-herdr.subagent-deps', {
|
|
105
105
|
client: m.client,
|
|
106
106
|
env: m.env,
|
|
107
107
|
extPath: m.extPath,
|
|
@@ -114,8 +114,10 @@ export async function mountMasterPlugins(m: MasterPluginMount): Promise<void> {
|
|
|
114
114
|
withReconcileNotes: m.withReconcileNotes,
|
|
115
115
|
claimSettleNotice: m.claimSettleNotice,
|
|
116
116
|
terminalState: terminalDeps.state,
|
|
117
|
+
...(m.jev ? { jev: m.jev } : {}),
|
|
118
|
+
...(m.appendRoutingLog ? { logRouting: m.appendRoutingLog } : {}),
|
|
117
119
|
});
|
|
118
|
-
await loadEntry(sessionRoot, useLoader, './
|
|
120
|
+
await loadEntry(sessionRoot, useLoader, './plugins/subagent.ts', subagentPlugin);
|
|
119
121
|
|
|
120
122
|
m.pi.on('session_shutdown', () => {
|
|
121
123
|
void disposeSessionRoot(sessionRoot);
|
package/src/index-notices.ts
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* D92 settlement-notice buffer (
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* tool calls — a long master run queues every settlement and floods at the
|
|
6
|
-
* end. Buffer while busy; flush at turn_end (steer) / agent_settled (followUp).
|
|
7
|
-
*/
|
|
8
|
-
import { NOTICE_MAX_SHOWN, collapseNotices } from './notice-buffer.ts';
|
|
2
|
+
* D92 settlement-notice buffer: `sendUserMessage(followUp)` only delivers when the agent has no
|
|
3
|
+
* more tool calls, so a long master run would queue every settlement and flood at the end. Buffer
|
|
4
|
+
* while busy; flush at turn_end (steer) / agent_settled (followUp), collapsing large batches. */
|
|
9
5
|
|
|
10
6
|
export type NoticeSendMode = 'steer' | 'followUp';
|
|
11
7
|
|
|
8
|
+
export const NOTICE_MAX_SHOWN = 3;
|
|
9
|
+
|
|
10
|
+
/** Collapse a batch into one message body: empty → null; above the cap, the first max originals plus
|
|
11
|
+
* a tail pointing at history / subagent list. A single-item batch stays byte-identical to the
|
|
12
|
+
* direct-send format. */
|
|
13
|
+
export function collapseNotices(contents: readonly string[], max = NOTICE_MAX_SHOWN): string | null {
|
|
14
|
+
if (contents.length === 0) return null;
|
|
15
|
+
if (contents.length <= max) return contents.join('\n\n');
|
|
16
|
+
const hidden = contents.length - max;
|
|
17
|
+
const tail = `…另有 ${hidden} 条结算未逐条展示。全量结果看 history 台账(路径公式见 subagent resume 工具描述);在跑代理用 subagent list 查看。`;
|
|
18
|
+
return [...contents.slice(0, max), tail].join('\n\n');
|
|
19
|
+
}
|
|
20
|
+
|
|
12
21
|
export interface NoticeBuffer {
|
|
13
22
|
deliverNotice(content: string, paneId?: string): Promise<void>;
|
|
14
23
|
noticePending(): ReadonlySet<string>;
|
|
@@ -20,19 +29,18 @@ export function createNoticeBuffer(opts: {
|
|
|
20
29
|
isBusy: () => boolean;
|
|
21
30
|
send: (content: string, mode: NoticeSendMode) => Promise<void>;
|
|
22
31
|
/**
|
|
23
|
-
* P0-2 (RFC docs/rfc-jev-integration.md §3): relevance reorder of a collapsed
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*/
|
|
32
|
+
* P0-2 (RFC docs/rfc-jev-integration.md §3): relevance reorder of a collapsed batch before truncation.
|
|
33
|
+
* Only consulted above the show cap; returning null (or omitting the hook) keeps arrival order.
|
|
34
|
+
* Implementations must not throw. */
|
|
27
35
|
rank?: (contents: readonly string[]) => Promise<readonly string[] | null>;
|
|
28
36
|
}): NoticeBuffer {
|
|
29
|
-
const pending: string
|
|
37
|
+
const pending: Array<{ content: string; paneId?: string }> = [];
|
|
30
38
|
const pendingPaneIds = new Set<string>();
|
|
31
39
|
return {
|
|
32
40
|
async deliverNotice(content, paneId) {
|
|
33
41
|
if (content !== '' && paneId !== undefined) pendingPaneIds.add(paneId);
|
|
34
42
|
if (opts.isBusy()) {
|
|
35
|
-
pending.push(content);
|
|
43
|
+
pending.push({ content, paneId });
|
|
36
44
|
return;
|
|
37
45
|
}
|
|
38
46
|
await opts.send(content, 'followUp');
|
|
@@ -41,12 +49,17 @@ export function createNoticeBuffer(opts: {
|
|
|
41
49
|
noticePending: () => pendingPaneIds,
|
|
42
50
|
async flush(mode) {
|
|
43
51
|
if (pending.length === 0) return;
|
|
44
|
-
const
|
|
52
|
+
const flushed = pending.splice(0);
|
|
53
|
+
const batch = flushed.map((n) => n.content);
|
|
45
54
|
let ordered: readonly string[] = batch;
|
|
46
55
|
if (opts.rank && batch.length > NOTICE_MAX_SHOWN) {
|
|
47
56
|
ordered = (await opts.rank(batch)) ?? batch;
|
|
48
57
|
}
|
|
49
|
-
|
|
58
|
+
// Release only this batch's panes: a notice buffered during the rank await is still undelivered,
|
|
59
|
+
// and GC must keep its pane until it is.
|
|
60
|
+
for (const { paneId } of flushed) {
|
|
61
|
+
if (paneId !== undefined && !pending.some((n) => n.paneId === paneId)) pendingPaneIds.delete(paneId);
|
|
62
|
+
}
|
|
50
63
|
const collapsed = collapseNotices(ordered);
|
|
51
64
|
if (collapsed) await opts.send(collapsed, mode);
|
|
52
65
|
},
|
package/src/index-pipe.ts
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Common-segment pipe request handler (prompt / interrupt / reply).
|
|
3
|
-
*
|
|
4
|
-
* Why: index.ts owned the NDJSON switch next to session lifecycle. The
|
|
5
|
-
* dispatch is a pure-ish adapter over the subagent port and notice buffer.
|
|
6
|
-
*/
|
|
1
|
+
/** Common-segment pipe request handler (prompt / interrupt / reply) over the subagent port and notice buffer. */
|
|
7
2
|
import { formatSettlementNotice } from './vocab.ts';
|
|
8
3
|
import type { PipeRequest, PipeResponse } from './pipe-channel.ts';
|
|
9
|
-
import type { SubagentPortBox } from './subagent-
|
|
4
|
+
import type { SubagentPortBox } from './subagent-core.ts';
|
|
10
5
|
|
|
11
6
|
export interface MachineRequest {
|
|
12
7
|
id: string;
|
|
@@ -24,6 +19,12 @@ export interface PipeHandlerSession {
|
|
|
24
19
|
sendUserMessageAs: (content: string, mode: 'steer' | 'followUp') => Promise<void>;
|
|
25
20
|
abort: () => void;
|
|
26
21
|
setPendingMachineRequest: (req: MachineRequest | null) => void;
|
|
22
|
+
/** P0 (RFC §4.4): apply a role switch requested over the pipe (master → worker). */
|
|
23
|
+
applyRoleSwitch: (role: string, switchedBy: string) => Promise<{ ok: boolean; message: string }>;
|
|
24
|
+
/** Phase 2 (RFC rfc-delegation-identity §3.2): worker → master progress note (display-less custom message). */
|
|
25
|
+
deliverProgress: (workerPaneId: string, text: string) => Promise<void>;
|
|
26
|
+
/** D113 (RFC rfc-unified-widget): worker → master full todo push; updates the widget cache. */
|
|
27
|
+
applyWorkerTodos: (workerPaneId: string, items: Array<{ content: string; status: string; blocker?: string }>, summary: string) => void;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
export async function handlePipeRequest(
|
|
@@ -51,13 +52,31 @@ export async function handlePipeRequest(
|
|
|
51
52
|
s.abort();
|
|
52
53
|
s.setPendingMachineRequest(null);
|
|
53
54
|
return { type: 'ok', id: req.id };
|
|
55
|
+
case 'role': {
|
|
56
|
+
const res = await s.applyRoleSwitch(req.role, s.paneId || 'pipe');
|
|
57
|
+
return res.ok
|
|
58
|
+
? { type: 'ok', id: req.id, detail: res.message }
|
|
59
|
+
: { type: 'error', id: req.id, message: res.message };
|
|
60
|
+
}
|
|
61
|
+
case 'progress': {
|
|
62
|
+
// Never triggers a master turn and never floods the visible chat (RFC §3.2); the
|
|
63
|
+
// display-less custom message rides context only, rendered as a compact card.
|
|
64
|
+
await s.deliverProgress(req.paneId, req.text);
|
|
65
|
+
return { type: 'ok', id: req.id };
|
|
66
|
+
}
|
|
67
|
+
case 'todo_sync': {
|
|
68
|
+
s.applyWorkerTodos(req.paneId, req.items, req.summary);
|
|
69
|
+
return { type: 'ok', id: req.id };
|
|
70
|
+
}
|
|
54
71
|
case 'reply': {
|
|
55
72
|
s.port.current?.applyReplySession(req.paneId, req.sessionFile);
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
//
|
|
60
|
-
// a
|
|
73
|
+
// Consume before the finish notice: otherwise listRunningSubs still reports the pane and the
|
|
74
|
+
// next master settle injects "still running" for an agent just announced finished.
|
|
75
|
+
s.port.current?.consumeReply(req.paneId, req.text);
|
|
76
|
+
// B8: the claim key `${paneId}:${requestId}` must match the poll loop's (subagent-poller.ts):
|
|
77
|
+
// a child echoes the id of the pipe request it answered (`id: req.id` in index.ts's settle
|
|
78
|
+
// push) and the parent hands that same id to startPoller (`prompt-<taskId>` / `fu-<ts>`).
|
|
79
|
+
// A mismatch surfaces as two "finished" notices for one settlement.
|
|
61
80
|
if (s.claimSettleNotice(`${req.paneId}:${req.id}`)) {
|
|
62
81
|
const notes = s.port.current?.reconcileOnReply(req.paneId) ?? [];
|
|
63
82
|
const statLine = await s.port.current?.settleStatLine(req.paneId) ?? null;
|