pi-agent-replicate 0.0.0-stage → 1.4.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 +27 -0
- package/CHANGELOG.md +152 -0
- package/CONTEXT.md +63 -0
- package/LICENSE +21 -0
- package/README.md +68 -2
- package/RELEASING.md +132 -0
- package/assets/banner.png +0 -0
- package/config.json.example +6 -0
- package/docs/README.md +23 -0
- package/docs/adr/0003-installable-role-packs.md +29 -0
- package/docs/adr/0008-adopt-pi-only-subagent-execution.md +22 -0
- package/docs/adr/0009-durable-recursive-subagent-ownership.md +23 -0
- package/docs/adr/0010-public-subagent-tree-registry.md +16 -0
- package/docs/configuration.md +14 -0
- package/docs/development.md +27 -0
- package/docs/public-subagent-tree-api.md +58 -0
- package/docs/retained-roles.md +71 -0
- package/docs/settled-lifecycle-contract.md +71 -0
- package/docs/worktree-subagents.md +58 -0
- package/examples/role-pack/extension.ts +18 -0
- package/examples/role-pack/package.json +16 -0
- package/examples/role-pack/roles/example-reviewer.md +12 -0
- package/index.d.ts +116 -0
- package/index.js +1064 -0
- package/index.ts +155 -0
- package/package.json +70 -4
- package/pi-extension/subagents/activity.js +575 -0
- package/pi-extension/subagents/activity.ts +980 -0
- package/pi-extension/subagents/completion.js +184 -0
- package/pi-extension/subagents/completion.ts +229 -0
- package/pi-extension/subagents/delivery-drain.js +15 -0
- package/pi-extension/subagents/delivery-drain.ts +20 -0
- package/pi-extension/subagents/delivery-files.js +86 -0
- package/pi-extension/subagents/delivery-files.ts +105 -0
- package/pi-extension/subagents/delivery-log.js +51 -0
- package/pi-extension/subagents/delivery-log.ts +51 -0
- package/pi-extension/subagents/delivery-payload.ts +55 -0
- package/pi-extension/subagents/delivery.ts +219 -0
- package/pi-extension/subagents/execution-owner.js +30 -0
- package/pi-extension/subagents/execution-owner.ts +38 -0
- package/pi-extension/subagents/herdr.js +645 -0
- package/pi-extension/subagents/herdr.ts +927 -0
- package/pi-extension/subagents/index.ts +3400 -0
- package/pi-extension/subagents/launch.js +774 -0
- package/pi-extension/subagents/launch.ts +1124 -0
- package/pi-extension/subagents/lifecycle.js +477 -0
- package/pi-extension/subagents/lifecycle.ts +582 -0
- package/pi-extension/subagents/lineage.js +806 -0
- package/pi-extension/subagents/lineage.ts +841 -0
- package/pi-extension/subagents/replica-background.js +32 -0
- package/pi-extension/subagents/replica-background.ts +38 -0
- package/pi-extension/subagents/replica-branch.ts +45 -0
- package/pi-extension/subagents/replica-directory-settings.ts +51 -0
- package/pi-extension/subagents/replica-history.js +22 -0
- package/pi-extension/subagents/replica-history.ts +21 -0
- package/pi-extension/subagents/replica-work.js +326 -0
- package/pi-extension/subagents/replica-work.ts +336 -0
- package/pi-extension/subagents/replica-worker.js +74 -0
- package/pi-extension/subagents/replica-worker.ts +71 -0
- package/pi-extension/subagents/replication.ts +99 -0
- package/pi-extension/subagents/result-delivery-queue.ts +33 -0
- package/pi-extension/subagents/role-catalog.js +123 -0
- package/pi-extension/subagents/role-catalog.ts +24 -0
- package/pi-extension/subagents/runtime-routing.js +154 -0
- package/pi-extension/subagents/runtime-routing.ts +239 -0
- package/pi-extension/subagents/session-binding.js +346 -0
- package/pi-extension/subagents/session-binding.ts +339 -0
- package/pi-extension/subagents/session-work.js +219 -0
- package/pi-extension/subagents/session-work.ts +167 -0
- package/pi-extension/subagents/session.js +318 -0
- package/pi-extension/subagents/session.ts +448 -0
- package/pi-extension/subagents/settled-contract.js +47 -0
- package/pi-extension/subagents/settled-contract.ts +175 -0
- package/pi-extension/subagents/source-state.js +180 -0
- package/pi-extension/subagents/source-state.ts +184 -0
- package/pi-extension/subagents/status.ts +493 -0
- package/pi-extension/subagents/subagent-done.js +805 -0
- package/pi-extension/subagents/subagent-done.ts +878 -0
- package/pi-extension/subagents/terminal.js +111 -0
- package/pi-extension/subagents/terminal.ts +208 -0
- package/pi-extension/subagents/watchdog.js +122 -0
- package/pi-extension/subagents/watchdog.ts +171 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { appendFileSync, copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
export function seedStandaloneSessionFile(params) {
|
|
5
|
+
const header = {
|
|
6
|
+
type: "session",
|
|
7
|
+
version: 3,
|
|
8
|
+
id: randomUUID(),
|
|
9
|
+
timestamp: new Date().toISOString(),
|
|
10
|
+
cwd: params.childCwd,
|
|
11
|
+
};
|
|
12
|
+
mkdirSync(dirname(params.childSessionFile), { recursive: true });
|
|
13
|
+
writeFileSync(params.childSessionFile, `${JSON.stringify(header)}\n`, "utf8");
|
|
14
|
+
}
|
|
15
|
+
function getForkContentLines(parentSessionFile) {
|
|
16
|
+
const raw = readFileSync(parentSessionFile, "utf8");
|
|
17
|
+
const lines = raw.split("\n").filter((line) => line.trim());
|
|
18
|
+
let truncateAt = lines.length;
|
|
19
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
20
|
+
try {
|
|
21
|
+
const entry = JSON.parse(lines[i]);
|
|
22
|
+
if (entry.type === "message" && entry.message?.role === "user") {
|
|
23
|
+
truncateAt = i;
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// ignore malformed lines
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return lines.slice(0, truncateAt).filter((line) => {
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(line).type !== "session";
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
export function seedSubagentSessionFile(params) {
|
|
41
|
+
const header = {
|
|
42
|
+
type: "session",
|
|
43
|
+
version: 3,
|
|
44
|
+
id: randomUUID(),
|
|
45
|
+
timestamp: new Date().toISOString(),
|
|
46
|
+
cwd: params.childCwd,
|
|
47
|
+
parentSession: params.parentSessionFile,
|
|
48
|
+
};
|
|
49
|
+
const contentLines = params.mode === "fork" ? getForkContentLines(params.parentSessionFile) : [];
|
|
50
|
+
const lines = [JSON.stringify(header), ...contentLines];
|
|
51
|
+
mkdirSync(dirname(params.childSessionFile), { recursive: true });
|
|
52
|
+
writeFileSync(params.childSessionFile, lines.join("\n") + "\n", "utf8");
|
|
53
|
+
}
|
|
54
|
+
/** Fork conversation and runtime, not extension control state; settle inherited calls without replay. */
|
|
55
|
+
export function seedReplicationSessionFile(params) {
|
|
56
|
+
const header = { type: "session", version: 3, id: params.resumeSessionId ?? randomUUID(), timestamp: new Date().toISOString(), cwd: params.childCwd, parentSession: params.parentSessionFile };
|
|
57
|
+
const rows = [header];
|
|
58
|
+
const pending = new Map();
|
|
59
|
+
let parentId = null;
|
|
60
|
+
for (const source of params.entries) {
|
|
61
|
+
// Custom messages are conversation. Custom entries are extension-owned
|
|
62
|
+
// controls: restoring them can start a parent turn before our assignment.
|
|
63
|
+
if (source.type === "custom") continue;
|
|
64
|
+
const entry = { ...source, parentId };
|
|
65
|
+
if (source.type === "compaction") {
|
|
66
|
+
const firstKeptIndex = params.entries.findIndex((row) => row.id === source.firstKeptEntryId);
|
|
67
|
+
if (params.entries[firstKeptIndex]?.type === "custom") {
|
|
68
|
+
entry.firstKeptEntryId = params.entries.slice(firstKeptIndex).find((row) => row.type !== "custom")?.id ?? source.id;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
rows.push(entry);
|
|
72
|
+
parentId = entry.id;
|
|
73
|
+
}
|
|
74
|
+
// Use the native projected context, not raw history before compaction or edits.
|
|
75
|
+
for (const message of params.contextMessages) {
|
|
76
|
+
if (message.role === "system")
|
|
77
|
+
continue;
|
|
78
|
+
if (message.role === "toolResult") {
|
|
79
|
+
pending.delete(message.toolCallId);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
// A later turn already closes older orphan calls during provider replay.
|
|
83
|
+
pending.clear();
|
|
84
|
+
if (message.role !== "assistant" || message.stopReason === "error" || message.stopReason === "aborted")
|
|
85
|
+
continue;
|
|
86
|
+
for (const part of message.content)
|
|
87
|
+
if (part.type === "toolCall")
|
|
88
|
+
pending.set(part.id, part.name);
|
|
89
|
+
}
|
|
90
|
+
for (const [toolCallId, toolName] of pending) {
|
|
91
|
+
const id = randomBytes(4).toString("hex");
|
|
92
|
+
rows.push({ type: "message", id, parentId, timestamp: new Date().toISOString(), message: {
|
|
93
|
+
role: "toolResult", toolCallId, toolName, isError: false, timestamp: Date.now(),
|
|
94
|
+
content: [{ type: "text", text: "This call belongs to the original session. This copy continues with its own assignment." }],
|
|
95
|
+
} });
|
|
96
|
+
parentId = id;
|
|
97
|
+
}
|
|
98
|
+
mkdirSync(dirname(params.childSessionFile), { recursive: true });
|
|
99
|
+
if (params.name)
|
|
100
|
+
rows.push({ type: "session_info", id: randomBytes(4).toString("hex"), parentId, timestamp: new Date().toISOString(), name: params.name });
|
|
101
|
+
writeFileSync(params.childSessionFile, rows.map((row) => JSON.stringify(row)).join("\n") + "\n", "utf8");
|
|
102
|
+
}
|
|
103
|
+
function parseEntry(line) {
|
|
104
|
+
try {
|
|
105
|
+
return JSON.parse(line);
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
throw new Error(`Invalid session entry: ${error instanceof Error ? error.message : String(error)}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function readEntries(sessionFile) {
|
|
112
|
+
return readFileSync(sessionFile, "utf8")
|
|
113
|
+
.split("\n")
|
|
114
|
+
.filter((line) => line.trim())
|
|
115
|
+
.map(parseEntry);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Read a session while ignoring one incomplete final JSONL record.
|
|
119
|
+
*
|
|
120
|
+
* Pi writes session entries atomically in normal operation, but a parent can
|
|
121
|
+
* observe the file between the write and the trailing newline. The next poll
|
|
122
|
+
* can read the same entry once it is complete, so the incomplete record is
|
|
123
|
+
* deliberately reported rather than treated as a session error.
|
|
124
|
+
*/
|
|
125
|
+
function readEntriesTolerant(sessionFile) {
|
|
126
|
+
if (!existsSync(sessionFile)) {
|
|
127
|
+
return { entries: [], partialTrailingLine: false };
|
|
128
|
+
}
|
|
129
|
+
const lines = readFileSync(sessionFile, "utf8")
|
|
130
|
+
.split("\n")
|
|
131
|
+
.filter((line) => line.trim());
|
|
132
|
+
const entries = [];
|
|
133
|
+
let partialTrailingLine = false;
|
|
134
|
+
for (let index = 0; index < lines.length; index++) {
|
|
135
|
+
try {
|
|
136
|
+
entries.push(parseEntry(lines[index]));
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
if (index === lines.length - 1) {
|
|
140
|
+
partialTrailingLine = true;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return { entries, partialTrailingLine };
|
|
147
|
+
}
|
|
148
|
+
/** Capture the session position and inherited assistant ids before a run. */
|
|
149
|
+
export function captureSessionBaseline(sessionFile) {
|
|
150
|
+
const { entries } = readEntriesTolerant(sessionFile);
|
|
151
|
+
return {
|
|
152
|
+
sessionFile,
|
|
153
|
+
entryCount: entries.length,
|
|
154
|
+
leafId: entries.length > 0 ? entries[entries.length - 1].id : null,
|
|
155
|
+
assistantEntryIds: entries
|
|
156
|
+
.filter((entry) => entry.type === "message" &&
|
|
157
|
+
entry.message.role === "assistant")
|
|
158
|
+
.map((entry) => entry.id),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/** Read only records appended after a pre-run session baseline. */
|
|
162
|
+
export function readEntriesAfterBaseline(sessionFile, baseline) {
|
|
163
|
+
const read = readEntriesTolerant(sessionFile);
|
|
164
|
+
return {
|
|
165
|
+
entries: read.entries.slice(baseline.entryCount),
|
|
166
|
+
partialTrailingLine: read.partialTrailingLine,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/** Return the newest assistant entry in a post-baseline append. */
|
|
170
|
+
export function findNewestAppendedAssistant(entries) {
|
|
171
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
172
|
+
const entry = entries[index];
|
|
173
|
+
if (entry.type !== "message")
|
|
174
|
+
continue;
|
|
175
|
+
const message = entry.message;
|
|
176
|
+
if (message.role !== "assistant")
|
|
177
|
+
continue;
|
|
178
|
+
const textBlocks = message.content.filter((block) => block.type === "text" && typeof block.text === "string");
|
|
179
|
+
const text = textBlocks.map((block) => block.text).join("\n");
|
|
180
|
+
const stopReason = message.stopReason;
|
|
181
|
+
const errorMessage = message.errorMessage;
|
|
182
|
+
return {
|
|
183
|
+
id: entry.id,
|
|
184
|
+
text: text.trim() ? text : null,
|
|
185
|
+
contentLength: text.length,
|
|
186
|
+
empty: !text.trim(),
|
|
187
|
+
...(typeof stopReason === "string" ? { stopReason } : {}),
|
|
188
|
+
...(typeof errorMessage === "string" && errorMessage.trim()
|
|
189
|
+
? { errorMessage: errorMessage.trim() }
|
|
190
|
+
: {}),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
/** Read and inspect the newest assistant appended after a child baseline. */
|
|
196
|
+
export function inspectNewestAppendedAssistant(sessionFile, baseline) {
|
|
197
|
+
return findNewestAppendedAssistant(readEntriesAfterBaseline(sessionFile, baseline).entries);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Return the id of the last entry in the session file (current branch point / leaf).
|
|
201
|
+
*/
|
|
202
|
+
export function getLeafId(sessionFile) {
|
|
203
|
+
const entries = readEntries(sessionFile);
|
|
204
|
+
return entries.length > 0 ? entries[entries.length - 1].id : null;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Return entries added after `afterLine` (1-indexed count of existing entries).
|
|
208
|
+
*/
|
|
209
|
+
export function getNewEntries(sessionFile, afterLine) {
|
|
210
|
+
return readFileSync(sessionFile, "utf8")
|
|
211
|
+
.split("\n")
|
|
212
|
+
.filter((line) => line.trim())
|
|
213
|
+
.slice(afterLine)
|
|
214
|
+
.map(parseEntry);
|
|
215
|
+
}
|
|
216
|
+
/** Read the effective model and thinking entries recorded by Pi at session startup. */
|
|
217
|
+
export function findObservedSessionRuntime(entries) {
|
|
218
|
+
const observed = {};
|
|
219
|
+
for (const entry of entries) {
|
|
220
|
+
if (entry.type === "model_change") {
|
|
221
|
+
if (typeof entry.provider === "string")
|
|
222
|
+
observed.provider = entry.provider;
|
|
223
|
+
if (typeof entry.modelId === "string")
|
|
224
|
+
observed.modelId = entry.modelId;
|
|
225
|
+
}
|
|
226
|
+
else if (entry.type === "thinking_level_change" &&
|
|
227
|
+
typeof entry.thinkingLevel === "string") {
|
|
228
|
+
observed.thinking = entry.thinkingLevel;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return observed;
|
|
232
|
+
}
|
|
233
|
+
/** Inspect only the final assistant message for completion evidence. */
|
|
234
|
+
export function inspectFinalAssistantMessage(entries) {
|
|
235
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
236
|
+
const entry = entries[i];
|
|
237
|
+
if (entry.type !== "message")
|
|
238
|
+
continue;
|
|
239
|
+
const msg = entry;
|
|
240
|
+
if (msg.message.role !== "assistant")
|
|
241
|
+
continue;
|
|
242
|
+
const texts = msg.message.content
|
|
243
|
+
.filter((block) => block.type === "text" && typeof block.text === "string")
|
|
244
|
+
.map((block) => block.text);
|
|
245
|
+
const text = texts.join("\n");
|
|
246
|
+
const stopReason = msg.message.stopReason;
|
|
247
|
+
return {
|
|
248
|
+
text: text.trim() ? text : null,
|
|
249
|
+
contentLength: text.length,
|
|
250
|
+
...(typeof stopReason === "string" ? { stopReason } : {}),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
return { text: null, contentLength: 0 };
|
|
254
|
+
}
|
|
255
|
+
export function findLastAssistantMessage(entries) {
|
|
256
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
257
|
+
const entry = entries[i];
|
|
258
|
+
if (entry.type !== "message")
|
|
259
|
+
continue;
|
|
260
|
+
const msg = entry;
|
|
261
|
+
if (msg.message.role !== "assistant")
|
|
262
|
+
continue;
|
|
263
|
+
const texts = msg.message.content
|
|
264
|
+
.filter((block) => block.type === "text" &&
|
|
265
|
+
typeof block.text === "string" &&
|
|
266
|
+
block.text.trim() !== "")
|
|
267
|
+
.map((block) => block.text);
|
|
268
|
+
if (texts.length > 0 && texts.join("").trim())
|
|
269
|
+
return texts.join("\n");
|
|
270
|
+
const stopReason = msg.message.stopReason;
|
|
271
|
+
const errorMessage = msg.message
|
|
272
|
+
.errorMessage;
|
|
273
|
+
if (stopReason === "error" &&
|
|
274
|
+
typeof errorMessage === "string" &&
|
|
275
|
+
errorMessage.trim() !== "") {
|
|
276
|
+
return `Subagent error: ${errorMessage.trim()}`;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Append a branch_summary entry to the session file.
|
|
283
|
+
* Returns the new entry's id.
|
|
284
|
+
*/
|
|
285
|
+
export function appendBranchSummary(sessionFile, branchPointId, fromId, summary) {
|
|
286
|
+
const id = randomBytes(4).toString("hex");
|
|
287
|
+
const entry = {
|
|
288
|
+
type: "branch_summary",
|
|
289
|
+
id,
|
|
290
|
+
parentId: branchPointId,
|
|
291
|
+
timestamp: new Date().toISOString(),
|
|
292
|
+
fromId: fromId ?? branchPointId,
|
|
293
|
+
summary,
|
|
294
|
+
};
|
|
295
|
+
appendFileSync(sessionFile, JSON.stringify(entry) + "\n", "utf8");
|
|
296
|
+
return id;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Copy the session file to destDir for parallel worker isolation.
|
|
300
|
+
* Returns the path of the copy.
|
|
301
|
+
*/
|
|
302
|
+
export function copySessionFile(sessionFile, destDir) {
|
|
303
|
+
const id = randomBytes(4).toString("hex");
|
|
304
|
+
const dest = join(destDir, `subagent-${id}.jsonl`);
|
|
305
|
+
copyFileSync(sessionFile, dest);
|
|
306
|
+
return dest;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Read new entries from sourceFile (after afterLine), append them to targetFile.
|
|
310
|
+
* Returns the appended entries.
|
|
311
|
+
*/
|
|
312
|
+
export function mergeNewEntries(sourceFile, targetFile, afterLine) {
|
|
313
|
+
const entries = getNewEntries(sourceFile, afterLine);
|
|
314
|
+
for (const entry of entries) {
|
|
315
|
+
appendFileSync(targetFile, JSON.stringify(entry) + "\n", "utf8");
|
|
316
|
+
}
|
|
317
|
+
return entries;
|
|
318
|
+
}
|