@llblab/pi-kit 0.1.5 → 0.1.7
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 +9 -0
- package/README.md +3 -3
- package/node_modules/@llblab/pi-actors/BACKLOG.md +1 -1
- package/node_modules/@llblab/pi-actors/CHANGELOG.md +14 -1
- package/node_modules/@llblab/pi-actors/README.md +19 -3
- package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.js +29 -10
- package/node_modules/@llblab/pi-actors/dist/lib/inspector-overlay.d.ts +3 -0
- package/node_modules/@llblab/pi-actors/dist/lib/inspector-overlay.js +120 -38
- package/node_modules/@llblab/pi-actors/dist/lib/limits.d.ts +1 -0
- package/node_modules/@llblab/pi-actors/dist/lib/limits.js +1 -0
- package/node_modules/@llblab/pi-actors/dist/lib/observability.js +6 -1
- package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.d.ts +10 -3
- package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.js +109 -52
- package/node_modules/@llblab/pi-actors/dist/lib/runs-artifacts.js +24 -4
- package/node_modules/@llblab/pi-actors/dist/lib/runtime.d.ts +2 -0
- package/node_modules/@llblab/pi-actors/dist/lib/runtime.js +24 -4
- package/node_modules/@llblab/pi-actors/dist/lib/session-evidence.d.ts +1 -0
- package/node_modules/@llblab/pi-actors/dist/lib/session-evidence.js +3 -2
- package/node_modules/@llblab/pi-actors/dist/lib/state-readers.d.ts +5 -1
- package/node_modules/@llblab/pi-actors/dist/lib/state-readers.js +30 -3
- package/node_modules/@llblab/pi-actors/dist/lib/tools-access.d.ts +1 -0
- package/node_modules/@llblab/pi-actors/dist/lib/tools-access.js +13 -11
- package/node_modules/@llblab/pi-actors/dist/lib/tools-inspect.js +5 -5
- package/node_modules/@llblab/pi-actors/dist/lib/tools-message.d.ts +1 -0
- package/node_modules/@llblab/pi-actors/dist/lib/tools-message.js +3 -1
- package/node_modules/@llblab/pi-actors/docs/README.md +1 -0
- package/node_modules/@llblab/pi-actors/docs/actor-inspector.md +21 -5
- package/node_modules/@llblab/pi-actors/docs/async-runs.md +16 -4
- package/node_modules/@llblab/pi-actors/docs/command-templates.md +5 -4
- package/node_modules/@llblab/pi-actors/docs/inspection.md +83 -0
- package/node_modules/@llblab/pi-actors/docs/recipe-library.md +1 -1
- package/node_modules/@llblab/pi-actors/docs/template-recipes.md +225 -66
- package/node_modules/@llblab/pi-actors/docs/tool-registry.md +24 -3
- package/node_modules/@llblab/pi-actors/lib/extension-runtime.ts +26 -10
- package/node_modules/@llblab/pi-actors/lib/inspector-overlay.ts +161 -33
- package/node_modules/@llblab/pi-actors/lib/limits.ts +1 -0
- package/node_modules/@llblab/pi-actors/lib/observability.ts +5 -1
- package/node_modules/@llblab/pi-actors/lib/run-ui-runtime.ts +128 -50
- package/node_modules/@llblab/pi-actors/lib/runs-artifacts.ts +34 -4
- package/node_modules/@llblab/pi-actors/lib/runtime.ts +26 -7
- package/node_modules/@llblab/pi-actors/lib/session-evidence.ts +7 -2
- package/node_modules/@llblab/pi-actors/lib/state-readers.ts +45 -3
- package/node_modules/@llblab/pi-actors/lib/tools-access.ts +26 -12
- package/node_modules/@llblab/pi-actors/lib/tools-inspect.ts +9 -5
- package/node_modules/@llblab/pi-actors/lib/tools-message.ts +8 -1
- package/node_modules/@llblab/pi-actors/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/AGENTS.md +1 -1
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +17 -0
- package/node_modules/@llblab/pi-telegram/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +5 -5
- package/node_modules/@llblab/pi-telegram/index.ts +25 -0
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +11 -0
- package/node_modules/@llblab/pi-telegram/lib/journal.ts +192 -9
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +14 -0
- package/node_modules/@llblab/pi-telegram/lib/runtime.ts +52 -2
- package/node_modules/@llblab/pi-telegram/lib/status.ts +3 -3
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +5 -1
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/SKILL.md +14 -6
- package/package.json +4 -4
|
@@ -8,22 +8,78 @@ import * as Observability from "./observability.js";
|
|
|
8
8
|
import * as Paths from "./paths.js";
|
|
9
9
|
import * as Pi from "./pi.js";
|
|
10
10
|
export function createRunUiRuntime(deps) {
|
|
11
|
+
let activeContext;
|
|
12
|
+
let activeOwnerId;
|
|
11
13
|
let animationInterval;
|
|
12
14
|
let notifyTimeout;
|
|
15
|
+
let running = false;
|
|
13
16
|
let lastWatcherDiagnosticId = 0;
|
|
14
17
|
const observation = Observability.createRunUiObservationState();
|
|
15
18
|
const retirementAttempts = new Set();
|
|
16
19
|
const terminalNotificationsInFlight = new Set();
|
|
20
|
+
const close = () => {
|
|
21
|
+
running = false;
|
|
22
|
+
activeContext = undefined;
|
|
23
|
+
activeOwnerId = undefined;
|
|
24
|
+
try {
|
|
25
|
+
watcher.close();
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
/* cleanup must not escape a host callback */
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
reconciliation.close();
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
/* cleanup must not escape a host callback */
|
|
35
|
+
}
|
|
36
|
+
if (notifyTimeout)
|
|
37
|
+
clearTimeout(notifyTimeout);
|
|
38
|
+
notifyTimeout = undefined;
|
|
39
|
+
if (animationInterval)
|
|
40
|
+
clearInterval(animationInterval);
|
|
41
|
+
animationInterval = undefined;
|
|
42
|
+
};
|
|
43
|
+
const stopAfterCallbackFailure = (label, error, expectedContext) => {
|
|
44
|
+
if (activeContext !== expectedContext)
|
|
45
|
+
return;
|
|
46
|
+
close();
|
|
47
|
+
try {
|
|
48
|
+
deps.onCallbackError?.(error);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
/* host callback containment must remain no-throw */
|
|
52
|
+
}
|
|
53
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
54
|
+
try {
|
|
55
|
+
expectedContext.ui.notify(`Actor ${label} failed: ${message}`, "error");
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
/* stale context or unavailable UI */
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const runActiveCallback = (label, callback) => {
|
|
62
|
+
if (!running || !activeContext || !activeOwnerId)
|
|
63
|
+
return;
|
|
64
|
+
const ctx = activeContext;
|
|
65
|
+
try {
|
|
66
|
+
if (deps.getActiveContext() !== ctx)
|
|
67
|
+
return;
|
|
68
|
+
callback(ctx, activeOwnerId);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
stopAfterCallbackFailure(label, error, ctx);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
17
74
|
const retireCandidateRuns = (ctx, summary) => {
|
|
18
75
|
void Observability.executeRunRetirements(summary, {
|
|
19
76
|
attempted: retirementAttempts,
|
|
20
77
|
cancelRun: (candidate) => AsyncRuns.cancelRun(candidate.stateDir),
|
|
21
78
|
notify: (message, level) => ctx.ui.notify(message, level),
|
|
22
79
|
sendStop: async (candidate) => AsyncRuns.cancelRun(candidate.stateDir),
|
|
23
|
-
});
|
|
80
|
+
}).catch((error) => stopAfterCallbackFailure("Run retirement callback", error, ctx));
|
|
24
81
|
};
|
|
25
|
-
const update = (ctx, notify = false, terminalOnly = false) => {
|
|
26
|
-
const ownerId = deps.getRunOwnerId(ctx);
|
|
82
|
+
const update = (ctx, ownerId, notify = false, terminalOnly = false) => {
|
|
27
83
|
const snapshot = Observability.readRunUiSnapshot(observation, ownerId);
|
|
28
84
|
ctx.ui.setStatus("zz-pi-actors-runs", snapshot.status ? ctx.ui.theme.fg("dim", snapshot.status) : undefined);
|
|
29
85
|
if (!notify)
|
|
@@ -45,62 +101,55 @@ export function createRunUiRuntime(deps) {
|
|
|
45
101
|
}
|
|
46
102
|
};
|
|
47
103
|
const scheduleUpdate = () => {
|
|
104
|
+
if (!running)
|
|
105
|
+
return;
|
|
48
106
|
if (notifyTimeout)
|
|
49
107
|
clearTimeout(notifyTimeout);
|
|
50
108
|
notifyTimeout = setTimeout(() => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}, 50);
|
|
109
|
+
runActiveCallback("Run watcher callback", (ctx, ownerId) => {
|
|
110
|
+
watcher.refresh();
|
|
111
|
+
update(ctx, ownerId, true);
|
|
112
|
+
deps.onRunEvent();
|
|
113
|
+
reportDiagnostics(ctx);
|
|
114
|
+
});
|
|
115
|
+
}, deps.notificationDelayMs ?? 50);
|
|
59
116
|
notifyTimeout.unref?.();
|
|
60
117
|
};
|
|
61
|
-
const watcher = Observability.createRunStateWatcher({
|
|
118
|
+
const watcher = (deps.createRunStateWatcher ?? Observability.createRunStateWatcher)({
|
|
62
119
|
stateRoot: Paths.EXTENSION_RUNTIME_PATHS.runStateRoot,
|
|
63
120
|
onChange: scheduleUpdate,
|
|
64
121
|
});
|
|
65
|
-
const reconciliation =
|
|
122
|
+
const reconciliation = (deps.createRunTerminalReconciliationLoop ??
|
|
123
|
+
Observability.createRunTerminalReconciliationLoop)({
|
|
66
124
|
onError: (error) => {
|
|
67
|
-
|
|
68
|
-
if (!ctx)
|
|
125
|
+
if (!running || !activeContext)
|
|
69
126
|
return;
|
|
70
|
-
|
|
71
|
-
ctx.ui.notify(`Actor terminal reconciliation failed: ${message}`, "error");
|
|
127
|
+
stopAfterCallbackFailure("terminal reconciliation callback", error, activeContext);
|
|
72
128
|
},
|
|
73
129
|
reconcile: () => {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
130
|
+
runActiveCallback("terminal reconciliation callback", (ctx, ownerId) => {
|
|
131
|
+
Observability.reconcileRunTerminalNotifications({
|
|
132
|
+
inFlight: terminalNotificationsInFlight,
|
|
133
|
+
ownerId,
|
|
134
|
+
sink: Pi.createNotificationSink(deps.pi, ctx),
|
|
135
|
+
state: observation,
|
|
136
|
+
includeAttention: true,
|
|
137
|
+
});
|
|
138
|
+
reportDiagnostics(ctx);
|
|
83
139
|
});
|
|
84
|
-
reportDiagnostics(ctx);
|
|
85
140
|
},
|
|
86
|
-
refreshWatcher: () =>
|
|
141
|
+
refreshWatcher: () => {
|
|
142
|
+
if (running)
|
|
143
|
+
watcher.refresh();
|
|
144
|
+
},
|
|
87
145
|
});
|
|
88
|
-
const close = () => {
|
|
89
|
-
watcher.close();
|
|
90
|
-
reconciliation.close();
|
|
91
|
-
if (notifyTimeout)
|
|
92
|
-
clearTimeout(notifyTimeout);
|
|
93
|
-
notifyTimeout = undefined;
|
|
94
|
-
if (animationInterval)
|
|
95
|
-
clearInterval(animationInterval);
|
|
96
|
-
animationInterval = undefined;
|
|
97
|
-
};
|
|
98
146
|
return {
|
|
99
147
|
close,
|
|
100
|
-
shutdown(eventReason, ctx) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
148
|
+
shutdown(eventReason, ownerId, ctx) {
|
|
149
|
+
if (!ownerId)
|
|
150
|
+
return;
|
|
151
|
+
const teardown = (deps.teardownRunsOwnedByParent ?? AsyncRuns.teardownRunsOwnedByParent)(ownerId, Paths.EXTENSION_RUNTIME_PATHS.runStateRoot, { trigger: `session_shutdown:${eventReason}` });
|
|
152
|
+
if (teardown.failed === 0 || !ctx)
|
|
104
153
|
return;
|
|
105
154
|
try {
|
|
106
155
|
ctx.ui.notify(`Actor shutdown teardown: killed=${teardown.killed} failed=${teardown.failed} skipped=${teardown.skipped} discovery_failed=${teardown.discoveryFailed}. Summary: ${teardown.summaryPath ?? "unavailable"}.`, "warning");
|
|
@@ -109,17 +158,25 @@ export function createRunUiRuntime(deps) {
|
|
|
109
158
|
/* stale shutdown context */
|
|
110
159
|
}
|
|
111
160
|
},
|
|
112
|
-
start(ctx) {
|
|
161
|
+
start(ctx, ownerId) {
|
|
113
162
|
close();
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
163
|
+
activeContext = ctx;
|
|
164
|
+
activeOwnerId = ownerId;
|
|
165
|
+
running = true;
|
|
166
|
+
try {
|
|
167
|
+
Observability.primeRunAttentionState(observation, ownerId);
|
|
168
|
+
update(ctx, ownerId, true, true);
|
|
169
|
+
watcher.refresh();
|
|
170
|
+
reconciliation.start();
|
|
171
|
+
animationInterval = setInterval(() => {
|
|
172
|
+
runActiveCallback("status animation callback", (current, currentOwnerId) => update(current, currentOwnerId));
|
|
173
|
+
}, deps.animationIntervalMs ?? 1000);
|
|
174
|
+
animationInterval.unref?.();
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
close();
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
123
180
|
},
|
|
124
181
|
};
|
|
125
182
|
}
|
|
@@ -3,8 +3,28 @@
|
|
|
3
3
|
* Owns: artifact path template expansion and filesystem-backed artifact metadata.
|
|
4
4
|
*/
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
|
-
import {
|
|
6
|
+
import { closeSync, fstatSync, openSync, readSync, } from "node:fs";
|
|
7
7
|
import { substituteCommandTemplateToken } from "./command-templates.js";
|
|
8
|
+
function hashArtifactFile(path) {
|
|
9
|
+
const fd = openSync(path, "r");
|
|
10
|
+
try {
|
|
11
|
+
const size = fstatSync(fd).size;
|
|
12
|
+
const hash = createHash("sha256");
|
|
13
|
+
const chunk = Buffer.allocUnsafe(64 * 1024);
|
|
14
|
+
let position = 0;
|
|
15
|
+
while (position < size) {
|
|
16
|
+
const bytesRead = readSync(fd, chunk, 0, Math.min(chunk.byteLength, size - position), position);
|
|
17
|
+
if (bytesRead === 0)
|
|
18
|
+
break;
|
|
19
|
+
hash.update(chunk.subarray(0, bytesRead));
|
|
20
|
+
position += bytesRead;
|
|
21
|
+
}
|
|
22
|
+
return { sha256: hash.digest("hex"), size: position };
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
closeSync(fd);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
8
28
|
export function resolveArtifactPaths(artifacts, values) {
|
|
9
29
|
if (!artifacts)
|
|
10
30
|
return undefined;
|
|
@@ -35,7 +55,7 @@ export function resolveArtifactManifest(artifacts) {
|
|
|
35
55
|
if (!declaration?.path)
|
|
36
56
|
continue;
|
|
37
57
|
try {
|
|
38
|
-
const
|
|
58
|
+
const hashed = hashArtifactFile(declaration.path);
|
|
39
59
|
manifest[name] = {
|
|
40
60
|
exists: true,
|
|
41
61
|
...(declaration.kind ? { kind: declaration.kind } : {}),
|
|
@@ -46,8 +66,8 @@ export function resolveArtifactManifest(artifacts) {
|
|
|
46
66
|
...(declaration.required !== undefined
|
|
47
67
|
? { required: declaration.required }
|
|
48
68
|
: {}),
|
|
49
|
-
sha256:
|
|
50
|
-
size:
|
|
69
|
+
sha256: hashed.sha256,
|
|
70
|
+
size: hashed.size,
|
|
51
71
|
};
|
|
52
72
|
}
|
|
53
73
|
catch {
|
|
@@ -63,7 +63,9 @@ export declare function createAutoToolsRuntime(deps: ToolRegistryRuntimeDeps): T
|
|
|
63
63
|
export interface RecipeToolReloadWatcherDeps {
|
|
64
64
|
exists?: (path: string) => boolean;
|
|
65
65
|
getResolutionContext?: () => RecipeResolutionContext | undefined;
|
|
66
|
+
onCallbackError?: (error: unknown) => void;
|
|
66
67
|
recipeRoot?: string;
|
|
68
|
+
reloadDelayMs?: number;
|
|
67
69
|
watchPath?: typeof watch;
|
|
68
70
|
}
|
|
69
71
|
export declare function createRecipeToolReloadWatcher(runtime: Pick<ToolRegistryRuntime, "loadTools"> & Partial<Pick<ToolRegistryRuntime, "setWatchStatus">>, deps?: RecipeToolReloadWatcherDeps): RecipeToolReloadWatcher;
|
|
@@ -236,21 +236,41 @@ export function createRecipeToolReloadWatcher(runtime, deps = {}) {
|
|
|
236
236
|
reloadTimeout = undefined;
|
|
237
237
|
setWatchStatus("closed");
|
|
238
238
|
};
|
|
239
|
+
const reportCallbackError = (error) => {
|
|
240
|
+
try {
|
|
241
|
+
deps.onCallbackError?.(error);
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
/* host callback containment must remain no-throw */
|
|
245
|
+
}
|
|
246
|
+
};
|
|
239
247
|
const notifyFailure = (ctx) => {
|
|
240
248
|
if (failureNotified)
|
|
241
249
|
return;
|
|
242
250
|
failureNotified = true;
|
|
243
251
|
setWatchStatus("failed");
|
|
244
|
-
|
|
252
|
+
try {
|
|
253
|
+
ctx.ui.notify("Recipe live reload watcher failed; restart the session or use register_tool again to refresh recipe tools.", "warning");
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
reportCallbackError(error);
|
|
257
|
+
}
|
|
245
258
|
};
|
|
246
259
|
const scheduleReload = (ctx) => {
|
|
247
260
|
failureNotified = false;
|
|
248
261
|
if (reloadTimeout)
|
|
249
262
|
clearTimeout(reloadTimeout);
|
|
250
263
|
reloadTimeout = setTimeout(() => {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
264
|
+
reloadTimeout = undefined;
|
|
265
|
+
try {
|
|
266
|
+
runtime.loadTools(ctx, deps.getResolutionContext?.());
|
|
267
|
+
ctx.ui.notify("Recipe tools refreshed from ~/.pi/agent/recipes", "info");
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
notifyFailure(ctx);
|
|
271
|
+
reportCallbackError(error);
|
|
272
|
+
}
|
|
273
|
+
}, deps.reloadDelayMs ?? 150);
|
|
254
274
|
reloadTimeout.unref?.();
|
|
255
275
|
};
|
|
256
276
|
const watchParent = (ctx, recipeRoot) => {
|
|
@@ -105,10 +105,11 @@ function toolCalls(message, maxTextChars, maxToolCalls) {
|
|
|
105
105
|
}));
|
|
106
106
|
}
|
|
107
107
|
export function readSessionEvidence(path, options = {}) {
|
|
108
|
+
const maxBytes = Math.max(1, options.maxBytes ?? Limits.SESSION_EVIDENCE_MAX_BYTES);
|
|
108
109
|
const maxTextChars = Math.max(1, options.maxTextChars ?? Limits.SESSION_EVIDENCE_TEXT_CHARS);
|
|
109
110
|
const maxToolCalls = Math.max(1, options.maxToolCalls ?? Limits.SESSION_EVIDENCE_MAX_TOOL_CALLS);
|
|
110
111
|
const maxTurns = Math.max(1, options.maxTurns ?? Limits.SESSION_EVIDENCE_MAX_TURNS);
|
|
111
|
-
const read = readJsonlFileResilient(path);
|
|
112
|
+
const read = readJsonlFileResilient(path, { maxBytes });
|
|
112
113
|
const diagnostics = [...read.diagnostics];
|
|
113
114
|
const header = read.records.find((entry) => entry.type === "session");
|
|
114
115
|
const branch = activeBranch(read.records, path, diagnostics);
|
|
@@ -198,7 +199,7 @@ export function readSessionEvidence(path, options = {}) {
|
|
|
198
199
|
path,
|
|
199
200
|
...(header ? { session: asRecord(header) } : {}),
|
|
200
201
|
totalTurns: turns.length,
|
|
201
|
-
truncated: turns.length > visibleTurns.length,
|
|
202
|
+
truncated: read.truncated === true || turns.length > visibleTurns.length,
|
|
202
203
|
turns: visibleTurns,
|
|
203
204
|
};
|
|
204
205
|
}
|
|
@@ -15,7 +15,11 @@ export interface JsonReadResult<T> {
|
|
|
15
15
|
export interface JsonlReadResult<T> {
|
|
16
16
|
diagnostics: StateReadDiagnostic[];
|
|
17
17
|
records: T[];
|
|
18
|
+
truncated?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface JsonlReadOptions {
|
|
21
|
+
maxBytes?: number;
|
|
18
22
|
}
|
|
19
23
|
export declare function readJsonFileResilient<T>(path: string, fallback: T): JsonReadResult<T>;
|
|
20
|
-
export declare function readJsonlFileResilient<T>(path: string): JsonlReadResult<T>;
|
|
24
|
+
export declare function readJsonlFileResilient<T>(path: string, options?: JsonlReadOptions): JsonlReadResult<T>;
|
|
21
25
|
export declare function formatStateReadDiagnostics(diagnostics: StateReadDiagnostic[], limit?: number): string[];
|
|
@@ -3,13 +3,37 @@
|
|
|
3
3
|
* Zones: file-backed actor state, JSON/JSONL diagnostics, inspect safety
|
|
4
4
|
* Owns best-effort JSON and JSONL parsing helpers for operator-facing state reads.
|
|
5
5
|
*/
|
|
6
|
-
import { readFileSync } from "node:fs";
|
|
6
|
+
import { closeSync, fstatSync, openSync, readFileSync, readSync, } from "node:fs";
|
|
7
7
|
function isEnoent(error) {
|
|
8
8
|
return error.code === "ENOENT";
|
|
9
9
|
}
|
|
10
10
|
function diagnosticMessage(error) {
|
|
11
11
|
return error instanceof Error ? error.message : String(error);
|
|
12
12
|
}
|
|
13
|
+
function readTextFileCapped(path, maxBytes) {
|
|
14
|
+
if (maxBytes === undefined)
|
|
15
|
+
return readFileSync(path, "utf8");
|
|
16
|
+
const limit = Math.max(0, Math.floor(maxBytes));
|
|
17
|
+
const fd = openSync(path, "r");
|
|
18
|
+
try {
|
|
19
|
+
const size = fstatSync(fd).size;
|
|
20
|
+
if (size > limit) {
|
|
21
|
+
throw Object.assign(new Error(`file exceeds bounded read limit (${size} > ${limit} bytes)`), { code: "EFBIG" });
|
|
22
|
+
}
|
|
23
|
+
const content = Buffer.allocUnsafe(size);
|
|
24
|
+
let offset = 0;
|
|
25
|
+
while (offset < size) {
|
|
26
|
+
const bytesRead = readSync(fd, content, offset, size - offset, offset);
|
|
27
|
+
if (bytesRead === 0)
|
|
28
|
+
break;
|
|
29
|
+
offset += bytesRead;
|
|
30
|
+
}
|
|
31
|
+
return content.subarray(0, offset).toString("utf8");
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
closeSync(fd);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
13
37
|
export function readJsonFileResilient(path, fallback) {
|
|
14
38
|
try {
|
|
15
39
|
return {
|
|
@@ -31,11 +55,11 @@ export function readJsonFileResilient(path, fallback) {
|
|
|
31
55
|
};
|
|
32
56
|
}
|
|
33
57
|
}
|
|
34
|
-
export function readJsonlFileResilient(path) {
|
|
58
|
+
export function readJsonlFileResilient(path, options = {}) {
|
|
35
59
|
try {
|
|
36
60
|
const records = [];
|
|
37
61
|
const diagnostics = [];
|
|
38
|
-
const lines =
|
|
62
|
+
const lines = readTextFileCapped(path, options.maxBytes).split("\n");
|
|
39
63
|
for (const [index, line] of lines.entries()) {
|
|
40
64
|
if (!line.trim())
|
|
41
65
|
continue;
|
|
@@ -63,6 +87,9 @@ export function readJsonlFileResilient(path) {
|
|
|
63
87
|
},
|
|
64
88
|
],
|
|
65
89
|
records: [],
|
|
90
|
+
...(error.code === "EFBIG"
|
|
91
|
+
? { truncated: true }
|
|
92
|
+
: {}),
|
|
66
93
|
};
|
|
67
94
|
}
|
|
68
95
|
}
|
|
@@ -16,4 +16,5 @@ export declare function sessionMismatchError(input: {
|
|
|
16
16
|
run?: string;
|
|
17
17
|
target?: string;
|
|
18
18
|
}): Error;
|
|
19
|
+
export declare function assertRunStatusAccessibleToContext(runId: string, status: Record<string, unknown>, ctx: unknown): Record<string, unknown>;
|
|
19
20
|
export declare function assertRunAccessibleToContext(runId: string, ctx: unknown): Record<string, unknown>;
|
|
@@ -10,7 +10,7 @@ export function getContextSessionId(ctx) {
|
|
|
10
10
|
export function requireContextSessionId(ctx, actor) {
|
|
11
11
|
const sessionId = getContextSessionId(ctx);
|
|
12
12
|
if (!sessionId) {
|
|
13
|
-
throw new Error(`${actor} requires a current coordinator session;
|
|
13
|
+
throw Object.assign(new Error(`${actor} reason=session_unavailable requires a current coordinator session; retry from an active coordinator session.`), { reason: "session_unavailable" });
|
|
14
14
|
}
|
|
15
15
|
return sessionId;
|
|
16
16
|
}
|
|
@@ -18,27 +18,29 @@ export function sessionMismatchError(input) {
|
|
|
18
18
|
const ownerSession = input.expectedSession ?? "none";
|
|
19
19
|
const currentSession = input.currentSession ?? "none";
|
|
20
20
|
const actor = input.run ? `run:${input.run}` : (input.target ?? "session");
|
|
21
|
-
|
|
22
|
-
? `session:${input.expectedSession}`
|
|
23
|
-
: "session:all";
|
|
24
|
-
return Object.assign(new Error(`${actor} reason=session_mismatch owner_session=${ownerSession} current_session=${currentSession} hint=inspect_session:${input.expectedSession ?? "all"}`), {
|
|
21
|
+
return Object.assign(new Error(`${actor} reason=session_mismatch owner_session=${ownerSession} current_session=${currentSession} hint=inspect_runtime_runs`), {
|
|
25
22
|
current_session: input.currentSession,
|
|
26
|
-
hint:
|
|
23
|
+
hint: "inspect target=runtime view=runs",
|
|
27
24
|
owner_session: input.expectedSession,
|
|
28
25
|
reason: "session_mismatch",
|
|
29
26
|
run: input.run,
|
|
30
27
|
target: input.target,
|
|
31
28
|
});
|
|
32
29
|
}
|
|
33
|
-
export function
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
|
|
30
|
+
export function assertRunStatusAccessibleToContext(runId, status, ctx) {
|
|
31
|
+
const sessionId = requireContextSessionId(ctx, `run:${runId}`);
|
|
32
|
+
const ownerId = typeof status.ownerId === "string" && status.ownerId
|
|
33
|
+
? status.ownerId
|
|
34
|
+
: undefined;
|
|
35
|
+
if (ownerId !== sessionId) {
|
|
37
36
|
throw sessionMismatchError({
|
|
38
37
|
currentSession: sessionId,
|
|
39
|
-
expectedSession:
|
|
38
|
+
expectedSession: ownerId,
|
|
40
39
|
run: runId,
|
|
41
40
|
});
|
|
42
41
|
}
|
|
43
42
|
return status;
|
|
44
43
|
}
|
|
44
|
+
export function assertRunAccessibleToContext(runId, ctx) {
|
|
45
|
+
return assertRunStatusAccessibleToContext(runId, AsyncRuns.getRunStatus(runId), ctx);
|
|
46
|
+
}
|
|
@@ -27,14 +27,14 @@ const asRecord = ToolsResponse.asRecord;
|
|
|
27
27
|
const maybeJsonText = ToolsResponse.maybeJsonText;
|
|
28
28
|
function runtimeStatus() {
|
|
29
29
|
return {
|
|
30
|
-
automatic_review:
|
|
30
|
+
automatic_review: Paths.isAutomaticRecipeReviewEnabled(),
|
|
31
31
|
run_root: Paths.getRunStateRoot(),
|
|
32
32
|
state_schema: RuntimeIdentity.RUN_STATE_SCHEMA,
|
|
33
33
|
version: RuntimeIdentity.getPackageVersion(),
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
36
|
function runtimeRuns(ctx, deps, status) {
|
|
37
|
-
const session = ToolsAccess.
|
|
37
|
+
const session = ToolsAccess.requireContextSessionId(ctx, "runtime Run inventory");
|
|
38
38
|
const listed = deps.listRuns
|
|
39
39
|
? deps.listRuns()
|
|
40
40
|
: AsyncRuns.listRuns(undefined, status);
|
|
@@ -49,8 +49,8 @@ function runtimeRuns(ctx, deps, status) {
|
|
|
49
49
|
return run;
|
|
50
50
|
}
|
|
51
51
|
})
|
|
52
|
-
.filter((run) =>
|
|
53
|
-
return {
|
|
52
|
+
.filter((run) => run.ownerId === session);
|
|
53
|
+
return { owner_session: session, runs };
|
|
54
54
|
}
|
|
55
55
|
function runtimeTriage(ctx, deps) {
|
|
56
56
|
const inventory = runtimeRuns(ctx, deps, undefined);
|
|
@@ -354,7 +354,7 @@ function inspectRun(run, view, input, ctx, deps) {
|
|
|
354
354
|
throw new Error("inspect run:<id> supports view=recipe, view=trace, or view=control.");
|
|
355
355
|
}
|
|
356
356
|
const status = deps.getRunStatus
|
|
357
|
-
? deps.getRunStatus(run)
|
|
357
|
+
? ToolsAccess.assertRunStatusAccessibleToContext(run, deps.getRunStatus(run), ctx)
|
|
358
358
|
: ToolsAccess.assertRunAccessibleToContext(run, ctx);
|
|
359
359
|
const stateDir = String(status.state_dir);
|
|
360
360
|
if (view === "recipe") {
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Owns public Control execution; journaling, delivery, and lifecycle mutation stay in Run domains.
|
|
5
5
|
*/
|
|
6
6
|
export interface ControlToolDeps {
|
|
7
|
+
getRunStatus?: (run: string) => Record<string, unknown>;
|
|
7
8
|
handleRuntimeControl?: (action: string, input: unknown) => Record<string, unknown>;
|
|
8
9
|
}
|
|
9
10
|
export declare function createControlToolDefinition<TContext = unknown>(deps?: ControlToolDeps): any;
|
|
@@ -67,7 +67,9 @@ export function createControlToolDefinition(deps = {}) {
|
|
|
67
67
|
}
|
|
68
68
|
else {
|
|
69
69
|
const run = request.target.slice(4);
|
|
70
|
-
const status =
|
|
70
|
+
const status = deps.getRunStatus
|
|
71
|
+
? ToolsAccess.assertRunStatusAccessibleToContext(run, deps.getRunStatus(run), ctx)
|
|
72
|
+
: ToolsAccess.assertRunAccessibleToContext(run, ctx);
|
|
71
73
|
const runInstanceId = typeof status.run_instance_id === "string"
|
|
72
74
|
? status.run_instance_id
|
|
73
75
|
: undefined;
|
|
@@ -8,6 +8,7 @@ Living index of all documentation in the `/docs` directory.
|
|
|
8
8
|
- [template-recipes.md](./template-recipes.md) — Saved JSON/Markdown recipe standard, imports, and reusable command-template graph composition
|
|
9
9
|
- [async-runs.md](./async-runs.md) — Run lifecycle, state, Control, Trace, cancellation, and terminal reconciliation
|
|
10
10
|
- [actor-inspector.md](./actor-inspector.md) — Owner-filtered actor-instance navigation through Recipe, Trace, and Control
|
|
11
|
+
- [inspection.md](./inspection.md) — Complete `inspect` target/view matrix, authorization boundaries, and diagnostic routes
|
|
11
12
|
- [tool-registry.md](./tool-registry.md) — Local `pi-actors` registry storage and `register_tool` adaptation
|
|
12
13
|
- [recipe-library.md](./recipe-library.md) — Packaged standard recipe library such as async subagents, coordinator pipelines, utilities, and music playback
|
|
13
14
|
- [releasing.md](./releasing.md) — Guarded tag validation, npm Trusted Publisher setup, registry verification, and GitHub Release convergence
|
|
@@ -19,13 +19,13 @@ Shows captured execution provenance:
|
|
|
19
19
|
- declared artifacts and actor-local actions;
|
|
20
20
|
- model/thinking policy and launch source.
|
|
21
21
|
|
|
22
|
-
Captured Recipe evidence belongs to the Run generation and does not change when an active Recipe file later changes. Skill components display logical identities such as `artifacts/report`; private physical `source_file`, `skill_dir`, and `recipe_dir` stay out of Inspector and model-facing views. Non-empty
|
|
22
|
+
Captured Recipe evidence belongs to the Run generation and does not change when an active Recipe file later changes. Skill components display logical identities such as `artifacts/report`; private physical `source_file`, `skill_dir`, and `recipe_dir` stay out of Inspector and model-facing views. Non-empty objects render as indented brace-delimited property lists. Complex arrays use compact zero-based entries such as `#0: {` rather than Markdown list markers.
|
|
23
23
|
|
|
24
24
|
## Trace
|
|
25
25
|
|
|
26
|
-
Shows the unified bounded Trace projection. Sources include lifecycle/runtime observations, Controls, owned Pi turns, command-log tails, results, artifacts, and diagnostics.
|
|
26
|
+
Shows the unified bounded Trace projection. Sources include lifecycle/runtime observations, Controls, owned Pi turns, command-log tails, results, artifacts, and diagnostics. The source selector displays only `all` plus sources present in the current projection from `lifecycle`, `control`, `process`, `agent`, `artifact`, and `runtime`. Select a row to open structured detail.
|
|
27
27
|
|
|
28
|
-
Trace ordering stays deterministic and newest-first: timestamp descending, same-source physical ordinal descending, fixed internal source rank, then stable id. Internal ordinals are never displayed or interpreted as cross-source causality. Row numbers
|
|
28
|
+
Trace ordering stays deterministic and newest-first: timestamp descending, same-source physical ordinal descending, fixed internal source rank, then stable id. Internal ordinals are never displayed or interpreted as cross-source causality. Row numbers are zero-based chronological identities even though display is newest-first: the oldest visible event is `#0` and the newest carries the highest number. The summary states whether retained history is complete; `runtime.trace_compacted` means older history was discarded and shows bounded cumulative drop evidence. Terminal/result/execution/artifact evidence keeps its own authority. The projection applies path containment and redaction before rendering.
|
|
29
29
|
|
|
30
30
|
## Control
|
|
31
31
|
|
|
@@ -39,12 +39,28 @@ Shows:
|
|
|
39
39
|
|
|
40
40
|
A service endpoint counts as ready only when `control-endpoint.json` matches the Run's immutable `run_instance_id`. Capacity reaches zero at 64 pending Controls; further requests are rejected before admission, while admitted nonterminal Controls never expire automatically. Runtime-owned kill remains available for a stuck saturated Run. Recent Control input and errors use the same bounded structured redaction as tool inspection. The durable `controls.jsonl` journal remains raw and local; rendering never mutates it or attaches an unredacted copy.
|
|
41
41
|
|
|
42
|
+
## Focus and Selectors
|
|
43
|
+
|
|
44
|
+
`selectedBg` marks the current focus or selection; `customMessageBg` remains reserved for alternating content stripes. Opening the Run or Trace-source selector preserves `selectedBg` on its parent control, so focus reads as parent → child menu → selected option. Menus are composited over only their bounded rectangle; base content before, beside, and below that rectangle remains rendered.
|
|
45
|
+
|
|
46
|
+
The Run selector uses aligned zero-based sequence, Run name, and semantic status columns. The Trace selector uses `Trace: <source>`; when a non-`all` source is active, the tab projects the same colon grammar and value color.
|
|
47
|
+
|
|
42
48
|
## Keys
|
|
43
49
|
|
|
44
|
-
The footer
|
|
50
|
+
The footer is authoritative for the current focus. The stable navigation contract is:
|
|
51
|
+
|
|
52
|
+
| Focus | Keys |
|
|
53
|
+
| --- | --- |
|
|
54
|
+
| Run control | `←`/`→` change Run, `↓` enters tabs, `Enter` opens the Run selector, `k` requests kill when available |
|
|
55
|
+
| Tabs | `←`/`→` or `Tab` changes tab, `↑` returns to Run, `↓` enters content, `Enter` opens content or the Trace-source selector |
|
|
56
|
+
| Trace tab | `f` cycles present sources without opening the selector |
|
|
57
|
+
| List/document/detail | `↑`/`↓` and `PgUp`/`PgDn` navigate; `→`/`Enter` opens a Trace row; `←` or `Esc` moves back |
|
|
58
|
+
| Selector | `↑`/`↓` chooses, `Enter`/`→` applies, `←`/`Esc` cancels |
|
|
59
|
+
| Kill confirmation | `←`/`→` or `Tab` chooses, `Enter`/`y` confirms, `Esc`/`n` cancels |
|
|
60
|
+
| Overlay | `Esc` closes from the top level; `Ctrl-C` closes immediately |
|
|
45
61
|
|
|
46
62
|
Run kill revalidates owner and generation through the canonical lifecycle path. After success, the Run status header is the sole confirmation; the content area does not duplicate it. The Inspector never edits state directly and never derives authority from displayed data.
|
|
47
63
|
|
|
48
64
|
## Scope
|
|
49
65
|
|
|
50
|
-
The Actor Inspector treats each Run as a concrete actor instance. It does not expose group conversations, peer addresses, routing, or communication topology. Use `inspect target=runtime view=status`, `inspect target=recipes view=status`, and `inspect target=tool:<name> view=status` for non-Run management targets.
|
|
66
|
+
The Actor Inspector treats each Run as a concrete actor instance. It does not expose group conversations, peer addresses, routing, or communication topology. Use `inspect target=runtime view=status|runs|triage`, `inspect target=recipes view=status|summary|doctor|imports|reviews`, and `inspect target=tool:<name> view=status|schema` for non-Run management targets. See [Management Inspection](./inspection.md) for exact applicability and authorization boundaries.
|