pi-pier 0.1.5 → 0.2.0
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/package.json +1 -1
- package/schemas/role-manifest.schema.json +1 -2
- package/src/bootstrap.ts +18 -22
- package/src/core/grid-shape.ts +13 -14
- package/src/core/subagent.ts +325 -857
- package/src/core/terminal.ts +25 -28
- package/src/core/todo.ts +47 -53
- package/src/gc-core.ts +15 -15
- package/src/git-adapter.ts +104 -0
- package/src/herdr-client.ts +76 -68
- package/src/history-store.ts +70 -36
- package/src/index-locks.ts +105 -0
- package/src/index-master.ts +113 -0
- package/src/index-notices.ts +43 -0
- package/src/index-pipe.ts +73 -0
- package/src/index-runtime.ts +23 -0
- package/src/index-worker.ts +39 -0
- package/src/index.ts +134 -386
- package/src/ledger.ts +15 -17
- package/src/lock-core.ts +15 -29
- package/src/manifest-compose.ts +9 -23
- package/src/notice-buffer.ts +9 -14
- package/src/pane-title.ts +24 -24
- package/src/pi-surface.ts +22 -29
- package/src/pipe-channel.ts +67 -34
- package/src/platform-paths.ts +72 -0
- package/src/progress-core.ts +26 -24
- package/src/reconcile-core.ts +25 -24
- package/src/role-loader.ts +32 -35
- package/src/role-manifest.ts +24 -28
- package/src/roles/master.json +2 -2
- package/src/roles/worker-default.json +3 -3
- package/src/runtime-policy.ts +66 -0
- package/src/session-tail.ts +46 -45
- package/src/settle-wake-core.ts +19 -18
- package/src/slim-frame.ts +45 -44
- package/src/stale-core.ts +23 -21
- package/src/storage-layout.ts +80 -0
- package/src/subagent-core.ts +151 -94
- package/src/subagent-gc.ts +237 -0
- package/src/subagent-git-io.ts +80 -0
- package/src/subagent-launch.ts +110 -0
- package/src/subagent-poll-loop.ts +285 -0
- package/src/subagent-poller.ts +90 -0
- package/src/subagent-port.ts +27 -0
- package/src/subagent-scope.ts +4 -4
- package/src/subagent-session-io.ts +143 -0
- package/src/subagent-spawn.ts +156 -0
- package/src/terminal-core.ts +56 -54
- package/src/todo-core.ts +27 -64
- package/src/todo-read-hook.ts +28 -24
- package/src/todo-reminder-core.ts +89 -0
- package/src/todos-service.ts +36 -25
- package/src/tool-gate.ts +7 -28
- package/src/vocab.ts +18 -19
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Master-process plugin mount (cordis loader + terminal/todo/subagent).
|
|
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
|
+
*/
|
|
7
|
+
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
8
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
9
|
+
import type { Server } from 'node:net';
|
|
10
|
+
import { PiSurface } from './pi-surface.ts';
|
|
11
|
+
import { createCordisApp } from './bootstrap.ts';
|
|
12
|
+
import { disposeSessionRoot } from './subagent-scope.ts';
|
|
13
|
+
import terminalPlugin from './core/terminal.ts';
|
|
14
|
+
import todoPlugin from './core/todo.ts';
|
|
15
|
+
import subagentPlugin from './core/subagent.ts';
|
|
16
|
+
import type { HerdrClientLike, HerdrEnv } from './herdr-client.ts';
|
|
17
|
+
import type { TodosService } from './todos-service.ts';
|
|
18
|
+
import type { TodoUiSlot } from './core/todo.ts';
|
|
19
|
+
import type { SubagentPortBox } from './subagent-port.ts';
|
|
20
|
+
|
|
21
|
+
export interface MasterPluginMount {
|
|
22
|
+
pi: ExtensionAPI;
|
|
23
|
+
client: HerdrClientLike;
|
|
24
|
+
env: HerdrEnv | null;
|
|
25
|
+
todos: TodosService;
|
|
26
|
+
todoUi: TodoUiSlot;
|
|
27
|
+
mirrorTodos: () => void;
|
|
28
|
+
extPath: string;
|
|
29
|
+
port: SubagentPortBox;
|
|
30
|
+
pipeServerBox: { current: Server | null };
|
|
31
|
+
deliverNotice: (content: string, paneId?: string) => Promise<void>;
|
|
32
|
+
noticePending: () => ReadonlySet<string>;
|
|
33
|
+
getSessionId: () => string;
|
|
34
|
+
getBlockedDepth: () => number;
|
|
35
|
+
reconcileOnSettlement: (description: string, outcome: 'settled' | 'failed') => string[];
|
|
36
|
+
withReconcileNotes: (base: string, notes: readonly string[]) => string;
|
|
37
|
+
claimSettleNotice: (key: string) => boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function loadEntry(
|
|
41
|
+
sessionRoot: Context,
|
|
42
|
+
loaderReady: boolean,
|
|
43
|
+
name: string,
|
|
44
|
+
plugin: (ctx: Context) => void,
|
|
45
|
+
): Promise<void> {
|
|
46
|
+
if (loaderReady) {
|
|
47
|
+
const withLoader = sessionRoot as typeof sessionRoot & {
|
|
48
|
+
loader?: { create: (o: { name: string }) => Promise<unknown> };
|
|
49
|
+
};
|
|
50
|
+
await withLoader.loader?.create({ name });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
await sessionRoot.plugin(plugin);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function mountMasterPlugins(m: MasterPluginMount): Promise<void> {
|
|
57
|
+
const cordisApp = await createCordisApp();
|
|
58
|
+
const sessionRoot = cordisApp.root;
|
|
59
|
+
|
|
60
|
+
// Pipe server is created in the common segment; closing it from a core
|
|
61
|
+
// plugin would kill it on hmr reload (d87).
|
|
62
|
+
sessionRoot.effect(() => () => {
|
|
63
|
+
if (m.pipeServerBox.current) {
|
|
64
|
+
try { m.pipeServerBox.current.close(); } catch { /* already closed */ }
|
|
65
|
+
m.pipeServerBox.current = null;
|
|
66
|
+
}
|
|
67
|
+
}, 'pipe-server');
|
|
68
|
+
|
|
69
|
+
const surface = new PiSurface(m.pi as unknown as object, cordisApp.ledger);
|
|
70
|
+
sessionRoot.provide('pi-herdr.surface', surface);
|
|
71
|
+
const terminalDeps = {
|
|
72
|
+
client: m.client,
|
|
73
|
+
env: m.env,
|
|
74
|
+
state: { activePaneIds: (): Set<string> => new Set() },
|
|
75
|
+
};
|
|
76
|
+
sessionRoot.provide('pi-herdr.terminal-deps', terminalDeps);
|
|
77
|
+
|
|
78
|
+
await loadEntry(sessionRoot, cordisApp.loaderReady, './core/terminal.ts', terminalPlugin);
|
|
79
|
+
|
|
80
|
+
sessionRoot.provide('pi-herdr.todo-deps', {
|
|
81
|
+
todos: m.todos,
|
|
82
|
+
allowParallelInProgress: m.todos.config.allowParallelInProgress,
|
|
83
|
+
maxItems: 15,
|
|
84
|
+
mirrorTodos: m.mirrorTodos,
|
|
85
|
+
appendEntry: (customType: string, data: unknown) => {
|
|
86
|
+
(m.pi as { appendEntry?: (t: string, d: unknown) => void }).appendEntry?.(customType, data);
|
|
87
|
+
},
|
|
88
|
+
state: m.todoUi,
|
|
89
|
+
});
|
|
90
|
+
await loadEntry(sessionRoot, cordisApp.loaderReady, './core/todo.ts', todoPlugin);
|
|
91
|
+
|
|
92
|
+
sessionRoot.provide('pi-herdr.subagent-deps', {
|
|
93
|
+
client: m.client,
|
|
94
|
+
env: m.env,
|
|
95
|
+
extPath: m.extPath,
|
|
96
|
+
sessionRoot,
|
|
97
|
+
port: m.port,
|
|
98
|
+
deliverNotice: m.deliverNotice,
|
|
99
|
+
noticePending: m.noticePending,
|
|
100
|
+
getSessionId: m.getSessionId,
|
|
101
|
+
getBlockedDepth: m.getBlockedDepth,
|
|
102
|
+
reconcileOnSettlement: m.reconcileOnSettlement,
|
|
103
|
+
withReconcileNotes: m.withReconcileNotes,
|
|
104
|
+
claimSettleNotice: m.claimSettleNotice,
|
|
105
|
+
terminalState: terminalDeps.state,
|
|
106
|
+
todos: m.todos,
|
|
107
|
+
});
|
|
108
|
+
await loadEntry(sessionRoot, cordisApp.loaderReady, './core/subagent.ts', subagentPlugin);
|
|
109
|
+
|
|
110
|
+
m.pi.on('session_shutdown', () => {
|
|
111
|
+
void disposeSessionRoot(sessionRoot);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D92 settlement-notice buffer (busy-run collapse).
|
|
3
|
+
*
|
|
4
|
+
* Why: `sendUserMessage(followUp)` only delivers when the agent has no more
|
|
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 { collapseNotices } from './notice-buffer.ts';
|
|
9
|
+
|
|
10
|
+
export type NoticeSendMode = 'steer' | 'followUp';
|
|
11
|
+
|
|
12
|
+
export interface NoticeBuffer {
|
|
13
|
+
deliverNotice(content: string, paneId?: string): Promise<void>;
|
|
14
|
+
noticePending(): ReadonlySet<string>;
|
|
15
|
+
flush(mode: NoticeSendMode): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createNoticeBuffer(opts: {
|
|
19
|
+
/** True while the agent is in a turn, or after an abort that must not wake a new run. */
|
|
20
|
+
isBusy: () => boolean;
|
|
21
|
+
send: (content: string, mode: NoticeSendMode) => Promise<void>;
|
|
22
|
+
}): NoticeBuffer {
|
|
23
|
+
const pending: string[] = [];
|
|
24
|
+
const pendingPaneIds = new Set<string>();
|
|
25
|
+
return {
|
|
26
|
+
async deliverNotice(content, paneId) {
|
|
27
|
+
if (content !== '' && paneId !== undefined) pendingPaneIds.add(paneId);
|
|
28
|
+
if (opts.isBusy()) {
|
|
29
|
+
pending.push(content);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
await opts.send(content, 'followUp');
|
|
33
|
+
if (content !== '' && paneId !== undefined) pendingPaneIds.delete(paneId);
|
|
34
|
+
},
|
|
35
|
+
noticePending: () => pendingPaneIds,
|
|
36
|
+
async flush(mode) {
|
|
37
|
+
if (pending.length === 0) return;
|
|
38
|
+
const collapsed = collapseNotices(pending.splice(0));
|
|
39
|
+
pendingPaneIds.clear();
|
|
40
|
+
if (collapsed) await opts.send(collapsed, mode);
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
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
|
+
*/
|
|
7
|
+
import { formatSettlementNotice } from './vocab.ts';
|
|
8
|
+
import type { PipeRequest, PipeResponse } from './pipe-channel.ts';
|
|
9
|
+
import type { SubagentPortBox } from './subagent-port.ts';
|
|
10
|
+
|
|
11
|
+
export interface MachineRequest {
|
|
12
|
+
id: string;
|
|
13
|
+
from: string | null;
|
|
14
|
+
push: boolean;
|
|
15
|
+
sinceTs: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface PipeHandlerSession {
|
|
19
|
+
paneId: string;
|
|
20
|
+
port: SubagentPortBox;
|
|
21
|
+
claimSettleNotice: (key: string) => boolean;
|
|
22
|
+
deliverNotice: (content: string, paneId?: string) => Promise<void>;
|
|
23
|
+
sendUserMessageIn: (content: string) => Promise<void>;
|
|
24
|
+
sendUserMessageAs: (content: string, mode: 'steer' | 'followUp') => Promise<void>;
|
|
25
|
+
abort: () => void;
|
|
26
|
+
setPendingMachineRequest: (req: MachineRequest | null) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function handlePipeRequest(
|
|
30
|
+
req: PipeRequest,
|
|
31
|
+
s: PipeHandlerSession,
|
|
32
|
+
): Promise<PipeResponse> {
|
|
33
|
+
switch (req.type) {
|
|
34
|
+
case 'ping':
|
|
35
|
+
return { type: 'ok', id: req.id, detail: s.paneId };
|
|
36
|
+
case 'prompt':
|
|
37
|
+
case 'follow_up': {
|
|
38
|
+
s.setPendingMachineRequest({
|
|
39
|
+
id: req.id,
|
|
40
|
+
from: req.from ?? null,
|
|
41
|
+
push: req.push === true,
|
|
42
|
+
sinceTs: Date.now(),
|
|
43
|
+
});
|
|
44
|
+
// follow_up + steer → deliver between tool calls (B3). Initial prompt
|
|
45
|
+
// stays followUp so an idle worker starts a new run (D96).
|
|
46
|
+
if (req.steer === true) await s.sendUserMessageAs(req.text, 'steer');
|
|
47
|
+
else await s.sendUserMessageIn(req.text);
|
|
48
|
+
return { type: 'ok', id: req.id };
|
|
49
|
+
}
|
|
50
|
+
case 'interrupt':
|
|
51
|
+
s.abort();
|
|
52
|
+
s.setPendingMachineRequest(null);
|
|
53
|
+
return { type: 'ok', id: req.id };
|
|
54
|
+
case 'reply': {
|
|
55
|
+
s.port.current?.applyReplySession(req.paneId, req.sessionFile);
|
|
56
|
+
if (s.claimSettleNotice(`${req.paneId}:${req.id}`)) {
|
|
57
|
+
const notes = s.port.current?.reconcileOnReply(req.paneId) ?? [];
|
|
58
|
+
const statLine = await s.port.current?.settleStatLine(req.paneId) ?? null;
|
|
59
|
+
const head = formatSettlementNotice(`${req.paneId}`, req.text);
|
|
60
|
+
const body = (req.sessionFile ? `${head}\nSession: ${req.sessionFile}` : head)
|
|
61
|
+
+ (statLine ? `\n${statLine}` : '')
|
|
62
|
+
+ (notes.length ? `\n${notes.join('\n')}` : '');
|
|
63
|
+
await s.deliverNotice(body, req.paneId);
|
|
64
|
+
}
|
|
65
|
+
return { type: 'ok', id: req.id };
|
|
66
|
+
}
|
|
67
|
+
default: {
|
|
68
|
+
// PipeRequest is closed; parsePipeLine may still forward an unknown type.
|
|
69
|
+
const unknownReq: { type: string; id: string } = req;
|
|
70
|
+
return { type: 'error', id: unknownReq.id, message: `unknown type ${unknownReq.type}` };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Index process-mode planner (master vs worker, master-manifest gate).
|
|
3
|
+
*
|
|
4
|
+
* Why: index.ts mixed env branching with cordis/plugin mounting. The mode
|
|
5
|
+
* decision is a pure function of process env and must stay testable without
|
|
6
|
+
* booting the composition root.
|
|
7
|
+
*/
|
|
8
|
+
import { detectHerdrEnv } from './herdr-client.ts';
|
|
9
|
+
|
|
10
|
+
export interface IndexMode {
|
|
11
|
+
/** PI_HERDR_SUBAGENT=1 → worker pane (no subagent tools, no cordis loader). */
|
|
12
|
+
readonly isSubagent: boolean;
|
|
13
|
+
/** herdr pane that is not a worker → apply builtin master manifest. */
|
|
14
|
+
readonly composeMaster: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function planIndexMode(env: NodeJS.ProcessEnv = process.env): IndexMode {
|
|
18
|
+
const isSubagent = env.PI_HERDR_SUBAGENT === '1';
|
|
19
|
+
return {
|
|
20
|
+
isSubagent,
|
|
21
|
+
composeMaster: Boolean(detectHerdrEnv(env)) && !isSubagent,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker-process todo plugin mount (bare cordis root, no loader/hmr).
|
|
3
|
+
*
|
|
4
|
+
* Why: short-lived worker panes only need the todo coordination plugin.
|
|
5
|
+
* Dynamically imported from the worker branch so bootstrap/loader stay out.
|
|
6
|
+
*/
|
|
7
|
+
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
8
|
+
import { Context } from '@deepseek-ai/cordis';
|
|
9
|
+
import { PiSurface } from './pi-surface.ts';
|
|
10
|
+
import todoPlugin from './core/todo.ts';
|
|
11
|
+
import type { TodosService } from './todos-service.ts';
|
|
12
|
+
import type { TodoUiSlot } from './core/todo.ts';
|
|
13
|
+
|
|
14
|
+
export interface WorkerPluginMount {
|
|
15
|
+
pi: ExtensionAPI;
|
|
16
|
+
todos: TodosService;
|
|
17
|
+
todoUi: TodoUiSlot;
|
|
18
|
+
mirrorTodos: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function mountWorkerTodo(m: WorkerPluginMount): Promise<void> {
|
|
22
|
+
const workerRoot = new Context();
|
|
23
|
+
const workerSurface = new PiSurface(m.pi as unknown as object);
|
|
24
|
+
workerRoot.provide('pi-herdr.surface', workerSurface);
|
|
25
|
+
workerRoot.provide('pi-herdr.todo-deps', {
|
|
26
|
+
todos: m.todos,
|
|
27
|
+
allowParallelInProgress: m.todos.config.allowParallelInProgress,
|
|
28
|
+
maxItems: 15,
|
|
29
|
+
mirrorTodos: m.mirrorTodos,
|
|
30
|
+
appendEntry: (customType: string, data: unknown) => {
|
|
31
|
+
(m.pi as { appendEntry?: (t: string, d: unknown) => void }).appendEntry?.(customType, data);
|
|
32
|
+
},
|
|
33
|
+
state: m.todoUi,
|
|
34
|
+
});
|
|
35
|
+
await workerRoot.plugin(todoPlugin);
|
|
36
|
+
m.pi.on('session_shutdown', () => {
|
|
37
|
+
void workerRoot.fiber.dispose();
|
|
38
|
+
});
|
|
39
|
+
}
|