@howaboua/pi-agent-board 0.0.0-stage → 0.0.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/CHANGELOG.md +15 -0
- package/INTEGRATION.md +31 -0
- package/LICENSE +21 -0
- package/README.md +80 -2
- package/changelog.ts +337 -0
- package/dist/viewer/archive.js +185 -0
- package/dist/viewer/config.js +85 -0
- package/dist/viewer/contracts.js +1 -0
- package/dist/viewer/server.js +172 -0
- package/dist/viewer/source.js +29 -0
- package/dist/viewer/web/app.js +485 -0
- package/dist/viewer/web/index.html +39 -0
- package/dist/viewer/web/style.css +481 -0
- package/index.ts +63 -0
- package/package.json +65 -5
- package/src/activity.ts +273 -0
- package/src/archive.ts +103 -0
- package/src/awareness.ts +181 -0
- package/src/briefing.ts +19 -0
- package/src/config.ts +104 -0
- package/src/context-briefing.ts +227 -0
- package/src/contract.ts +189 -0
- package/src/delivery.ts +76 -0
- package/src/identity.ts +174 -0
- package/src/integration.ts +54 -0
- package/src/paging.ts +55 -0
- package/src/protocol.ts +78 -0
- package/src/queries.ts +176 -0
- package/src/response.ts +56 -0
- package/src/runtime.ts +561 -0
- package/src/store.ts +327 -0
- package/src/tool.ts +22 -0
- package/src/turns.ts +101 -0
- package/src/viewer/archive.ts +227 -0
- package/src/viewer/config.ts +111 -0
- package/src/viewer/contracts.ts +82 -0
- package/src/viewer/index.ts +158 -0
- package/src/viewer/server.ts +199 -0
- package/src/viewer/source.ts +33 -0
- package/src/viewer/web/app.js +485 -0
- package/src/viewer/web/index.html +39 -0
- package/src/viewer/web/style.css +481 -0
- package/tsconfig.json +5 -0
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import {
|
|
6
|
+
recordBoardActivityMarker,
|
|
7
|
+
registerBoardActivityRenderer,
|
|
8
|
+
} from "./activity.js";
|
|
9
|
+
import { executeArchive, readSubscribedUpdates } from "./archive.js";
|
|
10
|
+
import { BoardAwareness } from "./awareness.js";
|
|
11
|
+
import { type BoardScope, writeBoardConfig } from "./config.js";
|
|
12
|
+
import {
|
|
13
|
+
agentPath,
|
|
14
|
+
type BoardParams,
|
|
15
|
+
boardHelp,
|
|
16
|
+
parseBoardRequest,
|
|
17
|
+
} from "./contract.js";
|
|
18
|
+
import { initializeDelivery, isMissingOptionalModule } from "./delivery.js";
|
|
19
|
+
import {
|
|
20
|
+
binding,
|
|
21
|
+
members,
|
|
22
|
+
rootBoardSetting,
|
|
23
|
+
saveBinding,
|
|
24
|
+
saveBoardSetting,
|
|
25
|
+
setBoardDefault,
|
|
26
|
+
} from "./identity.js";
|
|
27
|
+
import type { BoardAdapter } from "./integration.js";
|
|
28
|
+
import { type BoardEnvelope, parseEnvelope } from "./protocol.js";
|
|
29
|
+
import { createBoardTool } from "./tool.js";
|
|
30
|
+
import { BoardTurns } from "./turns.js";
|
|
31
|
+
|
|
32
|
+
export class BoardRuntime {
|
|
33
|
+
private readonly pi: ExtensionAPI;
|
|
34
|
+
private adapter: BoardAdapter | undefined;
|
|
35
|
+
private readonly turns: BoardTurns;
|
|
36
|
+
private readonly awareness: BoardAwareness;
|
|
37
|
+
private refreshTools: (() => void) | undefined;
|
|
38
|
+
private lastSetting: { sessionId: string; enabled: boolean } | undefined;
|
|
39
|
+
private configError: string | undefined;
|
|
40
|
+
private availabilityWarning: string | undefined;
|
|
41
|
+
constructor(pi: ExtensionAPI) {
|
|
42
|
+
this.pi = pi;
|
|
43
|
+
const availability = (ctx: ExtensionContext, error?: unknown) =>
|
|
44
|
+
this.reportAvailability(ctx, error);
|
|
45
|
+
this.awareness = new BoardAwareness(
|
|
46
|
+
pi,
|
|
47
|
+
(ctx) => this.population(ctx),
|
|
48
|
+
availability,
|
|
49
|
+
() => this.adapter?.briefing,
|
|
50
|
+
);
|
|
51
|
+
this.turns = new BoardTurns(
|
|
52
|
+
pi,
|
|
53
|
+
(ctx, request) => this.toOwner(ctx, request),
|
|
54
|
+
availability,
|
|
55
|
+
(ctx, ids) => this.awareness.markSeen(ctx, ids),
|
|
56
|
+
);
|
|
57
|
+
pi.on("session_start", (_event, ctx) => {
|
|
58
|
+
this.availabilityWarning = undefined;
|
|
59
|
+
setBoardDefault(ctx, true);
|
|
60
|
+
return this.refresh(ctx);
|
|
61
|
+
});
|
|
62
|
+
pi.on("before_agent_start", async (_event, ctx) => {
|
|
63
|
+
// Reconcile resumed/offline children before tool/prompt preparation.
|
|
64
|
+
const own = binding(ctx);
|
|
65
|
+
if (own.upstream) {
|
|
66
|
+
try {
|
|
67
|
+
const result = await this.toOwner(ctx, {
|
|
68
|
+
operation: "board-call",
|
|
69
|
+
caller: own,
|
|
70
|
+
params: { action: "help" },
|
|
71
|
+
requestId: "status",
|
|
72
|
+
});
|
|
73
|
+
availability(ctx);
|
|
74
|
+
if (
|
|
75
|
+
typeof result === "object" &&
|
|
76
|
+
result !== null &&
|
|
77
|
+
"enabled" in result &&
|
|
78
|
+
typeof result.enabled === "boolean" &&
|
|
79
|
+
result.enabled !== own.enabled
|
|
80
|
+
)
|
|
81
|
+
saveBinding(pi, { ...own, enabled: result.enabled });
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (own.enabled) availability(ctx, error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
await this.refresh(ctx);
|
|
87
|
+
const catchup = await this.promptCatchup(ctx);
|
|
88
|
+
catchup.accepted();
|
|
89
|
+
if (catchup.content)
|
|
90
|
+
return {
|
|
91
|
+
message: {
|
|
92
|
+
customType: "shepherdr-board-catchup",
|
|
93
|
+
content: catchup.content.trim(),
|
|
94
|
+
display: true,
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
private reportAvailability(ctx: ExtensionContext, error?: unknown): void {
|
|
100
|
+
const own = binding(ctx);
|
|
101
|
+
if (error === undefined || !own.enabled) {
|
|
102
|
+
this.availabilityWarning = undefined;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
106
|
+
const key = JSON.stringify([
|
|
107
|
+
own.sessionId,
|
|
108
|
+
own.boardId,
|
|
109
|
+
own.upstream,
|
|
110
|
+
message,
|
|
111
|
+
]);
|
|
112
|
+
if (this.availabilityWarning === key) return;
|
|
113
|
+
this.availabilityWarning = key;
|
|
114
|
+
ctx.ui.notify(`Board unavailable: ${message}`, "warning");
|
|
115
|
+
}
|
|
116
|
+
setToolRefresh(refresh: () => void) {
|
|
117
|
+
this.refreshTools = refresh;
|
|
118
|
+
}
|
|
119
|
+
private async population(ctx: ExtensionContext) {
|
|
120
|
+
const own = binding(ctx);
|
|
121
|
+
if (own.upstream) {
|
|
122
|
+
const status = await this.toOwner(
|
|
123
|
+
ctx,
|
|
124
|
+
{
|
|
125
|
+
operation: "board-call",
|
|
126
|
+
caller: own,
|
|
127
|
+
params: { action: "help" },
|
|
128
|
+
requestId: "briefing-status",
|
|
129
|
+
},
|
|
130
|
+
ctx.signal,
|
|
131
|
+
);
|
|
132
|
+
if (
|
|
133
|
+
!status ||
|
|
134
|
+
typeof status !== "object" ||
|
|
135
|
+
!("enabled" in status) ||
|
|
136
|
+
typeof status.enabled !== "boolean"
|
|
137
|
+
)
|
|
138
|
+
throw new Error("Board status unavailable");
|
|
139
|
+
if (status.enabled !== own.enabled) {
|
|
140
|
+
saveBinding(this.pi, { ...own, enabled: status.enabled });
|
|
141
|
+
await this.refresh(ctx);
|
|
142
|
+
}
|
|
143
|
+
if (!status.enabled) return;
|
|
144
|
+
}
|
|
145
|
+
return this.execute(
|
|
146
|
+
ctx,
|
|
147
|
+
{ action: "search_posts", limit: 1, max_chars_per_post: 1 },
|
|
148
|
+
"briefing",
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
enabled(ctx: ExtensionContext | undefined) {
|
|
152
|
+
return ctx ? binding(ctx).enabled : false;
|
|
153
|
+
}
|
|
154
|
+
async promptCatchup(
|
|
155
|
+
ctx: ExtensionContext,
|
|
156
|
+
): Promise<{ content: string; accepted: () => void }> {
|
|
157
|
+
const empty = { content: "", accepted: () => {} };
|
|
158
|
+
const own = binding(ctx);
|
|
159
|
+
if (!own.enabled) return empty;
|
|
160
|
+
try {
|
|
161
|
+
const result = await this.toOwner(ctx, {
|
|
162
|
+
operation: "board-catchup",
|
|
163
|
+
caller: own,
|
|
164
|
+
seen: this.awareness.seen(ctx),
|
|
165
|
+
});
|
|
166
|
+
if (
|
|
167
|
+
!Array.isArray(result) ||
|
|
168
|
+
!result.every(
|
|
169
|
+
(post) =>
|
|
170
|
+
post &&
|
|
171
|
+
typeof post.message_id === "string" &&
|
|
172
|
+
typeof post.thread_id === "string",
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
throw new Error("Invalid board catch-up response");
|
|
176
|
+
if (
|
|
177
|
+
!binding(ctx).enabled ||
|
|
178
|
+
binding(ctx).sessionId !== own.sessionId ||
|
|
179
|
+
binding(ctx).boardId !== own.boardId ||
|
|
180
|
+
binding(ctx).agentName !== own.agentName
|
|
181
|
+
)
|
|
182
|
+
return empty;
|
|
183
|
+
this.reportAvailability(ctx);
|
|
184
|
+
const nested = this.pi
|
|
185
|
+
.getActiveTools()
|
|
186
|
+
.some((tool) => ["exec", "code", "notebook"].includes(tool));
|
|
187
|
+
return {
|
|
188
|
+
content: result.length
|
|
189
|
+
? `\n\nUnread board updates available; read if relevant to the current task: ${[
|
|
190
|
+
...new Set(result.map((post) => post.thread_id)),
|
|
191
|
+
]
|
|
192
|
+
.map((thread_id) => {
|
|
193
|
+
const args = JSON.stringify({
|
|
194
|
+
action: "read_thread",
|
|
195
|
+
thread_id,
|
|
196
|
+
});
|
|
197
|
+
return nested ? `await tools.board(${args})` : `board ${args}`;
|
|
198
|
+
})
|
|
199
|
+
.join("; ")}`
|
|
200
|
+
: "",
|
|
201
|
+
accepted: () =>
|
|
202
|
+
this.awareness.markSeen(
|
|
203
|
+
ctx,
|
|
204
|
+
result.map((post) => post.message_id),
|
|
205
|
+
),
|
|
206
|
+
};
|
|
207
|
+
} catch (error) {
|
|
208
|
+
this.reportAvailability(ctx, error);
|
|
209
|
+
return empty;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async refresh(ctx: ExtensionContext) {
|
|
213
|
+
const own = binding(ctx);
|
|
214
|
+
this.awareness.refresh(ctx);
|
|
215
|
+
const setting = own.upstream ? undefined : rootBoardSetting(ctx);
|
|
216
|
+
if (setting?.error && setting.error !== this.configError)
|
|
217
|
+
ctx.ui.notify(`Board disabled: ${setting.error}`, "error");
|
|
218
|
+
this.configError = setting?.error;
|
|
219
|
+
const active = this.pi.getActiveTools().filter((name) => name !== "board");
|
|
220
|
+
if (own.enabled) active.push("board");
|
|
221
|
+
this.pi.setActiveTools(active);
|
|
222
|
+
this.refreshTools?.();
|
|
223
|
+
if (!own.enabled) this.turns.active.clear();
|
|
224
|
+
const changed =
|
|
225
|
+
this.lastSetting?.sessionId !== own.sessionId ||
|
|
226
|
+
this.lastSetting.enabled !== own.enabled;
|
|
227
|
+
this.lastSetting = { sessionId: own.sessionId, enabled: own.enabled };
|
|
228
|
+
if (changed && !own.upstream) await this.propagateEnabled(ctx, own.enabled);
|
|
229
|
+
}
|
|
230
|
+
async setSetting(
|
|
231
|
+
ctx: ExtensionContext,
|
|
232
|
+
scope: BoardScope,
|
|
233
|
+
enabled: boolean | undefined,
|
|
234
|
+
) {
|
|
235
|
+
if (!ctx.isIdle())
|
|
236
|
+
throw new Error("Change the board setting after this session settles");
|
|
237
|
+
const own = binding(ctx);
|
|
238
|
+
if (own.upstream)
|
|
239
|
+
throw new Error("Change the board setting in the owning root session");
|
|
240
|
+
if (scope === "session") saveBoardSetting(this.pi, ctx, enabled);
|
|
241
|
+
else writeBoardConfig(own.ownerFolder, scope, enabled);
|
|
242
|
+
await this.refresh(ctx);
|
|
243
|
+
}
|
|
244
|
+
status(ctx: ExtensionContext, includeArchive = true) {
|
|
245
|
+
const own = binding(ctx);
|
|
246
|
+
const setting = own.upstream ? undefined : rootBoardSetting(ctx);
|
|
247
|
+
return `Board ${own.enabled ? "on" : "off"} (${setting?.source ?? "inherited from owner"})${includeArchive ? `. ${own.boardId}\nArchive: ${own.databasePath}` : ""}${setting?.error ? `\n${setting.error}` : ""}`;
|
|
248
|
+
}
|
|
249
|
+
async execute(ctx: ExtensionContext, input: unknown, requestId: string) {
|
|
250
|
+
const params = parseBoardRequest(input);
|
|
251
|
+
const caller = binding(ctx);
|
|
252
|
+
if (!caller.upstream) {
|
|
253
|
+
const setting = rootBoardSetting(ctx);
|
|
254
|
+
if (setting.error) throw new Error(setting.error);
|
|
255
|
+
}
|
|
256
|
+
if (!caller.enabled)
|
|
257
|
+
throw new Error("Board is off; the user can enable it with /board on");
|
|
258
|
+
const value = await this.toOwner(
|
|
259
|
+
ctx,
|
|
260
|
+
{
|
|
261
|
+
operation: "board-call",
|
|
262
|
+
caller,
|
|
263
|
+
params,
|
|
264
|
+
requestId,
|
|
265
|
+
},
|
|
266
|
+
ctx.signal,
|
|
267
|
+
);
|
|
268
|
+
if (params.action === "read_thread" || params.action === "read_post")
|
|
269
|
+
this.awareness.markRead(ctx, value);
|
|
270
|
+
try {
|
|
271
|
+
recordBoardActivityMarker(this.pi, ctx, params, value, requestId);
|
|
272
|
+
} catch {
|
|
273
|
+
// Presentation failure must not turn a successful operation into a retry.
|
|
274
|
+
ctx.ui.notify(
|
|
275
|
+
"Board action succeeded. Its history notice is unavailable. Do not repeat writes just to restore the notice.",
|
|
276
|
+
"warning",
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
return value;
|
|
280
|
+
}
|
|
281
|
+
async handle(
|
|
282
|
+
ctx: ExtensionContext,
|
|
283
|
+
value: unknown,
|
|
284
|
+
signal?: AbortSignal,
|
|
285
|
+
): Promise<unknown> {
|
|
286
|
+
const request = parseEnvelope(value);
|
|
287
|
+
const own = binding(ctx);
|
|
288
|
+
if (request.operation === "board-bind") {
|
|
289
|
+
if (
|
|
290
|
+
!ctx.isIdle() ||
|
|
291
|
+
ctx.sessionManager
|
|
292
|
+
.getEntries()
|
|
293
|
+
.some(
|
|
294
|
+
(entry) =>
|
|
295
|
+
(entry.type === "message" &&
|
|
296
|
+
["user", "assistant"].includes(entry.message.role)) ||
|
|
297
|
+
(entry.type === "custom" &&
|
|
298
|
+
entry.customType === "shepherdr-board-binding"),
|
|
299
|
+
)
|
|
300
|
+
)
|
|
301
|
+
throw new Error(
|
|
302
|
+
"Bind board identity only in a fresh idle child before its first task",
|
|
303
|
+
);
|
|
304
|
+
const adopted = { ...request.binding, sessionId: own.sessionId };
|
|
305
|
+
saveBinding(this.pi, adopted);
|
|
306
|
+
await this.refresh(ctx);
|
|
307
|
+
return adopted;
|
|
308
|
+
}
|
|
309
|
+
if (request.operation === "board-enabled") {
|
|
310
|
+
if (!own.upstream || request.boardId !== own.boardId)
|
|
311
|
+
throw new Error("Invalid board setting route");
|
|
312
|
+
saveBinding(this.pi, { ...own, enabled: request.enabled });
|
|
313
|
+
await this.refresh(ctx);
|
|
314
|
+
await this.propagateEnabled(ctx, request.enabled);
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
if (request.operation === "board-notify") {
|
|
318
|
+
if (!own.enabled || request.boardId !== own.boardId) return false;
|
|
319
|
+
if (request.target === own.agentName) return this.turns.deliver(request);
|
|
320
|
+
return this.routeNotice(ctx, request, signal);
|
|
321
|
+
}
|
|
322
|
+
return this.toOwner(ctx, request, signal);
|
|
323
|
+
}
|
|
324
|
+
async toOwner(
|
|
325
|
+
ctx: ExtensionContext,
|
|
326
|
+
request: BoardEnvelope,
|
|
327
|
+
signal?: AbortSignal,
|
|
328
|
+
): Promise<unknown> {
|
|
329
|
+
const own = binding(ctx);
|
|
330
|
+
if (own.upstream) {
|
|
331
|
+
try {
|
|
332
|
+
const result = await this.requireAdapter().request(
|
|
333
|
+
own.upstream,
|
|
334
|
+
request,
|
|
335
|
+
signal,
|
|
336
|
+
);
|
|
337
|
+
this.reportAvailability(ctx);
|
|
338
|
+
return result;
|
|
339
|
+
} catch (error) {
|
|
340
|
+
if (
|
|
341
|
+
error instanceof Error &&
|
|
342
|
+
error.message.startsWith("Shared context response was lost")
|
|
343
|
+
)
|
|
344
|
+
throw new Error(
|
|
345
|
+
"Board response lost; a mutation may have completed. Read the channel/thread before retrying",
|
|
346
|
+
{ cause: error },
|
|
347
|
+
);
|
|
348
|
+
if (
|
|
349
|
+
error instanceof Error &&
|
|
350
|
+
error.message.startsWith("Shared context owner is unavailable")
|
|
351
|
+
)
|
|
352
|
+
throw new Error(
|
|
353
|
+
"Board owner unavailable; resume its Pi session and reconnect its machine",
|
|
354
|
+
{ cause: error },
|
|
355
|
+
);
|
|
356
|
+
throw error;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (!("caller" in request)) throw new Error("Invalid board owner request");
|
|
360
|
+
const directory = members(ctx);
|
|
361
|
+
const caller = directory.find(
|
|
362
|
+
(member) =>
|
|
363
|
+
member.sessionId === request.caller.sessionId &&
|
|
364
|
+
member.agentName === request.caller.agentName,
|
|
365
|
+
);
|
|
366
|
+
if (
|
|
367
|
+
!caller ||
|
|
368
|
+
request.caller.boardId !== own.boardId ||
|
|
369
|
+
request.caller.databasePath !== own.databasePath
|
|
370
|
+
)
|
|
371
|
+
throw new Error("Caller not bound to this board");
|
|
372
|
+
if (
|
|
373
|
+
request.operation === "board-unregister" ||
|
|
374
|
+
request.operation === "board-register"
|
|
375
|
+
)
|
|
376
|
+
return this.requireAdapter().commitDirectory(
|
|
377
|
+
own,
|
|
378
|
+
directory,
|
|
379
|
+
caller,
|
|
380
|
+
request,
|
|
381
|
+
);
|
|
382
|
+
if (request.operation === "board-active") {
|
|
383
|
+
if (own.enabled) this.turns.register(caller, request);
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
if (request.operation === "board-catchup")
|
|
387
|
+
return own.enabled ? readSubscribedUpdates(caller, request.seen) : [];
|
|
388
|
+
if (request.operation !== "board-call")
|
|
389
|
+
throw new Error("Invalid board owner operation");
|
|
390
|
+
const params = parseBoardRequest(request.params);
|
|
391
|
+
if (params.action === "help")
|
|
392
|
+
return {
|
|
393
|
+
...boardHelp,
|
|
394
|
+
rules: this.adapter
|
|
395
|
+
? boardHelp.rules
|
|
396
|
+
: {
|
|
397
|
+
...boardHelp.rules,
|
|
398
|
+
agents:
|
|
399
|
+
"Absolute /root/... or child path relative to you; notices require registered members",
|
|
400
|
+
},
|
|
401
|
+
enabled: own.enabled,
|
|
402
|
+
};
|
|
403
|
+
if (!own.enabled)
|
|
404
|
+
throw new Error("Board is off; the user can enable it with /board on");
|
|
405
|
+
const prepared: BoardParams =
|
|
406
|
+
params.author === undefined
|
|
407
|
+
? params
|
|
408
|
+
: { ...params, author: agentPath(params.author, caller.agentName) };
|
|
409
|
+
const active = new Map(this.turns.active);
|
|
410
|
+
const result = await executeArchive(
|
|
411
|
+
{ ...caller, enabled: own.enabled },
|
|
412
|
+
directory.map((member) => member.agentName),
|
|
413
|
+
prepared,
|
|
414
|
+
request.requestId,
|
|
415
|
+
);
|
|
416
|
+
if ("notice" in result && result.notice) {
|
|
417
|
+
for (let start = 0; start < result.recipients.length; start += 16) {
|
|
418
|
+
await Promise.all(
|
|
419
|
+
result.recipients.slice(start, start + 16).map(async (target) => {
|
|
420
|
+
const turn = active.get(target);
|
|
421
|
+
if (!turn) return;
|
|
422
|
+
try {
|
|
423
|
+
await this.handle(
|
|
424
|
+
ctx,
|
|
425
|
+
{
|
|
426
|
+
operation: "board-notify",
|
|
427
|
+
boardId: own.boardId,
|
|
428
|
+
target,
|
|
429
|
+
...turn,
|
|
430
|
+
notice: result.notice,
|
|
431
|
+
},
|
|
432
|
+
AbortSignal.timeout(750),
|
|
433
|
+
);
|
|
434
|
+
} catch {
|
|
435
|
+
/* Committed posts succeed even when a running turn stops accepting. */
|
|
436
|
+
}
|
|
437
|
+
}),
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return result.value;
|
|
442
|
+
}
|
|
443
|
+
private async routeNotice(
|
|
444
|
+
ctx: ExtensionContext,
|
|
445
|
+
request: Extract<BoardEnvelope, { operation: "board-notify" }>,
|
|
446
|
+
signal?: AbortSignal,
|
|
447
|
+
) {
|
|
448
|
+
return this.requireAdapter().routeNotice(ctx, request, signal);
|
|
449
|
+
}
|
|
450
|
+
private async propagateEnabled(ctx: ExtensionContext, enabled: boolean) {
|
|
451
|
+
await this.adapter?.propagateEnabled(ctx, enabled);
|
|
452
|
+
}
|
|
453
|
+
attach(adapter: BoardAdapter) {
|
|
454
|
+
if (this.adapter) throw new Error("Board integration already attached");
|
|
455
|
+
this.adapter = adapter;
|
|
456
|
+
}
|
|
457
|
+
private requireAdapter() {
|
|
458
|
+
if (!this.adapter)
|
|
459
|
+
throw new Error(
|
|
460
|
+
"Board member route unavailable; load the extension that manages this session's board membership",
|
|
461
|
+
);
|
|
462
|
+
return this.adapter;
|
|
463
|
+
}
|
|
464
|
+
get active() {
|
|
465
|
+
return this.turns.active;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const CLAIM = "@howaboua/pi-agent-board.owner/v1";
|
|
470
|
+
const AVAILABLE = "@howaboua/pi-agent-board.available/v1";
|
|
471
|
+
interface OwnerClaim {
|
|
472
|
+
runtime: Promise<BoardRuntime>;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** Discover an explicitly loaded owner, including one loaded after this adapter. */
|
|
476
|
+
export function discoverBoard(
|
|
477
|
+
pi: ExtensionAPI,
|
|
478
|
+
attach: (runtime: BoardRuntime) => void,
|
|
479
|
+
) {
|
|
480
|
+
let attached = false;
|
|
481
|
+
const accept = (runtime: BoardRuntime) => {
|
|
482
|
+
if (attached) return;
|
|
483
|
+
attached = true;
|
|
484
|
+
attach(runtime);
|
|
485
|
+
};
|
|
486
|
+
const request = () =>
|
|
487
|
+
pi.events.emit(CLAIM, {
|
|
488
|
+
accept: (claim: OwnerClaim) => {
|
|
489
|
+
void claim.runtime.then(accept);
|
|
490
|
+
},
|
|
491
|
+
});
|
|
492
|
+
const stop = pi.events.on(AVAILABLE, request);
|
|
493
|
+
request();
|
|
494
|
+
pi.on("session_shutdown", stop);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** Only the standalone extension creates and registers the board engine. */
|
|
498
|
+
export async function acquireBoard(pi: ExtensionAPI): Promise<BoardRuntime> {
|
|
499
|
+
let claim: OwnerClaim | undefined;
|
|
500
|
+
pi.events.emit(CLAIM, {
|
|
501
|
+
accept(value: OwnerClaim) {
|
|
502
|
+
claim = value;
|
|
503
|
+
},
|
|
504
|
+
});
|
|
505
|
+
if (!claim) {
|
|
506
|
+
const created: OwnerClaim = {
|
|
507
|
+
runtime: createRuntime(pi),
|
|
508
|
+
};
|
|
509
|
+
claim = created;
|
|
510
|
+
const stop = pi.events.on(CLAIM, (value) => {
|
|
511
|
+
if (
|
|
512
|
+
value &&
|
|
513
|
+
typeof value === "object" &&
|
|
514
|
+
"accept" in value &&
|
|
515
|
+
typeof value.accept === "function"
|
|
516
|
+
)
|
|
517
|
+
value.accept(created);
|
|
518
|
+
});
|
|
519
|
+
pi.on("session_shutdown", stop);
|
|
520
|
+
}
|
|
521
|
+
const runtime = await claim.runtime;
|
|
522
|
+
pi.events.emit(AVAILABLE, undefined);
|
|
523
|
+
return runtime;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
async function createRuntime(pi: ExtensionAPI) {
|
|
527
|
+
await initializeDelivery();
|
|
528
|
+
const runtime = new BoardRuntime(pi);
|
|
529
|
+
const tool = createBoardTool(runtime);
|
|
530
|
+
pi.registerTool(tool);
|
|
531
|
+
registerBoardActivityRenderer(pi);
|
|
532
|
+
try {
|
|
533
|
+
const { adaptToolForCodeMode, registerCodeModeExtensionTools } =
|
|
534
|
+
await import("@howaboua/pi-codex-conversion/code-mode");
|
|
535
|
+
const registration = registerCodeModeExtensionTools(
|
|
536
|
+
pi,
|
|
537
|
+
() => [
|
|
538
|
+
adaptToolForCodeMode(tool, {
|
|
539
|
+
prepareInput: (input) =>
|
|
540
|
+
input === undefined || input === null ? {} : input,
|
|
541
|
+
usage: "await tools.board() // Shared discussion archive",
|
|
542
|
+
}),
|
|
543
|
+
],
|
|
544
|
+
{ isActive: (ctx) => runtime.enabled(ctx) },
|
|
545
|
+
);
|
|
546
|
+
runtime.setToolRefresh(() => registration.refresh());
|
|
547
|
+
pi.on("session_shutdown", () => registration.unregister());
|
|
548
|
+
} catch (error) {
|
|
549
|
+
if (!isMissingOptionalModule(error, "code-mode")) throw error;
|
|
550
|
+
if (
|
|
551
|
+
error instanceof Error &&
|
|
552
|
+
"code" in error &&
|
|
553
|
+
error.code === "ERR_PACKAGE_PATH_NOT_EXPORTED"
|
|
554
|
+
)
|
|
555
|
+
throw new Error(
|
|
556
|
+
"Update Codex Conversion to use its Code and Notebook board tools",
|
|
557
|
+
{ cause: error },
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
return runtime;
|
|
561
|
+
}
|