@prismatic-io/lux 0.0.2-preview.4 → 0.0.2-preview.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.
Files changed (138) hide show
  1. package/lib/assertions/authoring.d.ts +2 -2
  2. package/lib/assertions/authoring.d.ts.map +1 -1
  3. package/lib/assertions/authoring.js.map +1 -1
  4. package/lib/assertions/core/index.d.ts.map +1 -1
  5. package/lib/assertions/core/index.js +2 -1
  6. package/lib/assertions/core/index.js.map +1 -1
  7. package/lib/assertions/core/resource-checks.d.ts +19 -0
  8. package/lib/assertions/core/resource-checks.d.ts.map +1 -1
  9. package/lib/assertions/core/resource-checks.js +32 -0
  10. package/lib/assertions/core/resource-checks.js.map +1 -1
  11. package/lib/cli/program.d.ts.map +1 -1
  12. package/lib/cli/program.js +6 -65
  13. package/lib/cli/program.js.map +1 -1
  14. package/lib/cli/view.d.ts +13 -0
  15. package/lib/cli/view.d.ts.map +1 -0
  16. package/lib/cli/view.js +126 -0
  17. package/lib/cli/view.js.map +1 -0
  18. package/lib/core/action-events.d.ts +29 -0
  19. package/lib/core/action-events.d.ts.map +1 -0
  20. package/lib/core/action-events.js +51 -0
  21. package/lib/core/action-events.js.map +1 -0
  22. package/lib/core/agent-activity.d.ts +49 -0
  23. package/lib/core/agent-activity.d.ts.map +1 -0
  24. package/lib/core/agent-activity.js +206 -0
  25. package/lib/core/agent-activity.js.map +1 -0
  26. package/lib/core/agent-events.d.ts +26 -0
  27. package/lib/core/agent-events.d.ts.map +1 -0
  28. package/lib/core/agent-events.js +13 -0
  29. package/lib/core/agent-events.js.map +1 -0
  30. package/lib/core/driver-metrics.d.ts +98 -0
  31. package/lib/core/driver-metrics.d.ts.map +1 -0
  32. package/lib/core/driver-metrics.js +36 -0
  33. package/lib/core/driver-metrics.js.map +1 -0
  34. package/lib/core/events.d.ts +34 -0
  35. package/lib/core/events.d.ts.map +1 -1
  36. package/lib/core/events.js +11 -2
  37. package/lib/core/events.js.map +1 -1
  38. package/lib/core/experiment.d.ts +19 -0
  39. package/lib/core/experiment.d.ts.map +1 -1
  40. package/lib/core/experiment.js +2 -0
  41. package/lib/core/experiment.js.map +1 -1
  42. package/lib/core/index.d.ts +4 -0
  43. package/lib/core/index.d.ts.map +1 -1
  44. package/lib/core/index.js +4 -0
  45. package/lib/core/index.js.map +1 -1
  46. package/lib/core/run.d.ts +149 -0
  47. package/lib/core/run.d.ts.map +1 -1
  48. package/lib/core/run.js +9 -0
  49. package/lib/core/run.js.map +1 -1
  50. package/lib/core/wire.d.ts +11 -0
  51. package/lib/core/wire.d.ts.map +1 -1
  52. package/lib/drivers/claude-code/parse-events.d.ts.map +1 -1
  53. package/lib/drivers/claude-code/parse-events.js +73 -8
  54. package/lib/drivers/claude-code/parse-events.js.map +1 -1
  55. package/lib/drivers/codex/app-events.d.ts +9 -2
  56. package/lib/drivers/codex/app-events.d.ts.map +1 -1
  57. package/lib/drivers/codex/app-events.js +93 -12
  58. package/lib/drivers/codex/app-events.js.map +1 -1
  59. package/lib/drivers/codex/index.d.ts.map +1 -1
  60. package/lib/drivers/codex/index.js +10 -14
  61. package/lib/drivers/codex/index.js.map +1 -1
  62. package/lib/index.d.ts +6 -2
  63. package/lib/index.d.ts.map +1 -1
  64. package/lib/index.js +6 -2
  65. package/lib/index.js.map +1 -1
  66. package/lib/orchestrator/compare.d.ts +13 -1
  67. package/lib/orchestrator/compare.d.ts.map +1 -1
  68. package/lib/orchestrator/compare.js +12 -0
  69. package/lib/orchestrator/compare.js.map +1 -1
  70. package/lib/orchestrator/experiment-contracts.d.ts +19 -0
  71. package/lib/orchestrator/experiment-contracts.d.ts.map +1 -1
  72. package/lib/orchestrator/experiment-evaluation.d.ts.map +1 -1
  73. package/lib/orchestrator/experiment-evaluation.js +17 -0
  74. package/lib/orchestrator/experiment-evaluation.js.map +1 -1
  75. package/lib/orchestrator/experiment-report.d.ts +19 -0
  76. package/lib/orchestrator/experiment-report.d.ts.map +1 -1
  77. package/lib/orchestrator/experiment-runs.d.ts +38 -0
  78. package/lib/orchestrator/experiment-runs.d.ts.map +1 -1
  79. package/lib/orchestrator/index.d.ts +1 -1
  80. package/lib/orchestrator/index.d.ts.map +1 -1
  81. package/lib/orchestrator/index.js +1 -1
  82. package/lib/orchestrator/index.js.map +1 -1
  83. package/lib/orchestrator/list-runs.d.ts +7 -1
  84. package/lib/orchestrator/list-runs.d.ts.map +1 -1
  85. package/lib/orchestrator/list-runs.js +50 -6
  86. package/lib/orchestrator/list-runs.js.map +1 -1
  87. package/lib/orchestrator/run-execution.d.ts.map +1 -1
  88. package/lib/orchestrator/run-execution.js +17 -3
  89. package/lib/orchestrator/run-execution.js.map +1 -1
  90. package/lib/orchestrator/suite-compare.d.ts +34 -0
  91. package/lib/orchestrator/suite-compare.d.ts.map +1 -1
  92. package/lib/orchestrator/suite-summary.d.ts +68 -0
  93. package/lib/orchestrator/suite-summary.d.ts.map +1 -1
  94. package/lib/orchestrator/suite-summary.js +45 -7
  95. package/lib/orchestrator/suite-summary.js.map +1 -1
  96. package/lib/previewer/artifacts.d.ts +25 -0
  97. package/lib/previewer/artifacts.d.ts.map +1 -0
  98. package/lib/previewer/artifacts.js +180 -0
  99. package/lib/previewer/artifacts.js.map +1 -0
  100. package/lib/previewer/server.d.ts +105 -0
  101. package/lib/previewer/server.d.ts.map +1 -0
  102. package/lib/previewer/server.js +542 -0
  103. package/lib/previewer/server.js.map +1 -0
  104. package/package.json +2 -1
  105. package/skills/lux/references/eval-authoring.md +4 -2
  106. package/skills/lux-answerer/SKILL.md +1 -1
  107. package/src/assertions/authoring.ts +2 -0
  108. package/src/assertions/core/index.ts +2 -0
  109. package/src/assertions/core/resource-checks.ts +35 -0
  110. package/src/cli/program.ts +5 -91
  111. package/src/cli/view.ts +168 -0
  112. package/src/core/action-events.ts +59 -0
  113. package/src/core/agent-activity.ts +236 -0
  114. package/src/core/agent-events.ts +17 -0
  115. package/src/core/driver-metrics.ts +51 -0
  116. package/src/core/events.ts +13 -2
  117. package/src/core/experiment.ts +2 -0
  118. package/src/core/index.ts +4 -0
  119. package/src/core/run.ts +9 -0
  120. package/src/drivers/claude-code/parse-events.ts +98 -12
  121. package/src/drivers/codex/app-events.ts +116 -16
  122. package/src/drivers/codex/index.ts +16 -14
  123. package/src/index.ts +40 -0
  124. package/src/orchestrator/compare.ts +18 -0
  125. package/src/orchestrator/experiment-evaluation.ts +23 -0
  126. package/src/orchestrator/index.ts +1 -0
  127. package/src/orchestrator/list-runs.ts +66 -5
  128. package/src/orchestrator/run-execution.ts +19 -2
  129. package/src/orchestrator/suite-summary.ts +101 -8
  130. package/src/previewer/artifacts.ts +221 -0
  131. package/src/previewer/server.ts +765 -0
  132. package/viewer/app.css +2075 -0
  133. package/viewer/app.js +606 -0
  134. package/viewer/artifact-view.js +48 -0
  135. package/viewer/charts.js +86 -0
  136. package/viewer/detail.js +1214 -0
  137. package/viewer/format.js +129 -0
  138. package/viewer/index.html +151 -0
@@ -1,5 +1,7 @@
1
1
  import {
2
+ type AgentAttribution,
2
3
  type ParsedDriverEvent,
4
+ type Progress,
3
5
  type TokenUsage,
4
6
  TURN_GLYPHS,
5
7
  toolResultProgress,
@@ -20,8 +22,98 @@ const resultText = (content: unknown): string => {
20
22
  .join("\n");
21
23
  };
22
24
 
25
+ const agentCompletionStatus = (status: unknown): "completed" | "failed" | "cancelled" => {
26
+ if (status === "failed") return "failed";
27
+ if (status === "cancelled" || status === "interrupted") return "cancelled";
28
+ return "completed";
29
+ };
30
+
31
+ export const appAgentLifecycleProgress = (
32
+ item: RpcRecord,
33
+ phase: "started" | "completed",
34
+ actor?: AgentAttribution,
35
+ ts = Date.now(),
36
+ ): Progress[] => {
37
+ if (item.type !== "collabAgentToolCall" || typeof item.id !== "string") return [];
38
+ const receiverThreadIds = Array.isArray(item.receiverThreadIds)
39
+ ? item.receiverThreadIds.filter((value): value is string => typeof value === "string")
40
+ : [];
41
+ const completionStatus = agentCompletionStatus(item.status);
42
+ return receiverThreadIds.map((receiverThreadId) =>
43
+ phase === "started"
44
+ ? {
45
+ id: `agent-spawn-${item.id}-${receiverThreadId}`,
46
+ kind: "agent-spawn",
47
+ payload: {
48
+ agent: {
49
+ id: receiverThreadId,
50
+ role: "subagent",
51
+ parentId: actor?.id ?? "codex",
52
+ spawnToolUseId: item.id as string,
53
+ ...(typeof item.model === "string" ? { model: item.model } : {}),
54
+ },
55
+ ...(typeof item.prompt === "string" ? { prompt: item.prompt } : {}),
56
+ },
57
+ ...(actor ? { agent: actor } : {}),
58
+ ts,
59
+ }
60
+ : {
61
+ id: `agent-completed-${item.id}-${receiverThreadId}`,
62
+ kind: "agent-completed",
63
+ payload: {
64
+ agentId: receiverThreadId,
65
+ status: completionStatus,
66
+ },
67
+ ...(actor ? { agent: actor } : {}),
68
+ ts,
69
+ },
70
+ );
71
+ };
72
+
23
73
  export type RpcId = string | number;
24
74
  export type RpcRecord = Record<string, unknown>;
75
+
76
+ export class CodexSubagentTracker {
77
+ private readonly spawnToolByThread = new Map<string, string>();
78
+ private readonly parentAgentByThread = new Map<string, string>();
79
+
80
+ record(
81
+ item: RpcRecord | null,
82
+ spawningThreadId: string | undefined,
83
+ mainThreadId: string | null,
84
+ ): void {
85
+ if (item?.type !== "collabAgentToolCall" || typeof item.id !== "string") return;
86
+ const receiverThreadIds = Array.isArray(item.receiverThreadIds)
87
+ ? item.receiverThreadIds.filter((value): value is string => typeof value === "string")
88
+ : [];
89
+ for (const receiverThreadId of receiverThreadIds) {
90
+ this.spawnToolByThread.set(receiverThreadId, item.id);
91
+ if (spawningThreadId) {
92
+ this.parentAgentByThread.set(
93
+ receiverThreadId,
94
+ spawningThreadId === mainThreadId ? "codex" : spawningThreadId,
95
+ );
96
+ }
97
+ }
98
+ }
99
+
100
+ attribution(
101
+ threadId: string | undefined,
102
+ mainThreadId: string | null,
103
+ ): AgentAttribution | undefined {
104
+ if (!threadId) return undefined;
105
+ const spawnToolUseId = this.spawnToolByThread.get(threadId);
106
+ if (!spawnToolUseId) {
107
+ return { id: threadId === mainThreadId ? "codex" : threadId, role: "main" };
108
+ }
109
+ return {
110
+ id: threadId,
111
+ role: "subagent",
112
+ parentId: this.parentAgentByThread.get(threadId) ?? mainThreadId ?? "codex",
113
+ spawnToolUseId,
114
+ };
115
+ }
116
+ }
25
117
  export type AppTurnFacets = {
26
118
  tools: string[];
27
119
  asked: boolean;
@@ -131,8 +223,10 @@ const appItemResultText = (item: RpcRecord, error: RpcRecord | null, content: un
131
223
  export const appItemProgress = (
132
224
  item: RpcRecord,
133
225
  phase: "started" | "completed",
134
- parentToolUseId?: string,
226
+ attribution?: AgentAttribution | string,
135
227
  ): ParsedDriverEvent | null => {
228
+ const agent = typeof attribution === "string" ? undefined : attribution;
229
+ const parentToolUseId = typeof attribution === "string" ? attribution : agent?.spawnToolUseId;
136
230
  const id = typeof item.id === "string" ? item.id : `${String(item.type)}-${Date.now()}`;
137
231
  if (item.type === "agentMessage" && phase === "completed" && typeof item.text === "string") {
138
232
  return {
@@ -142,6 +236,7 @@ export const appItemProgress = (
142
236
  kind: "agent-message",
143
237
  payload: { text: item.text, ...(parentToolUseId ? { parentToolUseId } : {}) },
144
238
  ts: Date.now(),
239
+ ...(agent ? { agent } : {}),
145
240
  },
146
241
  };
147
242
  }
@@ -162,6 +257,7 @@ export const appItemProgress = (
162
257
  ...(parentToolUseId ? { parentToolUseId } : {}),
163
258
  },
164
259
  ts: Date.now(),
260
+ ...(agent ? { agent } : {}),
165
261
  },
166
262
  };
167
263
  }
@@ -184,6 +280,7 @@ export const appItemProgress = (
184
280
  ...(parentToolUseId ? { parentToolUseId } : {}),
185
281
  },
186
282
  ts: Date.now(),
283
+ ...(agent ? { agent } : {}),
187
284
  },
188
285
  };
189
286
  }
@@ -199,20 +296,23 @@ export const appItemProgress = (
199
296
  error !== null;
200
297
  return {
201
298
  tag: "progress",
202
- progress: toolResultProgress(
203
- 0,
204
- {
205
- name: tool,
206
- toolUseId: id,
207
- content,
208
- ...(asRecord(structuredContent)
209
- ? { structuredContent: asRecord(structuredContent) ?? {} }
210
- : {}),
211
- isError,
212
- text: appItemResultText(item, error, content),
213
- ...(parentToolUseId ? { parentToolUseId } : {}),
214
- },
215
- Date.now(),
216
- ),
299
+ progress: {
300
+ ...toolResultProgress(
301
+ 0,
302
+ {
303
+ name: tool,
304
+ toolUseId: id,
305
+ content,
306
+ ...(asRecord(structuredContent)
307
+ ? { structuredContent: asRecord(structuredContent) ?? {} }
308
+ : {}),
309
+ isError,
310
+ text: appItemResultText(item, error, content),
311
+ ...(parentToolUseId ? { parentToolUseId } : {}),
312
+ },
313
+ Date.now(),
314
+ ),
315
+ ...(agent ? { agent } : {}),
316
+ },
217
317
  };
218
318
  };
@@ -24,11 +24,13 @@ import { copyRegularFileNoFollow } from "../shared/secure-copy.js";
24
24
  import { subprocessDriver } from "../subprocess/index.js";
25
25
  import {
26
26
  type AppTurnFacets,
27
+ appAgentLifecycleProgress,
27
28
  appItemProgress,
28
29
  appItemToolName,
29
30
  appTurnLabel,
30
31
  appUsage,
31
32
  asRecord,
33
+ CodexSubagentTracker,
32
34
  messageOf,
33
35
  type RpcId,
34
36
  type RpcRecord,
@@ -316,7 +318,7 @@ class CodexAppServerDriverImpl implements AgentDriver {
316
318
  { resolve: (result: RpcRecord) => void; reject: (error: Error) => void }
317
319
  >();
318
320
  private readonly interrupts = new Map<string, { id: RpcId; method: string; params: RpcRecord }>();
319
- private readonly subagentParents = new Map<string, string>();
321
+ private readonly subagents = new CodexSubagentTracker();
320
322
  private readonly turnFacets = new Map<string, AppTurnFacets>();
321
323
  private interruptCount = 0;
322
324
  private idleTimer: NodeJS.Timeout | null = null;
@@ -702,22 +704,22 @@ class CodexAppServerDriverImpl implements AgentDriver {
702
704
  const toolName = item ? appItemToolName(item) : null;
703
705
  if (toolName && !facets.tools.includes(toolName)) facets.tools.push(toolName);
704
706
  if (turnId) this.turnFacets.set(turnId, facets);
705
- this.recordSubagentParents(item);
706
- const parentToolUseId =
707
- typeof params.threadId === "string" ? this.subagentParents.get(params.threadId) : undefined;
707
+ const threadId = typeof params.threadId === "string" ? params.threadId : undefined;
708
+ this.subagents.record(item, threadId, this.threadId);
709
+ const agent = this.subagents.attribution(threadId, this.threadId);
708
710
  const event = item
709
- ? appItemProgress(item, method === "item/started" ? "started" : "completed", parentToolUseId)
711
+ ? appItemProgress(item, method === "item/started" ? "started" : "completed", agent)
710
712
  : null;
711
713
  if (event?.tag === "progress") await this.emitter.emit("progress", event.progress);
712
- }
713
-
714
- private recordSubagentParents(item: RpcRecord | null): void {
715
- if (item?.type !== "collabAgentToolCall" || typeof item.id !== "string") return;
716
- const receiverThreadIds = Array.isArray(item.receiverThreadIds)
717
- ? item.receiverThreadIds.filter((value): value is string => typeof value === "string")
718
- : [];
719
- for (const receiverThreadId of receiverThreadIds) {
720
- this.subagentParents.set(receiverThreadId, item.id);
714
+ if (item) {
715
+ for (const lifecycle of appAgentLifecycleProgress(
716
+ item,
717
+ method === "item/started" ? "started" : "completed",
718
+ agent,
719
+ event?.tag === "progress" ? event.progress.ts : Date.now(),
720
+ )) {
721
+ await this.emitter.emit("progress", lifecycle);
722
+ }
721
723
  }
722
724
  }
723
725
 
package/src/index.ts CHANGED
@@ -87,6 +87,8 @@ export {
87
87
  registerCoreAssertions,
88
88
  runSucceededAssertion,
89
89
  subagentToolCalls,
90
+ type TimeToFirstActionUnderConfig,
91
+ TimeToFirstActionUnderConfigSchema,
90
92
  type TokensUnderConfig,
91
93
  TokensUnderConfigSchema,
92
94
  type ToolCall,
@@ -104,6 +106,7 @@ export {
104
106
  ToolResultIsErrorConfigSchema,
105
107
  type ToolResultJsonPointerEqualsConfig,
106
108
  ToolResultJsonPointerEqualsConfigSchema,
109
+ timeToFirstActionUnderAssertion,
107
110
  tokensUnderAssertion,
108
111
  toolCallCountAssertion,
109
112
  toolCalledAssertion,
@@ -141,6 +144,28 @@ export {
141
144
  rubricAssertion,
142
145
  } from "./assertions/rubric/index.js";
143
146
  // Authoring and extension contracts.
147
+ export {
148
+ type ActionCategory,
149
+ ActionCategorySchema,
150
+ type ActionProgressPayload,
151
+ ActionProgressPayloadSchema,
152
+ type ActionTiming,
153
+ ActionTimingSchema,
154
+ actionTiming,
155
+ } from "./core/action-events.js";
156
+ export {
157
+ type AgentActivity,
158
+ AgentActivitySchema,
159
+ summarizeAgentActivity,
160
+ } from "./core/agent-activity.js";
161
+ export {
162
+ AGENT_COMPLETED_KIND,
163
+ AGENT_SPAWN_KIND,
164
+ type AgentCompletedPayload,
165
+ AgentCompletedPayloadSchema,
166
+ type AgentSpawnPayload,
167
+ AgentSpawnPayloadSchema,
168
+ } from "./core/agent-events.js";
144
169
  export {
145
170
  defineGraderAnnotations,
146
171
  type GraderAnnotation,
@@ -227,8 +252,23 @@ export {
227
252
  ResolvedFixturesSchema,
228
253
  type StartContext,
229
254
  } from "./core/driver.js";
255
+ export {
256
+ type DriverMetric,
257
+ type DriverMetricDescriptor,
258
+ DriverMetricDescriptorSchema,
259
+ DriverMetricSchema,
260
+ type DriverMetrics,
261
+ DriverMetricsSchema,
262
+ type DriverMetricUnit,
263
+ DriverMetricUnitSchema,
264
+ normalizeDriverMetrics,
265
+ type ReportedDriverMetrics,
266
+ ReportedDriverMetricsSchema,
267
+ } from "./core/driver-metrics.js";
230
268
  export { type EventHandler, TypedEmitter } from "./core/emitter.js";
231
269
  export {
270
+ type AgentAttribution,
271
+ AgentAttributionSchema,
232
272
  type Answer,
233
273
  AnswerSchema,
234
274
  ApproveAnswerSchema,
@@ -2,6 +2,9 @@ import { createHash } from "node:crypto";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import {
5
+ type ActionTiming,
6
+ type AgentActivity,
7
+ type DriverMetrics,
5
8
  emptyRunUsage,
6
9
  type GradingReport,
7
10
  GradingReportSchema,
@@ -48,6 +51,9 @@ export type RunComparison = {
48
51
  exitReason: { a: string; b: string };
49
52
  durationMsA: number | null;
50
53
  durationMsB: number | null;
54
+ actionTiming: { a: ActionTiming; b: ActionTiming };
55
+ driverMetrics: { a: DriverMetrics; b: DriverMetrics };
56
+ agentActivity: { a: AgentActivity[]; b: AgentActivity[] };
51
57
  interruptCounts: {
52
58
  a: { ask: number; approve: number; custom: number };
53
59
  b: { ask: number; approve: number; custom: number };
@@ -260,6 +266,18 @@ export const compareRuns = async (input: ComparisonInput): Promise<RunComparison
260
266
  },
261
267
  durationMsA: durationFromMeta(a.runJson),
262
268
  durationMsB: durationFromMeta(b.runJson),
269
+ actionTiming: {
270
+ a: a.runJson?.actionTiming ?? {},
271
+ b: b.runJson?.actionTiming ?? {},
272
+ },
273
+ driverMetrics: {
274
+ a: a.runJson?.driverMetrics ?? {},
275
+ b: b.runJson?.driverMetrics ?? {},
276
+ },
277
+ agentActivity: {
278
+ a: a.runJson?.agentActivity ?? [],
279
+ b: b.runJson?.agentActivity ?? [],
280
+ },
263
281
  interruptCounts: {
264
282
  a: a.runJson?.interruptCounts ?? { ask: 0, approve: 0, custom: 0 },
265
283
  b: b.runJson?.interruptCounts ?? { ask: 0, approve: 0, custom: 0 },
@@ -101,6 +101,23 @@ const evaluationMetrics = (
101
101
  ),
102
102
  });
103
103
 
104
+ const evaluationMetricDefinitions = (summary: SuiteSummary) =>
105
+ Object.fromEntries(
106
+ Object.entries(summary.metrics).flatMap(([name, metric]) => {
107
+ if (!metric.unit && !metric.label && !metric.better) return [];
108
+ return [
109
+ [
110
+ name,
111
+ {
112
+ unit: metric.unit ?? "number",
113
+ better: metric.better ?? "neutral",
114
+ ...(metric.label ? { label: metric.label } : {}),
115
+ },
116
+ ],
117
+ ];
118
+ }),
119
+ );
120
+
104
121
  const summaryUsage = (summary: SuiteSummary) =>
105
122
  summary.samples.reduce(
106
123
  (usage, sample) =>
@@ -277,6 +294,7 @@ const agentEvaluationFromSuite = (input: {
277
294
  summary.cases.map((entry) => [entry.canonicalCaseId, entry.passRate]),
278
295
  ),
279
296
  metrics: evaluationMetrics(summary, candidate),
297
+ metricDefinitions: evaluationMetricDefinitions(summary),
280
298
  samples: evaluationSamples(summary),
281
299
  metricCalls: summary.samples.length,
282
300
  judgeCalls: results.reduce(
@@ -352,6 +370,10 @@ const aggregateAgentUnits = (
352
370
  .map(([, values]) => average(values)),
353
371
  ]),
354
372
  );
373
+ const metricDefinitions = Object.assign(
374
+ {},
375
+ ...units.map(({ evaluation }) => evaluation.metricDefinitions ?? {}),
376
+ );
355
377
  return {
356
378
  candidateId: candidate.id,
357
379
  profileId,
@@ -367,6 +389,7 @@ const aggregateAgentUnits = (
367
389
  name === "subject.bytes" ? candidate.sourceBytes : average(values),
368
390
  ]),
369
391
  ),
392
+ metricDefinitions,
370
393
  samples,
371
394
  metricCalls: units.reduce((total, unit) => total + (unit.evaluation.metricCalls ?? 0), 0),
372
395
  judgeCalls: units.reduce((total, unit) => total + (unit.evaluation.judgeCalls ?? 0), 0),
@@ -122,6 +122,7 @@ export {
122
122
  formatRunsTable,
123
123
  type ListedRun,
124
124
  listRuns,
125
+ loadRunArtifacts,
125
126
  loadRunDetail,
126
127
  type RunDetail,
127
128
  } from "./list-runs.js";
@@ -2,6 +2,11 @@ import { existsSync } from "node:fs";
2
2
  import { constants, lstat, open, readdir } from "node:fs/promises";
3
3
  import { basename, join, relative, sep } from "node:path";
4
4
  import {
5
+ ActionProgressPayloadSchema,
6
+ AgentCompletedPayloadSchema,
7
+ AgentSpawnPayloadSchema,
8
+ type Artifact,
9
+ ArtifactSchema,
5
10
  type GradingReport,
6
11
  GradingReportSchema,
7
12
  harnessUsage,
@@ -31,6 +36,11 @@ export type ListedRun = {
31
36
  startedAt: string | null;
32
37
  endedAt: string | null;
33
38
  durationMs: number | null;
39
+ timeToFirstActionMs: number | null;
40
+ timeToFirstMutationMs: number | null;
41
+ timeToFirstDurableMutationMs: number | null;
42
+ driverMetrics: NonNullable<RunMetadata["driverMetrics"]>;
43
+ subagentCount: number;
34
44
  casePassed: boolean | null;
35
45
  caseScore: number | null;
36
46
  assertionPassed: number;
@@ -63,6 +73,12 @@ const summarizeRun = async (
63
73
  startedAt,
64
74
  endedAt,
65
75
  durationMs,
76
+ timeToFirstActionMs: meta?.actionTiming?.timeToFirstActionMs ?? null,
77
+ timeToFirstMutationMs: meta?.actionTiming?.timeToFirstMutationMs ?? null,
78
+ timeToFirstDurableMutationMs: meta?.actionTiming?.timeToFirstDurableMutationMs ?? null,
79
+ driverMetrics: meta?.driverMetrics ?? {},
80
+ subagentCount:
81
+ meta?.agentActivity?.filter((activity) => activity.agent.role === "subagent").length ?? 0,
66
82
  casePassed: grading?.casePassed ?? null,
67
83
  caseScore: grading?.caseScore ?? null,
68
84
  assertionPassed: passing,
@@ -127,7 +143,7 @@ export const listRuns = async (runsRoot: string): Promise<ListedRun[]> => {
127
143
  export const formatRunsTable = (runs: ListedRun[]): string => {
128
144
  if (runs.length === 0) return "(no runs found)";
129
145
  const rows: string[][] = [
130
- ["status", "case", "driver", "ts", "exit", "score", "duration"],
146
+ ["status", "case", "driver", "agents", "ts", "exit", "score", "duration"],
131
147
  ...runs.map((r) => {
132
148
  let status = "ungraded";
133
149
  if (r.casePassed === true) status = "PASS";
@@ -137,7 +153,8 @@ export const formatRunsTable = (runs: ListedRun[]): string => {
137
153
  ? `${(r.caseScore * 100).toFixed(0)}% (${r.assertionPassed}/${r.assertionTotal})`
138
154
  : "—";
139
155
  const duration = r.durationMs !== null ? `${(r.durationMs / 1000).toFixed(1)}s` : "—";
140
- return [status, r.caseSlug, r.driverName, r.ts, r.exitReason ?? "?", score, duration];
156
+ const agents = r.subagentCount > 0 ? `1+${r.subagentCount}` : "1";
157
+ return [status, r.caseSlug, r.driverName, agents, r.ts, r.exitReason ?? "?", score, duration];
141
158
  }),
142
159
  ];
143
160
  const header = rows[0] ?? [];
@@ -156,6 +173,9 @@ export type RunDetail = {
156
173
  artifacts: string[];
157
174
  };
158
175
 
176
+ export const loadRunArtifacts = async (runDir: string): Promise<Artifact[]> =>
177
+ (await safeReadJson(join(runDir, "artifacts.json"), ArtifactSchema.array())) ?? [];
178
+
159
179
  const readEventsJsonl = async (path: string): Promise<RunEvent[]> => {
160
180
  if (!existsSync(path)) return [];
161
181
  if ((await lstat(path)).isSymbolicLink()) {
@@ -291,6 +311,27 @@ const appendMetadataDetail = (lines: string[], detail: RunDetail): void => {
291
311
  `phases: agent=${secs(phases.agent)} collect=${secs(phases.collect)} grading=${secs(phases.grading)}`,
292
312
  );
293
313
  }
314
+ const actions = detail.metadata.actionTiming;
315
+ if (actions) {
316
+ const value = (ms: number | undefined): string => (ms === undefined ? "—" : `${ms}ms`);
317
+ lines.push(
318
+ `first action: any=${value(actions.timeToFirstActionMs)} mutation=${value(actions.timeToFirstMutationMs)} durable-mutation=${value(actions.timeToFirstDurableMutationMs)}`,
319
+ );
320
+ }
321
+ const activities = detail.metadata.agentActivity ?? [];
322
+ const subagents = activities.filter((activity) => activity.agent.role === "subagent");
323
+ if (subagents.length > 0) {
324
+ lines.push(`subagents: ${subagents.length}`);
325
+ for (const activity of subagents) {
326
+ const label = activity.agent.name
327
+ ? `${activity.agent.name} (${activity.agent.id})`
328
+ : activity.agent.id;
329
+ const agentActions = activity.actions;
330
+ lines.push(
331
+ ` ${label}: ${activity.status}, ${(activity.durationMs / 1000).toFixed(1)}s, events=${activity.eventCount}, tokens=${totalTokens(activity.usage)}, tools=${activity.toolCalls.total} (${activity.toolCalls.succeeded} ok/${activity.toolCalls.failed} failed/${activity.toolCalls.unresolved} unresolved), actions=${agentActions.total}, mutations=${agentActions.mutations}`,
332
+ );
333
+ }
334
+ }
294
335
  }
295
336
  };
296
337
 
@@ -361,17 +402,37 @@ const formatEventLine = (event: RunEvent): string => {
361
402
  }`;
362
403
  }
363
404
  return `responded(custom): ${JSON.stringify(event.payload).slice(0, 80)}`;
364
- case "progress":
405
+ case "progress": {
406
+ const agent = event.agent?.role === "subagent" ? ` [subagent ${event.agent.id}]` : "";
407
+ if (event.kind === "agent-spawn") {
408
+ const payload = AgentSpawnPayloadSchema.safeParse(event.payload).data;
409
+ return payload
410
+ ? `agent-spawn: ${payload.agent.name ?? payload.agent.id} (${payload.agent.id})`
411
+ : "agent-spawn: malformed";
412
+ }
413
+ if (event.kind === "agent-completed") {
414
+ const payload = AgentCompletedPayloadSchema.safeParse(event.payload).data;
415
+ return payload
416
+ ? `agent-completed: ${payload.agentId} (${payload.status})`
417
+ : "agent-completed: malformed";
418
+ }
419
+ if (event.kind === "action") {
420
+ const payload = ActionProgressPayloadSchema.safeParse(event.payload).data;
421
+ return payload
422
+ ? `action${agent}: ${payload.name} (${payload.category})`
423
+ : "action: malformed";
424
+ }
365
425
  if (event.kind === "tool-call") {
366
426
  const payload = ToolCallPayloadSchema.safeParse(event.payload).data;
367
- return payload ? `tool-call: ${payload.name}` : "tool-call: malformed";
427
+ return payload ? `tool-call${agent}: ${payload.name}` : "tool-call: malformed";
368
428
  }
369
429
  if (event.kind === "tool-result") {
370
430
  const payload = ToolResultPayloadSchema.safeParse(event.payload).data;
371
431
  if (!payload) return "tool-result: malformed";
372
- return `tool-result: ${payload.name} (${payload.isError ? "failed" : "succeeded"})`;
432
+ return `tool-result${agent}: ${payload.name} (${payload.isError ? "failed" : "succeeded"})`;
373
433
  }
374
434
  return `progress(${event.kind})`;
435
+ }
375
436
  case "lifecycle":
376
437
  return `lifecycle: ${event.stage}`;
377
438
  case "ready":
@@ -10,6 +10,7 @@ import {
10
10
  AnswererModelCallAccountingSchema,
11
11
  type AnswerHistoryEntry,
12
12
  type Artifact,
13
+ actionTiming,
13
14
  type EvalCase,
14
15
  emptyUsage,
15
16
  type FixturesRef,
@@ -18,6 +19,7 @@ import {
18
19
  guardRunObserver,
19
20
  inspectRunArtifact,
20
21
  type LifecycleStage,
22
+ normalizeDriverMetrics,
21
23
  type OperationalFailure,
22
24
  type PhaseDurations,
23
25
  type ReadyState,
@@ -28,6 +30,7 @@ import {
28
30
  type RunMetadata,
29
31
  type RunProvenance,
30
32
  type RunUsage,
33
+ summarizeAgentActivity,
31
34
  summarizeToolOutcomes,
32
35
  sumUsage,
33
36
  type TokenUsage,
@@ -183,6 +186,11 @@ const modelAccountingFromSummary = (
183
186
  ): { modelCalls: number; unpricedModelCalls?: number | undefined } | null =>
184
187
  SummaryModelAccountingSchema.safeParse(summary).data ?? null;
185
188
 
189
+ const metricsFromSummary = (summary: unknown) => {
190
+ if (!summary || typeof summary !== "object") return {};
191
+ return normalizeDriverMetrics((summary as { metrics?: unknown }).metrics);
192
+ };
193
+
186
194
  const artifactMeasurements = async (
187
195
  artifacts: readonly Artifact[],
188
196
  runDir: string,
@@ -390,6 +398,7 @@ class RunExecution implements RunLifecycleEffects<RunResult> {
390
398
  metadata.exitReason = "error";
391
399
  metadata.operationalFailure = { phase: operationalPhase, reason };
392
400
  metadata.phaseDurationMs = phaseDurations(this.runDir.recorded);
401
+ metadata.actionTiming = actionTiming(this.runDir.recorded);
393
402
  await this.runDir.writeJson("run.json", metadata);
394
403
  await this.args.request.recordPreGradingAccounting?.(metadata);
395
404
  await this.args.request.recordModelAttemptsComplete?.(this.subjectAttemptIds);
@@ -407,6 +416,7 @@ class RunExecution implements RunLifecycleEffects<RunResult> {
407
416
  this.lifecycle("collect-end");
408
417
  await this.runDir.events.drain();
409
418
  const completed = await this.completedAgentRun();
419
+ completed.metadata.actionTiming = actionTiming(this.runDir.recorded);
410
420
  this.metadata = completed.metadata;
411
421
  this.usage = completed.usage;
412
422
  this.run = completed.run;
@@ -430,6 +440,7 @@ class RunExecution implements RunLifecycleEffects<RunResult> {
430
440
  grading.assertions.flatMap((assertion) => (assertion.usage ? [assertion.usage] : [])),
431
441
  );
432
442
  metadata.phaseDurationMs = phaseDurations(this.runDir.recorded);
443
+ metadata.actionTiming = actionTiming(this.runDir.recorded);
433
444
  await this.runDir.events.drain();
434
445
  await this.runDir.writeJson("run.json", metadata);
435
446
  await this.runDir.writeJson("artifacts.json", this.collection.artifacts);
@@ -506,6 +517,7 @@ class RunExecution implements RunLifecycleEffects<RunResult> {
506
517
  private fallbackFailureMetadata(): RunMetadata {
507
518
  const failedAt = new Date();
508
519
  const answererAccounting = readAnswererModelAccounting(this.args.answerer);
520
+ const ready = this.ready ?? { agentId: "start-failed" };
509
521
  return {
510
522
  schemaVersion: 1,
511
523
  luxVersion: this.args.luxVersion,
@@ -514,7 +526,7 @@ class RunExecution implements RunLifecycleEffects<RunResult> {
514
526
  agentEndedAt: failedAt.toISOString(),
515
527
  endedAt: failedAt.toISOString(),
516
528
  exitReason: "error",
517
- driver: { name: this.args.driverName, ...(this.ready ?? { agentId: "start-failed" }) },
529
+ driver: { name: this.args.driverName, ...ready },
518
530
  answerer: answererMetadata(this.args.answererName, this.args.answererConfig),
519
531
  usage: {
520
532
  agent: emptyUsage(),
@@ -530,6 +542,7 @@ class RunExecution implements RunLifecycleEffects<RunResult> {
530
542
  interruptCounts: this.interruptCounts,
531
543
  toolCallCount: this.toolCallCount,
532
544
  toolOutcomes: summarizeToolOutcomes(this.runDir.recorded),
545
+ agentActivity: summarizeAgentActivity(this.runDir.recorded, ready),
533
546
  artifacts: { count: this.collection.artifacts.length },
534
547
  provenance: this.runProvenance(),
535
548
  };
@@ -563,6 +576,7 @@ class RunExecution implements RunLifecycleEffects<RunResult> {
563
576
  const summary = outcome.kind === "done" ? outcome.done.summary : outcome.error.summary;
564
577
  const driverCost = costFromSummary(summary);
565
578
  const reportedModelAccounting = modelAccountingFromSummary(summary);
579
+ const driverMetrics = metricsFromSummary(summary);
566
580
  const answererAccounting = readAnswererModelAccounting(this.args.answerer);
567
581
  const agentCalls = this.ready ? (reportedModelAccounting?.modelCalls ?? 1) : 0;
568
582
  const operationalFailure = loopFailure(outcome, this.collection.error);
@@ -571,6 +585,7 @@ class RunExecution implements RunLifecycleEffects<RunResult> {
571
585
  answerer: readAnswererUsage(this.args.answerer),
572
586
  grading: emptyUsage(),
573
587
  };
588
+ const ready = this.ready ?? { agentId: "not-started" };
574
589
  const metadata: RunMetadata = {
575
590
  schemaVersion: 1,
576
591
  luxVersion: this.args.luxVersion,
@@ -579,7 +594,7 @@ class RunExecution implements RunLifecycleEffects<RunResult> {
579
594
  agentEndedAt: agentEndedAt.toISOString(),
580
595
  endedAt: agentEndedAt.toISOString(),
581
596
  exitReason,
582
- driver: { name: this.args.driverName, ...(this.ready ?? { agentId: "not-started" }) },
597
+ driver: { name: this.args.driverName, ...ready },
583
598
  answerer: answererMetadata(this.args.answererName, this.args.answererConfig),
584
599
  usage,
585
600
  modelAccounting: {
@@ -593,10 +608,12 @@ class RunExecution implements RunLifecycleEffects<RunResult> {
593
608
  interruptCounts: this.interruptCounts,
594
609
  toolCallCount: this.toolCallCount,
595
610
  toolOutcomes: summarizeToolOutcomes(this.runDir.recorded),
611
+ agentActivity: summarizeAgentActivity(this.runDir.recorded, ready),
596
612
  artifacts: await artifactMeasurements(this.collection.artifacts, this.runDir.path),
597
613
  ...(operationalFailure ? { operationalFailure } : {}),
598
614
  provenance: this.runProvenance(),
599
615
  ...(driverCost !== undefined ? { cost: { usd: driverCost } } : {}),
616
+ ...(Object.keys(driverMetrics).length > 0 ? { driverMetrics } : {}),
600
617
  };
601
618
  return {
602
619
  metadata,