@llblab/pi-actors 0.40.1 → 0.41.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/AGENTS.md +1 -1
- package/BACKLOG.md +19 -0
- package/CHANGELOG.md +25 -0
- package/README.md +2 -1
- package/dist/index.js +30 -51
- package/dist/lib/inspector-overlay.d.ts +84 -0
- package/dist/lib/inspector-overlay.js +627 -0
- package/dist/lib/inspector.d.ts +17 -33
- package/dist/lib/inspector.js +98 -151
- package/dist/lib/limits.d.ts +3 -0
- package/dist/lib/limits.js +3 -0
- 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 +31 -0
- package/dist/scripts/async-runner.mjs +40 -5
- package/dist/scripts/coordinator.mjs +55 -9
- package/dist/skills/actors/SKILL.md +5 -7
- package/dist/skills/swarm/SKILL.md +1 -1
- package/docs/README.md +1 -0
- package/docs/actor-inspector.md +72 -0
- package/index.ts +32 -63
- package/lib/inspector-overlay.ts +735 -0
- package/lib/inspector.ts +132 -204
- package/lib/limits.ts +3 -0
- package/lib/recipes-context.ts +25 -0
- package/lib/session-evidence.ts +302 -0
- package/lib/tools-response.ts +33 -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 -7
- package/skills/swarm/SKILL.md +1 -1
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persisted Pi session evidence reader.
|
|
3
|
+
* Zones: subagent turns, active session branches, bounded/redacted previews
|
|
4
|
+
* Owns read-only normalization of session JSONL into inspector-ready turns.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as Limits from "./limits.ts";
|
|
8
|
+
import {
|
|
9
|
+
readJsonlFileResilient,
|
|
10
|
+
type StateReadDiagnostic,
|
|
11
|
+
} from "./state-readers.ts";
|
|
12
|
+
|
|
13
|
+
interface SessionEntry {
|
|
14
|
+
id?: unknown;
|
|
15
|
+
message?: unknown;
|
|
16
|
+
parentId?: unknown;
|
|
17
|
+
timestamp?: unknown;
|
|
18
|
+
type?: unknown;
|
|
19
|
+
[key: string]: unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SessionEvidenceToolCall {
|
|
23
|
+
arguments?: unknown;
|
|
24
|
+
id: string;
|
|
25
|
+
name: string;
|
|
26
|
+
result?: unknown;
|
|
27
|
+
resultError?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SessionEvidenceTurn {
|
|
31
|
+
assistantEntryId?: string;
|
|
32
|
+
assistantText?: string;
|
|
33
|
+
error?: string;
|
|
34
|
+
index: number;
|
|
35
|
+
model?: string;
|
|
36
|
+
provider?: string;
|
|
37
|
+
stopReason?: string;
|
|
38
|
+
thinking?: string;
|
|
39
|
+
timestamp?: string;
|
|
40
|
+
toolCalls: SessionEvidenceToolCall[];
|
|
41
|
+
unmatchedToolResults: number;
|
|
42
|
+
usage?: unknown;
|
|
43
|
+
userEntryId?: string;
|
|
44
|
+
userText?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface SessionEvidence {
|
|
48
|
+
activeLeafId?: string;
|
|
49
|
+
diagnostics: StateReadDiagnostic[];
|
|
50
|
+
path: string;
|
|
51
|
+
session?: Record<string, unknown>;
|
|
52
|
+
totalTurns: number;
|
|
53
|
+
truncated: boolean;
|
|
54
|
+
turns: SessionEvidenceTurn[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface SessionEvidenceReadOptions {
|
|
58
|
+
maxTextChars?: number;
|
|
59
|
+
maxToolCalls?: number;
|
|
60
|
+
maxTurns?: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const SENSITIVE_KEY = /(?:^|[_-])(?:api[-_]?key|authorization|cookie|credential|password|private[-_]?key|secret|secret[-_]?access[-_]?key|token)$|^(?:access|refresh|auth|api)Token$|^(?:clientSecret|privateKey|secretAccessKey)$/i;
|
|
64
|
+
const SENSITIVE_TEXT = /(bearer\s+)[A-Za-z0-9._~+/=-]+|["']?\b(api[-_]?key|authorization|clientSecret|cookie|password|private[-_]?key|privateKey|secret|secretAccessKey|token)["']?(\s*[:=]\s*)["']?([^\s,;"'}]+)/gi;
|
|
65
|
+
|
|
66
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
67
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
68
|
+
? (value as Record<string, unknown>)
|
|
69
|
+
: {};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function boundedText(value: unknown, maxChars: number): string | undefined {
|
|
73
|
+
if (typeof value !== "string") return undefined;
|
|
74
|
+
if (/^\s*[\[{]/.test(value)) {
|
|
75
|
+
try {
|
|
76
|
+
const structured = JSON.stringify(
|
|
77
|
+
redactSessionEvidenceValue(JSON.parse(value), maxChars),
|
|
78
|
+
);
|
|
79
|
+
return structured.length > maxChars
|
|
80
|
+
? `${structured.slice(0, Math.max(0, maxChars - 1))}…`
|
|
81
|
+
: structured;
|
|
82
|
+
} catch {
|
|
83
|
+
// Fall through to bounded text redaction.
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const redacted = value.replaceAll(
|
|
87
|
+
SENSITIVE_TEXT,
|
|
88
|
+
(match, bearer: string | undefined, key: string | undefined, separator: string | undefined) =>
|
|
89
|
+
bearer ? `${bearer}[REDACTED]` : `${key}${separator}[REDACTED]`,
|
|
90
|
+
);
|
|
91
|
+
return redacted.length > maxChars
|
|
92
|
+
? `${redacted.slice(0, Math.max(0, maxChars - 1))}…`
|
|
93
|
+
: redacted;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function redactSessionEvidenceValue(
|
|
97
|
+
value: unknown,
|
|
98
|
+
maxTextChars = Limits.SESSION_EVIDENCE_TEXT_CHARS,
|
|
99
|
+
seen = new WeakSet<object>(),
|
|
100
|
+
): unknown {
|
|
101
|
+
if (typeof value === "string") return boundedText(value, maxTextChars);
|
|
102
|
+
if (!value || typeof value !== "object") return value;
|
|
103
|
+
if (seen.has(value)) return "[CIRCULAR]";
|
|
104
|
+
seen.add(value);
|
|
105
|
+
if (Array.isArray(value)) {
|
|
106
|
+
return value.map((item) => redactSessionEvidenceValue(item, maxTextChars, seen));
|
|
107
|
+
}
|
|
108
|
+
return Object.fromEntries(
|
|
109
|
+
Object.entries(value as Record<string, unknown>).map(([key, item]) => [
|
|
110
|
+
key,
|
|
111
|
+
SENSITIVE_KEY.test(key)
|
|
112
|
+
? "[REDACTED]"
|
|
113
|
+
: redactSessionEvidenceValue(item, maxTextChars, seen),
|
|
114
|
+
]),
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function contentBlocks(message: Record<string, unknown>): Record<string, unknown>[] {
|
|
119
|
+
return Array.isArray(message.content)
|
|
120
|
+
? message.content.map(asRecord)
|
|
121
|
+
: typeof message.content === "string"
|
|
122
|
+
? [{ type: "text", text: message.content }]
|
|
123
|
+
: [];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function contentText(
|
|
127
|
+
message: Record<string, unknown>,
|
|
128
|
+
type: "text" | "thinking",
|
|
129
|
+
maxChars: number,
|
|
130
|
+
): string | undefined {
|
|
131
|
+
const text = contentBlocks(message)
|
|
132
|
+
.filter((block) => block.type === type && typeof block[type] === "string")
|
|
133
|
+
.map((block) => String(block[type]))
|
|
134
|
+
.join("\n");
|
|
135
|
+
return boundedText(text, maxChars);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function activeBranch(
|
|
139
|
+
entries: SessionEntry[],
|
|
140
|
+
path: string,
|
|
141
|
+
diagnostics: StateReadDiagnostic[],
|
|
142
|
+
): SessionEntry[] {
|
|
143
|
+
const treeEntries = entries.filter(
|
|
144
|
+
(entry) => entry.type !== "session" && typeof entry.id === "string",
|
|
145
|
+
);
|
|
146
|
+
const leaf = treeEntries.at(-1);
|
|
147
|
+
if (!leaf || typeof leaf.id !== "string") return [];
|
|
148
|
+
const byId = new Map(
|
|
149
|
+
treeEntries.map((entry) => [String(entry.id), entry] as const),
|
|
150
|
+
);
|
|
151
|
+
const branch: SessionEntry[] = [];
|
|
152
|
+
const visited = new Set<string>();
|
|
153
|
+
let current: SessionEntry | undefined = leaf;
|
|
154
|
+
while (current && typeof current.id === "string") {
|
|
155
|
+
if (visited.has(current.id)) {
|
|
156
|
+
diagnostics.push({ message: `session entry cycle at ${current.id}`, path });
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
visited.add(current.id);
|
|
160
|
+
branch.push(current);
|
|
161
|
+
if (current.parentId === null || current.parentId === undefined) break;
|
|
162
|
+
if (typeof current.parentId !== "string" || !byId.has(current.parentId)) {
|
|
163
|
+
diagnostics.push({
|
|
164
|
+
message: `missing parent ${String(current.parentId)} for ${current.id}`,
|
|
165
|
+
path,
|
|
166
|
+
});
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
current = byId.get(current.parentId);
|
|
170
|
+
}
|
|
171
|
+
return branch.reverse();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function toolCalls(
|
|
175
|
+
message: Record<string, unknown>,
|
|
176
|
+
maxTextChars: number,
|
|
177
|
+
maxToolCalls: number,
|
|
178
|
+
): SessionEvidenceToolCall[] {
|
|
179
|
+
return contentBlocks(message)
|
|
180
|
+
.filter(
|
|
181
|
+
(block) =>
|
|
182
|
+
block.type === "toolCall" &&
|
|
183
|
+
typeof block.id === "string" &&
|
|
184
|
+
typeof block.name === "string",
|
|
185
|
+
)
|
|
186
|
+
.slice(0, maxToolCalls)
|
|
187
|
+
.map((block) => ({
|
|
188
|
+
arguments: redactSessionEvidenceValue(block.arguments, maxTextChars),
|
|
189
|
+
id: String(block.id),
|
|
190
|
+
name: String(block.name),
|
|
191
|
+
}));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function readSessionEvidence(
|
|
195
|
+
path: string,
|
|
196
|
+
options: SessionEvidenceReadOptions = {},
|
|
197
|
+
): SessionEvidence {
|
|
198
|
+
const maxTextChars = Math.max(
|
|
199
|
+
1,
|
|
200
|
+
options.maxTextChars ?? Limits.SESSION_EVIDENCE_TEXT_CHARS,
|
|
201
|
+
);
|
|
202
|
+
const maxToolCalls = Math.max(
|
|
203
|
+
1,
|
|
204
|
+
options.maxToolCalls ?? Limits.SESSION_EVIDENCE_MAX_TOOL_CALLS,
|
|
205
|
+
);
|
|
206
|
+
const maxTurns = Math.max(
|
|
207
|
+
1,
|
|
208
|
+
options.maxTurns ?? Limits.SESSION_EVIDENCE_MAX_TURNS,
|
|
209
|
+
);
|
|
210
|
+
const read = readJsonlFileResilient<SessionEntry>(path);
|
|
211
|
+
const diagnostics = [...read.diagnostics];
|
|
212
|
+
const header = read.records.find((entry) => entry.type === "session");
|
|
213
|
+
const branch = activeBranch(read.records, path, diagnostics);
|
|
214
|
+
const turns: SessionEvidenceTurn[] = [];
|
|
215
|
+
let pendingUser: { id: string; text?: string } | undefined;
|
|
216
|
+
let currentTurn: SessionEvidenceTurn | undefined;
|
|
217
|
+
for (const entry of branch) {
|
|
218
|
+
if (entry.type !== "message") continue;
|
|
219
|
+
const message = asRecord(entry.message);
|
|
220
|
+
const role = message.role;
|
|
221
|
+
if (role === "user") {
|
|
222
|
+
pendingUser = {
|
|
223
|
+
id: String(entry.id),
|
|
224
|
+
text: contentText(message, "text", maxTextChars),
|
|
225
|
+
};
|
|
226
|
+
currentTurn = undefined;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (role === "assistant") {
|
|
230
|
+
currentTurn = {
|
|
231
|
+
...(typeof entry.id === "string" ? { assistantEntryId: entry.id } : {}),
|
|
232
|
+
...(contentText(message, "text", maxTextChars)
|
|
233
|
+
? { assistantText: contentText(message, "text", maxTextChars) }
|
|
234
|
+
: {}),
|
|
235
|
+
...(typeof message.errorMessage === "string"
|
|
236
|
+
? { error: boundedText(message.errorMessage, maxTextChars) }
|
|
237
|
+
: {}),
|
|
238
|
+
index: turns.length + 1,
|
|
239
|
+
...(typeof message.model === "string" ? { model: message.model } : {}),
|
|
240
|
+
...(typeof message.provider === "string"
|
|
241
|
+
? { provider: message.provider }
|
|
242
|
+
: {}),
|
|
243
|
+
...(typeof message.stopReason === "string"
|
|
244
|
+
? { stopReason: message.stopReason }
|
|
245
|
+
: {}),
|
|
246
|
+
...(contentText(message, "thinking", maxTextChars)
|
|
247
|
+
? { thinking: contentText(message, "thinking", maxTextChars) }
|
|
248
|
+
: {}),
|
|
249
|
+
...(typeof entry.timestamp === "string"
|
|
250
|
+
? { timestamp: entry.timestamp }
|
|
251
|
+
: {}),
|
|
252
|
+
toolCalls: toolCalls(message, maxTextChars, maxToolCalls),
|
|
253
|
+
unmatchedToolResults: 0,
|
|
254
|
+
...(message.usage !== undefined
|
|
255
|
+
? { usage: redactSessionEvidenceValue(message.usage, maxTextChars) }
|
|
256
|
+
: {}),
|
|
257
|
+
...(pendingUser
|
|
258
|
+
? {
|
|
259
|
+
userEntryId: pendingUser.id,
|
|
260
|
+
...(pendingUser.text ? { userText: pendingUser.text } : {}),
|
|
261
|
+
}
|
|
262
|
+
: {}),
|
|
263
|
+
};
|
|
264
|
+
pendingUser = undefined;
|
|
265
|
+
turns.push(currentTurn);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (role === "toolResult" && currentTurn) {
|
|
269
|
+
const callId = typeof message.toolCallId === "string" ? message.toolCallId : "";
|
|
270
|
+
const call = currentTurn.toolCalls.find((item) => item.id === callId);
|
|
271
|
+
if (!call) {
|
|
272
|
+
currentTurn.unmatchedToolResults += 1;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
call.result = redactSessionEvidenceValue(message.content, maxTextChars);
|
|
276
|
+
call.resultError = message.isError === true;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (pendingUser) {
|
|
280
|
+
turns.push({
|
|
281
|
+
index: turns.length + 1,
|
|
282
|
+
toolCalls: [],
|
|
283
|
+
unmatchedToolResults: 0,
|
|
284
|
+
userEntryId: pendingUser.id,
|
|
285
|
+
...(pendingUser.text ? { userText: pendingUser.text } : {}),
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
const firstVisibleIndex = Math.max(0, turns.length - maxTurns);
|
|
289
|
+
const visibleTurns = turns.slice(firstVisibleIndex).map((turn, index) => ({
|
|
290
|
+
...turn,
|
|
291
|
+
index: firstVisibleIndex + index + 1,
|
|
292
|
+
}));
|
|
293
|
+
return {
|
|
294
|
+
...(branch.at(-1)?.id ? { activeLeafId: String(branch.at(-1)?.id) } : {}),
|
|
295
|
+
diagnostics,
|
|
296
|
+
path,
|
|
297
|
+
...(header ? { session: asRecord(header) } : {}),
|
|
298
|
+
totalTurns: turns.length,
|
|
299
|
+
truncated: turns.length > visibleTurns.length,
|
|
300
|
+
turns: visibleTurns,
|
|
301
|
+
};
|
|
302
|
+
}
|
package/lib/tools-response.ts
CHANGED
|
@@ -7,6 +7,39 @@
|
|
|
7
7
|
import * as Limits from "./limits.ts";
|
|
8
8
|
import * as ToolsMailbox from "./tools-mailbox.ts";
|
|
9
9
|
|
|
10
|
+
export function withLeadingBlankLine(text: string): string {
|
|
11
|
+
if (text.startsWith("\n\n")) return text;
|
|
12
|
+
return text.startsWith("\n") ? `\n${text}` : `\n\n${text}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function spaceToolResult<T>(result: T): T {
|
|
16
|
+
if (!result || typeof result !== "object") return result;
|
|
17
|
+
const content = (result as { content?: unknown }).content;
|
|
18
|
+
if (!Array.isArray(content)) return result;
|
|
19
|
+
return {
|
|
20
|
+
...result,
|
|
21
|
+
content: content.map((item) =>
|
|
22
|
+
item &&
|
|
23
|
+
typeof item === "object" &&
|
|
24
|
+
(item as { type?: unknown }).type === "text" &&
|
|
25
|
+
typeof (item as { text?: unknown }).text === "string"
|
|
26
|
+
? {
|
|
27
|
+
...item,
|
|
28
|
+
text: withLeadingBlankLine((item as { text: string }).text),
|
|
29
|
+
}
|
|
30
|
+
: item,
|
|
31
|
+
),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function spaceToolError(error: unknown): unknown {
|
|
36
|
+
if (error instanceof Error) {
|
|
37
|
+
error.message = withLeadingBlankLine(error.message);
|
|
38
|
+
return error;
|
|
39
|
+
}
|
|
40
|
+
return new Error(withLeadingBlankLine(String(error)));
|
|
41
|
+
}
|
|
42
|
+
|
|
10
43
|
export function asRecord(value: unknown): Record<string, unknown> {
|
|
11
44
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
12
45
|
? (value as Record<string, unknown>)
|
package/package.json
CHANGED
package/scripts/async-runner.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
existsSync,
|
|
14
14
|
mkdirSync,
|
|
15
15
|
readFileSync,
|
|
16
|
+
readdirSync,
|
|
16
17
|
statSync,
|
|
17
18
|
} from "node:fs";
|
|
18
19
|
import { dirname, join, relative } from "node:path";
|
|
@@ -31,8 +32,11 @@ async function importRuntimeModule(name) {
|
|
|
31
32
|
);
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
const {
|
|
35
|
-
|
|
35
|
+
const {
|
|
36
|
+
appendRecipeContextToPiArgs,
|
|
37
|
+
attachPiSessionDir,
|
|
38
|
+
materializePiPrintPromptArg,
|
|
39
|
+
} = await importRuntimeModule("recipes-context");
|
|
36
40
|
const { buildReviewPreflightDiagnostic, formatReviewPreflightDiagnostic } =
|
|
37
41
|
await importRuntimeModule("preflight-diagnostics");
|
|
38
42
|
const { execCommandTemplate } = await importRuntimeModule("command-templates");
|
|
@@ -126,11 +130,24 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
|
|
|
126
130
|
});
|
|
127
131
|
}
|
|
128
132
|
|
|
133
|
+
function commandSessionFiles(sessionDir) {
|
|
134
|
+
if (!sessionDir) return [];
|
|
135
|
+
try {
|
|
136
|
+
return readdirSync(sessionDir, { withFileTypes: true })
|
|
137
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl"))
|
|
138
|
+
.map((entry) => relative(stateDir, join(sessionDir, entry.name)))
|
|
139
|
+
.sort();
|
|
140
|
+
} catch {
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
129
145
|
function commandEvidenceStartRecord({
|
|
130
146
|
commandDetail,
|
|
131
147
|
commandId,
|
|
132
148
|
materialized,
|
|
133
149
|
options,
|
|
150
|
+
sessionDir,
|
|
134
151
|
stage,
|
|
135
152
|
occurrence,
|
|
136
153
|
}) {
|
|
@@ -155,6 +172,7 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
|
|
|
155
172
|
...(materialized.promptBytes
|
|
156
173
|
? { prompt_bytes: materialized.promptBytes }
|
|
157
174
|
: {}),
|
|
175
|
+
...(sessionDir ? { session_dir: relative(stateDir, sessionDir) } : {}),
|
|
158
176
|
attempts: [],
|
|
159
177
|
semantic_acceptance:
|
|
160
178
|
options?.evidenceContext?.acceptOutput === "review_evidence" ||
|
|
@@ -172,6 +190,7 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
|
|
|
172
190
|
options,
|
|
173
191
|
result,
|
|
174
192
|
rawExitCode,
|
|
193
|
+
sessionDir,
|
|
175
194
|
stage,
|
|
176
195
|
occurrence,
|
|
177
196
|
startedAt,
|
|
@@ -234,6 +253,10 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
|
|
|
234
253
|
...(materialized.promptBytes
|
|
235
254
|
? { prompt_bytes: materialized.promptBytes }
|
|
236
255
|
: {}),
|
|
256
|
+
...(sessionDir ? { session_dir: relative(stateDir, sessionDir) } : {}),
|
|
257
|
+
...(commandSessionFiles(sessionDir).length > 0
|
|
258
|
+
? { session_files: commandSessionFiles(sessionDir) }
|
|
259
|
+
: {}),
|
|
237
260
|
attempts,
|
|
238
261
|
exit_code: rawExitCode ?? result.code,
|
|
239
262
|
effective_exit_code: result.code,
|
|
@@ -301,21 +324,26 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
|
|
|
301
324
|
}
|
|
302
325
|
|
|
303
326
|
async function observedExec(command, args, options) {
|
|
327
|
+
captureCounter += 1;
|
|
328
|
+
const commandId = `command-${String(captureCounter).padStart(3, "0")}`;
|
|
304
329
|
const contextArgs = appendRecipeContextToPiArgs(
|
|
305
330
|
command,
|
|
306
331
|
args,
|
|
307
332
|
meta.recipe_context_records,
|
|
308
333
|
options?.actorRecipeContext,
|
|
309
334
|
);
|
|
310
|
-
const
|
|
335
|
+
const session = attachPiSessionDir(
|
|
311
336
|
command,
|
|
312
337
|
contextArgs,
|
|
338
|
+
join(stateDir, "sessions", commandId),
|
|
339
|
+
);
|
|
340
|
+
const materialized = materializePiPrintPromptArg(
|
|
341
|
+
command,
|
|
342
|
+
session.args,
|
|
313
343
|
promptFilePath,
|
|
314
344
|
);
|
|
315
345
|
const execArgs = materialized.args;
|
|
316
346
|
const commandDetail = formatCommandDetail(command, execArgs);
|
|
317
|
-
captureCounter += 1;
|
|
318
|
-
const commandId = `command-${String(captureCounter).padStart(3, "0")}`;
|
|
319
347
|
const stage =
|
|
320
348
|
options?.actorRecipeContext?.alias ||
|
|
321
349
|
options?.actorRecipeContext?.name ||
|
|
@@ -346,6 +374,7 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
|
|
|
346
374
|
commandId,
|
|
347
375
|
materialized,
|
|
348
376
|
options,
|
|
377
|
+
sessionDir: session.sessionDir,
|
|
349
378
|
stage,
|
|
350
379
|
occurrence,
|
|
351
380
|
});
|
|
@@ -357,6 +386,7 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
|
|
|
357
386
|
command: commandDetail,
|
|
358
387
|
...(materialized.promptFile ? { prompt_file: materialized.promptFile } : {}),
|
|
359
388
|
...(materialized.promptBytes ? { prompt_bytes: materialized.promptBytes } : {}),
|
|
389
|
+
...(session.sessionDir ? { session_dir: relative(stateDir, session.sessionDir) } : {}),
|
|
360
390
|
});
|
|
361
391
|
progressRunning();
|
|
362
392
|
const captureDir = join(stateDir, "captures", commandId);
|
|
@@ -400,6 +430,7 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
|
|
|
400
430
|
options,
|
|
401
431
|
result,
|
|
402
432
|
rawExitCode: rawResult.code,
|
|
433
|
+
sessionDir: session.sessionDir,
|
|
403
434
|
stage,
|
|
404
435
|
occurrence,
|
|
405
436
|
startedAt: startedEvidence.started_at,
|
|
@@ -426,6 +457,10 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
|
|
|
426
457
|
...captureDetails(result),
|
|
427
458
|
...(materialized.promptFile ? { prompt_file: materialized.promptFile } : {}),
|
|
428
459
|
...(materialized.promptBytes ? { prompt_bytes: materialized.promptBytes } : {}),
|
|
460
|
+
...(session.sessionDir ? { session_dir: relative(stateDir, session.sessionDir) } : {}),
|
|
461
|
+
...(commandSessionFiles(session.sessionDir).length > 0
|
|
462
|
+
? { session_files: commandSessionFiles(session.sessionDir) }
|
|
463
|
+
: {}),
|
|
429
464
|
...(preflightDiagnostic ? { preflight: preflightDiagnostic } : {}),
|
|
430
465
|
});
|
|
431
466
|
outbox(
|
package/scripts/coordinator.mjs
CHANGED
|
@@ -238,14 +238,24 @@ function terminateProcessGroup(child, signal) {
|
|
|
238
238
|
}
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
-
function
|
|
241
|
+
function sessionKey(value) {
|
|
242
|
+
return String(value).replaceAll(/[^A-Za-z0-9_.-]+/g, "-");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function runPi(prompt, model, thinking, ttlMs = 0, sessionDir, ownerSessionId) {
|
|
242
246
|
return new Promise((resolve) => {
|
|
243
247
|
const args = [
|
|
244
248
|
"--tools",
|
|
245
249
|
"inspect,message",
|
|
246
250
|
"--no-context-files",
|
|
247
251
|
"--no-skills",
|
|
248
|
-
|
|
252
|
+
...(sessionDir
|
|
253
|
+
? [
|
|
254
|
+
"--session-dir",
|
|
255
|
+
sessionDir,
|
|
256
|
+
...(ownerSessionId ? ["--session-id", ownerSessionId] : []),
|
|
257
|
+
]
|
|
258
|
+
: ["--no-session"]),
|
|
249
259
|
];
|
|
250
260
|
if (model) {
|
|
251
261
|
args.push("--model", model);
|
|
@@ -346,7 +356,14 @@ async function synthesize(config, locker) {
|
|
|
346
356
|
}
|
|
347
357
|
const transcript = await readRoomTranscript(config);
|
|
348
358
|
const prompt = `Synthesize this transcript into a concise Markdown artifact. Mission: ${config.mission}. Include: Title, Consensus, Roles, Protocol, Final Artifact Shape, Next Actions, Open Questions. Use only the transcript evidence below.\n\nTRANSCRIPT:\n${transcript.slice(-24000)}`;
|
|
349
|
-
const result = await runPi(
|
|
359
|
+
const result = await runPi(
|
|
360
|
+
prompt,
|
|
361
|
+
config.model,
|
|
362
|
+
config.thinking,
|
|
363
|
+
config.subagentTtlMs,
|
|
364
|
+
`${runStateDir(config.runId)}/sessions/coordinator-synthesis`,
|
|
365
|
+
config.ownerSessionId,
|
|
366
|
+
);
|
|
350
367
|
const output = result.stdout.trim();
|
|
351
368
|
const diagnostics = config.stats
|
|
352
369
|
? `\n\n## Diagnostics\n\nparticipant_attempts=${config.stats.participantAttempts}\nparticipant_success=${config.stats.participantSuccess}\nparticipant_failures=${config.stats.participantFailures}\nsynthesis_code=${result.code}\ntranscript_messages=${transcript.trim() ? transcript.split("\n").length : 0}\n`
|
|
@@ -443,7 +460,7 @@ async function updateInboxMessagesStatus(runId, branchName, ids, status) {
|
|
|
443
460
|
});
|
|
444
461
|
}
|
|
445
462
|
|
|
446
|
-
async function executeParticipantPrompt(role, basePrompt, config) {
|
|
463
|
+
async function executeParticipantPrompt(role, basePrompt, config, phase) {
|
|
447
464
|
const branchName = role.name;
|
|
448
465
|
const queuedMessages = await claimQueuedInboxMessages(
|
|
449
466
|
config.runId,
|
|
@@ -469,7 +486,14 @@ async function executeParticipantPrompt(role, basePrompt, config) {
|
|
|
469
486
|
finalPrompt += inboxSection;
|
|
470
487
|
}
|
|
471
488
|
|
|
472
|
-
const result = await runPi(
|
|
489
|
+
const result = await runPi(
|
|
490
|
+
finalPrompt,
|
|
491
|
+
config.model,
|
|
492
|
+
config.thinking,
|
|
493
|
+
config.subagentTtlMs,
|
|
494
|
+
`${runStateDir(config.runId)}/sessions/${sessionKey(`${role.name}-${phase}`)}`,
|
|
495
|
+
config.ownerSessionId,
|
|
496
|
+
);
|
|
473
497
|
|
|
474
498
|
if (claimedIds.length > 0) {
|
|
475
499
|
const finalStatus = result.code === 0 ? "handled" : "failed";
|
|
@@ -488,7 +512,7 @@ async function participantRound(role, round, config) {
|
|
|
488
512
|
const displayName = role.name;
|
|
489
513
|
const address = `branch:${config.runId}/${role.name}`;
|
|
490
514
|
const prompt = `You are ${displayName} (${address}), ${role.persona}. Mission: ${config.mission}. Round ${round}/${config.rounds}. First call inspect target=${config.room} view=previews lines=30 and inspect target=${config.room} view=contacts. Then call message once to ${config.room} from ${address} type=chat.message. Body: 2-4 sentences that react to a named participant, propose the next coordination step, and refine the shared artifact. Use contacts for peer names and addresses. End stdout with summary <=160 chars.`;
|
|
491
|
-
const result = await executeParticipantPrompt(role, prompt, config);
|
|
515
|
+
const result = await executeParticipantPrompt(role, prompt, config, `round-${round}`);
|
|
492
516
|
if (config.stats) {
|
|
493
517
|
config.stats.participantAttempts += 1;
|
|
494
518
|
if (result.code === 0) config.stats.participantSuccess += 1;
|
|
@@ -506,14 +530,28 @@ async function participantJoin(role, config) {
|
|
|
506
530
|
const displayName = role.name;
|
|
507
531
|
const address = `branch:${config.runId}/${role.name}`;
|
|
508
532
|
const joinPrompt = `You are ${displayName}, ${role.persona}. Mission: ${config.mission}. Call tool message exactly once with to=${shellQuote(config.room)}, from=${shellQuote(address)}, type='actor.join', summary='${displayName} joined', body JSON {"role":${JSON.stringify(role.persona)},"display":${JSON.stringify(displayName)},"caps":["coordination","synthesis"],"claim":"coordinate on mission"}. Then print one short line.`;
|
|
509
|
-
await runPi(
|
|
533
|
+
await runPi(
|
|
534
|
+
joinPrompt,
|
|
535
|
+
config.model,
|
|
536
|
+
config.thinking,
|
|
537
|
+
config.subagentTtlMs,
|
|
538
|
+
`${runStateDir(config.runId)}/sessions/${sessionKey(`${role.name}-join`)}`,
|
|
539
|
+
config.ownerSessionId,
|
|
540
|
+
);
|
|
510
541
|
}
|
|
511
542
|
|
|
512
543
|
async function participantLeave(role, config) {
|
|
513
544
|
const displayName = role.name;
|
|
514
545
|
const address = `branch:${config.runId}/${role.name}`;
|
|
515
546
|
const leavePrompt = `Call tool message exactly once with to=${shellQuote(config.room)}, from=${shellQuote(address)}, type='actor.leave', summary='${displayName} left', body='finished coordinated work'. Then print goodbye.`;
|
|
516
|
-
await runPi(
|
|
547
|
+
await runPi(
|
|
548
|
+
leavePrompt,
|
|
549
|
+
config.model,
|
|
550
|
+
config.thinking,
|
|
551
|
+
config.subagentTtlMs,
|
|
552
|
+
`${runStateDir(config.runId)}/sessions/${sessionKey(`${role.name}-leave`)}`,
|
|
553
|
+
config.ownerSessionId,
|
|
554
|
+
);
|
|
517
555
|
}
|
|
518
556
|
|
|
519
557
|
// 1. consensus / swarm mode: iterative chat in a room
|
|
@@ -601,7 +639,12 @@ async function runPool(config, locker) {
|
|
|
601
639
|
|
|
602
640
|
const taskId = assignedTask.task;
|
|
603
641
|
const prompt = `You are ${role.name}, ${role.persona}. You have been assigned task ${taskId}: "${assignedTask.task}". Solve it as part of the overall mission: ${config.mission}. End your response with a clear summary.`;
|
|
604
|
-
const result = await executeParticipantPrompt(
|
|
642
|
+
const result = await executeParticipantPrompt(
|
|
643
|
+
role,
|
|
644
|
+
prompt,
|
|
645
|
+
config,
|
|
646
|
+
`task-${taskId}`,
|
|
647
|
+
);
|
|
605
648
|
|
|
606
649
|
process.stdout.write(
|
|
607
650
|
`[${role.name} completed ${taskId}] code=${result.code}\n${result.stdout}\n`,
|
|
@@ -711,6 +754,9 @@ if (!config.model) {
|
|
|
711
754
|
process.exit(2);
|
|
712
755
|
}
|
|
713
756
|
config.room = `room:${config.runId}`;
|
|
757
|
+
config.ownerSessionId = String(
|
|
758
|
+
(await readJsonFile(`${runStateDir(config.runId)}/run.json`)).ownerId ?? "",
|
|
759
|
+
);
|
|
714
760
|
|
|
715
761
|
const failures = [];
|
|
716
762
|
const locker = await startLocker(config);
|
package/skills/actors/SKILL.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: actors
|
|
3
3
|
description: Required practical guide for non-trivial pi-actors use, including parallel actor launches, subagent fanout, and autonomous coordinator workflows. Read before using or changing spawn, message, inspect, actor runs, tools, recipes, command templates, async lifecycle, mailboxes, artifacts, and local orchestration mechanics.
|
|
4
4
|
metadata:
|
|
5
|
-
version: 0.
|
|
5
|
+
version: 0.41.0
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Actors (pi-actors)
|
|
@@ -125,13 +125,9 @@ Views:
|
|
|
125
125
|
- `artifacts`: declared artifact paths/status plus the same bounded owned review-evidence manifest when present.
|
|
126
126
|
- `recipes` target: registry summary for active, shadowed, invalid, disabled, and diagnostic recipe entries.
|
|
127
127
|
|
|
128
|
-
Actor inspector
|
|
128
|
+
Actor inspector navigation uses one command: `/actors-inspector-toggle` opens the centered overlay. It exposes explicit Tabs → Filters → List → Detail focus zones over the first run owned by the current session. Tabs use ←/→; ↓ enters Filters; ↑ at the top boundary does nothing. The filter bar contains only selectable controls; arrows move focus, Enter opens a compact value popup anchored beneath that filter, ↑/↓ hover a value, and Enter applies it. A filter cannot move down into an empty list. Accent text marks current values, a light background marks keyboard focus, and opening a nested popup preserves both its blue parent filter and the striped timeline outside the occluded cells. Lists use ↑/↓ and Enter; detail uses ↑/↓ and Enter/←. Escape cancels options or closes the overlay. The list body carries the selected run and live status above compact striped evidence rows; tabs, live refresh, empty states, and key hints remain visible so no subcommand grammar is required.
|
|
129
129
|
|
|
130
|
-
|
|
131
|
-
- `/actors-inspector-filter all|room|direct|broadcast|unread|branch <name>|current-branch <name>|mention <text>`: narrow table previews without changing room/run state.
|
|
132
|
-
- `/actors-inspect <number>`: open one visible row as a full-message view.
|
|
133
|
-
|
|
134
|
-
The table is compact and optimistic by default: bounded body previews, capped noisy room rows, branch-local inbox previews, stable event ids in selected-message details, and an inline roster summary in the form `name/role` that wraps only when needed. Use `unread` for queued branch inbox work and `branch <name>` / `current-branch <name>` for one branch's room/direct/inbox traffic. Rows with `metadata.requires_response=true` show a `!` attention marker. `/actors-inspect <number>` marks that row read for the current session filter. Active roster members use the target color; members that sent `actor.leave` stay visible as inactive/muted participants from the current run. Actor display names come from `actor.join` bodies (`display`) or branch addresses, keeping debugger output plain and name-driven.
|
|
130
|
+
Communication rows retain bounded body previews, capped noisy room traffic, branch-local inbox state, stable event ids, attention markers, and compact roster summaries. Turn rows come only from persisted owned child-session evidence and show source model/text at a glance. Turn detail remains bounded and includes command/stage, session/prompt/recipe provenance, user/assistant text, persisted thinking or explicit reasoning unavailability, stop/usage/error metadata, correlated tool arguments/results, truncation, and parse diagnostics. Active roster members use the target color, departed members stay muted, and display names come from `actor.join` bodies or branch addresses.
|
|
135
131
|
|
|
136
132
|
Let terminal notifications arrive. They queue through Pi's follow-up delivery mode, so a busy coordinator finishes its current work before receiving concurrently completed actor results; the host's `followUpMode` controls whether queued results arrive together or one at a time. When a deferred actor result gates the next step, wait for that terminal follow-up instead of scheduling continuation loops, repeatedly inspecting, or mutating the actor's reviewed scope. Idle coordinators still start a normal turn through `triggerTurn: true`. Inspect early only for an operator request, a meaningful actor event, or diagnosis of an overdue or stuck run.
|
|
137
133
|
|
|
@@ -166,6 +162,8 @@ Controls:
|
|
|
166
162
|
- `repeat`: repeated node expansion.
|
|
167
163
|
- `output`: output behavior selection.
|
|
168
164
|
- Command stdout/stderr use bounded tails plus complete spill files; tool/run diagnostics expose byte counts, truncation, and spill paths, while pipelines fail with `incomplete pipeline stdin` rather than consuming a partial tail.
|
|
165
|
+
- Detached child `pi -p` commands receive isolated session storage under `sessions/command-NNN` in their owned run state, and command evidence records any resulting JSONL files. Coordinator-managed room/swarm participants use role/phase-scoped directories under the same run-local `sessions/` root so their turns remain discoverable too. Explicit `--no-session`, `--session`, `--session-id`, `--session-dir`, or `--fork` policy remains caller-owned and is never replaced.
|
|
166
|
+
- Persisted child-session inspection follows the latest JSONL entry branch, correlates tool results by call id, bounds previews, and redacts common secret-bearing fields/text. Thinking content is evidence only when Pi persisted an explicit `thinking` block; never infer or advertise hidden reasoning.
|
|
169
167
|
|
|
170
168
|
Placeholders:
|
|
171
169
|
|
package/skills/swarm/SKILL.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: swarm
|
|
3
3
|
description: Subagent and actor orchestration with scoped locks, fanout, and quorum consensus. Use before launching multiple parallel actors or subagents for independent implementation, artifact generation, review, delegated audit, coordinated execution, or any workflow that needs autonomous coordinator decomposition and integration.
|
|
4
4
|
metadata:
|
|
5
|
-
version: 0.
|
|
5
|
+
version: 0.41.0
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Swarm
|