@fitchmultz/pi-subagents 0.50.0 → 0.52.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/CHANGELOG.md +63 -2
- package/README.md +36 -5
- package/dist/agent/child-runtime.js +80 -0
- package/dist/agent/contracts.js +15 -24
- package/dist/agent/host.js +3 -16
- package/dist/agent/index.js +212 -192
- package/dist/agent/journal-tail.js +115 -156
- package/dist/agent/launch.js +20 -8
- package/dist/agent/legacy.js +65 -23
- package/dist/agent/notifications.js +138 -47
- package/dist/agent/observation-state.js +246 -0
- package/dist/agent/observation-view.js +258 -0
- package/dist/agent/observation.js +350 -0
- package/dist/agent/observe-card.js +243 -0
- package/dist/agent/observe-chrome.js +85 -0
- package/dist/agent/observe-client.js +285 -0
- package/dist/agent/observe-layout.js +65 -0
- package/dist/agent/observe-reading.js +84 -0
- package/dist/agent/observe-screen.js +400 -96
- package/dist/agent/observe-transcript.js +382 -0
- package/dist/agent/observe.js +250 -43
- package/dist/agent/parking.js +253 -0
- package/dist/agent/process.js +228 -28
- package/dist/agent/receipts.js +92 -20
- package/dist/agent/runner.js +229 -126
- package/dist/agent/shell.js +2 -1
- package/dist/agent/storage.js +47 -12
- package/dist/agent/tool-schemas.js +20 -0
- package/dist/agent/typebox.js +10 -5
- package/dist/agent/usage.js +0 -115
- package/dist/agent/view.js +46 -16
- package/dist/agent/wake.js +99 -0
- package/dist/build-info.js +1 -1
- package/dist/intercom-client.d.ts +26 -0
- package/dist/intercom-client.js +45 -0
- package/dist/pi-intercom/agent-dir.d.ts +1 -0
- package/dist/pi-intercom/bridge-session.js +2 -1
- package/dist/pi-intercom/broker/broker.js +62 -43
- package/dist/pi-intercom/broker/client-last.d.ts +24 -0
- package/dist/pi-intercom/broker/client-last.js +42 -5
- package/dist/pi-intercom/broker/client-park.d.ts +9 -0
- package/dist/pi-intercom/broker/client-park.js +21 -0
- package/dist/pi-intercom/broker/client.d.ts +59 -0
- package/dist/pi-intercom/broker/client.js +55 -29
- package/dist/pi-intercom/broker/connection.d.ts +29 -0
- package/dist/pi-intercom/broker/framing.d.ts +17 -0
- package/dist/pi-intercom/broker/framing.js +10 -7
- package/dist/pi-intercom/broker/guard.js +23 -0
- package/dist/pi-intercom/broker/last.d.ts +30 -0
- package/dist/pi-intercom/broker/last.js +87 -40
- package/dist/pi-intercom/broker/parked.js +107 -0
- package/dist/pi-intercom/broker/paths.d.ts +13 -0
- package/dist/pi-intercom/broker/paths.js +35 -12
- package/dist/pi-intercom/broker/pid.js +5 -1
- package/dist/pi-intercom/broker/server-connection.js +20 -1
- package/dist/pi-intercom/broker/snapshot-reader.d.ts +9 -0
- package/dist/pi-intercom/broker/snapshots.d.ts +29 -0
- package/dist/pi-intercom/broker/targets.d.ts +9 -0
- package/dist/pi-intercom/connection.js +71 -20
- package/dist/pi-intercom/identity.js +6 -5
- package/dist/pi-intercom/inbound-delivery.js +16 -60
- package/dist/pi-intercom/inbound-journal.js +83 -6
- package/dist/pi-intercom/inbound-record.js +18 -1
- package/dist/pi-intercom/inbound.js +22 -37
- package/dist/pi-intercom/index.js +41 -0
- package/dist/pi-intercom/last-message.js +4 -16
- package/dist/pi-intercom/lifecycle.js +88 -3
- package/dist/pi-intercom/outbound.js +5 -15
- package/dist/pi-intercom/reply-tracker.js +7 -6
- package/dist/pi-intercom/reply-wait.js +2 -2
- package/dist/pi-intercom/session-targets.d.ts +24 -0
- package/dist/pi-intercom/tool-registration.js +4 -1
- package/dist/pi-intercom/tool-results.js +5 -7
- package/dist/pi-intercom/tool-schemas.js +12 -26
- package/dist/pi-intercom/types.d.ts +212 -0
- package/dist/pi-intercom/types.js +54 -3
- package/dist/pi-intercom/validation.d.ts +12 -0
- package/dist/pi-intercom/validation.js +3 -0
- package/dist/shared/bounded-json.js +151 -0
- package/dist/shared/compact-cards.js +66 -0
- package/dist/shared/journal-frames.js +3 -6
- package/dist/shared/json-projection.js +267 -129
- package/dist/shared/native-journal.js +123 -56
- package/dist/shared/parking.js +12 -0
- package/dist/shared/post-exit-stdio-guard.js +120 -7
- package/dist/shared/tui-action.js +29 -0
- package/dist/shared/unknown.d.ts +9 -0
- package/docs/code-quality.md +5 -3
- package/docs/development.md +42 -3
- package/docs/intercom-bridge.md +9 -4
- package/docs/intercom.md +228 -7
- package/docs/performance.md +118 -14
- package/docs/reference.md +168 -20
- package/docs/retired-history-maintenance.md +252 -0
- package/package.json +28 -15
- package/scripts/archive-retired-history.mjs +464 -0
- package/scripts/build.mjs +48 -28
- package/scripts/measure-size.mjs +1 -1
- package/scripts/native-package-smoke.mjs +153 -12
- package/scripts/package-smoke.mjs +80 -11
- package/scripts/prepare.mjs +2 -2
- package/scripts/real-pi-live.mjs +377 -0
- package/scripts/real-pi-processes.mjs +6 -1
- package/scripts/real-pi-smoke.mjs +6 -282
- package/scripts/run-tests.mjs +4 -2
- package/scripts/typescript.mjs +35 -0
- package/skills/pi-intercom/SKILL.md +2 -1
- package/skills/pi-subagents/SKILL.md +23 -5
- package/src/agent/child-runtime.ts +105 -0
- package/src/agent/contracts.ts +18 -34
- package/src/agent/host.ts +39 -20
- package/src/agent/index.ts +234 -207
- package/src/agent/journal-tail.ts +141 -153
- package/src/agent/launch.ts +33 -9
- package/src/agent/legacy.ts +85 -31
- package/src/agent/notifications.ts +192 -67
- package/src/agent/observation-state.ts +277 -0
- package/src/agent/observation-view.ts +267 -0
- package/src/agent/observation.ts +409 -0
- package/src/agent/observe-card.ts +304 -0
- package/src/agent/observe-chrome.ts +170 -0
- package/src/agent/observe-client.ts +318 -0
- package/src/agent/observe-layout.ts +79 -0
- package/src/agent/observe-reading.ts +97 -0
- package/src/agent/observe-screen.ts +443 -129
- package/src/agent/observe-transcript.ts +434 -0
- package/src/agent/observe.ts +304 -71
- package/src/agent/parking.ts +341 -0
- package/src/agent/process.ts +280 -37
- package/src/agent/receipts.ts +114 -24
- package/src/agent/runner.ts +227 -149
- package/src/agent/shell.ts +2 -1
- package/src/agent/storage.ts +44 -11
- package/src/agent/tool-schemas.ts +33 -0
- package/src/agent/typebox.ts +12 -7
- package/src/agent/usage.ts +0 -130
- package/src/agent/view.ts +70 -20
- package/src/agent/wake.ts +107 -0
- package/src/intercom-client.ts +108 -0
- package/src/pi-intercom/bridge-session.ts +2 -2
- package/src/pi-intercom/broker/broker.ts +64 -53
- package/src/pi-intercom/broker/client-last.ts +65 -12
- package/src/pi-intercom/broker/client-park.ts +23 -0
- package/src/pi-intercom/broker/client.ts +63 -45
- package/src/pi-intercom/broker/framing.ts +11 -8
- package/src/pi-intercom/broker/guard.ts +33 -0
- package/src/pi-intercom/broker/last.ts +131 -43
- package/src/pi-intercom/broker/parked.ts +130 -0
- package/src/pi-intercom/broker/paths.ts +39 -12
- package/src/pi-intercom/broker/pid.ts +6 -1
- package/src/pi-intercom/broker/server-connection.ts +27 -2
- package/src/pi-intercom/connection.ts +103 -39
- package/src/pi-intercom/identity.ts +8 -5
- package/src/pi-intercom/inbound-delivery.ts +20 -66
- package/src/pi-intercom/inbound-journal.ts +103 -7
- package/src/pi-intercom/inbound-record.ts +21 -4
- package/src/pi-intercom/inbound.ts +32 -49
- package/src/pi-intercom/index.ts +44 -0
- package/src/pi-intercom/last-message.ts +11 -20
- package/src/pi-intercom/lifecycle.ts +117 -14
- package/src/pi-intercom/outbound.ts +7 -16
- package/src/pi-intercom/reply-tracker.ts +8 -6
- package/src/pi-intercom/reply-wait.ts +3 -2
- package/src/pi-intercom/runtime-types.ts +0 -1
- package/src/pi-intercom/tool-registration.ts +4 -1
- package/src/pi-intercom/tool-results.ts +10 -9
- package/src/pi-intercom/tool-schemas.ts +12 -44
- package/src/pi-intercom/types.ts +115 -4
- package/src/pi-intercom/validation.ts +4 -0
- package/src/shared/bounded-json.ts +162 -0
- package/src/shared/compact-cards.ts +118 -0
- package/src/shared/journal-frames.ts +3 -19
- package/src/shared/json-projection.ts +267 -147
- package/src/shared/native-journal.ts +157 -75
- package/src/shared/parking.ts +22 -0
- package/src/shared/post-exit-stdio-guard.ts +165 -7
- package/src/shared/tui-action.ts +39 -0
- package/tsconfig.client.json +8 -0
- package/tsconfig.json +1 -0
- package/src/shared/stream-json.d.ts +0 -10
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
// Live model smoke for scripts/real-pi-smoke.mjs: persistent native RPC mains, launch
|
|
2
|
+
// receipts, completion notifications, and saved child evidence. Credentials stay isolated.
|
|
3
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, isAbsolute, join, relative } from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
7
|
+
import { isChildTreeAlive } from "../src/shared/post-exit-stdio-guard.ts";
|
|
8
|
+
import { filesIn, processRef } from "./real-pi-processes.mjs";
|
|
9
|
+
|
|
10
|
+
const authAgentDir =
|
|
11
|
+
process.env.PI_REAL_SMOKE_AUTH_AGENT_DIR ??
|
|
12
|
+
(process.env.HOME ? join(process.env.HOME, ".pi", "agent") : undefined);
|
|
13
|
+
|
|
14
|
+
function copyLiveAuth(agentDir) {
|
|
15
|
+
if (!authAgentDir || !existsSync(authAgentDir)) {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
mkdirSync(agentDir, { recursive: true });
|
|
19
|
+
const copied = [];
|
|
20
|
+
for (const filename of ["auth.json", "models.json"]) {
|
|
21
|
+
const source = join(authAgentDir, filename);
|
|
22
|
+
if (!existsSync(source)) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
copyFileSync(source, join(agentDir, filename));
|
|
26
|
+
copied.push(filename);
|
|
27
|
+
}
|
|
28
|
+
return copied;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function modelArgs() {
|
|
32
|
+
const args = [];
|
|
33
|
+
if (process.env.PI_REAL_SMOKE_PROVIDER) {
|
|
34
|
+
args.push("--provider", process.env.PI_REAL_SMOKE_PROVIDER);
|
|
35
|
+
}
|
|
36
|
+
if (process.env.PI_REAL_SMOKE_MODEL) {
|
|
37
|
+
args.push(
|
|
38
|
+
"--model",
|
|
39
|
+
process.env.PI_REAL_SMOKE_MODEL,
|
|
40
|
+
"--models",
|
|
41
|
+
process.env.PI_REAL_SMOKE_MODEL,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return args;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function addMessage(view, message, index) {
|
|
48
|
+
if (message?.customType === "agent-completion") {
|
|
49
|
+
view.notices.set(message.details?.id, { ...message.details, index });
|
|
50
|
+
} else if (message?.role === "assistant") {
|
|
51
|
+
const text = message.content.filter((part) => part.type === "text");
|
|
52
|
+
view.replies.push({
|
|
53
|
+
index,
|
|
54
|
+
text: text.map((part) => part.text).join("\n"),
|
|
55
|
+
stopReason: message.stopReason,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function sessionView(events) {
|
|
61
|
+
const view = { events, receipts: [], notices: new Map(), replies: [] };
|
|
62
|
+
for (const [index, event] of events.entries()) {
|
|
63
|
+
const details = event.result?.details;
|
|
64
|
+
if (event.type === "message_end") {
|
|
65
|
+
addMessage(view, event.message, index);
|
|
66
|
+
} else if (
|
|
67
|
+
event.type === "tool_execution_end" &&
|
|
68
|
+
["launch", "continuation"].includes(details?.receipt)
|
|
69
|
+
) {
|
|
70
|
+
view.receipts.push({ ...details, index, tool: event.toolName });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return view;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Persistent RPC main: launch/continuation receipts are not completion, so the main stays
|
|
77
|
+
// alive until every receipt's native completion notification has resumed and settled it.
|
|
78
|
+
function rpcSession(child, prompt) {
|
|
79
|
+
const events = [];
|
|
80
|
+
let buffer = "";
|
|
81
|
+
let settle;
|
|
82
|
+
const quiescent = new Promise((resolve, reject) => {
|
|
83
|
+
settle = { resolve, reject };
|
|
84
|
+
});
|
|
85
|
+
child.stdout.on("data", (chunk) => {
|
|
86
|
+
buffer += chunk;
|
|
87
|
+
for (let end = buffer.indexOf("\n"); end >= 0; end = buffer.indexOf("\n")) {
|
|
88
|
+
const line = buffer.slice(0, end).replace(/\r$/, "");
|
|
89
|
+
buffer = buffer.slice(end + 1);
|
|
90
|
+
let record;
|
|
91
|
+
try {
|
|
92
|
+
record = JSON.parse(line);
|
|
93
|
+
} catch {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
events.push(record);
|
|
97
|
+
if (record.type === "response" && record.id === "smoke" && record.success !== true) {
|
|
98
|
+
settle.reject(new Error(`RPC prompt was rejected: ${record.error}`));
|
|
99
|
+
}
|
|
100
|
+
if (record.type === "agent_settled") {
|
|
101
|
+
if (record.aborted) {
|
|
102
|
+
settle.reject(new Error("RPC main was aborted before the live smoke completed."));
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const view = sessionView(events);
|
|
106
|
+
if (view.receipts.every(({ id }) => view.notices.has(id))) {
|
|
107
|
+
settle.resolve(view);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
child.stdin.write(`${JSON.stringify({ id: "smoke", type: "prompt", message: prompt })}\n`);
|
|
113
|
+
return { events, quiescent, close: () => child.stdin.end() };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function runLiveSession(options, { label, prompt, tool, verify }) {
|
|
117
|
+
let session;
|
|
118
|
+
let view;
|
|
119
|
+
const args = ["--mode", "rpc", "--session-dir", join(options.root, "sessions"), "--approve"];
|
|
120
|
+
const exited = options.processes.run(label, "pi", [...args, ...modelArgs()], (child) => {
|
|
121
|
+
session = rpcSession(child, prompt);
|
|
122
|
+
});
|
|
123
|
+
try {
|
|
124
|
+
view = await Promise.race([
|
|
125
|
+
session.quiescent,
|
|
126
|
+
exited.then((output) => {
|
|
127
|
+
throw new Error(`${label} main exited before settling every receipt:\n${output}`);
|
|
128
|
+
}),
|
|
129
|
+
]);
|
|
130
|
+
if (
|
|
131
|
+
!view.events.some((event) => event.type === "tool_execution_start" && event.toolName === tool)
|
|
132
|
+
) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`${label} never invoked ${tool}; a matching final sentence is not execution evidence.`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
const errors = view.events.filter(
|
|
138
|
+
(event) => event.type === "tool_execution_end" && event.isError,
|
|
139
|
+
);
|
|
140
|
+
if (errors.length) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
`${label} had ${errors.length} failed tool call(s); inspect its saved wire log.`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
// Verify while the main is alive; only then request its orderly shutdown.
|
|
146
|
+
await verify(view);
|
|
147
|
+
} finally {
|
|
148
|
+
session?.close();
|
|
149
|
+
writeFileSync(
|
|
150
|
+
join(options.root, `${label.replace(/ /g, "-")}.jsonl`),
|
|
151
|
+
session ? session.events.map((event) => JSON.stringify(event)).join("\n") : "",
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
await exited;
|
|
155
|
+
return view;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function* savedAssistantModels(file) {
|
|
159
|
+
for (const line of readFileSync(file, "utf8").split("\n")) {
|
|
160
|
+
if (!line.trim()) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const entry = JSON.parse(line);
|
|
164
|
+
if (entry.type === "message" && entry.message?.role === "assistant") {
|
|
165
|
+
yield `${entry.message.provider}/${entry.message.model}`;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function auditSavedModels(directories, expectedModel) {
|
|
171
|
+
const models = new Set();
|
|
172
|
+
let responses = 0;
|
|
173
|
+
for (const file of directories.flatMap((dir) => [...filesIn(dir)])) {
|
|
174
|
+
if (!file.endsWith(".jsonl")) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
for (const model of savedAssistantModels(file)) {
|
|
178
|
+
responses++;
|
|
179
|
+
models.add(model);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (!responses) {
|
|
183
|
+
throw new Error("No saved assistant messages found for model audit.");
|
|
184
|
+
}
|
|
185
|
+
if (expectedModel?.includes("/") && [...models].some((model) => model !== expectedModel)) {
|
|
186
|
+
throw new Error(`Unexpected model in saved parent/child sessions: ${[...models].join(", ")}`);
|
|
187
|
+
}
|
|
188
|
+
console.log(
|
|
189
|
+
`[real-pi-smoke] audited ${responses} saved parent/child responses: ${[...models].join(", ")}`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function requireOutput(label, output, pattern) {
|
|
194
|
+
if (!pattern.test(output)) {
|
|
195
|
+
throw new Error(`${label} did not include expected evidence ${pattern}.\nOutput:\n${output}`);
|
|
196
|
+
}
|
|
197
|
+
const compact = output.trim().split(/\r?\n/).slice(-8).join("\n");
|
|
198
|
+
console.log(`[real-pi-smoke] ${label} output evidence:\n${compact}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function asyncCompletion(dir, runId) {
|
|
202
|
+
const resultPath = join(dir, "result.json");
|
|
203
|
+
if (!existsSync(resultPath)) {
|
|
204
|
+
return { status: "missing" };
|
|
205
|
+
}
|
|
206
|
+
const result = JSON.parse(readFileSync(resultPath, "utf8"));
|
|
207
|
+
if (result.id !== runId) {
|
|
208
|
+
throw new Error("Async completion belongs to another saved run");
|
|
209
|
+
}
|
|
210
|
+
if (result.status !== "completed") {
|
|
211
|
+
throw new Error(`async run ${runId} ended ${result.status}: ${result.error ?? ""}`);
|
|
212
|
+
}
|
|
213
|
+
const processPath = join(dir, "actor.lock", "process.json");
|
|
214
|
+
if (!existsSync(processPath)) {
|
|
215
|
+
return { status: "completed; actor exit unconfirmed" };
|
|
216
|
+
}
|
|
217
|
+
const actor = JSON.parse(readFileSync(processPath, "utf8"));
|
|
218
|
+
return {
|
|
219
|
+
status: result.status,
|
|
220
|
+
output: isChildTreeAlive(processRef(actor.pid))
|
|
221
|
+
? undefined
|
|
222
|
+
: readFileSync(join(dir, "output.txt"), "utf8"),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function waitForAsyncCompletion(run, pattern, { env, timeoutMs, signal }) {
|
|
227
|
+
const dir = dirname(run.sessionFile);
|
|
228
|
+
const within = relative(join(env.PI_CODING_AGENT_DIR, "sessions", "agent-runs"), dir);
|
|
229
|
+
if (isAbsolute(within) || within.startsWith("..") || !/^[a-zA-Z0-9_-]{1,128}$/.test(run.id)) {
|
|
230
|
+
throw new Error("Async receipt did not identify this smoke's private saved run");
|
|
231
|
+
}
|
|
232
|
+
const deadline = Date.now() + timeoutMs;
|
|
233
|
+
let lastState = "missing";
|
|
234
|
+
while (Date.now() < deadline) {
|
|
235
|
+
signal.throwIfAborted();
|
|
236
|
+
const completion = asyncCompletion(dir, run.id);
|
|
237
|
+
lastState = completion.status;
|
|
238
|
+
if (completion.output !== undefined) {
|
|
239
|
+
requireOutput(`async run ${run.id} completed and actor exited`, completion.output, pattern);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
// Result publication is not process exit; retain resources until final actor writes finish.
|
|
243
|
+
// oxlint-disable-next-line no-await-in-loop
|
|
244
|
+
await delay(100, undefined, { signal });
|
|
245
|
+
}
|
|
246
|
+
throw new Error(
|
|
247
|
+
`async run ${run.id} did not complete and exit after ${timeoutMs}ms (last state: ${lastState}).`,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export async function runLiveSmoke(options, runOptions, agentDir) {
|
|
252
|
+
const copiedAuthFiles = copyLiveAuth(agentDir);
|
|
253
|
+
if (copiedAuthFiles.length > 0) {
|
|
254
|
+
console.log(
|
|
255
|
+
`[real-pi-smoke] copied ${copiedAuthFiles.join(" and ")} into isolated Pi agent dir for live provider auth`,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
const childModelInstruction = process.env.PI_REAL_SMOKE_MODEL
|
|
259
|
+
? ` Pass model '${process.env.PI_REAL_SMOKE_MODEL}' to every delegate call.`
|
|
260
|
+
: "";
|
|
261
|
+
await runBasicLiveSmoke(runOptions, childModelInstruction);
|
|
262
|
+
if (options.llmFull) {
|
|
263
|
+
await runFullLiveSmoke(runOptions, childModelInstruction);
|
|
264
|
+
}
|
|
265
|
+
auditSavedModels(
|
|
266
|
+
[join(runOptions.root, "sessions"), join(agentDir, "sessions", "agent-runs")],
|
|
267
|
+
process.env.PI_REAL_SMOKE_MODEL,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function requireFinalReply(label, view, pattern) {
|
|
272
|
+
const last = view.replies.at(-1);
|
|
273
|
+
const notified = Math.max(-1, ...[...view.notices.values()].map(({ index }) => index));
|
|
274
|
+
if (last === undefined || last.stopReason !== "stop" || last.index < notified) {
|
|
275
|
+
throw new Error(
|
|
276
|
+
`${label} did not finish a successful reply after its final completion notification.`,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
requireOutput(label, last.text, pattern);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function requireCompletion(view, receipt, expected) {
|
|
283
|
+
const notice = view.notices.get(receipt.id);
|
|
284
|
+
if (notice?.result?.status !== "completed" || notice.page?.text?.trim() !== expected) {
|
|
285
|
+
throw new Error(`Run ${receipt.id} completion notification did not report: ${expected}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function runBasicLiveSmoke(runOptions, childModelInstruction) {
|
|
290
|
+
const prompts = [
|
|
291
|
+
[
|
|
292
|
+
"intercom",
|
|
293
|
+
"Call the intercom tool with action status. Reply exactly 'real-pi-smoke intercom ok' only if it reports 'Connected: Yes'.",
|
|
294
|
+
"intercom",
|
|
295
|
+
],
|
|
296
|
+
[
|
|
297
|
+
"list",
|
|
298
|
+
"Call agent_runs with action list. Reply exactly 'real-pi-smoke list ok' only if it successfully returns a saved-run list. There are no profiles to discover.",
|
|
299
|
+
"agent_runs",
|
|
300
|
+
],
|
|
301
|
+
];
|
|
302
|
+
for (const [kind, prompt, tool] of prompts) {
|
|
303
|
+
const label = `real Pi ${kind} prompt`;
|
|
304
|
+
// Keep separate native parent sessions so each smoke owns its saved runs.
|
|
305
|
+
// oxlint-disable-next-line no-await-in-loop
|
|
306
|
+
await runLiveSession(runOptions, {
|
|
307
|
+
label,
|
|
308
|
+
prompt,
|
|
309
|
+
tool,
|
|
310
|
+
verify: (view) => requireFinalReply(label, view, new RegExp(`real-pi-smoke ${kind} ok`)),
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
const expected = "real-pi-smoke delegate ok";
|
|
314
|
+
const label = "real Pi delegate prompt";
|
|
315
|
+
const { receipts } = await runLiveSession(runOptions, {
|
|
316
|
+
label,
|
|
317
|
+
prompt: `Call delegate once with prompt 'Reply exactly: ${expected}' and name 'smoke-delegate'.${childModelInstruction} It returns only a launch receipt, not completion; never wait, poll, or call agent_runs. When that run's completion notification arrives, reply exactly '${expected}' only if it reports completed with that output.`,
|
|
318
|
+
tool: "delegate",
|
|
319
|
+
verify: (view) => {
|
|
320
|
+
const [receipt] = view.receipts;
|
|
321
|
+
if (
|
|
322
|
+
view.receipts.length !== 1 ||
|
|
323
|
+
receipt.receipt !== "launch" ||
|
|
324
|
+
receipt.name !== "smoke-delegate" ||
|
|
325
|
+
!receipt.id ||
|
|
326
|
+
!receipt.sessionFile
|
|
327
|
+
) {
|
|
328
|
+
throw new Error("Native delegate did not return one named saved launch receipt");
|
|
329
|
+
}
|
|
330
|
+
requireCompletion(view, receipt, expected);
|
|
331
|
+
requireFinalReply(label, view, new RegExp(expected));
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
// A completed actor keeps serving its attached main; closing that main must retire it.
|
|
335
|
+
await waitForAsyncCompletion(receipts[0], new RegExp(`^${expected}$`, "m"), runOptions);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function requireForkTopology(view) {
|
|
339
|
+
const kinds = view.receipts.map(({ tool, receipt, context }) => `${tool}:${receipt}:${context}`);
|
|
340
|
+
const expected = [
|
|
341
|
+
"agent_runs:continuation:fresh",
|
|
342
|
+
"delegate:launch:fork",
|
|
343
|
+
"delegate:launch:fresh",
|
|
344
|
+
];
|
|
345
|
+
const [first] = view.receipts;
|
|
346
|
+
const forked = view.receipts.find(({ context }) => context === "fork");
|
|
347
|
+
const continued = view.receipts.find(({ tool }) => tool === "agent_runs");
|
|
348
|
+
if (
|
|
349
|
+
kinds.sort().join() !== expected.join() ||
|
|
350
|
+
first?.context !== "fresh" ||
|
|
351
|
+
!(forked.index > view.notices.get(first.id).index) ||
|
|
352
|
+
continued.predecessor !== first.id ||
|
|
353
|
+
continued.sessionFile !== first.sessionFile
|
|
354
|
+
) {
|
|
355
|
+
throw new Error(
|
|
356
|
+
"Fork/continuation smoke did not fork after the first completion and continue its saved conversation",
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function runFullLiveSmoke(runOptions, childModelInstruction) {
|
|
362
|
+
const fact = "CALIBRATION_FACT=packed-otter-7319";
|
|
363
|
+
const prompt = `First call delegate with prompt 'Reply exactly ${fact}', name 'fact', and context fresh.${childModelInstruction} Delegate and agent_runs return receipts only; never wait or poll, and end your turn after each receipt. When the fact run's completion notification arrives, call delegate with context fork and prompt 'Read the inherited conversation. Without tools, filesystem reads, or guessing, return the CALIBRATION_FACT line from the preceding agent completion notification exactly; return FACT_NOT_VISIBLE if absent.' and call agent_runs with action continue, the fact run id, and prompt 'Without tools, repeat the CALIBRATION_FACT line from your own saved conversation exactly.' After both of those runs' completion notifications arrive, reply exactly 'real-pi-smoke fork ok' only if both report completed with ${fact}. There are no profiles, task prefixes, output settings, or canned role instructions.`;
|
|
364
|
+
const label = "real Pi fork prompt";
|
|
365
|
+
await runLiveSession(runOptions, {
|
|
366
|
+
label,
|
|
367
|
+
prompt,
|
|
368
|
+
tool: "delegate",
|
|
369
|
+
verify: (view) => {
|
|
370
|
+
requireForkTopology(view);
|
|
371
|
+
for (const receipt of view.receipts) {
|
|
372
|
+
requireCompletion(view, receipt, fact);
|
|
373
|
+
}
|
|
374
|
+
requireFinalReply(label, view, /real-pi-smoke fork ok/);
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
}
|
|
@@ -123,7 +123,12 @@ export class SmokeProcesses {
|
|
|
123
123
|
child.stdin.on("error", () => {
|
|
124
124
|
// A command may exit without reading RPC input; execFile reports its exit failure.
|
|
125
125
|
});
|
|
126
|
-
|
|
126
|
+
// A function input owns a persistent RPC stdin and closes it to request orderly shutdown.
|
|
127
|
+
if (typeof input === "function") {
|
|
128
|
+
input(child);
|
|
129
|
+
} else {
|
|
130
|
+
child.stdin.end(input);
|
|
131
|
+
}
|
|
127
132
|
});
|
|
128
133
|
}
|
|
129
134
|
|