@llblab/pi-actors 0.29.3 → 0.30.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/BACKLOG.md +118 -199
- package/CHANGELOG.md +10 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +71 -315
- package/dist/lib/actor-inspector-tui.d.ts +35 -0
- package/dist/lib/actor-inspector-tui.js +159 -1
- package/dist/lib/observability.d.ts +34 -0
- package/dist/lib/observability.js +103 -1
- package/dist/lib/paths.d.ts +9 -0
- package/dist/lib/paths.js +15 -0
- package/dist/lib/pi.d.ts +19 -0
- package/dist/lib/pi.js +21 -0
- package/dist/lib/runtime.d.ts +5 -0
- package/dist/lib/runtime.js +48 -0
- package/dist/lib/tools.d.ts +9 -0
- package/dist/lib/tools.js +58 -8
- package/dist/skills/actors/SKILL.md +1 -1
- package/dist/skills/swarm/SKILL.md +1 -1
- package/index.ts +99 -379
- package/lib/actor-inspector-tui.ts +221 -1
- package/lib/observability.ts +194 -34
- package/lib/paths.ts +27 -0
- package/lib/pi.ts +47 -0
- package/lib/runtime.ts +55 -0
- package/lib/tools.ts +119 -24
- package/package.json +1 -1
- package/skills/actors/SKILL.md +1 -1
- package/skills/swarm/SKILL.md +1 -1
|
@@ -11,7 +11,10 @@ import { visibleWidth } from "@earendil-works/pi-tui";
|
|
|
11
11
|
import type { ActorMessage } from "./actor-messages.ts";
|
|
12
12
|
import * as Limits from "./limits.ts";
|
|
13
13
|
import * as Paths from "./paths.ts";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
readJsonFileResilient,
|
|
16
|
+
readJsonlFileResilient,
|
|
17
|
+
} from "./state-readers.ts";
|
|
15
18
|
|
|
16
19
|
export interface ActorInspectorPreview {
|
|
17
20
|
body_preview?: string;
|
|
@@ -66,6 +69,223 @@ export interface ActorInspectorPreviewReadOptions {
|
|
|
66
69
|
unreadOnly?: boolean;
|
|
67
70
|
}
|
|
68
71
|
|
|
72
|
+
export interface ActorInspectorControllerState {
|
|
73
|
+
branch?: string;
|
|
74
|
+
channels?: ActorInspectorPreview["channel"][];
|
|
75
|
+
mention?: string;
|
|
76
|
+
readKeys: Set<string>;
|
|
77
|
+
roomLimitPerRun: number;
|
|
78
|
+
rows: number;
|
|
79
|
+
selectedSequence?: number;
|
|
80
|
+
unreadOnly: boolean;
|
|
81
|
+
visible: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface ActorInspectorCommandResult {
|
|
85
|
+
notify: string;
|
|
86
|
+
type: "info" | "warning";
|
|
87
|
+
update: boolean;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const DEFAULT_ACTOR_INSPECTOR_ROWS = 12;
|
|
91
|
+
|
|
92
|
+
export const ACTOR_INSPECTOR_COMMAND_DESCRIPTIONS = {
|
|
93
|
+
filter:
|
|
94
|
+
"Filter actor inspector rows: all, room, direct, broadcast, unread, branch <name>, mention <text>",
|
|
95
|
+
inspect: "Inspect actor message by visible number",
|
|
96
|
+
toggle: "Toggle actor inspector widget; optional row count",
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export function createActorInspectorControllerState(): ActorInspectorControllerState {
|
|
100
|
+
return {
|
|
101
|
+
readKeys: new Set<string>(),
|
|
102
|
+
roomLimitPerRun: DEFAULT_ACTOR_INSPECTOR_ROWS,
|
|
103
|
+
rows: DEFAULT_ACTOR_INSPECTOR_ROWS,
|
|
104
|
+
unreadOnly: false,
|
|
105
|
+
visible: false,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function getActorInspectorPreviewOptions(
|
|
110
|
+
state: ActorInspectorControllerState,
|
|
111
|
+
ownerId: string,
|
|
112
|
+
): ActorInspectorPreviewReadOptions {
|
|
113
|
+
return {
|
|
114
|
+
channels: state.channels,
|
|
115
|
+
currentRunOnly: true,
|
|
116
|
+
branch: state.branch,
|
|
117
|
+
mention: state.mention,
|
|
118
|
+
ownerId,
|
|
119
|
+
readKeys: state.readKeys,
|
|
120
|
+
roomLimitPerRun: state.roomLimitPerRun,
|
|
121
|
+
unreadOnly: state.unreadOnly,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function handleActorInspectorToggle(
|
|
126
|
+
state: ActorInspectorControllerState,
|
|
127
|
+
args: unknown,
|
|
128
|
+
): ActorInspectorCommandResult {
|
|
129
|
+
const raw = Array.isArray(args) ? args[0] : String(args ?? "");
|
|
130
|
+
if (String(raw).trim()) {
|
|
131
|
+
const rows = Number.parseInt(String(raw), 10);
|
|
132
|
+
if (!Number.isFinite(rows) || rows <= 0) {
|
|
133
|
+
return {
|
|
134
|
+
notify: "Usage: /actors-inspector-toggle [rows] where rows > 0",
|
|
135
|
+
type: "warning",
|
|
136
|
+
update: false,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
state.rows = rows;
|
|
140
|
+
state.roomLimitPerRun = rows;
|
|
141
|
+
state.selectedSequence = undefined;
|
|
142
|
+
state.visible = true;
|
|
143
|
+
return {
|
|
144
|
+
notify: `Actor inspector rows ${rows}`,
|
|
145
|
+
type: "info",
|
|
146
|
+
update: true,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
if (state.selectedSequence !== undefined) {
|
|
150
|
+
state.selectedSequence = undefined;
|
|
151
|
+
state.visible = true;
|
|
152
|
+
return { notify: "Actor inspector table", type: "info", update: true };
|
|
153
|
+
}
|
|
154
|
+
if (state.visible) state.visible = false;
|
|
155
|
+
else {
|
|
156
|
+
state.rows = DEFAULT_ACTOR_INSPECTOR_ROWS;
|
|
157
|
+
state.roomLimitPerRun = DEFAULT_ACTOR_INSPECTOR_ROWS;
|
|
158
|
+
state.visible = true;
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
notify: `Actor inspector ${state.visible ? "shown" : "hidden"}`,
|
|
162
|
+
type: "info",
|
|
163
|
+
update: true,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function handleActorInspectorFilter(
|
|
168
|
+
state: ActorInspectorControllerState,
|
|
169
|
+
args: unknown,
|
|
170
|
+
): ActorInspectorCommandResult {
|
|
171
|
+
const parts = Array.isArray(args)
|
|
172
|
+
? args.map(String)
|
|
173
|
+
: String(args ?? "").split(/\s+/);
|
|
174
|
+
const mode = (parts[0] ?? "").trim().toLowerCase();
|
|
175
|
+
if (!mode || mode === "all" || mode === "clear") {
|
|
176
|
+
state.channels = undefined;
|
|
177
|
+
state.mention = undefined;
|
|
178
|
+
state.branch = undefined;
|
|
179
|
+
state.unreadOnly = false;
|
|
180
|
+
} else if (mode === "room" || mode === "direct" || mode === "broadcast") {
|
|
181
|
+
state.channels = [mode];
|
|
182
|
+
state.mention = undefined;
|
|
183
|
+
} else if (mode === "unread") {
|
|
184
|
+
state.unreadOnly = true;
|
|
185
|
+
} else if (mode === "branch" || mode === "current-branch") {
|
|
186
|
+
const branch = parts.slice(1).join(" ").trim();
|
|
187
|
+
if (!branch) {
|
|
188
|
+
return {
|
|
189
|
+
notify: `Usage: /actors-inspector-filter ${mode} <branch-name>`,
|
|
190
|
+
type: "warning",
|
|
191
|
+
update: false,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
state.branch = branch;
|
|
195
|
+
} else if (mode === "mention") {
|
|
196
|
+
const mention = parts.slice(1).join(" ").trim();
|
|
197
|
+
if (!mention) {
|
|
198
|
+
return {
|
|
199
|
+
notify: "Usage: /actors-inspector-filter mention <text>",
|
|
200
|
+
type: "warning",
|
|
201
|
+
update: false,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
state.channels = undefined;
|
|
205
|
+
state.mention = mention;
|
|
206
|
+
} else {
|
|
207
|
+
return {
|
|
208
|
+
notify:
|
|
209
|
+
"Usage: /actors-inspector-filter all|room|direct|broadcast|unread|branch <name>|mention <text>",
|
|
210
|
+
type: "warning",
|
|
211
|
+
update: false,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
state.selectedSequence = undefined;
|
|
215
|
+
state.visible = true;
|
|
216
|
+
return {
|
|
217
|
+
notify: `Actor inspector filter ${mode || "all"}`,
|
|
218
|
+
type: "info",
|
|
219
|
+
update: true,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function handleActorInspectorInspect(
|
|
224
|
+
state: ActorInspectorControllerState,
|
|
225
|
+
args: unknown,
|
|
226
|
+
previews: ActorInspectorPreview[],
|
|
227
|
+
): ActorInspectorCommandResult {
|
|
228
|
+
const raw = Array.isArray(args) ? args[0] : String(args ?? "");
|
|
229
|
+
return selectActorInspectorSequence(
|
|
230
|
+
state,
|
|
231
|
+
Number.parseInt(String(raw), 10),
|
|
232
|
+
previews,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function selectActorInspectorSequence(
|
|
237
|
+
state: ActorInspectorControllerState,
|
|
238
|
+
sequence: number,
|
|
239
|
+
previews: ActorInspectorPreview[],
|
|
240
|
+
): ActorInspectorCommandResult {
|
|
241
|
+
if (!Number.isFinite(sequence) || sequence <= 0) {
|
|
242
|
+
return {
|
|
243
|
+
notify: "Usage: /actors-inspect <number>",
|
|
244
|
+
type: "warning",
|
|
245
|
+
update: false,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
const preview = previews.find((item) => item.sequence === sequence);
|
|
249
|
+
if (preview) state.readKeys.add(inspectorPreviewReadKey(preview));
|
|
250
|
+
state.selectedSequence = sequence;
|
|
251
|
+
state.visible = true;
|
|
252
|
+
return {
|
|
253
|
+
notify: `Actor inspect item ${sequence}`,
|
|
254
|
+
type: "info",
|
|
255
|
+
update: true,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function renderActorInspectorPanel(input: {
|
|
260
|
+
stateRoot: string;
|
|
261
|
+
state: ActorInspectorControllerState;
|
|
262
|
+
ownerId: string;
|
|
263
|
+
width: number;
|
|
264
|
+
style: ActorInspectorWidgetStyle;
|
|
265
|
+
}): string[] {
|
|
266
|
+
const previews = readActorInspectorPreviews(
|
|
267
|
+
input.stateRoot,
|
|
268
|
+
input.state.rows,
|
|
269
|
+
getActorInspectorPreviewOptions(input.state, input.ownerId),
|
|
270
|
+
);
|
|
271
|
+
const rows =
|
|
272
|
+
(input.state.selectedSequence !== undefined
|
|
273
|
+
? renderInspectorItemView(previews, input.width, input.style, {
|
|
274
|
+
sequence: input.state.selectedSequence,
|
|
275
|
+
})
|
|
276
|
+
: renderInspectorWidget(previews, input.width, input.style)) ?? [];
|
|
277
|
+
const run = previews[0]?.run;
|
|
278
|
+
const roster =
|
|
279
|
+
input.state.selectedSequence === undefined && run
|
|
280
|
+
? renderInspectorRosterPanel(
|
|
281
|
+
readActorInspectorRoster(input.stateRoot, run),
|
|
282
|
+
input.width,
|
|
283
|
+
input.style,
|
|
284
|
+
)
|
|
285
|
+
: undefined;
|
|
286
|
+
return roster ? [...roster, ...rows] : rows;
|
|
287
|
+
}
|
|
288
|
+
|
|
69
289
|
function asRecord(value: unknown): Record<string, unknown> {
|
|
70
290
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
71
291
|
? (value as Record<string, unknown>)
|
package/lib/observability.ts
CHANGED
|
@@ -4,7 +4,13 @@
|
|
|
4
4
|
* Owns ambient summaries, terminal events, and run outbox delivery for detached command-template runs
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
existsSync,
|
|
9
|
+
readdirSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
watch,
|
|
12
|
+
type FSWatcher,
|
|
13
|
+
} from "node:fs";
|
|
8
14
|
import {
|
|
9
15
|
basename,
|
|
10
16
|
dirname,
|
|
@@ -58,6 +64,108 @@ export interface RunSummary {
|
|
|
58
64
|
total: number;
|
|
59
65
|
}
|
|
60
66
|
|
|
67
|
+
export interface RunUiObservationState {
|
|
68
|
+
eventLines: Map<string, number>;
|
|
69
|
+
frame: number;
|
|
70
|
+
observed: Map<string, RunObservedStatus>;
|
|
71
|
+
outboxEventIds: Map<string, Set<string>>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface RunUiSnapshot {
|
|
75
|
+
outboxEvents: RunOutboxEvent[];
|
|
76
|
+
status: string | undefined;
|
|
77
|
+
summary: RunSummary;
|
|
78
|
+
transitions: RunTransition[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface RunUiNotificationSink {
|
|
82
|
+
notify(message: string, level: "info" | "warning" | "error"): void;
|
|
83
|
+
sendFollowUp(message: {
|
|
84
|
+
customType: string;
|
|
85
|
+
content: string;
|
|
86
|
+
display: true;
|
|
87
|
+
details: unknown;
|
|
88
|
+
}): void;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function createRunUiObservationState(): RunUiObservationState {
|
|
92
|
+
return {
|
|
93
|
+
eventLines: new Map<string, number>(),
|
|
94
|
+
frame: 0,
|
|
95
|
+
observed: new Map<string, RunObservedStatus>(),
|
|
96
|
+
outboxEventIds: new Map<string, Set<string>>(),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function readRunUiSnapshot(
|
|
101
|
+
state: RunUiObservationState,
|
|
102
|
+
ownerId: string,
|
|
103
|
+
): RunUiSnapshot {
|
|
104
|
+
const summary = summarizeRuns(undefined, ownerId);
|
|
105
|
+
const status = renderRunStatus(summary, state.frame++);
|
|
106
|
+
return {
|
|
107
|
+
outboxEvents: detectRunOutboxEvents(
|
|
108
|
+
state.eventLines,
|
|
109
|
+
summary,
|
|
110
|
+
state.outboxEventIds,
|
|
111
|
+
),
|
|
112
|
+
status,
|
|
113
|
+
summary,
|
|
114
|
+
transitions: detectRunTransitions(state.observed, summary),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function pruneRunUiObservationState(
|
|
119
|
+
state: RunUiObservationState,
|
|
120
|
+
snapshot: Pick<RunUiSnapshot, "summary" | "transitions">,
|
|
121
|
+
): void {
|
|
122
|
+
pruneRunObservationState(
|
|
123
|
+
state.observed,
|
|
124
|
+
state.eventLines,
|
|
125
|
+
snapshot.summary,
|
|
126
|
+
snapshot.transitions.map(
|
|
127
|
+
(transition) => transition.stateDir ?? transition.run,
|
|
128
|
+
),
|
|
129
|
+
state.outboxEventIds,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function deliverRunTransitionNotifications(
|
|
134
|
+
transitions: RunTransition[],
|
|
135
|
+
sink: RunUiNotificationSink,
|
|
136
|
+
): void {
|
|
137
|
+
for (const transition of transitions) {
|
|
138
|
+
if (!shouldNotifyRunTransition(transition)) continue;
|
|
139
|
+
const text = formatRunTransitionMessage(transition);
|
|
140
|
+
sink.notify(text, getRunTransitionNotificationType(transition));
|
|
141
|
+
if (!shouldSendRunTransitionFollowUp(transition)) continue;
|
|
142
|
+
sink.sendFollowUp({
|
|
143
|
+
customType: "pi-actors-run",
|
|
144
|
+
content: text,
|
|
145
|
+
display: true,
|
|
146
|
+
details: transition,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function deliverRunOutboxNotifications(
|
|
152
|
+
events: RunOutboxEvent[],
|
|
153
|
+
sink: RunUiNotificationSink,
|
|
154
|
+
): void {
|
|
155
|
+
for (const event of events) {
|
|
156
|
+
if (!shouldNotifyRunOutboxEvent(event)) continue;
|
|
157
|
+
const text = formatRunOutboxMessage(event);
|
|
158
|
+
sink.notify(text, getRunOutboxNotificationType(event));
|
|
159
|
+
if (!shouldSendRunOutboxFollowUp(event)) continue;
|
|
160
|
+
sink.sendFollowUp({
|
|
161
|
+
customType: "pi-actors-run-message",
|
|
162
|
+
content: text,
|
|
163
|
+
display: true,
|
|
164
|
+
details: event,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
61
169
|
export interface RunRetirementCandidate {
|
|
62
170
|
activeSubagents: number;
|
|
63
171
|
childRuns: number;
|
|
@@ -74,6 +182,58 @@ export interface RunRetirementExecution {
|
|
|
74
182
|
stateDir: string;
|
|
75
183
|
}
|
|
76
184
|
|
|
185
|
+
export interface RunStateWatcher {
|
|
186
|
+
close(): void;
|
|
187
|
+
refresh(): void;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function createRunStateWatcher(input: {
|
|
191
|
+
stateRoot?: string;
|
|
192
|
+
onChange: () => void;
|
|
193
|
+
}): RunStateWatcher {
|
|
194
|
+
const stateRoot = input.stateRoot ?? Paths.getRunStateRoot();
|
|
195
|
+
let stateRootWatcher: FSWatcher | undefined;
|
|
196
|
+
const runDirWatchers = new Map<string, FSWatcher>();
|
|
197
|
+
const close = (): void => {
|
|
198
|
+
stateRootWatcher?.close();
|
|
199
|
+
stateRootWatcher = undefined;
|
|
200
|
+
for (const watcher of runDirWatchers.values()) watcher.close();
|
|
201
|
+
runDirWatchers.clear();
|
|
202
|
+
};
|
|
203
|
+
const watchRunDir = (stateDir: string): void => {
|
|
204
|
+
if (runDirWatchers.has(stateDir) || !existsSync(stateDir)) return;
|
|
205
|
+
try {
|
|
206
|
+
const watcher = watch(stateDir, input.onChange);
|
|
207
|
+
watcher.on("error", () => {
|
|
208
|
+
watcher.close();
|
|
209
|
+
runDirWatchers.delete(stateDir);
|
|
210
|
+
});
|
|
211
|
+
runDirWatchers.set(stateDir, watcher);
|
|
212
|
+
} catch {
|
|
213
|
+
// Watching is best-effort; explicit inspect remains available.
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
function refresh(): void {
|
|
217
|
+
if (!existsSync(stateRoot)) return;
|
|
218
|
+
if (!stateRootWatcher) {
|
|
219
|
+
try {
|
|
220
|
+
stateRootWatcher = watch(stateRoot, input.onChange);
|
|
221
|
+
stateRootWatcher.on("error", () => {
|
|
222
|
+
stateRootWatcher?.close();
|
|
223
|
+
stateRootWatcher = undefined;
|
|
224
|
+
});
|
|
225
|
+
} catch {
|
|
226
|
+
// Watching is best-effort; explicit inspect remains available.
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
for (const entry of readdirSync(stateRoot, { withFileTypes: true })) {
|
|
230
|
+
if (!entry.isDirectory()) continue;
|
|
231
|
+
watchRunDir(`${stateRoot}/${entry.name}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return { close, refresh };
|
|
235
|
+
}
|
|
236
|
+
|
|
77
237
|
export interface RunRetirementExecutorOptions {
|
|
78
238
|
attempted?: Set<string>;
|
|
79
239
|
cancelRun: (candidate: RunRetirementCandidate) => Record<string, unknown>;
|
|
@@ -612,39 +772,39 @@ function parseOutboxRecord(
|
|
|
612
772
|
index: number,
|
|
613
773
|
): RunOutboxEvent | undefined {
|
|
614
774
|
if (!run.stateDir) return undefined;
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
775
|
+
const event =
|
|
776
|
+
typeof raw.event === "string" && raw.event.trim()
|
|
777
|
+
? raw.event.trim()
|
|
778
|
+
: "run.event";
|
|
779
|
+
const summary =
|
|
780
|
+
typeof raw.summary === "string" && raw.summary.trim()
|
|
781
|
+
? raw.summary.trim()
|
|
782
|
+
: event;
|
|
783
|
+
const ts =
|
|
784
|
+
typeof raw.ts === "string" && raw.ts.trim()
|
|
785
|
+
? raw.ts.trim()
|
|
786
|
+
: new Date(0).toISOString();
|
|
787
|
+
const id =
|
|
788
|
+
typeof raw.id === "string" && raw.id.trim()
|
|
789
|
+
? raw.id.trim()
|
|
790
|
+
: `${run.run}:${index}`;
|
|
791
|
+
return {
|
|
792
|
+
...(raw.body !== undefined ? { body: raw.body } : {}),
|
|
793
|
+
...(raw.data !== undefined ? { data: raw.data } : {}),
|
|
794
|
+
delivery: normalizeOutboxDelivery(raw.delivery),
|
|
795
|
+
event,
|
|
796
|
+
id,
|
|
797
|
+
level: normalizeOutboxLevel(raw.level),
|
|
798
|
+
...(raw.metadata &&
|
|
799
|
+
typeof raw.metadata === "object" &&
|
|
800
|
+
!Array.isArray(raw.metadata)
|
|
801
|
+
? { metadata: raw.metadata as Record<string, unknown> }
|
|
802
|
+
: {}),
|
|
803
|
+
run: run.run,
|
|
804
|
+
stateDir: run.stateDir,
|
|
805
|
+
summary,
|
|
806
|
+
ts,
|
|
807
|
+
};
|
|
648
808
|
}
|
|
649
809
|
|
|
650
810
|
function readOutboxRecords(run: RunObservation): Record<string, unknown>[] {
|
package/lib/paths.ts
CHANGED
|
@@ -17,6 +17,12 @@ export function getAgentDir(
|
|
|
17
17
|
: join(homedir(), ".pi", "agent");
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
export interface ExtensionRuntimePaths {
|
|
21
|
+
configPath: string;
|
|
22
|
+
runStateRoot: string;
|
|
23
|
+
tempDir: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
20
26
|
export function getConfigPath(agentDir = getAgentDir()): string {
|
|
21
27
|
return join(agentDir, "legacy-tool-registry.json");
|
|
22
28
|
}
|
|
@@ -32,6 +38,27 @@ export function getRunStateRoot(agentDir = getAgentDir()): string {
|
|
|
32
38
|
return join(getExtensionTmpDir(agentDir), "runs");
|
|
33
39
|
}
|
|
34
40
|
|
|
41
|
+
export function getExtensionRuntimePaths(
|
|
42
|
+
agentDir = getAgentDir(),
|
|
43
|
+
): ExtensionRuntimePaths {
|
|
44
|
+
return {
|
|
45
|
+
configPath: getConfigPath(agentDir),
|
|
46
|
+
runStateRoot: getRunStateRoot(agentDir),
|
|
47
|
+
tempDir: getExtensionTmpDir(agentDir),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const EXTENSION_RUNTIME_PATHS = getExtensionRuntimePaths();
|
|
52
|
+
|
|
53
|
+
export function getExtensionSkillsDir(extensionUrl: string): string {
|
|
54
|
+
return join(dirname(fileURLToPath(extensionUrl)), "skills");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function getExistingExtensionSkillPaths(extensionUrl: string): string[] {
|
|
58
|
+
const skillsDir = getExtensionSkillsDir(extensionUrl);
|
|
59
|
+
return existsSync(skillsDir) ? [skillsDir] : [];
|
|
60
|
+
}
|
|
61
|
+
|
|
35
62
|
export function getRecipeRoot(agentDir = getAgentDir()): string {
|
|
36
63
|
return join(agentDir, "recipes");
|
|
37
64
|
}
|
package/lib/pi.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi SDK adapter boundary.
|
|
3
|
+
* Zones: pi agent sdk boundary, extension host adapters
|
|
4
|
+
* Owns direct pi SDK imports and exposes narrow pi-actors-facing helpers/types for the composition root.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
ExtensionAPI,
|
|
9
|
+
ExtensionContext,
|
|
10
|
+
} from "@earendil-works/pi-coding-agent";
|
|
11
|
+
|
|
12
|
+
export type { ExtensionAPI, ExtensionContext };
|
|
13
|
+
|
|
14
|
+
export interface PiNotificationSink {
|
|
15
|
+
notify(message: string, level: "info" | "warning" | "error"): void;
|
|
16
|
+
sendFollowUp(message: {
|
|
17
|
+
customType: string;
|
|
18
|
+
content: string;
|
|
19
|
+
display: true;
|
|
20
|
+
details: unknown;
|
|
21
|
+
}): void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getSessionId(ctx: ExtensionContext): string {
|
|
25
|
+
return ctx.sessionManager.getSessionId();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createNotificationSink(
|
|
29
|
+
pi: ExtensionAPI,
|
|
30
|
+
ctx: ExtensionContext,
|
|
31
|
+
): PiNotificationSink {
|
|
32
|
+
return {
|
|
33
|
+
notify: (message, level) => ctx.ui.notify(message, level),
|
|
34
|
+
sendFollowUp: (message) =>
|
|
35
|
+
pi.sendMessage(message, {
|
|
36
|
+
deliverAs: "followUp",
|
|
37
|
+
triggerTurn: true,
|
|
38
|
+
}),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function registerToolDefinitions(
|
|
43
|
+
pi: ExtensionAPI,
|
|
44
|
+
definitions: Iterable<unknown>,
|
|
45
|
+
): void {
|
|
46
|
+
for (const definition of definitions) pi.registerTool(definition as never);
|
|
47
|
+
}
|
package/lib/runtime.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Owns persisted tool loading, conflict detection, runtime registration, and warning notification
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { existsSync, watch, type FSWatcher } from "node:fs";
|
|
8
|
+
|
|
7
9
|
import * as Config from "./config.ts";
|
|
8
10
|
import type { RegisteredToolExec } from "./execution.ts";
|
|
9
11
|
import * as Paths from "./paths.ts";
|
|
@@ -47,6 +49,11 @@ export interface ToolRegistryRuntime {
|
|
|
47
49
|
registerRuntimeTool(cfg: Config.RegisteredTool): void;
|
|
48
50
|
}
|
|
49
51
|
|
|
52
|
+
export interface RecipeToolReloadWatcher {
|
|
53
|
+
close(): void;
|
|
54
|
+
watch(ctx: RuntimeContext): void;
|
|
55
|
+
}
|
|
56
|
+
|
|
50
57
|
export function createAutoToolsRuntime(
|
|
51
58
|
deps: ToolRegistryRuntimeDeps,
|
|
52
59
|
): ToolRegistryRuntime {
|
|
@@ -179,3 +186,51 @@ export function createAutoToolsRuntime(
|
|
|
179
186
|
registerRuntimeTool,
|
|
180
187
|
};
|
|
181
188
|
}
|
|
189
|
+
|
|
190
|
+
export function createRecipeToolReloadWatcher(
|
|
191
|
+
runtime: Pick<ToolRegistryRuntime, "loadTools">,
|
|
192
|
+
): RecipeToolReloadWatcher {
|
|
193
|
+
let reloadTimeout: NodeJS.Timeout | undefined;
|
|
194
|
+
let rootWatcher: FSWatcher | undefined;
|
|
195
|
+
let failureNotified = false;
|
|
196
|
+
const close = (): void => {
|
|
197
|
+
rootWatcher?.close();
|
|
198
|
+
rootWatcher = undefined;
|
|
199
|
+
if (reloadTimeout) clearTimeout(reloadTimeout);
|
|
200
|
+
reloadTimeout = undefined;
|
|
201
|
+
};
|
|
202
|
+
const notifyFailure = (ctx: RuntimeContext): void => {
|
|
203
|
+
if (failureNotified) return;
|
|
204
|
+
failureNotified = true;
|
|
205
|
+
ctx.ui.notify(
|
|
206
|
+
"Recipe live reload watcher failed; restart the session or use register_tool again to refresh recipe tools.",
|
|
207
|
+
"warning",
|
|
208
|
+
);
|
|
209
|
+
};
|
|
210
|
+
const scheduleReload = (ctx: RuntimeContext): void => {
|
|
211
|
+
failureNotified = false;
|
|
212
|
+
if (reloadTimeout) clearTimeout(reloadTimeout);
|
|
213
|
+
reloadTimeout = setTimeout(() => {
|
|
214
|
+
runtime.loadTools(ctx);
|
|
215
|
+
ctx.ui.notify("Recipe tools refreshed from ~/.pi/agent/recipes", "info");
|
|
216
|
+
}, 150);
|
|
217
|
+
reloadTimeout.unref?.();
|
|
218
|
+
};
|
|
219
|
+
return {
|
|
220
|
+
close,
|
|
221
|
+
watch(ctx: RuntimeContext): void {
|
|
222
|
+
const recipeRoot = Paths.getRecipeRoot();
|
|
223
|
+
if (rootWatcher || !existsSync(recipeRoot)) return;
|
|
224
|
+
try {
|
|
225
|
+
rootWatcher = watch(recipeRoot, () => scheduleReload(ctx));
|
|
226
|
+
rootWatcher.on("error", () => {
|
|
227
|
+
rootWatcher?.close();
|
|
228
|
+
rootWatcher = undefined;
|
|
229
|
+
notifyFailure(ctx);
|
|
230
|
+
});
|
|
231
|
+
} catch {
|
|
232
|
+
notifyFailure(ctx);
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
}
|