pi-pier 0.1.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/README.md +62 -0
- package/package.json +55 -0
- package/schemas/role-manifest.schema.json +53 -0
- package/skills/pi-herdr-workflow/SKILL.md +86 -0
- package/src/bootstrap.ts +106 -0
- package/src/core/grid-shape.ts +86 -0
- package/src/core/subagent.ts +1256 -0
- package/src/core/terminal.ts +359 -0
- package/src/core/todo.ts +313 -0
- package/src/gc-core.ts +56 -0
- package/src/herdr-client.ts +579 -0
- package/src/history-store.ts +115 -0
- package/src/index.ts +751 -0
- package/src/ledger.ts +71 -0
- package/src/lock-core.ts +155 -0
- package/src/manifest-compose.ts +102 -0
- package/src/notice-buffer.ts +28 -0
- package/src/pane-title.ts +84 -0
- package/src/pi-surface.ts +135 -0
- package/src/pipe-channel.ts +181 -0
- package/src/progress-core.ts +109 -0
- package/src/reconcile-core.ts +130 -0
- package/src/role-loader.ts +164 -0
- package/src/role-manifest.ts +166 -0
- package/src/roles/master.json +21 -0
- package/src/roles/worker-default.json +30 -0
- package/src/session-tail.ts +169 -0
- package/src/subagent-core.ts +310 -0
- package/src/subagent-scope.ts +39 -0
- package/src/terminal-core.ts +328 -0
- package/src/todo-core.ts +331 -0
- package/src/todo-read-hook.ts +54 -0
- package/src/todos-service.ts +66 -0
- package/src/tool-gate.ts +106 -0
- package/src/vocab.ts +82 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,751 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-herdr 扩展入口(v1.1:todo 闭环 + 交互式子代理,DESIGN.md §12 方案 C)。
|
|
3
|
+
*
|
|
4
|
+
* 安装:pi package(package.json 的 pi.extensions)或 ~/.pi/agent/extensions/。
|
|
5
|
+
*
|
|
6
|
+
* 能力:
|
|
7
|
+
* - 工具 `todo_write`:全量替换式 todo 列表(权威 = 本 pi 会话 JSONL,
|
|
8
|
+
* 分支自动回滚;语义对齐 DSH)。
|
|
9
|
+
* - 工具 `subagent`:前台/后台委派。子 pane = 交互式 pi TUI(独立会话、
|
|
10
|
+
* 独立上下文),人类可随时进入直接对话;主控经 herdr 通道派活
|
|
11
|
+
* (状态门:仅 idle 注入)、等待(agent.wait)、取结果(子会话 JSONL)。
|
|
12
|
+
* - 工具 `list_agents` / `send_message`(followUp 队列语义)/ `interrupt_agent`(esc)。
|
|
13
|
+
* - 命令 `/todos`、TUI widget、herdr 标题投影(无 herdr 环境优雅降级)。
|
|
14
|
+
*
|
|
15
|
+
* ⚠️ pi 0.84.2 契约(实测):
|
|
16
|
+
* - onUpdate 必须是 AgentToolResult 形状(字符串会让 TUI 崩溃退出 pi);
|
|
17
|
+
* - 工具结果 details 随会话 JSONL 持久化、getBranch() 重放实现分支回滚。
|
|
18
|
+
*/
|
|
19
|
+
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
20
|
+
import { Type } from 'typebox';
|
|
21
|
+
import {
|
|
22
|
+
formatSettlementNotice,
|
|
23
|
+
} from './vocab.ts';
|
|
24
|
+
import { collapseNotices } from './notice-buffer.ts';
|
|
25
|
+
import {
|
|
26
|
+
TODO_EDIT_CUSTOM_TYPE,
|
|
27
|
+
applyTodoEdits,
|
|
28
|
+
currentActivity,
|
|
29
|
+
foldLatestTodos,
|
|
30
|
+
} from './todo-core.ts';
|
|
31
|
+
import { pipeNameFor, pipeRequest, startPipeServer } from './pipe-channel.ts';
|
|
32
|
+
|
|
33
|
+
/** 档2:worker 会话内记录运行所依据的合成 manifest(D38 同构,分支可回放)。 */
|
|
34
|
+
const ROLE_MANIFEST_CUSTOM_TYPE = 'pi-herdr.role-manifest';
|
|
35
|
+
|
|
36
|
+
import { createHerdrClient, type HerdrClientLike, type HerdrAgentState } from './herdr-client.ts';
|
|
37
|
+
// subagent 族已迁 core/subagent.ts(loader entry,D78/D81);subagent-core/history-store/
|
|
38
|
+
// session-tail/gc-core 导入随迁(index 仅留 common 面用到的 lastAssistantText/readSessionFile)。
|
|
39
|
+
import { lastAssistantText, readSessionFile } from './session-tail.ts';
|
|
40
|
+
import { fileURLToPath } from 'node:url';
|
|
41
|
+
import { TodosService } from './todos-service.ts';
|
|
42
|
+
import { reconcileTodos } from './reconcile-core.ts';
|
|
43
|
+
import {
|
|
44
|
+
estimateEta,
|
|
45
|
+
formatProgressSuffix,
|
|
46
|
+
planToolBadge,
|
|
47
|
+
progressOf,
|
|
48
|
+
} from './progress-core.ts';
|
|
49
|
+
import {
|
|
50
|
+
WRITE_LOCK_ENV,
|
|
51
|
+
acquireTokensFor,
|
|
52
|
+
isLockTokenKey,
|
|
53
|
+
parseLockTokenValue,
|
|
54
|
+
planWriteGuard,
|
|
55
|
+
releaseTokensFor,
|
|
56
|
+
type LockAgentView,
|
|
57
|
+
} from './lock-core.ts';
|
|
58
|
+
import { composeForRole } from './manifest-compose.ts';
|
|
59
|
+
import { parseRuntimeManifest, planActiveTools, planToolGate, type RuntimeRoleManifest } from './tool-gate.ts';
|
|
60
|
+
import { detectHerdrEnv } from './herdr-client.ts';
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* WS-D7:master pane 自应用 manifest——与 subagent 同一条强制链
|
|
64
|
+
* (闸门 + 可见层 + 徽标 + 会话记录),档案基线 = 自研核心 + 检视/执行工具。
|
|
65
|
+
* 判别:herdr pane(HERDR_ENV=1)且非 subagent 且未被显式下发 manifest。
|
|
66
|
+
* 独立 pi 会话(无 herdr env)不命中 → 保持全量(向后兼容)。
|
|
67
|
+
* master.json 畸形 → fail-open 到无角色态(与 env 畸形同语义)。
|
|
68
|
+
*/
|
|
69
|
+
function composeMasterRuntime(): RuntimeRoleManifest | null {
|
|
70
|
+
try {
|
|
71
|
+
// v1.1:内置名直读——workspace 放 master.json 诱饵不影响自应用(保留名检查只对 spawn 响亮报错)
|
|
72
|
+
const { role, manifest } = composeForRole('master', [], { loadRoleOpts: { builtinDirect: true } });
|
|
73
|
+
return {
|
|
74
|
+
role: role.role,
|
|
75
|
+
version: role.version,
|
|
76
|
+
tools: manifest.tools,
|
|
77
|
+
permissions: manifest.permissions,
|
|
78
|
+
unknownTools: manifest.unknownTools,
|
|
79
|
+
services: role.services ?? {},
|
|
80
|
+
};
|
|
81
|
+
} catch (err) {
|
|
82
|
+
console.error(`[pi-herdr] master manifest invalid (fail-open): ${err instanceof Error ? err.message : String(err)}`);
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// C3:cordis 只进 master 进程——subagent-scope(内含 @deepseek-ai/cordis)
|
|
87
|
+
// 在 master 分支内动态 import,worker 进程永不加载该模块。
|
|
88
|
+
// subagent 族常量/助手已随迁 core/subagent.ts。
|
|
89
|
+
|
|
90
|
+
export default async function (pi: ExtensionAPI) {
|
|
91
|
+
// 档2(Week4-5):worker 运行时 manifest(env 下发;显式 > master 自应用 > 无)
|
|
92
|
+
// WS-D7:herdr 内非 subagent pane = master → 自应用 master 档案(同一条强制链)
|
|
93
|
+
const runtimeManifest =
|
|
94
|
+
parseRuntimeManifest(process.env.PI_HERDR_ROLE_MANIFEST) ??
|
|
95
|
+
(detectHerdrEnv() && process.env.PI_HERDR_SUBAGENT !== '1' ? composeMasterRuntime() : null);
|
|
96
|
+
const todos = new TodosService(TodosService.configFromRuntime(runtimeManifest, process.env.PI_HERDR_SUBAGENT === '1')); // D75 阶段 3
|
|
97
|
+
const { client, env } = createHerdrClient();
|
|
98
|
+
|
|
99
|
+
let sessionId: string = process.env.PI_SESSION_FILE ?? process.env.PI_SESSION_ID ?? '';
|
|
100
|
+
/** M16:todo 完成时间戳序列(速率估算原料;随 todo.completed 事件追加)。 */
|
|
101
|
+
const completedStamps: number[] = [];
|
|
102
|
+
|
|
103
|
+
/* ── 状态重建(分支正确性:取分支路径上最后一次 todo_write 快照)── */
|
|
104
|
+
|
|
105
|
+
function rebuildFromBranch(ctx: unknown): void {
|
|
106
|
+
try {
|
|
107
|
+
const entries = (ctx as { sessionManager?: { getBranch?: () => readonly unknown[] } })
|
|
108
|
+
?.sessionManager?.getBranch?.() ?? [];
|
|
109
|
+
todos.rebuild(entries);
|
|
110
|
+
} catch {
|
|
111
|
+
// 重建失败不影响主流程;下次 todo_write 会重新锚定。
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* ── herdr 上报(Noop 时零成本;失败静默,绝不影响 pi 主流程) ── */
|
|
116
|
+
|
|
117
|
+
/** 档2:worker 的 role 徽标(env 下发 manifest 解析出;idle 态粘性显示)。 */
|
|
118
|
+
let roleBadge: string | null = null;
|
|
119
|
+
|
|
120
|
+
function reportAgent(state: 'working' | 'idle' | 'blocked', activity: string | null): void {
|
|
121
|
+
if (!client.available) return;
|
|
122
|
+
const message = activity ?? (state === 'idle' ? roleBadge : null);
|
|
123
|
+
client.reportAgent(state, message).catch(() => {});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function reportSession(path: string): void {
|
|
127
|
+
if (!client.available) return;
|
|
128
|
+
client.reportAgentSession(path).catch(() => {});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** session 标识:sessionManager 权威(print/rpc 模式下 env 可能未设),env 兜底。 */
|
|
132
|
+
function resolveSessionId(ctx: unknown): string {
|
|
133
|
+
try {
|
|
134
|
+
const sm = (ctx as {
|
|
135
|
+
sessionManager?: {
|
|
136
|
+
getSessionFile?: () => string | undefined;
|
|
137
|
+
getSessionId?: () => string;
|
|
138
|
+
};
|
|
139
|
+
}).sessionManager;
|
|
140
|
+
return (
|
|
141
|
+
sm?.getSessionFile?.() ??
|
|
142
|
+
sm?.getSessionId?.() ??
|
|
143
|
+
process.env.PI_SESSION_FILE ??
|
|
144
|
+
process.env.PI_SESSION_ID ??
|
|
145
|
+
''
|
|
146
|
+
);
|
|
147
|
+
} catch {
|
|
148
|
+
return process.env.PI_SESSION_FILE ?? '';
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function mirrorTodos(): void {
|
|
153
|
+
if (!client.available) return;
|
|
154
|
+
const label = sessionId || (env ? `pane:${env.paneId}` : '');
|
|
155
|
+
// M16:进度徽标(保守 N/M + 置信 ETA;估算不可信回退纯计数)
|
|
156
|
+
const p = progressOf(todos.items);
|
|
157
|
+
const eta = estimateEta({ completedAt: completedStamps, total: p.total, now: Date.now() });
|
|
158
|
+
const suffix = formatProgressSuffix({ completed: p.completed, total: p.total, eta });
|
|
159
|
+
client.reportMetadata({ session: label, items: todos.items, progressSuffix: suffix }).catch(() => {});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/* ── M17:结算自动对账(纯规划器 + D38 权威路径;幂等,双路径双跑无害) ── */
|
|
163
|
+
|
|
164
|
+
function reconcileOnSettlement(description: string, outcome: 'settled' | 'failed'): string[] {
|
|
165
|
+
if (!description) return [];
|
|
166
|
+
try {
|
|
167
|
+
const plan = reconcileTodos(todos.items, { description, outcome });
|
|
168
|
+
if (plan.edits.length > 0) {
|
|
169
|
+
try {
|
|
170
|
+
(pi as { appendEntry?: (customType: string, data: unknown) => void }).appendEntry?.(
|
|
171
|
+
TODO_EDIT_CUSTOM_TYPE,
|
|
172
|
+
{ version: 1, edits: plan.edits, ts: Date.now() },
|
|
173
|
+
);
|
|
174
|
+
} catch {
|
|
175
|
+
/* 持久化尽力而为(内存态仍推进) */
|
|
176
|
+
}
|
|
177
|
+
todos.applyEdits(plan.edits);
|
|
178
|
+
mirrorTodos();
|
|
179
|
+
}
|
|
180
|
+
return plan.noteLines;
|
|
181
|
+
} catch {
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** M17:对账提示行拼进结算通知(空数组 = 原文返回)。 */
|
|
187
|
+
function withReconcileNotes(base: string, notes: readonly string[]): string {
|
|
188
|
+
return notes.length ? `${base}\n${notes.join('\n')}` : base;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/* ── M16:进度徽标 + 工具徽标(title 后缀 + report_agent.message;无新协议) ── */
|
|
192
|
+
|
|
193
|
+
todos.on('todo.completed', (e: { count: number; at: number }) => {
|
|
194
|
+
for (let i = 0; i < e.count; i++) completedStamps.push(e.at);
|
|
195
|
+
mirrorTodos();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// 工具徽标:并行模式 start 源序 / end 完成序(pi docs 实测契约)→ Map 跟踪在途
|
|
199
|
+
const runningTools = new Map<string, string>(); // toolCallId → toolName
|
|
200
|
+
let lastToolBadge: string | null = null;
|
|
201
|
+
function reportToolBadge(): void {
|
|
202
|
+
const badge = planToolBadge([...new Set(runningTools.values())]);
|
|
203
|
+
if (badge === lastToolBadge) return; // 幂等:变化才报(M13 渲染纪律)
|
|
204
|
+
lastToolBadge = badge;
|
|
205
|
+
if (badge) reportAgent('working', badge);
|
|
206
|
+
else reportAgent(agentActive ? 'working' : 'idle', agentActive ? currentActivity(todos.items) : null);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
pi.on('tool_execution_start', async (event: { toolCallId?: string; toolName?: string }) => {
|
|
210
|
+
if (typeof event?.toolCallId === 'string' && typeof event?.toolName === 'string') {
|
|
211
|
+
runningTools.set(event.toolCallId, event.toolName);
|
|
212
|
+
reportToolBadge();
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
pi.on('tool_execution_end', async (event: { toolCallId?: string }) => {
|
|
216
|
+
if (typeof event?.toolCallId === 'string') {
|
|
217
|
+
runningTools.delete(event.toolCallId);
|
|
218
|
+
reportToolBadge();
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
/* ── M18:文件级写锁(S2:默认软 veto;PI_HERDR_WRITE_LOCK=1 硬启) ──
|
|
223
|
+
* 登记 = report_metadata tokens(键 lock-<归一路径哈希>,值 paneId|path——schema 键模式
|
|
224
|
+
* ^[A-Za-z0-9_-]{1,32}$ 装不下路径);检查 = agent.list 读全 pane tokens;
|
|
225
|
+
* 释放 = settled 置 null(pane 关闭 = agent 条目消失,锁自灭)。
|
|
226
|
+
* v1 范围:write/edit(bash 文件目标提取不可靠,声明为 seam)。 */
|
|
227
|
+
|
|
228
|
+
const writeLockHard = process.env[WRITE_LOCK_ENV] === '1';
|
|
229
|
+
/** 本 pane 当前持有的锁(归一路径;settled 时统一释放)。 */
|
|
230
|
+
const heldLocks = new Set<string>();
|
|
231
|
+
/** 软 veto 待附警告的 toolCall → 警告文本。 */
|
|
232
|
+
const lockWarnByToolCall = new Map<string, string>();
|
|
233
|
+
|
|
234
|
+
async function acquireLocks(paths: readonly string[]): Promise<void> {
|
|
235
|
+
for (const p of paths) heldLocks.add(p);
|
|
236
|
+
await client.reportLockTokens(acquireTokensFor(paths, env?.paneId ?? ''));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
pi.on('tool_call', async (event: { toolName?: string; toolCallId?: string; input?: unknown }, ctx: { cwd?: string }) => {
|
|
240
|
+
if (!client.available || !env) return;
|
|
241
|
+
const cwd = (ctx as { cwd?: string }).cwd ?? process.cwd();
|
|
242
|
+
let agents: LockAgentView[];
|
|
243
|
+
try {
|
|
244
|
+
agents = (await client.listAgents()).map((a) => ({ paneId: a.paneId, tokens: a.tokens }));
|
|
245
|
+
} catch {
|
|
246
|
+
return; // 查询失败放行(fail-open:锁是协作性保护,不作单点故障)
|
|
247
|
+
}
|
|
248
|
+
const plan = planWriteGuard({
|
|
249
|
+
toolName: event.toolName ?? '',
|
|
250
|
+
input: event.input,
|
|
251
|
+
agents,
|
|
252
|
+
ownPaneId: env.paneId,
|
|
253
|
+
cwd,
|
|
254
|
+
hard: writeLockHard,
|
|
255
|
+
});
|
|
256
|
+
if (plan.kind === 'skip') return;
|
|
257
|
+
if (plan.kind === 'block') {
|
|
258
|
+
return { block: true, reason: plan.reason };
|
|
259
|
+
}
|
|
260
|
+
if (plan.kind === 'warn' && typeof event.toolCallId === 'string') {
|
|
261
|
+
lockWarnByToolCall.set(event.toolCallId, plan.warning);
|
|
262
|
+
// 软模式:写入照常进行,但本 pane 也成为编辑者 → 接管锁(对称可见)
|
|
263
|
+
}
|
|
264
|
+
await acquireLocks(plan.paths);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
pi.on('tool_result', async (event: { toolCallId?: string; content?: Array<{ type: string; text: string }> }) => {
|
|
268
|
+
if (typeof event?.toolCallId !== 'string') return;
|
|
269
|
+
const warning = lockWarnByToolCall.get(event.toolCallId);
|
|
270
|
+
if (!warning) return;
|
|
271
|
+
lockWarnByToolCall.delete(event.toolCallId);
|
|
272
|
+
const content = Array.isArray(event.content) ? event.content : [];
|
|
273
|
+
return { content: [...content, { type: 'text', text: warning }] };
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// 锁释放:settled(一轮结束)置 null;pane 关闭由 agent 条目消失兜底
|
|
277
|
+
pi.on('agent_settled', async () => {
|
|
278
|
+
if (heldLocks.size === 0) return;
|
|
279
|
+
const paths = [...heldLocks];
|
|
280
|
+
heldLocks.clear();
|
|
281
|
+
await client.reportLockTokens(releaseTokensFor(paths));
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// 人类可读:/locks 查看本 pane 持有 + 全局锁视图
|
|
285
|
+
pi.registerCommand('locks', {
|
|
286
|
+
description: 'Show write locks held by this pane and all live panes (M18)',
|
|
287
|
+
handler: async (_args, ctx) => {
|
|
288
|
+
const ui = (ctx as { ui?: { notify?: (text: string, level?: string) => void } }).ui;
|
|
289
|
+
const mine = [...heldLocks];
|
|
290
|
+
const lines = [`held by this pane (${mine.length}):`];
|
|
291
|
+
lines.push(...(mine.length ? mine.map((p) => ` ${p}`) : [' (none)']));
|
|
292
|
+
lines.push('all live locks:');
|
|
293
|
+
let any = false;
|
|
294
|
+
try {
|
|
295
|
+
for (const a of await client.listAgents()) {
|
|
296
|
+
for (const [k, v] of Object.entries(a.tokens)) {
|
|
297
|
+
if (!isLockTokenKey(k) || typeof v !== 'string' || !v) continue;
|
|
298
|
+
const parsed = parseLockTokenValue(v);
|
|
299
|
+
if (!parsed) continue;
|
|
300
|
+
any = true;
|
|
301
|
+
lines.push(` ${parsed.path} → pane ${parsed.holderPaneId}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
} catch {
|
|
305
|
+
lines.push(' (agent.list failed)');
|
|
306
|
+
}
|
|
307
|
+
if (!any && lines[lines.length - 1] !== ' (agent.list failed)') lines.push(' (none)');
|
|
308
|
+
ui?.notify?.(lines.join('\n'), 'info');
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
/* ── todo 族槽(core/todo.ts 插件回填;widget 渲染已随族迁移) ── */
|
|
313
|
+
const todoUi: { renderWidget: (ctx: unknown) => void } = {
|
|
314
|
+
renderWidget: () => { /* 插件挂载前 no-op(挂载后回填) */ },
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
/* ── 生命周期 ────────────────────────────────────────────────────── */
|
|
318
|
+
|
|
319
|
+
pi.on('session_start', async (event, ctx) => {
|
|
320
|
+
sessionId = resolveSessionId(ctx);
|
|
321
|
+
rebuildFromBranch(ctx);
|
|
322
|
+
// v1.3 M9 修复(实测):resume 时 pi 会从会话恢复 widget 状态;
|
|
323
|
+
// 此时再 setWidget 会破坏 TUI 的 '/' 命令面板输入路由('/' 被当消息文本送给模型)。
|
|
324
|
+
const reason = (event as { reason?: string } | undefined)?.reason;
|
|
325
|
+
if (reason !== 'resume') todoUi.renderWidget(ctx);
|
|
326
|
+
mirrorTodos();
|
|
327
|
+
reportSession(sessionId);
|
|
328
|
+
// D93:侧边栏身份 = role 名(display_agent 优先于 agent 检测值,0.8.2 actions.rs:563)。
|
|
329
|
+
// master → 'master';worker → manifest.role(worker-default 美化为 worker)。
|
|
330
|
+
// 无 manifest 的裸 pi(独立会话)不上报——不打扰普通 pi 使用。
|
|
331
|
+
if (runtimeManifest) {
|
|
332
|
+
const roleDisplay = runtimeManifest.role === 'worker-default' ? 'worker' : runtimeManifest.role;
|
|
333
|
+
void client.reportDisplayAgent(roleDisplay);
|
|
334
|
+
}
|
|
335
|
+
// 档2:worker role manifest(env,进程启动时已解析)→ 徽标 + 会话权威记录
|
|
336
|
+
// (custom 条目与 D38 todo-edit 同构:执行期强化的回放锚点)
|
|
337
|
+
if (runtimeManifest) {
|
|
338
|
+
roleBadge = `role ${runtimeManifest.role} v${runtimeManifest.version ?? '?'} (${runtimeManifest.tools.length} tools)`;
|
|
339
|
+
try {
|
|
340
|
+
(pi as { appendEntry?: (customType: string, data: unknown) => void }).appendEntry?.(
|
|
341
|
+
ROLE_MANIFEST_CUSTOM_TYPE,
|
|
342
|
+
{ version: 1, role: runtimeManifest.role, manifestVersion: runtimeManifest.version, tools: runtimeManifest.tools, permissions: runtimeManifest.permissions, unknownTools: runtimeManifest.unknownTools ?? 'deny', ts: Date.now() },
|
|
343
|
+
);
|
|
344
|
+
} catch {
|
|
345
|
+
/* 记录尽力而为 */
|
|
346
|
+
}
|
|
347
|
+
// D77 可见层:manifest 外的工具从模型视野移除(session_start = 所有插件已加载完)。
|
|
348
|
+
// 交集语义防清空;master 无 manifest 不动(全量)。底层 API 缺失则静默跳过(旧 pi 兼容)。
|
|
349
|
+
const piTools = pi as { getActiveTools?: () => string[]; setActiveTools?: (names: string[]) => void };
|
|
350
|
+
if (typeof piTools.getActiveTools === 'function' && typeof piTools.setActiveTools === 'function') {
|
|
351
|
+
try {
|
|
352
|
+
const active = piTools.getActiveTools();
|
|
353
|
+
const vis = planActiveTools(runtimeManifest.tools, active, {
|
|
354
|
+
unknownTools: runtimeManifest.unknownTools,
|
|
355
|
+
permissions: runtimeManifest.permissions,
|
|
356
|
+
});
|
|
357
|
+
if (vis?.changed) {
|
|
358
|
+
piTools.setActiveTools?.(vis.next);
|
|
359
|
+
console.error(`[pi-herdr] D77 visible-layer: role ${runtimeManifest.role} tools ${active.length} → ${vis.next.length}`);
|
|
360
|
+
}
|
|
361
|
+
} catch {
|
|
362
|
+
/* 可见层尽力而为(强制层仍在) */
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
reportAgent('idle', null);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
/* ── 档2 Week4-5:worker 执行期强制(manifest 最后一道闸) ──
|
|
370
|
+
* deny / 不在 manifest → block(reason 给模型);ask → v1 退化放行 + stderr 日志
|
|
371
|
+
* (V56 验收锚点)。master / 纯标签 worker 无 manifest = open。
|
|
372
|
+
* 限速已移除(WS-D6):权限边界归我们,资源配额归插件引入者。 */
|
|
373
|
+
pi.on('tool_call', async (event: { toolName?: string }) => {
|
|
374
|
+
if (!runtimeManifest) return;
|
|
375
|
+
const tool = typeof event?.toolName === 'string' ? event.toolName : '';
|
|
376
|
+
const gate = planToolGate(tool, runtimeManifest);
|
|
377
|
+
if (gate.kind === 'deny') {
|
|
378
|
+
return { block: true, reason: gate.reason };
|
|
379
|
+
}
|
|
380
|
+
if (gate.kind === 'ask') {
|
|
381
|
+
console.error(`${gate.notice} (v1 soft-approval: allowed, hard gate lands in v2)`);
|
|
382
|
+
// 持久留痕(V56 锚点):stderr 会被 TUI 重绘刷掉,会话 custom 条目才是权威
|
|
383
|
+
try {
|
|
384
|
+
(pi as { appendEntry?: (customType: string, data: unknown) => void }).appendEntry?.(
|
|
385
|
+
'pi-herdr.approval-needed',
|
|
386
|
+
{ role: runtimeManifest.role, tool, ts: Date.now() },
|
|
387
|
+
);
|
|
388
|
+
} catch {
|
|
389
|
+
/* 尽力而为 */
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
// 会话树跳转(/tree、/fork 后):分支正确性靠重建。
|
|
396
|
+
pi.on('session_tree', async (_event, ctx) => {
|
|
397
|
+
sessionId = resolveSessionId(ctx);
|
|
398
|
+
rebuildFromBranch(ctx);
|
|
399
|
+
todoUi.renderWidget(ctx);
|
|
400
|
+
mirrorTodos();
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
let agentActive = false;
|
|
404
|
+
pi.on('turn_start', async () => {
|
|
405
|
+
agentActive = true;
|
|
406
|
+
reportAgent('working', currentActivity(todos.items));
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
pi.on('agent_settled', async () => {
|
|
410
|
+
agentActive = false;
|
|
411
|
+
reportAgent('idle', null);
|
|
412
|
+
// D96:master settled(run 结束/想总结)时仍有后台 subagent running → 注入提醒。
|
|
413
|
+
// worker 侧 subagentSlots.listRunningSubs 为 null(不注入)。
|
|
414
|
+
const running = subagentSlots.listRunningSubs?.() ?? [];
|
|
415
|
+
if (running.length > 0 && !isSubagent) {
|
|
416
|
+
const brief = running.map((s) => `${s.paneId} (${s.description})`).join('、');
|
|
417
|
+
const notice = `注意:仍有 ${running.length} 个后台 subagent 在运行:${brief}。若你的任务依赖它们,请等待其结算(list_agents 查看状态);若不等待,请说明放弃原因。`;
|
|
418
|
+
void sendUserMessageIn(notice);
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
// v1.3 M8:blocked 自上报(D29 豁免依赖 blocked 可见)。
|
|
423
|
+
// 实测:pi 0.84.2 核心不发 "herdr:blocked"(官方集成的监听是死代码)→
|
|
424
|
+
// 本扩展自行管理:等待人类回答(ask_user_question)期间 enterBlocked/exitBlocked。
|
|
425
|
+
let blockedDepth = 0;
|
|
426
|
+
function enterBlocked(label: string | null): void {
|
|
427
|
+
blockedDepth += 1;
|
|
428
|
+
reportAgent('blocked', label);
|
|
429
|
+
// D95:人类闸门标志(workbench 热力分级区分 ask vs block)
|
|
430
|
+
if (label) void client.reportAskFlag(label).catch(() => {});
|
|
431
|
+
}
|
|
432
|
+
function exitBlocked(): void {
|
|
433
|
+
blockedDepth = Math.max(0, blockedDepth - 1);
|
|
434
|
+
if (blockedDepth === 0) {
|
|
435
|
+
reportAgent(agentActive ? 'working' : 'idle', agentActive ? currentActivity(todos.items) : null);
|
|
436
|
+
// D95:闸门解除 → 清标志
|
|
437
|
+
void client.reportAskFlag(null).catch(() => {});
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
(pi as { events?: { on?: (ev: string, cb: (data: unknown) => void) => void } }).events?.on?.(
|
|
441
|
+
'herdr:blocked',
|
|
442
|
+
(data) => {
|
|
443
|
+
const d = data as { active?: boolean; label?: string } | undefined;
|
|
444
|
+
if (d?.active) enterBlocked(typeof d.label === 'string' ? d.label : null);
|
|
445
|
+
else exitBlocked();
|
|
446
|
+
},
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
/* ── 工具:ask_user_question(v1.3 M8 人类闸门,主控与子代理都有) ── */
|
|
450
|
+
|
|
451
|
+
pi.registerTool({
|
|
452
|
+
name: 'ask_user_question',
|
|
453
|
+
label: 'Ask User',
|
|
454
|
+
description: [
|
|
455
|
+
'Ask the human a question and wait for their typed answer.',
|
|
456
|
+
'Use this when you genuinely need a human decision (approval, direction, trade-off choice) — not for information you can find yourself.',
|
|
457
|
+
'While waiting, the pane shows as blocked in herdr (the human sees it and can step in).',
|
|
458
|
+
'The answer comes back as the tool result; then continue your work.',
|
|
459
|
+
].join(' '),
|
|
460
|
+
parameters: Type.Object({
|
|
461
|
+
question: Type.String({ description: 'The question to ask the human' }),
|
|
462
|
+
}),
|
|
463
|
+
async execute(_tc, params, _sig, _upd, ctx) {
|
|
464
|
+
const question = String(params?.question ?? '').trim();
|
|
465
|
+
if (!question) {
|
|
466
|
+
return { content: [{ type: 'text', text: 'Error: `question` must be a non-empty string' }], details: {} };
|
|
467
|
+
}
|
|
468
|
+
const ui = (ctx as {
|
|
469
|
+
ui?: { input?: (title: string, placeholder?: string) => Promise<string | undefined> };
|
|
470
|
+
}).ui;
|
|
471
|
+
enterBlocked(question);
|
|
472
|
+
// v1.3 M8 实测:等待人类期间 herdr 检测持续显示 working,会覆盖单次 blocked 上报
|
|
473
|
+
// → 5s 心跳重报,保证 pane 在 herdr 里呈 blocked(人类可见 + GC 豁免)。
|
|
474
|
+
const hb = setInterval(() => { reportAgent('blocked', question); }, 5000);
|
|
475
|
+
try {
|
|
476
|
+
const answer = await ui?.input?.(question, 'your answer');
|
|
477
|
+
return {
|
|
478
|
+
content: [{ type: 'text', text: `The human answered: ${answer ?? '(no answer given)'}` }],
|
|
479
|
+
details: { question, answer: answer ?? null },
|
|
480
|
+
};
|
|
481
|
+
} finally {
|
|
482
|
+
clearInterval(hb);
|
|
483
|
+
exitBlocked();
|
|
484
|
+
}
|
|
485
|
+
},
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
pi.on('session_shutdown', async () => {
|
|
489
|
+
client.close();
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
/* ── M12:双向消息通道(D49/D50/D48;每个 pane 都在自己名字上监听,无主从) ── */
|
|
493
|
+
|
|
494
|
+
const settleNoticeLatch = new Set<string>();
|
|
495
|
+
/** 结算通知去重(push 快路径与 pollLoop 兜底二选一;按 paneId+请求id 记轮次,每轮各一条)。 */
|
|
496
|
+
function claimSettleNotice(key: string): boolean {
|
|
497
|
+
if (settleNoticeLatch.has(key)) return false;
|
|
498
|
+
settleNoticeLatch.add(key);
|
|
499
|
+
return true;
|
|
500
|
+
}
|
|
501
|
+
/** O6:master 收到 worker reply 时回写 sessionFile(core/subagent 插件挂载时回填槽)。 */
|
|
502
|
+
const subagentSlots: {
|
|
503
|
+
applyReplySession: ((paneId: string, sessionFile: string | null) => void) | null;
|
|
504
|
+
reconcileOnReply: ((paneId: string) => string[]) | null;
|
|
505
|
+
/** D96:处于 running 状态的后台 subagent 描述列表(master settled 时检查用)。 */
|
|
506
|
+
listRunningSubs: (() => Array<{ paneId: string; description: string }>) | null;
|
|
507
|
+
} = { applyReplySession: null, reconcileOnReply: null, listRunningSubs: null };
|
|
508
|
+
/** pipe server 盒(common 持有创建/关闭;core/subagent 的 root effect 也经盒关)。 */
|
|
509
|
+
const pipeServerBox: { current: ReturnType<typeof startPipeServer> | null } = { current: null };
|
|
510
|
+
/** 最近一次机器请求(结算时按它决定是否 push、push 给谁)。 */
|
|
511
|
+
let pendingMachineRequest: { id: string; from: string | null; push: boolean; sinceTs: number } | null = null;
|
|
512
|
+
/** 缓存的扩展上下文(interrupt 用 ctx.abort(),D48)。 */
|
|
513
|
+
let latestCtx: { abort?: () => void } | null = null;
|
|
514
|
+
|
|
515
|
+
// D96:triggerTurn:true——followUp 在 idle 时立即触发新 turn(否则通知只排队不唤醒,
|
|
516
|
+
// 用户实机:subagent 结算后 idle master 不醒来处理,任务丢失;extensions.md:1410)。
|
|
517
|
+
const sendUserMessageIn = (content: string): Promise<void> =>
|
|
518
|
+
(pi as { sendUserMessage?: (content: string, opts?: { deliverAs?: string; triggerTurn?: boolean }) => Promise<void> })
|
|
519
|
+
.sendUserMessage?.(content, { deliverAs: 'followUp', triggerTurn: true }) ?? Promise.resolve();
|
|
520
|
+
const sendUserMessageAs = (content: string, mode: 'steer' | 'followUp'): Promise<void> =>
|
|
521
|
+
(pi as { sendUserMessage?: (content: string, opts?: { deliverAs?: string; triggerTurn?: boolean }) => Promise<void> })
|
|
522
|
+
.sendUserMessage?.(content, { deliverAs: mode, triggerTurn: true }) ?? Promise.resolve();
|
|
523
|
+
|
|
524
|
+
/* ── D92:结算通知缓冲(followup 堆积修复)─────────────────────────────
|
|
525
|
+
* 旧路径 pi.sendUserMessage(followUp) 的 pi 语义 =「agent 没有更多 tool call
|
|
526
|
+
* 时才投递」= 整个 run 结束才回填——master 长任务执行期 N 条结算全攒队列,
|
|
527
|
+
* 结束瞬间洪水灌入(用户实机红框实证)。
|
|
528
|
+
* 新路径:忙时(agentActive)入扩展缓冲;
|
|
529
|
+
* - turn_end(LLM 迭代边界)折叠成一条以 steer 注入——pi 文档:steer =
|
|
530
|
+
* 「当前 assistant 的 tool call 执行完、下一次 LLM 调用前投递」,即 turn 间隙;
|
|
531
|
+
* - agent_settled 兜底:run 真正结束后残余缓冲直投(触发新 run)。
|
|
532
|
+
* 折叠规则(notice-buffer.ts):≤3 条原文逐条,>3 条前 3 + 尾行计数/全量指引。 */
|
|
533
|
+
const pendingSettleNotices: string[] = [];
|
|
534
|
+
const deliverNotice = async (content: string): Promise<void> => {
|
|
535
|
+
if (agentActive) {
|
|
536
|
+
pendingSettleNotices.push(content);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
await sendUserMessageIn(content);
|
|
540
|
+
};
|
|
541
|
+
const flushSettleNotices = async (mode: 'steer' | 'followUp'): Promise<void> => {
|
|
542
|
+
if (pendingSettleNotices.length === 0) return;
|
|
543
|
+
const collapsed = collapseNotices(pendingSettleNotices.splice(0));
|
|
544
|
+
if (collapsed) await sendUserMessageAs(collapsed, mode);
|
|
545
|
+
};
|
|
546
|
+
pi.on('turn_end', () => {
|
|
547
|
+
void flushSettleNotices('steer');
|
|
548
|
+
});
|
|
549
|
+
pi.on('agent_settled', () => {
|
|
550
|
+
void flushSettleNotices('followUp');
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
pi.on('session_start', async (_event, ctx) => {
|
|
554
|
+
latestCtx = ctx as { abort?: () => void } | null;
|
|
555
|
+
const cwd = (ctx as { cwd?: string }).cwd ?? process.cwd();
|
|
556
|
+
const paneId = env?.paneId ?? '';
|
|
557
|
+
if (!paneId) return;
|
|
558
|
+
const name = pipeNameFor(cwd, paneId);
|
|
559
|
+
if (pipeServerBox.current) {
|
|
560
|
+
try { pipeServerBox.current.close(); } catch { /* 旧实例 */ }
|
|
561
|
+
pipeServerBox.current = null;
|
|
562
|
+
}
|
|
563
|
+
try {
|
|
564
|
+
pipeServerBox.current = startPipeServer(name, async (req) => {
|
|
565
|
+
if (req.type === 'ping') return { type: 'ok', id: req.id, detail: paneId };
|
|
566
|
+
if (req.type === 'prompt' || req.type === 'follow_up') {
|
|
567
|
+
pendingMachineRequest = {
|
|
568
|
+
id: req.id,
|
|
569
|
+
from: req.from ?? null,
|
|
570
|
+
push: req.push === true,
|
|
571
|
+
sinceTs: Date.now(),
|
|
572
|
+
};
|
|
573
|
+
await sendUserMessageIn(req.text);
|
|
574
|
+
return { type: 'ok', id: req.id };
|
|
575
|
+
}
|
|
576
|
+
if (req.type === 'interrupt') {
|
|
577
|
+
// D48:进程内中止(ctx.abort(),pi 0.84.2 实测存在);排队机器消息保持排队
|
|
578
|
+
latestCtx?.abort?.();
|
|
579
|
+
pendingMachineRequest = null; // 被中断的轮次不 push(请求方已知情)
|
|
580
|
+
return { type: 'ok', id: req.id };
|
|
581
|
+
}
|
|
582
|
+
if (req.type === 'reply') {
|
|
583
|
+
// D50 / O6:回写 sessionFile 与通知去重解耦——账本必须先纠正
|
|
584
|
+
subagentSlots.applyReplySession?.(req.paneId, req.sessionFile);
|
|
585
|
+
if (claimSettleNotice(`${req.paneId}:${req.id}`)) {
|
|
586
|
+
// M17:快路径结算 → 自动对账(幂等;与 pollLoop 兜底双跑无害)
|
|
587
|
+
const notes = subagentSlots.reconcileOnReply?.(req.paneId) ?? [];
|
|
588
|
+
const head = formatSettlementNotice(`${req.paneId}`, req.text);
|
|
589
|
+
const body = (req.sessionFile ? `${head}\nSession: ${req.sessionFile}` : head)
|
|
590
|
+
+ (notes.length ? `\n${notes.join('\n')}` : '');
|
|
591
|
+
// D92:经缓冲注入(忙时 turn_end 折叠 steer,闲时直投)
|
|
592
|
+
await deliverNotice(body);
|
|
593
|
+
}
|
|
594
|
+
return { type: 'ok', id: req.id };
|
|
595
|
+
}
|
|
596
|
+
return { type: 'error', id: req.id, message: `unknown type ${req.type}` };
|
|
597
|
+
});
|
|
598
|
+
} catch {
|
|
599
|
+
/* 管道名占用(罕见):本次会话不提供通道,调用方 ping 超时后报错 */
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
pi.on('session_shutdown', () => {
|
|
603
|
+
if (pipeServerBox.current) {
|
|
604
|
+
try { pipeServerBox.current.close(); } catch { /* 已关 */ }
|
|
605
|
+
pipeServerBox.current = null;
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
// D50:本 pane 自己的结算 → 若有待回信的机器请求,push 摘要+会话路径给请求方
|
|
610
|
+
pi.on('agent_settled', async () => {
|
|
611
|
+
const req = pendingMachineRequest;
|
|
612
|
+
if (!req || !req.push || !req.from) return;
|
|
613
|
+
pendingMachineRequest = null;
|
|
614
|
+
try {
|
|
615
|
+
let text: string | null = null;
|
|
616
|
+
if (sessionId && /\.jsonl$/.test(sessionId)) {
|
|
617
|
+
const entries = readSessionFile(sessionId);
|
|
618
|
+
if (entries) text = lastAssistantText(entries, { sinceTs: req.sinceTs })?.text ?? null;
|
|
619
|
+
}
|
|
620
|
+
await pipeRequest(req.from, {
|
|
621
|
+
type: 'reply',
|
|
622
|
+
id: req.id,
|
|
623
|
+
paneId: env?.paneId ?? '',
|
|
624
|
+
text,
|
|
625
|
+
sessionFile: sessionId || null,
|
|
626
|
+
}, 5000);
|
|
627
|
+
} catch {
|
|
628
|
+
/* push 失败静默:请求方的 pollLoop 兜底 */
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
/* ── 工具:subagent / list_agents / send_message / interrupt_agent(v1.1 交互式子 pane;子代理侧不注册,深度限 1) ── */
|
|
633
|
+
|
|
634
|
+
const isSubagent = process.env.PI_HERDR_SUBAGENT === '1';
|
|
635
|
+
if (!isSubagent) {
|
|
636
|
+
// 档1:cordis 树根升级为 bootstrap(Loader + group builtin + 条件 hmr,D78/D80/D81);
|
|
637
|
+
// subagent scope 挂载/拆除仍走 subagent-scope(worker 进程两模块都不加载,C3)。
|
|
638
|
+
const { disposeSessionRoot } = await import('./subagent-scope.ts');
|
|
639
|
+
const { createCordisApp } = await import('./bootstrap.ts');
|
|
640
|
+
const { PiSurface } = await import('./pi-surface.ts');
|
|
641
|
+
const cordisApp = await createCordisApp();
|
|
642
|
+
const sessionRoot = cordisApp.root;
|
|
643
|
+
|
|
644
|
+
// pipe server 关闭挂在树根(common 段创建、树根 LIFO 拆除)——不进任何 core
|
|
645
|
+
// 插件:hmr 重载插件不能误杀 index 拥有的跨属资源(d87 修)。
|
|
646
|
+
sessionRoot.effect(() => () => {
|
|
647
|
+
if (pipeServerBox.current) {
|
|
648
|
+
try { pipeServerBox.current.close(); } catch { /* 已关 */ }
|
|
649
|
+
pipeServerBox.current = null;
|
|
650
|
+
}
|
|
651
|
+
}, 'pipe-server');
|
|
652
|
+
|
|
653
|
+
// D79 pi 注册面:core 模块经 surface 注册(tombstone + ledger 咬合);
|
|
654
|
+
// index 自身的注册仍走 pi 原面(逐模块迁移,本批 = terminal 族)。
|
|
655
|
+
const surface = new PiSurface(pi as unknown as object, cordisApp.ledger);
|
|
656
|
+
sessionRoot.provide('pi-herdr.surface', surface);
|
|
657
|
+
// terminal 依赖槽:client/env 直传 + state 槽回填(GC 豁免查询)
|
|
658
|
+
const terminalDeps = {
|
|
659
|
+
client,
|
|
660
|
+
env,
|
|
661
|
+
state: { activePaneIds: (): Set<string> => new Set() },
|
|
662
|
+
};
|
|
663
|
+
sessionRoot.provide('pi-herdr.terminal-deps', terminalDeps);
|
|
664
|
+
|
|
665
|
+
// M14 terminal 族迁 loader entry(热换面 = core/terminal.ts;loader 降级 → 直接 root.plugin)
|
|
666
|
+
const terminalMod = await import('./core/terminal.ts');
|
|
667
|
+
if (cordisApp.loaderReady) {
|
|
668
|
+
const withLoader = sessionRoot as typeof sessionRoot & {
|
|
669
|
+
loader?: { create: (o: { name: string }) => Promise<unknown> };
|
|
670
|
+
};
|
|
671
|
+
await withLoader.loader?.create({ name: './core/terminal.ts' });
|
|
672
|
+
} else {
|
|
673
|
+
await sessionRoot.plugin(terminalMod.default);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// todo 族迁 loader entry(协调工具:master/worker 都挂;本分支 = master 走 loader)
|
|
677
|
+
sessionRoot.provide('pi-herdr.todo-deps', {
|
|
678
|
+
todos,
|
|
679
|
+
allowParallelInProgress: todos.config.allowParallelInProgress,
|
|
680
|
+
maxItems: 15,
|
|
681
|
+
mirrorTodos,
|
|
682
|
+
appendEntry: (customType: string, data: unknown) => {
|
|
683
|
+
(pi as { appendEntry?: (t: string, d: unknown) => void }).appendEntry?.(customType, data);
|
|
684
|
+
},
|
|
685
|
+
state: todoUi,
|
|
686
|
+
});
|
|
687
|
+
const todoMod = await import('./core/todo.ts');
|
|
688
|
+
if (cordisApp.loaderReady) {
|
|
689
|
+
const withLoader = sessionRoot as typeof sessionRoot & {
|
|
690
|
+
loader?: { create: (o: { name: string }) => Promise<unknown> };
|
|
691
|
+
};
|
|
692
|
+
await withLoader.loader?.create({ name: './core/todo.ts' });
|
|
693
|
+
} else {
|
|
694
|
+
await sessionRoot.plugin(todoMod.default);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// subagent 族迁 loader entry(master-only:五工具 + 注册表 + poller + 历史 + GC)
|
|
698
|
+
sessionRoot.provide('pi-herdr.subagent-deps', {
|
|
699
|
+
client,
|
|
700
|
+
env,
|
|
701
|
+
extPath: fileURLToPath(import.meta.url),
|
|
702
|
+
sessionRoot,
|
|
703
|
+
slots: subagentSlots,
|
|
704
|
+
getSessionId: () => sessionId,
|
|
705
|
+
getBlockedDepth: () => blockedDepth,
|
|
706
|
+
reconcileOnSettlement,
|
|
707
|
+
withReconcileNotes,
|
|
708
|
+
claimSettleNotice,
|
|
709
|
+
deliverNotice,
|
|
710
|
+
terminalState: terminalDeps.state,
|
|
711
|
+
todos,
|
|
712
|
+
});
|
|
713
|
+
const subagentMod = await import('./core/subagent.ts');
|
|
714
|
+
if (cordisApp.loaderReady) {
|
|
715
|
+
const withLoader = sessionRoot as typeof sessionRoot & {
|
|
716
|
+
loader?: { create: (o: { name: string }) => Promise<unknown> };
|
|
717
|
+
};
|
|
718
|
+
await withLoader.loader?.create({ name: './core/subagent.ts' });
|
|
719
|
+
} else {
|
|
720
|
+
await sessionRoot.plugin(subagentMod.default);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// 档0 起的拆除链:session_shutdown → dispose 整树(scope/effect LIFO)
|
|
724
|
+
pi.on('session_shutdown', () => {
|
|
725
|
+
void disposeSessionRoot(sessionRoot);
|
|
726
|
+
});
|
|
727
|
+
} else {
|
|
728
|
+
// C3/D81:worker = 裸根 + 手动 mount(短命进程,无 loader/hmr/timer)。
|
|
729
|
+
// todo 族是协调工具(任何角色档案必有)→ worker 也挂同一 core/todo.ts 插件。
|
|
730
|
+
const { Context } = await import('@deepseek-ai/cordis');
|
|
731
|
+
const { PiSurface } = await import('./pi-surface.ts');
|
|
732
|
+
const workerRoot = new Context();
|
|
733
|
+
const workerSurface = new PiSurface(pi as unknown as object);
|
|
734
|
+
workerRoot.provide('pi-herdr.surface', workerSurface);
|
|
735
|
+
workerRoot.provide('pi-herdr.todo-deps', {
|
|
736
|
+
todos,
|
|
737
|
+
allowParallelInProgress: todos.config.allowParallelInProgress,
|
|
738
|
+
maxItems: 15,
|
|
739
|
+
mirrorTodos,
|
|
740
|
+
appendEntry: (customType: string, data: unknown) => {
|
|
741
|
+
(pi as { appendEntry?: (t: string, d: unknown) => void }).appendEntry?.(customType, data);
|
|
742
|
+
},
|
|
743
|
+
state: todoUi,
|
|
744
|
+
});
|
|
745
|
+
const todoMod = await import('./core/todo.ts');
|
|
746
|
+
await workerRoot.plugin(todoMod.default);
|
|
747
|
+
pi.on('session_shutdown', () => {
|
|
748
|
+
void workerRoot.fiber.dispose();
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
}
|