@llblab/pi-actors 0.43.1 → 0.45.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.
Files changed (79) hide show
  1. package/AGENTS.md +6 -6
  2. package/BACKLOG.md +1 -189
  3. package/CHANGELOG.md +419 -402
  4. package/README.md +8 -6
  5. package/dist/index.js +7 -3
  6. package/dist/lib/async-runs.d.ts +2 -0
  7. package/dist/lib/async-runs.js +41 -34
  8. package/dist/lib/config.js +1 -0
  9. package/dist/lib/file-state.d.ts +3 -1
  10. package/dist/lib/file-state.js +25 -7
  11. package/dist/lib/inspector-overlay.js +46 -30
  12. package/dist/lib/inspector.js +8 -2
  13. package/dist/lib/limits.d.ts +10 -0
  14. package/dist/lib/limits.js +10 -0
  15. package/dist/lib/observability.d.ts +4 -2
  16. package/dist/lib/observability.js +43 -36
  17. package/dist/lib/recipes-discovery.js +20 -3
  18. package/dist/lib/recipes-references.d.ts +10 -0
  19. package/dist/lib/recipes-references.js +168 -5
  20. package/dist/lib/run-evidence-policy.d.ts +95 -0
  21. package/dist/lib/run-evidence-policy.js +177 -0
  22. package/dist/lib/run-ui-runtime.js +2 -0
  23. package/dist/lib/runs-controls.d.ts +7 -5
  24. package/dist/lib/runs-controls.js +186 -49
  25. package/dist/lib/runs-retention.js +27 -14
  26. package/dist/lib/runs-trace.d.ts +25 -1
  27. package/dist/lib/runs-trace.js +410 -21
  28. package/dist/lib/runtime-triage.js +6 -22
  29. package/dist/lib/schema.d.ts +3 -0
  30. package/dist/lib/schema.js +34 -1
  31. package/dist/lib/tools-inspect.js +58 -14
  32. package/dist/lib/tools-local.js +27 -10
  33. package/dist/lib/tools-spawn.js +2 -2
  34. package/dist/lib/trace-projection.js +90 -44
  35. package/dist/scripts/conformance.mjs +5 -0
  36. package/dist/scripts/locker.mjs +31 -74
  37. package/dist/scripts/music-player.mjs +41 -129
  38. package/dist/scripts/release-gates.mjs +26 -4
  39. package/dist/skills/actors/SKILL.md +9 -8
  40. package/dist/skills/swarm/SKILL.md +2 -2
  41. package/docs/README.md +0 -1
  42. package/docs/actor-inspector.md +3 -2
  43. package/docs/async-runs.md +10 -8
  44. package/docs/command-templates.md +2 -2
  45. package/docs/recipe-library.md +4 -2
  46. package/docs/template-recipes.md +10 -4
  47. package/docs/tool-registry.md +1 -1
  48. package/index.ts +9 -3
  49. package/lib/async-runs.ts +51 -54
  50. package/lib/config.ts +4 -0
  51. package/lib/file-state.ts +28 -7
  52. package/lib/inspector-overlay.ts +29 -17
  53. package/lib/inspector.ts +16 -2
  54. package/lib/limits.ts +10 -0
  55. package/lib/observability.ts +55 -57
  56. package/lib/recipes-discovery.ts +23 -3
  57. package/lib/recipes-references.ts +227 -7
  58. package/lib/run-evidence-policy.ts +242 -0
  59. package/lib/run-ui-runtime.ts +2 -0
  60. package/lib/runs-controls.ts +177 -108
  61. package/lib/runs-retention.ts +28 -20
  62. package/lib/runs-trace.ts +496 -21
  63. package/lib/runtime-triage.ts +11 -25
  64. package/lib/schema.ts +53 -1
  65. package/lib/tools-inspect.ts +56 -15
  66. package/lib/tools-local.ts +49 -19
  67. package/lib/tools-spawn.ts +2 -2
  68. package/lib/trace-projection.ts +137 -75
  69. package/package.json +1 -1
  70. package/scripts/conformance.mjs +5 -0
  71. package/scripts/locker.mjs +31 -74
  72. package/scripts/music-player.mjs +41 -129
  73. package/scripts/release-gates.mjs +26 -4
  74. package/skills/actors/SKILL.md +9 -8
  75. package/skills/swarm/SKILL.md +2 -2
  76. package/dist/lib/runtime-notifier.d.ts +0 -48
  77. package/dist/lib/runtime-notifier.js +0 -138
  78. package/docs/0.43-baseline.md +0 -39
  79. package/lib/runtime-notifier.ts +0 -211
package/lib/schema.ts CHANGED
@@ -185,7 +185,11 @@ function isValidDefault(type: ToolArgType, value: string): boolean {
185
185
  }
186
186
 
187
187
  export function parseToolArgDeclarations(value: string): ToolArgSpec {
188
- const source = splitArgDeclarations(value);
188
+ return parseToolArgDeclarationList(splitArgDeclarations(value));
189
+ }
190
+
191
+ export function parseToolArgDeclarationList(value: unknown[]): ToolArgSpec {
192
+ const source = value.map(String);
189
193
  const seen = new Set<string>();
190
194
  const duplicates = new Set<string>();
191
195
  const args: string[] = [];
@@ -324,6 +328,7 @@ function collectCommandTemplateConfigDeclarations(
324
328
  collectTemplatePlaceholderDeclarations(config, declarations);
325
329
  return;
326
330
  }
331
+ if (!config || typeof config !== "object") return;
327
332
  if (Array.isArray(config)) {
328
333
  for (const step of config)
329
334
  collectCommandTemplateConfigDeclarations(step, declarations);
@@ -356,11 +361,42 @@ export function getTemplatePlaceholderNames(
356
361
  );
357
362
  }
358
363
 
364
+ function toolArgTypesEqual(left: ToolArgType, right: ToolArgType): boolean {
365
+ return JSON.stringify(left) === JSON.stringify(right);
366
+ }
367
+
368
+ export function assertCompatibleToolArgTypes(
369
+ ...sources: Array<Record<string, ToolArgType> | undefined>
370
+ ): void {
371
+ const seen: Record<string, ToolArgType> = {};
372
+ for (const source of sources) {
373
+ for (const [name, type] of Object.entries(source ?? {})) {
374
+ const previous = seen[name];
375
+ if (previous && !toolArgTypesEqual(previous, type)) {
376
+ throw new Error(
377
+ `Conflicting argument type for ${name}: ${canonicalArgDeclaration(name, previous)} vs ${canonicalArgDeclaration(name, type)}`,
378
+ );
379
+ }
380
+ seen[name] = type;
381
+ }
382
+ }
383
+ }
384
+
359
385
  export function getTemplateArgTypes(
360
386
  config: CommandTemplates.CommandTemplateConfig,
361
387
  ): Record<string, ToolArgType> {
362
388
  const argTypes: Record<string, ToolArgType> = {};
363
389
  for (const declaration of getTemplatePlaceholderDeclarations(config)) {
390
+ const previous = argTypes[declaration.arg];
391
+ if (
392
+ previous &&
393
+ declaration.type.kind !== "string" &&
394
+ !toolArgTypesEqual(previous, declaration.type)
395
+ ) {
396
+ throw new Error(
397
+ `Conflicting argument type for ${declaration.arg}: ${canonicalArgDeclaration(declaration.arg, previous)} vs ${declaration.declaration}`,
398
+ );
399
+ }
364
400
  if (declaration.type.kind !== "string")
365
401
  argTypes[declaration.arg] = declaration.type;
366
402
  }
@@ -373,6 +409,22 @@ export function getExplicitToolArgNames(args: string[] | undefined): string[] {
373
409
  );
374
410
  }
375
411
 
412
+ export function getExplicitToolArgDefaults(
413
+ args: string[] | string | undefined,
414
+ ): Record<string, string> {
415
+ const declarations =
416
+ typeof args === "string" ? splitArgDeclarations(args) : (args ?? []);
417
+ return Object.fromEntries(
418
+ declarations
419
+ .map((item) => parseToolArgToken(String(item)))
420
+ .filter(
421
+ (item): item is ParsedToolArgToken & { defaultValue: string } =>
422
+ item.defaultValue !== undefined,
423
+ )
424
+ .map((item) => [item.arg, item.defaultValue]),
425
+ );
426
+ }
427
+
376
428
  export function getToolArgNames(
377
429
  config: CommandTemplates.CommandTemplateConfig,
378
430
  ): string[] {
@@ -4,14 +4,17 @@
4
4
  * Owns exact inspect target/view dispatch; source projection stays in domain modules.
5
5
  */
6
6
 
7
+ import { statSync } from "node:fs";
7
8
  import { join } from "node:path";
8
9
 
9
10
  import * as AsyncRuns from "./async-runs.ts";
10
11
  import * as ControlProjection from "./control-projection.ts";
11
12
  import * as Inspector from "./inspector.ts";
12
13
  import * as Limits from "./limits.ts";
14
+ import { computeRunControlCapacity } from "./run-evidence-policy.ts";
13
15
  import * as Paths from "./paths.ts";
14
16
  import * as RecipesDiscovery from "./recipes-discovery.ts";
17
+ import * as RecipesReferences from "./recipes-references.ts";
15
18
  import * as ReviewDiagnostics from "./review-diagnostics.ts";
16
19
  import * as RunsControls from "./runs-controls.ts";
17
20
  import * as RunControlDelivery from "./runs-control-delivery.ts";
@@ -75,20 +78,26 @@ function runtimeTriage(
75
78
  const failed = runs.filter((run) => run.status === "failed");
76
79
  const pendingControls: RuntimeTriage.RuntimePendingControl[] = [];
77
80
  const staleControls: RuntimeTriage.RuntimePendingControl[] = [];
81
+ const backpressuredRuns: Array<Record<string, unknown>> = [];
78
82
  const controlDiagnostics: Array<Record<string, unknown>> = [];
83
+ const traceDiagnostics: Array<Record<string, unknown>> = [];
79
84
  const nowMs = Date.now();
80
85
  for (const run of runs) {
81
86
  const stateDir = typeof run.state_dir === "string" ? run.state_dir : undefined;
82
87
  if (!stateDir) continue;
83
88
  const runId = String(run.run ?? "unknown");
84
89
  const journal = RunsControls.readRunControlJournalFromStateDir(stateDir);
90
+ const capacity = computeRunControlCapacity(journal.records);
85
91
  controlDiagnostics.push(...journal.diagnostics.map((diagnostic) => ({
86
92
  ...(diagnostic.line === undefined ? {} : { line: diagnostic.line }),
87
- reason: diagnostic.line === undefined
88
- ? "unreadable_control_journal"
89
- : "invalid_control_json",
90
- run: runId,
93
+ reason: diagnostic.line === undefined ? "unreadable_control_journal" : "invalid_control_json", run: runId,
91
94
  })));
95
+ if (capacity.backpressured) backpressuredRuns.push({
96
+ run: runId, pending: capacity.pending, pending_limit: capacity.limit,
97
+ journal_bytes: statSync(RunsControls.runControlsFile(stateDir)).size,
98
+ });
99
+ const traceSummary = RunsTrace.summarizeRunTraceJournal(RunsTrace.readRunTraceJournal(stateDir));
100
+ if (!traceSummary.history_complete) traceDiagnostics.push({ run: runId, ...traceSummary });
92
101
  for (const control of journal.records) {
93
102
  const classified = RuntimeTriage.classifyRuntimeControl(
94
103
  {
@@ -137,14 +146,15 @@ function runtimeTriage(
137
146
  pending_controls: boundedPendingControls,
138
147
  stale_control_count: staleControls.length,
139
148
  stale_controls: boundedStaleControls,
140
- control_diagnostic_count: controlDiagnostics.length,
141
- control_diagnostics: controlDiagnostics.slice(0, 40),
149
+ backpressured_run_count: backpressuredRuns.length, backpressured_runs: backpressuredRuns.slice(0, 40),
150
+ control_diagnostic_count: controlDiagnostics.length, control_diagnostics: controlDiagnostics.slice(0, 40),
151
+ trace_diagnostic_count: traceDiagnostics.length, trace_diagnostics: traceDiagnostics.slice(0, 40),
142
152
  attention_events: attentionEvents.slice(-40).reverse(),
143
153
  next_actions: [
144
154
  ...(failed.length ? ["inspect target=runtime view=runs status=failed"] : []),
145
- ...staleControls.slice(0, 3).map((entry) =>
155
+ ...new Set([...staleControls, ...backpressuredRuns].slice(0, 3).map((entry) =>
146
156
  `inspect target=run:${entry.run} view=control`,
147
- ),
157
+ )),
148
158
  ...(attentionEvents.length
149
159
  ? attentionEvents.slice(-3).map((entry) =>
150
160
  `inspect target=run:${String(entry.run)} view=trace source=runtime`,
@@ -159,7 +169,7 @@ function compactRuntime(view: string, details: Record<string, unknown>): string
159
169
  return `\nruntime runs=${(details.runs as unknown[] | undefined)?.length ?? 0}`;
160
170
  }
161
171
  if (view === "triage") {
162
- return `\nruntime failed=${(details.failed_runs as unknown[] | undefined)?.length ?? 0} pending_controls=${Number(details.pending_control_count ?? 0)} stale_controls=${Number(details.stale_control_count ?? 0)} attention=${(details.attention_events as unknown[] | undefined)?.length ?? 0}`;
172
+ return `\nruntime failed=${(details.failed_runs as unknown[] | undefined)?.length ?? 0} pending_controls=${Number(details.pending_control_count ?? 0)} stale_controls=${Number(details.stale_control_count ?? 0)} backpressured=${Number(details.backpressured_run_count ?? 0)} incomplete_trace=${Number(details.trace_diagnostic_count ?? 0)} attention=${(details.attention_events as unknown[] | undefined)?.length ?? 0}`;
163
173
  }
164
174
  return `\nruntime version=${String(details.version)} state_schema=${String(details.state_schema)} automatic_review=${String(details.automatic_review)}`;
165
175
  }
@@ -205,8 +215,18 @@ function inspectRecipes(
205
215
  { root: recipeRoot, defaultTool: true, mutableUsage: true },
206
216
  { root: deps.packagedRecipeRoot ?? Paths.getPackagedRecipeRoot() },
207
217
  ]);
218
+ const skillRecipeNamespaces =
219
+ RecipesReferences.getActiveSkillRecipeNamespaces();
208
220
  const summary = {
209
221
  ...RecipesDiscovery.summarizeDiscovery(discovered),
222
+ skill_recipe_namespaces: skillRecipeNamespaces,
223
+ skill_recipe_namespace_diagnostics: Object.entries(skillRecipeNamespaces)
224
+ .filter(([, roots]) => roots.length > 1)
225
+ .map(([name, roots]) => ({
226
+ name,
227
+ roots,
228
+ error: `Ambiguous active Skill Recipe namespace ${name}`,
229
+ })),
210
230
  drafts: RecipesDiscovery.listDraftRecipes(join(recipeRoot, "drafts")),
211
231
  };
212
232
  return {
@@ -252,8 +272,8 @@ function inspectRun(
252
272
  throw new Error(`unsupported Trace source: ${source}`);
253
273
  }
254
274
  return {
255
- run,
256
- source,
275
+ run, source,
276
+ summary: RunsTrace.summarizeRunTraceJournal(RunsTrace.readRunTraceJournal(stateDir)),
257
277
  items: TraceProjection.projectRunTrace(stateDir, {
258
278
  artifacts: status.artifacts as
259
279
  | Record<string, AsyncRuns.RunArtifactDeclaration>
@@ -264,19 +284,40 @@ function inspectRun(
264
284
  };
265
285
  }
266
286
  const runInstanceId = String(status.run_instance_id ?? "");
287
+ const journal = RunsControls.readRunControlJournalFromStateDir(stateDir);
288
+ const capacity = computeRunControlCapacity(journal.records);
289
+ const structuralDiagnostics = journal.records.flatMap((value) => {
290
+ const classified = RuntimeTriage.classifyRuntimeControl({ run,
291
+ runInstanceId: runInstanceId || undefined, status: String(status.status ?? "unknown") }, value, Date.now());
292
+ return classified.diagnostic ? [classified.diagnostic.reason] : [];
293
+ });
294
+ let journalBytes = 0;
295
+ try { journalBytes = statSync(RunsControls.runControlsFile(stateDir)).size; } catch {}
296
+ const diagnostics = [...journal.diagnostics.map((item) => ({
297
+ ...(item.line === undefined ? {} : { line: item.line }),
298
+ reason: item.line === undefined ? "unreadable_control_journal" : "invalid_control_json",
299
+ })), ...structuralDiagnostics.map((reason) => ({ reason }))].slice(0, 40);
300
+ const nowMs = Date.now();
301
+ const stalePending = journal.records.reduce<number>((count, control) => count + Number(Boolean(
302
+ RuntimeTriage.classifyRuntimeControl({ run, runInstanceId: runInstanceId || undefined,
303
+ status: String(status.status ?? "unknown") }, control, nowMs).stale)), 0);
267
304
  return {
268
305
  run,
269
306
  run_instance_id: runInstanceId,
270
307
  status: status.status,
308
+ pending: capacity.pending, pending_limit: capacity.limit,
309
+ available: capacity.available, backpressured: capacity.backpressured,
310
+ journal_bytes: journalBytes, journal_limit: Limits.RUN_CONTROL_JOURNAL_MAX_BYTES,
311
+ ...(capacity.oldest_pending_at ? { oldest_pending_at: capacity.oldest_pending_at } : {}),
312
+ stale_pending: stalePending,
313
+ diagnostics,
271
314
  actor_actions: Array.isArray(status.control) ? status.control : [],
272
315
  runtime_actions: status.status === "running" ? ["kill"] : ["archive", "prune"],
273
316
  endpoint: runInstanceId
274
317
  ? RunControlDelivery.readRunControlEndpoint(stateDir, runInstanceId)
275
318
  : undefined,
276
- recent_controls: RunsControls.readRunControlsFromStateDir(stateDir)
277
- .slice(-Math.max(1, Number(input.lines || 20)))
278
- .reverse()
279
- .map(ControlProjection.projectRunControl),
319
+ recent_controls: journal.records.slice(-Math.max(1, Number(input.lines || 20))).reverse()
320
+ .map((control) => ControlProjection.projectRunControl(control as RunsControls.RunControlRecord)),
280
321
  };
281
322
  }
282
323
 
@@ -67,11 +67,16 @@ function formatRuntimeToolUsageHint(
67
67
  includeRunId: boolean,
68
68
  ): string {
69
69
  const optional = cfg.args.filter((arg) => !required.includes(arg));
70
+ const exampleDefaults = {
71
+ ...Schema.getExplicitToolArgDefaults(cfg.recipe?.args),
72
+ ...cfg.defaults,
73
+ ...(cfg.recipe?.values ?? {}),
74
+ };
70
75
  const example: Record<string, unknown> = {};
71
76
  for (const arg of required)
72
- example[arg] = sampleValueForArg(arg, cfg.argTypes?.[arg], cfg.defaults);
77
+ example[arg] = sampleValueForArg(arg, cfg.argTypes?.[arg], exampleDefaults);
73
78
  for (const arg of optional)
74
- example[arg] = sampleValueForArg(arg, cfg.argTypes?.[arg], cfg.defaults);
79
+ example[arg] = sampleValueForArg(arg, cfg.argTypes?.[arg], exampleDefaults);
75
80
  if (includeRunId) example.run_id = `${cfg.name}-1`;
76
81
  const lines = [
77
82
  `Expected call shape for ${cfg.name}:`,
@@ -85,6 +90,32 @@ function formatRuntimeToolUsageHint(
85
90
  return lines.join("\n");
86
91
  }
87
92
 
93
+ function resolveRecipeToolValues(
94
+ cfg: RegisteredTool,
95
+ callerValues: Record<string, unknown>,
96
+ ctx: RuntimeToolContext,
97
+ ): Record<string, unknown> {
98
+ const recipeRuntimeValues = {
99
+ ...(cfg.recipe?.recipe_dir
100
+ ? { recipe_dir: cfg.recipe.recipe_dir }
101
+ : {}),
102
+ ...(cfg.recipe?.skill_dir ? { skill_dir: cfg.recipe.skill_dir } : {}),
103
+ };
104
+ return Schema.normalizeRuntimeValues(
105
+ ModelContext.withCurrentModelValues(
106
+ {
107
+ ...Schema.getExplicitToolArgDefaults(cfg.recipe?.args),
108
+ ...cfg.defaults,
109
+ ...(cfg.recipe?.values ?? {}),
110
+ ...callerValues,
111
+ ...recipeRuntimeValues,
112
+ },
113
+ ctx,
114
+ ),
115
+ cfg.argTypes,
116
+ );
117
+ }
118
+
88
119
  function formatRuntimeToolArgumentError(
89
120
  cfg: RegisteredTool,
90
121
  error: unknown,
@@ -128,9 +159,18 @@ export function createRuntimeToolDefinition(
128
159
  defaults: cfg.defaults,
129
160
  template: requiredTemplate,
130
161
  };
162
+ const recipeInlineDefaults = Schema.getExplicitToolArgDefaults(
163
+ cfg.recipe?.args,
164
+ );
131
165
  const requiredArgs =
132
166
  isRecipe && cfg.storedArgs !== undefined
133
- ? new Set(cfg.args.filter((arg) => !Object.hasOwn(cfg.defaults, arg)))
167
+ ? new Set(
168
+ cfg.args.filter(
169
+ (arg) =>
170
+ !Object.hasOwn(cfg.defaults, arg) &&
171
+ !Object.hasOwn(recipeInlineDefaults, arg),
172
+ ),
173
+ )
134
174
  : RecipesReferences.isRecipeReference(cfg.template) && !recipeTemplate
135
175
  ? new Set(cfg.args.filter((arg) => !Object.hasOwn(cfg.defaults, arg)))
136
176
  : Schema.getRequiredToolArgNames(requiredTemplateConfig);
@@ -200,13 +240,7 @@ export function createRuntimeToolDefinition(
200
240
  { ...(cfg.recipe?.values ?? {}), ...values },
201
241
  ctx,
202
242
  ),
203
- values: Schema.normalizeRuntimeValues(
204
- ModelContext.withCurrentModelValues(
205
- { ...(cfg.recipe?.values ?? {}), ...cfg.defaults, ...values },
206
- ctx,
207
- ),
208
- cfg.argTypes,
209
- ),
243
+ values: resolveRecipeToolValues(cfg, values, ctx),
210
244
  },
211
245
  ctx.cwd,
212
246
  );
@@ -221,17 +255,13 @@ export function createRuntimeToolDefinition(
221
255
  };
222
256
  }
223
257
  if (isRecipe && recipeTemplate) {
224
- const paramsWithDefaults = ModelContext.withCurrentModelValues(
225
- {
226
- ...(cfg.recipe?.values ?? {}),
227
- ...cfg.defaults,
228
- ...(params as Record<string, unknown>),
229
- },
230
- ctx,
231
- );
232
258
  return await Execution.executeRegisteredTool(
233
259
  { ...cfg, template: recipeTemplate },
234
- Schema.normalizeRuntimeValues(paramsWithDefaults, cfg.argTypes),
260
+ resolveRecipeToolValues(
261
+ cfg,
262
+ params as Record<string, unknown>,
263
+ ctx,
264
+ ),
235
265
  exec,
236
266
  ctx.cwd,
237
267
  signal,
@@ -138,10 +138,10 @@ export function createSpawnToolDefinition<
138
138
  "Optional Run identity in exact run:<id> form.",
139
139
  ),
140
140
  file: Schema.stringSchema(
141
- "Optional template recipe JSON file. Bare names resolve under ~/.pi/agent/recipes.",
141
+ "Optional template Recipe reference. Supports paths, bare user/adjacent/std names, std:<name>, and skill:<skill>/<recipe>.",
142
142
  ),
143
143
  recipe: Schema.stringSchema(
144
- "Alias for file; template recipe JSON file/name to spawn.",
144
+ "Alias for file; qualified Skill/std or ordinary Recipe reference to spawn.",
145
145
  ),
146
146
  template: Schema.unionSchema([
147
147
  Schema.stringSchema("Inline command template string"),
@@ -14,13 +14,13 @@ import {
14
14
  resolveArtifactManifest,
15
15
  type RunArtifactDeclaration,
16
16
  } from "./runs-artifacts.ts";
17
+ import * as RunsControls from "./runs-controls.ts";
17
18
  import type { RunControlRecord } from "./runs-controls.ts";
18
19
  import { tailFile } from "./runs-status.ts";
19
- import type { TraceEvent } from "./runs-trace.ts";
20
+ import * as RunsTrace from "./runs-trace.ts";
20
21
  import * as SessionEvidence from "./session-evidence.ts";
21
22
  import {
22
23
  readJsonFileResilient,
23
- readJsonlFileResilient,
24
24
  type StateReadDiagnostic,
25
25
  } from "./state-readers.ts";
26
26
 
@@ -44,19 +44,52 @@ export interface TraceItem {
44
44
  detail: unknown;
45
45
  }
46
46
 
47
+ interface OrderedTraceItem {
48
+ item: TraceItem;
49
+ ordinal: number;
50
+ sourceKey: string;
51
+ }
52
+
47
53
  const LIFECYCLE_PREFIXES = ["run.", "command."];
48
54
  const SOURCE_LIMIT = 100;
55
+ const SOURCE_RANK: Record<TraceSource, number> = {
56
+ lifecycle: 6,
57
+ control: 5,
58
+ process: 4,
59
+ agent: 3,
60
+ artifact: 2,
61
+ runtime: 1,
62
+ };
49
63
 
50
64
  function redact(value: unknown): unknown {
51
65
  return SessionEvidence.redactSessionEvidenceValue(value);
52
66
  }
53
67
 
54
- function sourceForEvent(event: TraceEvent): TraceSource {
68
+ function sourceForEvent(event: RunsTrace.TraceEvent): TraceSource {
55
69
  return LIFECYCLE_PREFIXES.some((prefix) => event.kind.startsWith(prefix))
56
70
  ? "lifecycle"
57
71
  : "runtime";
58
72
  }
59
73
 
74
+ function ordered(
75
+ item: TraceItem,
76
+ sourceKey: string,
77
+ ordinal: number,
78
+ ): OrderedTraceItem {
79
+ return { item, ordinal, sourceKey };
80
+ }
81
+
82
+ function compareTraceItems(left: OrderedTraceItem, right: OrderedTraceItem): number {
83
+ const timestamp = right.item.ts.localeCompare(left.item.ts);
84
+ if (timestamp) return timestamp;
85
+ if (left.sourceKey === right.sourceKey) {
86
+ const ordinal = right.ordinal - left.ordinal;
87
+ if (ordinal) return ordinal;
88
+ }
89
+ return SOURCE_RANK[right.item.source] - SOURCE_RANK[left.item.source] ||
90
+ right.item.id.localeCompare(left.item.id);
91
+ }
92
+
60
93
  function latestControlTimestamp(control: RunControlRecord): string {
61
94
  return (
62
95
  control.failed_at ??
@@ -98,125 +131,154 @@ function controlDiagnosticItems(
98
131
  }));
99
132
  }
100
133
 
101
- function traceEventItems(stateDir: string): TraceItem[] {
102
- const read = readJsonlFileResilient<TraceEvent>(join(stateDir, "trace.jsonl"));
103
- return [
104
- ...read.records.slice(-SOURCE_LIMIT).map((event) => ({
105
- id: event.id,
106
- ts: event.ts,
107
- source: sourceForEvent(event),
108
- kind: event.kind,
109
- summary: event.summary ?? event.kind,
110
- ...(event.level ? { level: event.level } : {}),
111
- detail: redact({
112
- ...(event.attention ? { attention: event.attention } : {}),
113
- ...(event.data !== undefined ? { data: event.data } : {}),
114
- }),
115
- } satisfies TraceItem)),
116
- ...diagnosticItems(read.diagnostics),
117
- ];
134
+ function traceEventItems(stateDir: string): OrderedTraceItem[] {
135
+ const read = RunsTrace.readRunTraceJournal(stateDir);
136
+ const retained = read.events.slice(-SOURCE_LIMIT);
137
+ const projected = retained.map(({ event, ordinal }) => ordered({
138
+ id: event.id,
139
+ ts: event.ts,
140
+ source: sourceForEvent(event),
141
+ kind: event.kind,
142
+ summary: event.summary ?? event.kind,
143
+ ...(event.level ? { level: event.level } : {}),
144
+ detail: redact({
145
+ ...(event.attention ? { attention: event.attention } : {}),
146
+ ...(event.data !== undefined ? { data: event.data } : {}),
147
+ }),
148
+ }, "trace", ordinal));
149
+ const diagnostics = diagnosticItems(read.diagnostics).map((item, index) =>
150
+ ordered(item, "trace", read.events.length + index + 1)
151
+ );
152
+ const hasMarker = read.events.some(
153
+ ({ event }) => event.kind === "runtime.trace_compacted",
154
+ );
155
+ if (read.omittedPrefixBytes > 0 && !hasMarker) {
156
+ diagnostics.push(ordered({
157
+ id: "runtime:trace-history-incomplete",
158
+ ts: new Date(0).toISOString(),
159
+ source: "runtime",
160
+ kind: "runtime.trace_history_incomplete",
161
+ summary: "Trace history is incomplete",
162
+ level: "warning",
163
+ detail: {
164
+ omitted_prefix_bytes: read.omittedPrefixBytes,
165
+ read_bytes: read.readBytes,
166
+ retained_events: read.events.length,
167
+ },
168
+ }, "trace", 0));
169
+ }
170
+ return [...projected, ...diagnostics];
118
171
  }
119
172
 
120
- function controlItems(stateDir: string): TraceItem[] {
121
- const read = readJsonlFileResilient<RunControlRecord>(
122
- join(stateDir, "controls.jsonl"),
123
- );
173
+ function controlItems(stateDir: string): OrderedTraceItem[] {
174
+ const read = RunsControls.readRunControlJournalFromStateDir(stateDir);
175
+ const start = Math.max(0, read.records.length - SOURCE_LIMIT);
176
+ const projected = read.records.slice(start).map((control, index) => {
177
+ const value = ControlProjection.projectRunControl(control as RunControlRecord);
178
+ return ordered({
179
+ id: `control:${value.id}`,
180
+ ts: latestControlTimestamp(value),
181
+ source: "control",
182
+ kind: `control.${value.status}`,
183
+ summary: `${value.action} ${value.status}`,
184
+ ...(value.status === "failed" ? { level: "error" as const } : {}),
185
+ detail: value,
186
+ }, "control", start + index + 1);
187
+ });
124
188
  return [
125
- ...read.records.slice(-SOURCE_LIMIT).map((control) => {
126
- const projected = ControlProjection.projectRunControl(control);
127
- return {
128
- id: `control:${projected.id}`,
129
- ts: latestControlTimestamp(projected),
130
- source: "control" as const,
131
- kind: `control.${projected.status}`,
132
- summary: `${projected.action} ${projected.status}`,
133
- ...(projected.status === "failed" ? { level: "error" as const } : {}),
134
- detail: projected,
135
- };
136
- }),
137
- ...controlDiagnosticItems(read.diagnostics),
189
+ ...projected,
190
+ ...controlDiagnosticItems(read.diagnostics).map((item, index) =>
191
+ ordered(item, "control", read.records.length + index + 1)
192
+ ),
138
193
  ];
139
194
  }
140
195
 
141
- function agentItems(stateDir: string): TraceItem[] {
196
+ function agentItems(stateDir: string): OrderedTraceItem[] {
142
197
  return ExecutionSessions.readExecutionTurns(stateDir)
143
198
  .slice(-SOURCE_LIMIT)
144
199
  .map((turn) => {
145
200
  const text = turn.assistantText ?? turn.userText ?? "Agent turn";
146
201
  const summary = text.replaceAll(/\s+/g, " ").trim().slice(0, 160);
147
- return {
202
+ return ordered({
148
203
  id: `agent:${turn.commandId}:${turn.sessionFile}:${turn.index}`,
149
204
  ts: turn.timestamp ?? new Date(0).toISOString(),
150
- source: "agent" as const,
205
+ source: "agent",
151
206
  kind: "agent.turn",
152
207
  summary: summary || "Agent turn",
153
208
  ...(turn.error ? { level: "error" as const } : {}),
154
209
  detail: redact(turn),
155
- };
210
+ }, "agent", turn.index);
156
211
  });
157
212
  }
158
213
 
159
- function processItems(stateDir: string): TraceItem[] {
214
+ function processItems(stateDir: string): OrderedTraceItem[] {
160
215
  return ["stdout", "stderr"].flatMap((stream) => {
161
216
  const path = join(stateDir, `${stream}.log`);
162
217
  if (!existsSync(path)) return [];
163
218
  const stat = statSync(path);
164
219
  const tail = tailFile(path, Limits.DEFAULT_INSPECT_LINES);
165
220
  if (!tail && stat.size === 0) return [];
166
- return [{
221
+ return [ordered({
167
222
  id: `process:${stream}`,
168
223
  ts: stat.mtime.toISOString(),
169
- source: "process" as const,
224
+ source: "process",
170
225
  kind: `process.${stream}`,
171
226
  summary: `${stream} (${stat.size} bytes)`,
172
227
  ...(stream === "stderr" ? { level: "warning" as const } : {}),
173
228
  detail: redact({ bytes: stat.size, path, tail }),
174
- }];
229
+ }, `process:${stream}`, 1)];
175
230
  });
176
231
  }
177
232
 
178
- function resultItems(stateDir: string): TraceItem[] {
233
+ function resultItems(stateDir: string): OrderedTraceItem[] {
179
234
  const path = join(stateDir, "result.json");
180
235
  const read = readJsonFileResilient<Record<string, unknown>>(path, {});
181
- if (Object.keys(read.value).length === 0) return diagnosticItems(read.diagnostics);
236
+ if (Object.keys(read.value).length === 0) {
237
+ return diagnosticItems(read.diagnostics).map((item, index) =>
238
+ ordered(item, "process:result", index)
239
+ );
240
+ }
182
241
  const stat = statSync(path);
183
242
  const failed = Number(read.value.code ?? 0) !== 0;
184
- return [{
243
+ return [ordered({
185
244
  id: "process:result",
186
- ts:
187
- typeof read.value.completedAt === "string"
188
- ? read.value.completedAt
189
- : stat.mtime.toISOString(),
245
+ ts: typeof read.value.completedAt === "string"
246
+ ? read.value.completedAt
247
+ : stat.mtime.toISOString(),
190
248
  source: "process",
191
249
  kind: failed ? "process.failed" : "process.result",
192
250
  summary: failed ? "Run process failed" : "Run process completed",
193
251
  ...(failed ? { level: "error" as const } : {}),
194
252
  detail: redact(read.value),
195
- }, ...diagnosticItems(read.diagnostics)];
253
+ }, "process:result", 1), ...diagnosticItems(read.diagnostics).map(
254
+ (item, index) => ordered(item, "process:result", index + 2),
255
+ )];
196
256
  }
197
257
 
198
258
  function artifactItems(
199
259
  stateDir: string,
200
260
  declarations: Record<string, RunArtifactDeclaration> | undefined,
201
- ): TraceItem[] {
261
+ ): OrderedTraceItem[] {
202
262
  const manifest = resolveArtifactManifest(declarations);
203
263
  if (!manifest) return [];
204
- return Object.entries(manifest).slice(0, SOURCE_LIMIT).map(([name, artifact]) => ({
205
- id: `artifact:${name}`,
206
- ts: artifact.exists && existsSync(artifact.path)
207
- ? statSync(artifact.path).mtime.toISOString()
208
- : new Date(0).toISOString(),
209
- source: "artifact",
210
- kind: artifact.exists ? "artifact.ready" : "artifact.missing",
211
- summary: `${name}: ${artifact.exists ? "ready" : "missing"}`,
212
- ...(artifact.required && !artifact.exists ? { level: "warning" as const } : {}),
213
- detail: redact({
214
- exists: artifact.exists,
215
- path: relative(stateDir, artifact.path).replaceAll("\\", "/"),
216
- required: artifact.required,
217
- ...(artifact.size !== undefined ? { size: artifact.size } : {}),
218
- }),
219
- }));
264
+ return Object.entries(manifest).slice(0, SOURCE_LIMIT).map(
265
+ ([name, artifact], index) => ordered({
266
+ id: `artifact:${name}`,
267
+ ts: artifact.exists && existsSync(artifact.path)
268
+ ? statSync(artifact.path).mtime.toISOString()
269
+ : new Date(0).toISOString(),
270
+ source: "artifact",
271
+ kind: artifact.exists ? "artifact.ready" : "artifact.missing",
272
+ summary: `${name}: ${artifact.exists ? "ready" : "missing"}`,
273
+ ...(artifact.required && !artifact.exists ? { level: "warning" as const } : {}),
274
+ detail: redact({
275
+ exists: artifact.exists,
276
+ path: relative(stateDir, artifact.path).replaceAll("\\", "/"),
277
+ required: artifact.required,
278
+ ...(artifact.size !== undefined ? { size: artifact.size } : {}),
279
+ }),
280
+ }, "artifact", index + 1),
281
+ );
220
282
  }
221
283
 
222
284
  export function projectRunTrace(
@@ -236,9 +298,9 @@ export function projectRunTrace(
236
298
  ...resultItems(stateDir),
237
299
  ...artifactItems(stateDir, options.artifacts),
238
300
  ]
239
- .filter((item) => source === "all" || item.source === source)
240
- .sort((left, right) =>
241
- right.ts.localeCompare(left.ts) || right.id.localeCompare(left.id),
242
- );
243
- return items.slice(0, Math.max(1, Math.min(options.limit ?? 100, 200)));
301
+ .filter(({ item }) => source === "all" || item.source === source)
302
+ .sort(compareTraceItems);
303
+ return items
304
+ .slice(0, Math.max(1, Math.min(options.limit ?? 100, 200)))
305
+ .map(({ item }) => item);
244
306
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-actors",
3
- "version": "0.43.1",
3
+ "version": "0.45.0",
4
4
  "private": false,
5
5
  "description": "Local Actor Kernel for Pi",
6
6
  "keywords": [