@llblab/pi-actors 0.40.1 → 0.41.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/AGENTS.md +3 -3
- package/CHANGELOG.md +32 -0
- package/README.md +2 -1
- package/dist/index.js +80 -58
- package/dist/lib/inspector-overlay.d.ts +95 -0
- package/dist/lib/inspector-overlay.js +873 -0
- package/dist/lib/inspector.d.ts +17 -33
- package/dist/lib/inspector.js +101 -151
- package/dist/lib/limits.d.ts +3 -0
- package/dist/lib/limits.js +3 -0
- package/dist/lib/observability.d.ts +38 -3
- package/dist/lib/observability.js +151 -33
- package/dist/lib/recipes-context.d.ts +5 -0
- package/dist/lib/recipes-context.js +15 -0
- package/dist/lib/session-evidence.d.ts +45 -0
- package/dist/lib/session-evidence.js +204 -0
- package/dist/lib/tools-response.d.ts +3 -0
- package/dist/lib/tools-response.js +29 -0
- package/dist/scripts/async-runner.mjs +40 -5
- package/dist/scripts/coordinator.mjs +55 -9
- package/dist/skills/actors/SKILL.md +5 -11
- package/dist/skills/swarm/SKILL.md +1 -1
- package/docs/README.md +1 -0
- package/docs/actor-inspector.md +77 -0
- package/docs/async-runs.md +1 -1
- package/index.ts +80 -70
- package/lib/inspector-overlay.ts +1071 -0
- package/lib/inspector.ts +135 -204
- package/lib/limits.ts +3 -0
- package/lib/observability.ts +205 -39
- package/lib/recipes-context.ts +25 -0
- package/lib/session-evidence.ts +302 -0
- package/lib/tools-response.ts +32 -0
- package/package.json +1 -1
- package/scripts/async-runner.mjs +40 -5
- package/scripts/coordinator.mjs +55 -9
- package/skills/actors/SKILL.md +5 -11
- package/skills/swarm/SKILL.md +1 -1
package/lib/inspector.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { visibleWidth } from "@earendil-works/pi-tui";
|
|
|
12
12
|
import type { ActorMessage } from "./messages.ts";
|
|
13
13
|
import * as Limits from "./limits.ts";
|
|
14
14
|
import * as Paths from "./paths.ts";
|
|
15
|
+
import * as SessionEvidence from "./session-evidence.ts";
|
|
15
16
|
import {
|
|
16
17
|
readJsonFileResilient,
|
|
17
18
|
readJsonlFileResilient,
|
|
@@ -63,6 +64,7 @@ export interface ActorInspectorPreviewReadOptions {
|
|
|
63
64
|
ownerId?: string;
|
|
64
65
|
branch?: string;
|
|
65
66
|
currentRunOnly?: boolean;
|
|
67
|
+
run?: string;
|
|
66
68
|
channels?: ActorInspectorPreview["channel"][];
|
|
67
69
|
mention?: string;
|
|
68
70
|
readKeys?: Iterable<string>;
|
|
@@ -70,221 +72,147 @@ export interface ActorInspectorPreviewReadOptions {
|
|
|
70
72
|
unreadOnly?: boolean;
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
export interface
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
readKeys: Set<string>;
|
|
78
|
-
roomLimitPerRun: number;
|
|
79
|
-
rows: number;
|
|
80
|
-
selectedSequence?: number;
|
|
81
|
-
unreadOnly: boolean;
|
|
82
|
-
visible: boolean;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export interface ActorInspectorCommandResult {
|
|
86
|
-
notify: string;
|
|
87
|
-
type: "info" | "warning";
|
|
88
|
-
update: boolean;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export const DEFAULT_ACTOR_INSPECTOR_ROWS = 12;
|
|
92
|
-
|
|
93
|
-
export const ACTOR_INSPECTOR_COMMAND_DESCRIPTIONS = {
|
|
94
|
-
filter:
|
|
95
|
-
"Filter actor inspector rows: all, room, direct, broadcast, unread, branch <name>, mention <text>",
|
|
96
|
-
inspect: "Inspect actor message by visible number",
|
|
97
|
-
toggle: "Toggle actor inspector widget; optional row count",
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
export function createActorInspectorControllerState(): ActorInspectorControllerState {
|
|
101
|
-
return {
|
|
102
|
-
readKeys: new Set<string>(),
|
|
103
|
-
roomLimitPerRun: DEFAULT_ACTOR_INSPECTOR_ROWS,
|
|
104
|
-
rows: DEFAULT_ACTOR_INSPECTOR_ROWS,
|
|
105
|
-
unreadOnly: false,
|
|
106
|
-
visible: false,
|
|
107
|
-
};
|
|
75
|
+
export interface ActorInspectorRunItem {
|
|
76
|
+
run: string;
|
|
77
|
+
status: string;
|
|
78
|
+
updatedAt?: string;
|
|
108
79
|
}
|
|
109
80
|
|
|
110
|
-
export function
|
|
111
|
-
|
|
81
|
+
export function readActorInspectorRuns(
|
|
82
|
+
stateRoot: string,
|
|
112
83
|
ownerId: string,
|
|
113
|
-
):
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
if (state.selectedSequence !== undefined) {
|
|
151
|
-
state.selectedSequence = undefined;
|
|
152
|
-
state.visible = true;
|
|
153
|
-
return { notify: "Actor inspector table", type: "info", update: true };
|
|
154
|
-
}
|
|
155
|
-
if (state.visible) state.visible = false;
|
|
156
|
-
else {
|
|
157
|
-
state.rows = DEFAULT_ACTOR_INSPECTOR_ROWS;
|
|
158
|
-
state.roomLimitPerRun = DEFAULT_ACTOR_INSPECTOR_ROWS;
|
|
159
|
-
state.visible = true;
|
|
160
|
-
}
|
|
161
|
-
return {
|
|
162
|
-
notify: `Actor inspector ${state.visible ? "shown" : "hidden"}`,
|
|
163
|
-
type: "info",
|
|
164
|
-
update: true,
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export function handleActorInspectorFilter(
|
|
169
|
-
state: ActorInspectorControllerState,
|
|
170
|
-
args: unknown,
|
|
171
|
-
): ActorInspectorCommandResult {
|
|
172
|
-
const parts = Array.isArray(args)
|
|
173
|
-
? args.map(String)
|
|
174
|
-
: String(args ?? "").split(/\s+/);
|
|
175
|
-
const mode = (parts[0] ?? "").trim().toLowerCase();
|
|
176
|
-
if (!mode || mode === "all" || mode === "clear") {
|
|
177
|
-
state.channels = undefined;
|
|
178
|
-
state.mention = undefined;
|
|
179
|
-
state.branch = undefined;
|
|
180
|
-
state.unreadOnly = false;
|
|
181
|
-
} else if (mode === "room" || mode === "direct" || mode === "broadcast") {
|
|
182
|
-
state.channels = [mode];
|
|
183
|
-
state.mention = undefined;
|
|
184
|
-
} else if (mode === "unread") {
|
|
185
|
-
state.unreadOnly = true;
|
|
186
|
-
} else if (mode === "branch" || mode === "current-branch") {
|
|
187
|
-
const branch = parts.slice(1).join(" ").trim();
|
|
188
|
-
if (!branch) {
|
|
189
|
-
return {
|
|
190
|
-
notify: `Usage: /actors-inspector-filter ${mode} <branch-name>`,
|
|
191
|
-
type: "warning",
|
|
192
|
-
update: false,
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
state.branch = branch;
|
|
196
|
-
} else if (mode === "mention") {
|
|
197
|
-
const mention = parts.slice(1).join(" ").trim();
|
|
198
|
-
if (!mention) {
|
|
199
|
-
return {
|
|
200
|
-
notify: "Usage: /actors-inspector-filter mention <text>",
|
|
201
|
-
type: "warning",
|
|
202
|
-
update: false,
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
state.channels = undefined;
|
|
206
|
-
state.mention = mention;
|
|
207
|
-
} else {
|
|
208
|
-
return {
|
|
209
|
-
notify:
|
|
210
|
-
"Usage: /actors-inspector-filter all|room|direct|broadcast|unread|branch <name>|mention <text>",
|
|
211
|
-
type: "warning",
|
|
212
|
-
update: false,
|
|
213
|
-
};
|
|
84
|
+
): ActorInspectorRunItem[] {
|
|
85
|
+
try {
|
|
86
|
+
return fs
|
|
87
|
+
.readdirSync(stateRoot, { withFileTypes: true })
|
|
88
|
+
.filter((entry) => entry.isDirectory())
|
|
89
|
+
.flatMap((entry) => {
|
|
90
|
+
const stateDir = path.join(stateRoot, entry.name);
|
|
91
|
+
if (!matchesOwner(stateDir, ownerId)) return [];
|
|
92
|
+
const progress = readJsonFileResilient<Record<string, unknown>>(
|
|
93
|
+
path.join(stateDir, "progress.json"),
|
|
94
|
+
{},
|
|
95
|
+
).value;
|
|
96
|
+
const result = readJsonFileResilient<Record<string, unknown>>(
|
|
97
|
+
path.join(stateDir, "result.json"),
|
|
98
|
+
{},
|
|
99
|
+
).value;
|
|
100
|
+
return [{
|
|
101
|
+
run: entry.name,
|
|
102
|
+
status: String(progress.phase ?? (Object.keys(result).length ? "terminal" : "unknown")),
|
|
103
|
+
...(typeof progress.updatedAt === "string"
|
|
104
|
+
? { updatedAt: progress.updatedAt }
|
|
105
|
+
: typeof result.completedAt === "string"
|
|
106
|
+
? { updatedAt: result.completedAt }
|
|
107
|
+
: {}),
|
|
108
|
+
}];
|
|
109
|
+
})
|
|
110
|
+
.sort((left, right) => {
|
|
111
|
+
const recency = String(right.updatedAt ?? "").localeCompare(
|
|
112
|
+
String(left.updatedAt ?? ""),
|
|
113
|
+
);
|
|
114
|
+
return recency || right.run.localeCompare(left.run);
|
|
115
|
+
});
|
|
116
|
+
} catch (error) {
|
|
117
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
118
|
+
return [];
|
|
214
119
|
}
|
|
215
|
-
state.selectedSequence = undefined;
|
|
216
|
-
state.visible = true;
|
|
217
|
-
return {
|
|
218
|
-
notify: `Actor inspector filter ${mode || "all"}`,
|
|
219
|
-
type: "info",
|
|
220
|
-
update: true,
|
|
221
|
-
};
|
|
222
120
|
}
|
|
223
121
|
|
|
224
|
-
export
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
previews,
|
|
234
|
-
);
|
|
122
|
+
export interface ActorInspectorTurnItem extends SessionEvidence.SessionEvidenceTurn {
|
|
123
|
+
commandId: string;
|
|
124
|
+
diagnostics: string[];
|
|
125
|
+
promptBytes?: number;
|
|
126
|
+
promptFile?: string;
|
|
127
|
+
recipeContext?: unknown;
|
|
128
|
+
sessionFile: string;
|
|
129
|
+
sessionTruncated: boolean;
|
|
130
|
+
stage?: string;
|
|
235
131
|
}
|
|
236
132
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
)
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
133
|
+
function ownedSessionPath(
|
|
134
|
+
stateDir: string,
|
|
135
|
+
sessionFile: string,
|
|
136
|
+
): string | undefined {
|
|
137
|
+
if (path.isAbsolute(sessionFile)) return undefined;
|
|
138
|
+
const sessionsRoot = path.resolve(stateDir, "sessions");
|
|
139
|
+
const resolved = path.resolve(stateDir, sessionFile);
|
|
140
|
+
if (!resolved.startsWith(`${sessionsRoot}${path.sep}`)) return undefined;
|
|
141
|
+
try {
|
|
142
|
+
const canonicalRoot = fs.realpathSync(sessionsRoot);
|
|
143
|
+
const canonicalFile = fs.realpathSync(resolved);
|
|
144
|
+
return canonicalFile.startsWith(`${canonicalRoot}${path.sep}`)
|
|
145
|
+
? canonicalFile
|
|
146
|
+
: undefined;
|
|
147
|
+
} catch {
|
|
148
|
+
return undefined;
|
|
248
149
|
}
|
|
249
|
-
const preview = previews.find((item) => item.sequence === sequence);
|
|
250
|
-
if (preview) state.readKeys.add(inspectorPreviewReadKey(preview));
|
|
251
|
-
state.selectedSequence = sequence;
|
|
252
|
-
state.visible = true;
|
|
253
|
-
return {
|
|
254
|
-
notify: `Actor inspect item ${sequence}`,
|
|
255
|
-
type: "info",
|
|
256
|
-
update: true,
|
|
257
|
-
};
|
|
258
150
|
}
|
|
259
151
|
|
|
260
|
-
export function
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
152
|
+
export function readActorInspectorTurns(stateDir: string): ActorInspectorTurnItem[] {
|
|
153
|
+
const evidence = readJsonFileResilient<Record<string, unknown>>(
|
|
154
|
+
path.join(stateDir, "review-evidence.json"),
|
|
155
|
+
{},
|
|
156
|
+
).value;
|
|
157
|
+
const commands = Array.isArray(evidence.commands)
|
|
158
|
+
? (evidence.commands as Array<Record<string, unknown>>)
|
|
159
|
+
: [];
|
|
160
|
+
const recordedFiles = new Set(
|
|
161
|
+
commands.flatMap((command) =>
|
|
162
|
+
(Array.isArray(command.session_files) ? command.session_files : []).filter(
|
|
163
|
+
(file): file is string => typeof file === "string",
|
|
164
|
+
),
|
|
165
|
+
),
|
|
271
166
|
);
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
167
|
+
let discoveredCommands: Array<Record<string, unknown>> = [];
|
|
168
|
+
try {
|
|
169
|
+
const sessionsDir = path.join(stateDir, "sessions");
|
|
170
|
+
discoveredCommands = fs
|
|
171
|
+
.readdirSync(sessionsDir, { withFileTypes: true })
|
|
172
|
+
.filter((entry) => entry.isDirectory())
|
|
173
|
+
.flatMap((entry) => {
|
|
174
|
+
const files = fs
|
|
175
|
+
.readdirSync(path.join(sessionsDir, entry.name), {
|
|
176
|
+
withFileTypes: true,
|
|
177
|
+
})
|
|
178
|
+
.filter((file) => file.isFile() && file.name.endsWith(".jsonl"))
|
|
179
|
+
.map((file) => path.join("sessions", entry.name, file.name))
|
|
180
|
+
.filter((file) => !recordedFiles.has(file));
|
|
181
|
+
return files.length > 0
|
|
182
|
+
? [{ id: entry.name, session_files: files, stage: "subagent" }]
|
|
183
|
+
: [];
|
|
184
|
+
});
|
|
185
|
+
} catch {
|
|
186
|
+
discoveredCommands = [];
|
|
187
|
+
}
|
|
188
|
+
return [...commands, ...discoveredCommands].flatMap((command) =>
|
|
189
|
+
(Array.isArray(command.session_files) ? command.session_files : [])
|
|
190
|
+
.filter((file): file is string => typeof file === "string")
|
|
191
|
+
.flatMap((file) => {
|
|
192
|
+
const sessionPath = ownedSessionPath(stateDir, file);
|
|
193
|
+
if (!sessionPath) return [];
|
|
194
|
+
const session = SessionEvidence.readSessionEvidence(sessionPath);
|
|
195
|
+
return session.turns.map((turn) => ({
|
|
196
|
+
...turn,
|
|
197
|
+
commandId: String(command.id ?? "unknown"),
|
|
198
|
+
diagnostics: session.diagnostics.map((item) =>
|
|
199
|
+
`${item.line === undefined ? "" : `line ${item.line}: `}${item.message}`,
|
|
200
|
+
),
|
|
201
|
+
...(typeof command.prompt_bytes === "number"
|
|
202
|
+
? { promptBytes: command.prompt_bytes }
|
|
203
|
+
: {}),
|
|
204
|
+
...(typeof command.prompt_file === "string"
|
|
205
|
+
? { promptFile: command.prompt_file }
|
|
206
|
+
: {}),
|
|
207
|
+
...(command.recipe_context !== undefined
|
|
208
|
+
? { recipeContext: command.recipe_context }
|
|
209
|
+
: {}),
|
|
210
|
+
sessionFile: file,
|
|
211
|
+
sessionTruncated: session.truncated,
|
|
212
|
+
...(typeof command.stage === "string" ? { stage: command.stage } : {}),
|
|
213
|
+
}));
|
|
214
|
+
}),
|
|
215
|
+
).map((turn, index) => ({ ...turn, index: index + 1 }));
|
|
288
216
|
}
|
|
289
217
|
|
|
290
218
|
function asRecord(value: unknown): Record<string, unknown> {
|
|
@@ -302,7 +230,9 @@ function previewValue(
|
|
|
302
230
|
maxLength = Limits.INSPECTOR_BODY_PREVIEW_CHARS,
|
|
303
231
|
): string | undefined {
|
|
304
232
|
if (value === undefined) return undefined;
|
|
305
|
-
const
|
|
233
|
+
const redacted = SessionEvidence.redactSessionEvidenceValue(value);
|
|
234
|
+
const text =
|
|
235
|
+
typeof redacted === "string" ? redacted : JSON.stringify(redacted);
|
|
306
236
|
const compact = text.replaceAll(/\s+/g, " ").trim();
|
|
307
237
|
if (!compact) return undefined;
|
|
308
238
|
return compact.length > maxLength
|
|
@@ -606,6 +536,7 @@ export function readActorInspectorPreviews(
|
|
|
606
536
|
.flatMap((entry) => {
|
|
607
537
|
const stateDir = path.join(stateRoot, entry.name);
|
|
608
538
|
if (!matchesOwner(stateDir, options.ownerId)) return [];
|
|
539
|
+
if (options.run && entry.name !== options.run) return [];
|
|
609
540
|
return [
|
|
610
541
|
...readRoomPreviews(entry.name, stateDir),
|
|
611
542
|
...readInboxPreviews(entry.name, stateDir),
|
package/lib/limits.ts
CHANGED
|
@@ -10,3 +10,6 @@ export const COMPACT_PREVIEW_CHARS = 160;
|
|
|
10
10
|
export const INSPECTOR_BODY_PREVIEW_CHARS = 320;
|
|
11
11
|
export const ROOM_MESSAGE_PREVIEW_CHARS = 320;
|
|
12
12
|
export const DOCTOR_ACTION_PREVIEW_CHARS = 72;
|
|
13
|
+
export const SESSION_EVIDENCE_MAX_TURNS = 100;
|
|
14
|
+
export const SESSION_EVIDENCE_TEXT_CHARS = 4_000;
|
|
15
|
+
export const SESSION_EVIDENCE_MAX_TOOL_CALLS = 100;
|