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
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import type { HerdrAgentState, HerdrClientLike } from './herdr-client.ts';
|
|
2
|
-
import type { SubEntry } from './subagent-core.ts';
|
|
3
|
-
import {
|
|
4
|
-
computeSubagentOutputDelta,
|
|
5
|
-
formatSubagentOutput,
|
|
6
|
-
resolveSubagentStatus,
|
|
7
|
-
type SubagentOutputCursor,
|
|
8
|
-
} from './subagent-output-core.ts';
|
|
9
|
-
import { readSubagentOutput } from './subagent-output-adapter.ts';
|
|
10
|
-
import { toolError } from './tool-error.ts';
|
|
11
|
-
|
|
12
|
-
export interface OutputActionParams {
|
|
13
|
-
agentId?: unknown;
|
|
14
|
-
taskId?: unknown;
|
|
15
|
-
max_chars?: unknown;
|
|
16
|
-
maxChars?: unknown;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface OutputActionResult {
|
|
20
|
-
content: Array<{ type: 'text'; text: string }>;
|
|
21
|
-
details: Record<string, unknown>;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface OutputActionDeps {
|
|
25
|
-
client: HerdrClientLike;
|
|
26
|
-
resolveEntry: (rawId: string, cwd: string) => { entry: SubEntry } | { error: string };
|
|
27
|
-
readAskFlag: (paneId: string) => Promise<string | null>;
|
|
28
|
-
outputCursors: Map<string, SubagentOutputCursor>;
|
|
29
|
-
getCwd: (toolCtx: unknown) => string;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export async function executeSubagentOutput(
|
|
33
|
-
params: OutputActionParams | undefined,
|
|
34
|
-
toolCtx: unknown,
|
|
35
|
-
deps: OutputActionDeps,
|
|
36
|
-
): Promise<OutputActionResult> {
|
|
37
|
-
const rawId = String(params?.agentId ?? params?.taskId ?? '').trim();
|
|
38
|
-
if (!rawId) return toolError('Error: missing agentId for output (see action list)') as OutputActionResult;
|
|
39
|
-
|
|
40
|
-
const cwd = deps.getCwd(toolCtx);
|
|
41
|
-
const resolved = deps.resolveEntry(rawId, cwd);
|
|
42
|
-
if ('error' in resolved) return toolError(resolved.error) as OutputActionResult;
|
|
43
|
-
|
|
44
|
-
const entry = resolved.entry;
|
|
45
|
-
let agentState: HerdrAgentState | null = null;
|
|
46
|
-
let askFlag: string | null = null;
|
|
47
|
-
try {
|
|
48
|
-
const agents = await deps.client.listAgents();
|
|
49
|
-
const agent = agents.find((candidate) => candidate.paneId === entry.paneId);
|
|
50
|
-
agentState = agent?.status ?? null;
|
|
51
|
-
askFlag = agent?.tokens?.['pi-ask'] ?? null;
|
|
52
|
-
} catch {
|
|
53
|
-
/* Best effort: output remains useful without live status. */
|
|
54
|
-
}
|
|
55
|
-
if (!askFlag && agentState === 'blocked') {
|
|
56
|
-
try {
|
|
57
|
-
askFlag = await deps.readAskFlag(entry.paneId);
|
|
58
|
-
} catch {
|
|
59
|
-
/* Best effort. */
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const status = resolveSubagentStatus({
|
|
64
|
-
localStatus: entry.status,
|
|
65
|
-
herdrStatus: agentState,
|
|
66
|
-
hasAskFlag: Boolean(askFlag),
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
let rawRead: { text: string; revision: number; truncated: boolean };
|
|
70
|
-
try {
|
|
71
|
-
rawRead = await readSubagentOutput(deps.client, entry.paneId);
|
|
72
|
-
} catch (err) {
|
|
73
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
74
|
-
return toolError(`Error: failed to read output for subagent ${entry.paneId}: ${message}`) as OutputActionResult;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const prevCursor = deps.outputCursors.get(entry.paneId);
|
|
78
|
-
const maxChars = typeof params?.max_chars === 'number'
|
|
79
|
-
? params.max_chars
|
|
80
|
-
: typeof params?.maxChars === 'number' ? params.maxChars : undefined;
|
|
81
|
-
const deltaResult = computeSubagentOutputDelta(prevCursor, rawRead.text, { maxChars });
|
|
82
|
-
deps.outputCursors.set(entry.paneId, deltaResult.nextCursor);
|
|
83
|
-
|
|
84
|
-
const formatted = formatSubagentOutput({
|
|
85
|
-
paneId: entry.paneId,
|
|
86
|
-
status,
|
|
87
|
-
revision: rawRead.revision,
|
|
88
|
-
bufferTruncated: rawRead.truncated,
|
|
89
|
-
deltaResult,
|
|
90
|
-
askQuestion: askFlag,
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
return {
|
|
94
|
-
content: [{ type: 'text', text: formatted }],
|
|
95
|
-
details: {
|
|
96
|
-
paneId: entry.paneId,
|
|
97
|
-
status,
|
|
98
|
-
revision: rawRead.revision,
|
|
99
|
-
truncated: rawRead.truncated || deltaResult.truncated,
|
|
100
|
-
restart: deltaResult.restart,
|
|
101
|
-
deltaLength: deltaResult.delta.length,
|
|
102
|
-
},
|
|
103
|
-
};
|
|
104
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Herdr output-read adapter for subagents.
|
|
3
|
-
*
|
|
4
|
-
* Newer herdr exposes agent.read; older versions only expose pane.read. Prefer the semantic agent
|
|
5
|
-
* endpoint and fall back to the pane endpoint so output observation stays version-compatible.
|
|
6
|
-
*/
|
|
7
|
-
import type { HerdrClientLike } from './herdr-client.ts';
|
|
8
|
-
|
|
9
|
-
export interface SubagentOutputRead {
|
|
10
|
-
text: string;
|
|
11
|
-
revision: number;
|
|
12
|
-
truncated: boolean;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export async function readSubagentOutput(
|
|
16
|
-
client: HerdrClientLike,
|
|
17
|
-
paneId: string,
|
|
18
|
-
): Promise<SubagentOutputRead> {
|
|
19
|
-
if (typeof client.readAgent === 'function') {
|
|
20
|
-
try {
|
|
21
|
-
return await client.readAgent(paneId, { source: 'recent', stripAnsi: true });
|
|
22
|
-
} catch {
|
|
23
|
-
return await client.readPane(paneId, { source: 'recent', stripAnsi: true });
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return await client.readPane(paneId, { source: 'recent', stripAnsi: true });
|
|
27
|
-
}
|
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* subagent-output-core.ts
|
|
3
|
-
*
|
|
4
|
-
* Pure functions for subagent incremental output extraction:
|
|
5
|
-
* - Cursor management (in-memory per-pane tail tracking)
|
|
6
|
-
* - Longest common suffix/prefix delta computation
|
|
7
|
-
* - Boundary handling for scrolling, clear-screen, repeated text (graceful degradation with restart: true)
|
|
8
|
-
* - Bounded character budgeting and metadata formatting
|
|
9
|
-
*/
|
|
10
|
-
import type { HerdrAgentState } from './herdr-client.ts';
|
|
11
|
-
import type { SubEntry } from './subagent-core.ts';
|
|
12
|
-
|
|
13
|
-
export const OUTPUT_DEFAULT_MAX_CHARS = 6000;
|
|
14
|
-
export const OUTPUT_HARD_CAP_CHARS = 16000;
|
|
15
|
-
export const OUTPUT_MIN_OVERLAP = 16;
|
|
16
|
-
export const OUTPUT_TAIL_CHARS = 500;
|
|
17
|
-
|
|
18
|
-
export type SubagentStatus = 'running' | 'idle' | 'blocked' | 'settled';
|
|
19
|
-
|
|
20
|
-
export interface SubagentOutputCursor {
|
|
21
|
-
/** Suffix of previous full text used for boundary matching */
|
|
22
|
-
tail: string;
|
|
23
|
-
/** Length of previous full text */
|
|
24
|
-
fullLength: number;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface SubagentOutputDeltaOptions {
|
|
28
|
-
/** Max characters of delta to return (default: 6000) */
|
|
29
|
-
maxChars?: number;
|
|
30
|
-
/** Minimum overlap length required for partial scroll matching (default: 16) */
|
|
31
|
-
minOverlap?: number;
|
|
32
|
-
/** Length of tail to retain in cursor (default: 500) */
|
|
33
|
-
tailChars?: number;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface SubagentOutputDeltaResult {
|
|
37
|
-
/** The new output text (or full bounded text on initial/restart) */
|
|
38
|
-
delta: string;
|
|
39
|
-
/** Whether the increment could not be determined reliably (scrolled away, cleared, ambiguous repeat) */
|
|
40
|
-
restart: boolean;
|
|
41
|
-
/** Whether delta was truncated to maxChars */
|
|
42
|
-
truncated: boolean;
|
|
43
|
-
/** New cursor to save for next call */
|
|
44
|
-
nextCursor: SubagentOutputCursor;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Extract tail characters from a text buffer for cursor matching.
|
|
49
|
-
*/
|
|
50
|
-
export function extractTail(text: string, tailChars = OUTPUT_TAIL_CHARS): string {
|
|
51
|
-
if (text.length <= tailChars) return text;
|
|
52
|
-
return text.slice(text.length - tailChars);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Bound text to a maximum character budget, retaining the tail (most recent output).
|
|
57
|
-
*/
|
|
58
|
-
export function boundText(text: string, maxChars: number): { text: string; truncated: boolean } {
|
|
59
|
-
if (text.length <= maxChars) {
|
|
60
|
-
return { text, truncated: false };
|
|
61
|
-
}
|
|
62
|
-
return {
|
|
63
|
-
text: text.slice(text.length - maxChars),
|
|
64
|
-
truncated: true,
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Pure function: Given previous cursor (tail + fullLength) and current full buffer text,
|
|
70
|
-
* compute the incremental delta since last read.
|
|
71
|
-
*
|
|
72
|
-
* Boundary handling:
|
|
73
|
-
* 1. Initial read (prevCursor is null/empty): returns full bounded text, restart=false.
|
|
74
|
-
* 2. No change: fullLength and tail match, delta is empty, restart=false.
|
|
75
|
-
* 3. Normal append: prevTail appears uniquely in currText -> delta is text after prevTail.
|
|
76
|
-
* 4. Repeated text: prevTail appears multiple times -> ambiguous, returns full bounded text with restart=true.
|
|
77
|
-
* 5. Partial buffer scroll: prevTail partially scrolled off top -> longest suffix of prevTail
|
|
78
|
-
* matching prefix of currText (>= minOverlap) identifies boundary.
|
|
79
|
-
* 6. Full scroll / clear screen: no valid overlap found -> returns full bounded text with restart=true.
|
|
80
|
-
*/
|
|
81
|
-
export function computeSubagentOutputDelta(
|
|
82
|
-
prevCursor: SubagentOutputCursor | null | undefined,
|
|
83
|
-
currText: string,
|
|
84
|
-
options?: SubagentOutputDeltaOptions,
|
|
85
|
-
): SubagentOutputDeltaResult {
|
|
86
|
-
const rawMaxChars = options?.maxChars ?? OUTPUT_DEFAULT_MAX_CHARS;
|
|
87
|
-
const maxChars = Math.max(1, Math.min(rawMaxChars, OUTPUT_HARD_CAP_CHARS));
|
|
88
|
-
const minOverlap = options?.minOverlap ?? OUTPUT_MIN_OVERLAP;
|
|
89
|
-
const tailChars = options?.tailChars ?? OUTPUT_TAIL_CHARS;
|
|
90
|
-
|
|
91
|
-
// Case 1: First call (no previous cursor)
|
|
92
|
-
if (!prevCursor || !prevCursor.tail) {
|
|
93
|
-
const bounded = boundText(currText, maxChars);
|
|
94
|
-
return {
|
|
95
|
-
delta: bounded.text,
|
|
96
|
-
restart: false,
|
|
97
|
-
truncated: bounded.truncated,
|
|
98
|
-
nextCursor: {
|
|
99
|
-
tail: extractTail(currText, tailChars),
|
|
100
|
-
fullLength: currText.length,
|
|
101
|
-
},
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const prevTail = prevCursor.tail;
|
|
106
|
-
|
|
107
|
-
// Case 2: Exact same content (no new output)
|
|
108
|
-
if (currText.length === prevCursor.fullLength && currText.endsWith(prevTail)) {
|
|
109
|
-
return {
|
|
110
|
-
delta: '',
|
|
111
|
-
restart: false,
|
|
112
|
-
truncated: false,
|
|
113
|
-
nextCursor: prevCursor,
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Case 3: prevTail appears in currText
|
|
118
|
-
const firstIdx = currText.indexOf(prevTail);
|
|
119
|
-
const lastIdx = currText.lastIndexOf(prevTail);
|
|
120
|
-
|
|
121
|
-
if (firstIdx !== -1) {
|
|
122
|
-
// 3a: Ambiguous duplicate occurrences in current text
|
|
123
|
-
if (firstIdx !== lastIdx) {
|
|
124
|
-
const bounded = boundText(currText, maxChars);
|
|
125
|
-
return {
|
|
126
|
-
delta: bounded.text,
|
|
127
|
-
restart: true,
|
|
128
|
-
truncated: bounded.truncated,
|
|
129
|
-
nextCursor: {
|
|
130
|
-
tail: extractTail(currText, tailChars),
|
|
131
|
-
fullLength: currText.length,
|
|
132
|
-
},
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// 3b: Unique match
|
|
137
|
-
const rawDelta = currText.slice(firstIdx + prevTail.length);
|
|
138
|
-
if (rawDelta.length === 0) {
|
|
139
|
-
return {
|
|
140
|
-
delta: '',
|
|
141
|
-
restart: false,
|
|
142
|
-
truncated: false,
|
|
143
|
-
nextCursor: prevCursor,
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const bounded = boundText(rawDelta, maxChars);
|
|
148
|
-
return {
|
|
149
|
-
delta: bounded.text,
|
|
150
|
-
restart: false,
|
|
151
|
-
truncated: bounded.truncated,
|
|
152
|
-
nextCursor: {
|
|
153
|
-
tail: extractTail(currText, tailChars),
|
|
154
|
-
fullLength: currText.length,
|
|
155
|
-
},
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Case 4: prevTail is not found in full. Check if a suffix of prevTail matches a prefix of currText
|
|
160
|
-
// (terminal buffer scrolled lines off the top).
|
|
161
|
-
let longestOverlap = 0;
|
|
162
|
-
const maxPossibleOverlap = Math.min(prevTail.length - 1, currText.length);
|
|
163
|
-
const effectiveMinOverlap = Math.min(minOverlap, prevTail.length);
|
|
164
|
-
|
|
165
|
-
for (let k = maxPossibleOverlap; k >= effectiveMinOverlap; k--) {
|
|
166
|
-
const suffix = prevTail.slice(prevTail.length - k);
|
|
167
|
-
if (currText.startsWith(suffix)) {
|
|
168
|
-
longestOverlap = k;
|
|
169
|
-
break;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (longestOverlap >= effectiveMinOverlap) {
|
|
174
|
-
const rawDelta = currText.slice(longestOverlap);
|
|
175
|
-
const bounded = boundText(rawDelta, maxChars);
|
|
176
|
-
return {
|
|
177
|
-
delta: bounded.text,
|
|
178
|
-
restart: false,
|
|
179
|
-
truncated: bounded.truncated,
|
|
180
|
-
nextCursor: {
|
|
181
|
-
tail: extractTail(currText, tailChars),
|
|
182
|
-
fullLength: currText.length,
|
|
183
|
-
},
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Case 5: Clear screen or full scroll out -> degraded reset
|
|
188
|
-
const bounded = boundText(currText, maxChars);
|
|
189
|
-
return {
|
|
190
|
-
delta: bounded.text,
|
|
191
|
-
restart: true,
|
|
192
|
-
truncated: bounded.truncated,
|
|
193
|
-
nextCursor: {
|
|
194
|
-
tail: extractTail(currText, tailChars),
|
|
195
|
-
fullLength: currText.length,
|
|
196
|
-
},
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Resolve subagent semantic status from local subs registry and herdr agent state.
|
|
202
|
-
*/
|
|
203
|
-
export function resolveSubagentStatus(opts: {
|
|
204
|
-
localStatus: SubEntry['status'];
|
|
205
|
-
herdrStatus?: HerdrAgentState | null;
|
|
206
|
-
hasAskFlag?: boolean;
|
|
207
|
-
}): SubagentStatus {
|
|
208
|
-
if (opts.hasAskFlag || opts.herdrStatus === 'blocked') {
|
|
209
|
-
return 'blocked';
|
|
210
|
-
}
|
|
211
|
-
if (opts.herdrStatus === 'working') {
|
|
212
|
-
return 'running';
|
|
213
|
-
}
|
|
214
|
-
if (opts.localStatus === 'settled' || opts.localStatus === 'closed' || opts.herdrStatus === 'done') {
|
|
215
|
-
return 'settled';
|
|
216
|
-
}
|
|
217
|
-
if (opts.herdrStatus === 'idle') {
|
|
218
|
-
return 'idle';
|
|
219
|
-
}
|
|
220
|
-
if (opts.localStatus === 'running') {
|
|
221
|
-
return 'running';
|
|
222
|
-
}
|
|
223
|
-
return 'running';
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
export interface FormatSubagentOutputOpts {
|
|
227
|
-
paneId: string;
|
|
228
|
-
status: SubagentStatus;
|
|
229
|
-
revision: number;
|
|
230
|
-
bufferTruncated: boolean;
|
|
231
|
-
deltaResult: SubagentOutputDeltaResult;
|
|
232
|
-
askQuestion?: string | null;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Format subagent output for display to the master model.
|
|
237
|
-
*/
|
|
238
|
-
export function formatSubagentOutput(opts: FormatSubagentOutputOpts): string {
|
|
239
|
-
const parts: string[] = [];
|
|
240
|
-
const metaItems: string[] = [
|
|
241
|
-
`pane: ${opts.paneId}`,
|
|
242
|
-
`status: ${opts.status}${opts.status === 'blocked' && opts.askQuestion ? ` (question: "${opts.askQuestion}")` : ''}`,
|
|
243
|
-
`revision: ${opts.revision}`,
|
|
244
|
-
];
|
|
245
|
-
|
|
246
|
-
if (opts.deltaResult.restart) {
|
|
247
|
-
metaItems.push('restart: true (buffer reset or scrolled)');
|
|
248
|
-
}
|
|
249
|
-
if (opts.bufferTruncated || opts.deltaResult.truncated) {
|
|
250
|
-
metaItems.push('truncated: true');
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const header = `[Subagent Output | ${metaItems.join(' | ')}]`;
|
|
254
|
-
parts.push(header);
|
|
255
|
-
|
|
256
|
-
if (opts.deltaResult.delta.length === 0) {
|
|
257
|
-
parts.push('(no new output since last read)');
|
|
258
|
-
} else {
|
|
259
|
-
parts.push(opts.deltaResult.delta);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
return parts.join('\n');
|
|
263
|
-
}
|
|
@@ -1,305 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Background settlement poller (takeover, blocked gate, observation, vacuum).
|
|
3
|
-
*
|
|
4
|
-
* Why: planners already live in subagent-poller.ts; this adapter owns the
|
|
5
|
-
* herdr/session I/O loop and pane-scope lifecycle so the plugin only starts it.
|
|
6
|
-
*/
|
|
7
|
-
import { appendFileSync } from 'node:fs';
|
|
8
|
-
import type { Context } from '@deepseek-ai/cordis';
|
|
9
|
-
import type { HerdrClientLike, HerdrAgentState } from './herdr-client.ts';
|
|
10
|
-
import { applyReportedSessionFile } from './history-store.ts';
|
|
11
|
-
import { runtimePolicy, type RuntimePolicy } from './runtime-policy.ts';
|
|
12
|
-
import { mountSubagentScope } from './subagent-scope.ts';
|
|
13
|
-
import {
|
|
14
|
-
OBSERVATION_TICK_MS,
|
|
15
|
-
TAKEOVER_RECHECK_MS,
|
|
16
|
-
buildSettlementNoticeText,
|
|
17
|
-
formatObservationTimeoutNotice,
|
|
18
|
-
formatPaneClosedNotice,
|
|
19
|
-
isSettlementCandidate,
|
|
20
|
-
planBlockedGate,
|
|
21
|
-
planObservationTick,
|
|
22
|
-
planTakeoverTick,
|
|
23
|
-
planVacuumTick,
|
|
24
|
-
} from './subagent-poller.ts';
|
|
25
|
-
import { buildBlockedGateNotice, type SubEntry } from './subagent-core.ts';
|
|
26
|
-
import type { SessionIo } from './subagent-session-io.ts';
|
|
27
|
-
import type { GitIo } from './subagent-git-io.ts';
|
|
28
|
-
|
|
29
|
-
export function sleep(ms: number): Promise<void> {
|
|
30
|
-
const wait = Promise.withResolvers<void>();
|
|
31
|
-
setTimeout(wait.resolve, ms);
|
|
32
|
-
return wait.promise;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface PollerHost {
|
|
36
|
-
client: HerdrClientLike;
|
|
37
|
-
sessionRoot: Context;
|
|
38
|
-
subs: Map<string, SubEntry>;
|
|
39
|
-
persistSubs(): void;
|
|
40
|
-
writeHistory(e: SubEntry, patch?: { outcome?: string | null; status?: SubEntry['status']; closedAt?: number }, via?: string): void;
|
|
41
|
-
blockedGateNotified: Set<string>;
|
|
42
|
-
lastMachineInjectAt: Map<string, number>;
|
|
43
|
-
session: SessionIo;
|
|
44
|
-
git: GitIo;
|
|
45
|
-
injectNotice(content: string): Promise<void>;
|
|
46
|
-
reconcileOnSettlement(description: string, outcome: 'settled' | 'failed'): string[];
|
|
47
|
-
withReconcileNotes(base: string, notes: readonly string[]): string;
|
|
48
|
-
claimSettleNotice(key: string): boolean;
|
|
49
|
-
/** Optional sleep seam for timing control without real delays */
|
|
50
|
-
sleep?: (ms: number) => Promise<void>;
|
|
51
|
-
/** Optional clock seam for deterministic virtual timestamps */
|
|
52
|
-
now?: () => number;
|
|
53
|
-
/** Optional runtime policy overrides for testing */
|
|
54
|
-
policy?: Partial<RuntimePolicy>;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export interface Poller {
|
|
58
|
-
startPoller(paneId: string, cwd: string, spawnedAt: number, injectTs: number, description: string, requestId: string): Promise<void>;
|
|
59
|
-
readonly pollers: Set<string>;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function createPoller(h: PollerHost): Poller {
|
|
63
|
-
const pollers = new Set<string>();
|
|
64
|
-
const subScopes = new Map<string, { dispose: () => Promise<void> }>();
|
|
65
|
-
const doSleep = h.sleep ?? sleep;
|
|
66
|
-
const now = h.now ?? Date.now;
|
|
67
|
-
const observeWindowMs = h.policy?.observationWindowMs ?? runtimePolicy.observationWindowMs;
|
|
68
|
-
const machineInjectGraceMs = h.policy?.settlementWindowMs ?? runtimePolicy.settlementWindowMs;
|
|
69
|
-
const takeoverIdleMs = h.policy?.settlementWindowMs ?? runtimePolicy.settlementWindowMs;
|
|
70
|
-
const timeoutMs = h.policy?.subagentTimeoutMs ?? runtimePolicy.subagentTimeoutMs;
|
|
71
|
-
const pollIntervalMs = h.policy?.pollIntervalMs ?? runtimePolicy.pollIntervalMs;
|
|
72
|
-
|
|
73
|
-
async function pollLoop(
|
|
74
|
-
paneId: string,
|
|
75
|
-
cwd: string,
|
|
76
|
-
spawnedAt: number,
|
|
77
|
-
injectTs: number,
|
|
78
|
-
description: string,
|
|
79
|
-
requestId: string,
|
|
80
|
-
): Promise<void> {
|
|
81
|
-
void spawnedAt;
|
|
82
|
-
const startedAt = now();
|
|
83
|
-
let lastActivityAt = now();
|
|
84
|
-
const pollTrace = process.env.PI_HERDR_TRACE
|
|
85
|
-
? (msg: string) => { try { appendFileSync(process.env.PI_HERDR_TRACE!, `d98poll ${now()} ${paneId} ${msg}\n`); } catch { /* best-effort */ } }
|
|
86
|
-
: null;
|
|
87
|
-
try {
|
|
88
|
-
while (true) {
|
|
89
|
-
const entry = h.subs.get(paneId);
|
|
90
|
-
if (!entry || entry.status === 'settled') return;
|
|
91
|
-
|
|
92
|
-
if (entry.userTakeover) {
|
|
93
|
-
try {
|
|
94
|
-
const agents = await h.client.listAgents();
|
|
95
|
-
const agent = agents.find((a) => a.paneId === paneId);
|
|
96
|
-
const tick = planTakeoverTick({
|
|
97
|
-
currentStatus: agent?.status ?? null,
|
|
98
|
-
previousStatus: entry.lastAgentStatus,
|
|
99
|
-
idleStartedAt: entry.observationStartedAt,
|
|
100
|
-
now: now(),
|
|
101
|
-
idleMs: takeoverIdleMs,
|
|
102
|
-
});
|
|
103
|
-
if (tick.kind === 'start-idle') {
|
|
104
|
-
entry.observationStartedAt = now();
|
|
105
|
-
entry.lastAgentStatus = 'idle';
|
|
106
|
-
h.persistSubs();
|
|
107
|
-
} else if (tick.kind === 'return-control') {
|
|
108
|
-
entry.userTakeover = false;
|
|
109
|
-
entry.observationStartedAt = null;
|
|
110
|
-
entry.lastAgentStatus = null;
|
|
111
|
-
h.persistSubs();
|
|
112
|
-
} else if (tick.kind === 'hold' && tick.clearIdleTimer) {
|
|
113
|
-
entry.lastAgentStatus = tick.lastAgentStatus;
|
|
114
|
-
entry.observationStartedAt = null;
|
|
115
|
-
h.persistSubs();
|
|
116
|
-
}
|
|
117
|
-
} catch {
|
|
118
|
-
/* status probe failure must not stop the poller */
|
|
119
|
-
}
|
|
120
|
-
if (entry.userTakeover) {
|
|
121
|
-
await doSleep(TAKEOVER_RECHECK_MS);
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
let state: HerdrAgentState | null;
|
|
127
|
-
try {
|
|
128
|
-
state = await h.client.waitAgent(paneId, ['idle', 'done', 'blocked'], pollIntervalMs);
|
|
129
|
-
} catch {
|
|
130
|
-
state = null;
|
|
131
|
-
}
|
|
132
|
-
const gate = planBlockedGate(state, h.blockedGateNotified.has(paneId));
|
|
133
|
-
if (gate.kind === 'stay-blocked') {
|
|
134
|
-
if (gate.notify) {
|
|
135
|
-
h.blockedGateNotified.add(paneId);
|
|
136
|
-
const question = await h.session.readAskFlag(paneId);
|
|
137
|
-
try {
|
|
138
|
-
await h.injectNotice(buildBlockedGateNotice({ paneId, description, question }));
|
|
139
|
-
} catch {
|
|
140
|
-
/* list_agents can recover a missed notice */
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
145
|
-
if (gate.kind === 'clear-gate') h.blockedGateNotified.delete(paneId);
|
|
146
|
-
if (state === 'idle' || state === 'done') {
|
|
147
|
-
const s = await h.session.subSessionState(paneId, cwd, injectTs);
|
|
148
|
-
pollTrace?.(`state=${state} text=${s.text ? s.text.length : 'null'} pend=${s.pendingTool} act=${s.activity} obs=${String(entry.observationStartedAt ?? null)} takeover=${String(Boolean(entry.userTakeover))}`);
|
|
149
|
-
if (isSettlementCandidate(s)) {
|
|
150
|
-
const closing = s.text;
|
|
151
|
-
const obs = planObservationTick({
|
|
152
|
-
observationStartedAt: entry.observationStartedAt,
|
|
153
|
-
now: now(),
|
|
154
|
-
windowMs: observeWindowMs,
|
|
155
|
-
agentStatus: null,
|
|
156
|
-
machineInjectAgoMs: now() - (h.lastMachineInjectAt.get(paneId) ?? 0),
|
|
157
|
-
machineInjectGraceMs,
|
|
158
|
-
});
|
|
159
|
-
if (obs.kind === 'start-observation') {
|
|
160
|
-
entry.observationStartedAt = now();
|
|
161
|
-
entry.lastAgentStatus = 'idle';
|
|
162
|
-
h.persistSubs();
|
|
163
|
-
await doSleep(OBSERVATION_TICK_MS);
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
let agentStatus: string | null = null;
|
|
167
|
-
try {
|
|
168
|
-
const agents = await h.client.listAgents();
|
|
169
|
-
agentStatus = agents.find((a) => a.paneId === paneId)?.status ?? null;
|
|
170
|
-
} catch {
|
|
171
|
-
/* observation continues */
|
|
172
|
-
}
|
|
173
|
-
const obs2 = planObservationTick({
|
|
174
|
-
observationStartedAt: entry.observationStartedAt,
|
|
175
|
-
now: now(),
|
|
176
|
-
windowMs: observeWindowMs,
|
|
177
|
-
agentStatus,
|
|
178
|
-
machineInjectAgoMs: now() - (h.lastMachineInjectAt.get(paneId) ?? 0),
|
|
179
|
-
machineInjectGraceMs,
|
|
180
|
-
});
|
|
181
|
-
if (obs2.kind === 'user-takeover') {
|
|
182
|
-
entry.userTakeover = true;
|
|
183
|
-
entry.observationStartedAt = now();
|
|
184
|
-
entry.lastAgentStatus = 'working';
|
|
185
|
-
h.persistSubs();
|
|
186
|
-
continue;
|
|
187
|
-
}
|
|
188
|
-
if (obs2.kind === 'machine-inject-reset') {
|
|
189
|
-
entry.observationStartedAt = now();
|
|
190
|
-
h.persistSubs();
|
|
191
|
-
await doSleep(OBSERVATION_TICK_MS);
|
|
192
|
-
continue;
|
|
193
|
-
}
|
|
194
|
-
if (obs2.kind === 'wait') {
|
|
195
|
-
await doSleep(OBSERVATION_TICK_MS);
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
entry.observationStartedAt = null;
|
|
199
|
-
if (!entry.sessionFile) {
|
|
200
|
-
entry.sessionFile = applyReportedSessionFile(
|
|
201
|
-
entry.sessionFile,
|
|
202
|
-
await h.session.resolveSessionFile(paneId, cwd),
|
|
203
|
-
);
|
|
204
|
-
}
|
|
205
|
-
entry.status = 'consumed';
|
|
206
|
-
entry.consumedAt = now();
|
|
207
|
-
// A3: this path consumed the sub too - persist, or the session snapshot keeps it
|
|
208
|
-
// "running" and a restart replays a ghost subagent into list/settle-wake/zombie sweep.
|
|
209
|
-
h.persistSubs();
|
|
210
|
-
h.writeHistory(entry, { outcome: closing }, 'poll-settle');
|
|
211
|
-
const notes = h.reconcileOnSettlement(description, 'settled');
|
|
212
|
-
const statLine = await h.git.worktreeStatLine(entry);
|
|
213
|
-
const notice = h.withReconcileNotes(
|
|
214
|
-
buildSettlementNoticeText(`${paneId} (${description})`, closing, statLine),
|
|
215
|
-
notes,
|
|
216
|
-
);
|
|
217
|
-
if (h.claimSettleNotice(`${paneId}:${requestId}`)) {
|
|
218
|
-
try {
|
|
219
|
-
await h.injectNotice(notice);
|
|
220
|
-
} catch {
|
|
221
|
-
/* list_agents can recover */
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
let alive = false;
|
|
228
|
-
try {
|
|
229
|
-
alive = (await h.client.listPanes()).some((p) => p.paneId === paneId);
|
|
230
|
-
} catch {
|
|
231
|
-
alive = true;
|
|
232
|
-
}
|
|
233
|
-
const vacuum = planVacuumTick({
|
|
234
|
-
waitState: state,
|
|
235
|
-
paneAlive: alive,
|
|
236
|
-
now: now(),
|
|
237
|
-
lastActivityAt,
|
|
238
|
-
timeoutMs,
|
|
239
|
-
});
|
|
240
|
-
if (vacuum.refreshActivity) lastActivityAt = now();
|
|
241
|
-
if (vacuum.action === 'pane-closed') {
|
|
242
|
-
entry.status = 'consumed';
|
|
243
|
-
entry.consumedAt = now();
|
|
244
|
-
h.persistSubs();
|
|
245
|
-
h.writeHistory(entry, { outcome: 'pane closed before settling' }, 'poll-pane-closed');
|
|
246
|
-
const notes = h.reconcileOnSettlement(description, 'failed');
|
|
247
|
-
const notice = h.withReconcileNotes(
|
|
248
|
-
formatPaneClosedNotice(paneId, description),
|
|
249
|
-
notes,
|
|
250
|
-
);
|
|
251
|
-
try {
|
|
252
|
-
await h.injectNotice(notice);
|
|
253
|
-
} catch { /* non-fatal */ }
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
if (vacuum.action === 'timeout') {
|
|
257
|
-
entry.status = 'consumed';
|
|
258
|
-
entry.consumedAt = now();
|
|
259
|
-
h.persistSubs();
|
|
260
|
-
h.writeHistory(entry, { outcome: 'observation timeout' }, 'poll-timeout');
|
|
261
|
-
const notes = h.reconcileOnSettlement(description, 'failed');
|
|
262
|
-
const notice = h.withReconcileNotes(
|
|
263
|
-
formatObservationTimeoutNotice({
|
|
264
|
-
paneId,
|
|
265
|
-
description,
|
|
266
|
-
idleSeconds: Math.round((now() - lastActivityAt) / 1000),
|
|
267
|
-
startedAtIso: new Date(startedAt).toISOString(),
|
|
268
|
-
}),
|
|
269
|
-
notes,
|
|
270
|
-
);
|
|
271
|
-
try {
|
|
272
|
-
await h.injectNotice(notice);
|
|
273
|
-
} catch { /* non-fatal */ }
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
} finally {
|
|
278
|
-
pollers.delete(paneId);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
function startPoller(paneId: string, cwd: string, spawnedAt: number, injectTs: number, description: string, requestId: string): Promise<void> {
|
|
283
|
-
if (pollers.has(paneId)) return Promise.resolve();
|
|
284
|
-
pollers.add(paneId);
|
|
285
|
-
return (async () => {
|
|
286
|
-
try {
|
|
287
|
-
if (!subScopes.has(paneId)) {
|
|
288
|
-
const fiber = await mountSubagentScope(h.sessionRoot, paneId, {
|
|
289
|
-
onDispose: () => { pollers.delete(paneId); },
|
|
290
|
-
});
|
|
291
|
-
subScopes.set(paneId, fiber);
|
|
292
|
-
}
|
|
293
|
-
await pollLoop(paneId, cwd, spawnedAt, injectTs, description, requestId);
|
|
294
|
-
const fiber = subScopes.get(paneId);
|
|
295
|
-
subScopes.delete(paneId);
|
|
296
|
-
try { await fiber?.dispose(); } catch { /* already gone */ }
|
|
297
|
-
} catch (err) {
|
|
298
|
-
pollers.delete(paneId);
|
|
299
|
-
console.error(`pier: subagent poller ${paneId} crashed: ${(err as Error)?.message ?? err}`);
|
|
300
|
-
}
|
|
301
|
-
})();
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
return { startPoller, pollers };
|
|
305
|
-
}
|