@prismatic-io/lux 0.0.1 → 0.0.2-preview.11

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 (256) hide show
  1. package/lib/answerers/persona/index.d.ts +2 -1
  2. package/lib/answerers/persona/index.d.ts.map +1 -1
  3. package/lib/answerers/persona/index.js +20 -5
  4. package/lib/answerers/persona/index.js.map +1 -1
  5. package/lib/answerers/terminal/index.js +1 -1
  6. package/lib/answerers/terminal/index.js.map +1 -1
  7. package/lib/assertions/authoring.d.ts +14 -0
  8. package/lib/assertions/authoring.d.ts.map +1 -0
  9. package/lib/assertions/authoring.js +3 -0
  10. package/lib/assertions/authoring.js.map +1 -0
  11. package/lib/assertions/core/helpers.d.ts +2 -7
  12. package/lib/assertions/core/helpers.d.ts.map +1 -1
  13. package/lib/assertions/core/helpers.js +2 -6
  14. package/lib/assertions/core/helpers.js.map +1 -1
  15. package/lib/assertions/core/index.d.ts.map +1 -1
  16. package/lib/assertions/core/index.js +2 -1
  17. package/lib/assertions/core/index.js.map +1 -1
  18. package/lib/assertions/core/resource-checks.d.ts +19 -0
  19. package/lib/assertions/core/resource-checks.d.ts.map +1 -1
  20. package/lib/assertions/core/resource-checks.js +32 -0
  21. package/lib/assertions/core/resource-checks.js.map +1 -1
  22. package/lib/authoring.d.ts +1 -0
  23. package/lib/authoring.d.ts.map +1 -1
  24. package/lib/authoring.js +1 -0
  25. package/lib/authoring.js.map +1 -1
  26. package/lib/cli/program.d.ts.map +1 -1
  27. package/lib/cli/program.js +12 -67
  28. package/lib/cli/program.js.map +1 -1
  29. package/lib/cli/render/reporter.d.ts.map +1 -1
  30. package/lib/cli/render/reporter.js +16 -3
  31. package/lib/cli/render/reporter.js.map +1 -1
  32. package/lib/cli/run-options.d.ts +1 -0
  33. package/lib/cli/run-options.d.ts.map +1 -1
  34. package/lib/cli/run-options.js.map +1 -1
  35. package/lib/cli/view.d.ts +13 -0
  36. package/lib/cli/view.d.ts.map +1 -0
  37. package/lib/cli/view.js +126 -0
  38. package/lib/cli/view.js.map +1 -0
  39. package/lib/core/action-events.d.ts +29 -0
  40. package/lib/core/action-events.d.ts.map +1 -0
  41. package/lib/core/action-events.js +51 -0
  42. package/lib/core/action-events.js.map +1 -0
  43. package/lib/core/agent-activity.d.ts +49 -0
  44. package/lib/core/agent-activity.d.ts.map +1 -0
  45. package/lib/core/agent-activity.js +206 -0
  46. package/lib/core/agent-activity.js.map +1 -0
  47. package/lib/core/agent-events.d.ts +26 -0
  48. package/lib/core/agent-events.d.ts.map +1 -0
  49. package/lib/core/agent-events.js +13 -0
  50. package/lib/core/agent-events.js.map +1 -0
  51. package/lib/core/artifact-evidence.d.ts +8 -0
  52. package/lib/core/artifact-evidence.d.ts.map +1 -1
  53. package/lib/core/artifact-evidence.js +36 -4
  54. package/lib/core/artifact-evidence.js.map +1 -1
  55. package/lib/core/assertion.d.ts +19 -9
  56. package/lib/core/assertion.d.ts.map +1 -1
  57. package/lib/core/assertion.js +26 -0
  58. package/lib/core/assertion.js.map +1 -1
  59. package/lib/core/case-authoring-state.d.ts +12 -0
  60. package/lib/core/case-authoring-state.d.ts.map +1 -0
  61. package/lib/core/case-authoring-state.js +43 -0
  62. package/lib/core/case-authoring-state.js.map +1 -0
  63. package/lib/core/case.d.ts +29 -1
  64. package/lib/core/case.d.ts.map +1 -1
  65. package/lib/core/case.js +14 -6
  66. package/lib/core/case.js.map +1 -1
  67. package/lib/core/driver-metrics.d.ts +98 -0
  68. package/lib/core/driver-metrics.d.ts.map +1 -0
  69. package/lib/core/driver-metrics.js +36 -0
  70. package/lib/core/driver-metrics.js.map +1 -0
  71. package/lib/core/driver.d.ts +3 -0
  72. package/lib/core/driver.d.ts.map +1 -1
  73. package/lib/core/driver.js.map +1 -1
  74. package/lib/core/events.d.ts +36 -0
  75. package/lib/core/events.d.ts.map +1 -1
  76. package/lib/core/events.js +12 -2
  77. package/lib/core/events.js.map +1 -1
  78. package/lib/core/experiment.d.ts +19 -0
  79. package/lib/core/experiment.d.ts.map +1 -1
  80. package/lib/core/experiment.js +2 -0
  81. package/lib/core/experiment.js.map +1 -1
  82. package/lib/core/index.d.ts +4 -0
  83. package/lib/core/index.d.ts.map +1 -1
  84. package/lib/core/index.js +4 -0
  85. package/lib/core/index.js.map +1 -1
  86. package/lib/core/lifecycle-fixtures.d.ts +3 -1
  87. package/lib/core/lifecycle-fixtures.d.ts.map +1 -1
  88. package/lib/core/lifecycle-fixtures.js +4 -2
  89. package/lib/core/lifecycle-fixtures.js.map +1 -1
  90. package/lib/core/lifecycle-hooks.d.ts +9 -0
  91. package/lib/core/lifecycle-hooks.d.ts.map +1 -1
  92. package/lib/core/lifecycle-hooks.js +8 -2
  93. package/lib/core/lifecycle-hooks.js.map +1 -1
  94. package/lib/core/run.d.ts +150 -0
  95. package/lib/core/run.d.ts.map +1 -1
  96. package/lib/core/run.js +9 -0
  97. package/lib/core/run.js.map +1 -1
  98. package/lib/core/wire.d.ts +12 -0
  99. package/lib/core/wire.d.ts.map +1 -1
  100. package/lib/core/wire.js +5 -1
  101. package/lib/core/wire.js.map +1 -1
  102. package/lib/drivers/claude-code/parse-events.d.ts.map +1 -1
  103. package/lib/drivers/claude-code/parse-events.js +73 -8
  104. package/lib/drivers/claude-code/parse-events.js.map +1 -1
  105. package/lib/drivers/codex/app-events.d.ts +9 -2
  106. package/lib/drivers/codex/app-events.d.ts.map +1 -1
  107. package/lib/drivers/codex/app-events.js +93 -12
  108. package/lib/drivers/codex/app-events.js.map +1 -1
  109. package/lib/drivers/codex/index.d.ts.map +1 -1
  110. package/lib/drivers/codex/index.js +10 -14
  111. package/lib/drivers/codex/index.js.map +1 -1
  112. package/lib/drivers/subprocess/index.d.ts.map +1 -1
  113. package/lib/drivers/subprocess/index.js +6 -0
  114. package/lib/drivers/subprocess/index.js.map +1 -1
  115. package/lib/index.d.ts +10 -6
  116. package/lib/index.d.ts.map +1 -1
  117. package/lib/index.js +10 -6
  118. package/lib/index.js.map +1 -1
  119. package/lib/orchestrator/annotation-store.d.ts.map +1 -1
  120. package/lib/orchestrator/annotation-store.js +6 -2
  121. package/lib/orchestrator/annotation-store.js.map +1 -1
  122. package/lib/orchestrator/case-extensions.d.ts +6 -0
  123. package/lib/orchestrator/case-extensions.d.ts.map +1 -0
  124. package/lib/orchestrator/case-extensions.js +28 -0
  125. package/lib/orchestrator/case-extensions.js.map +1 -0
  126. package/lib/orchestrator/compare.d.ts +13 -1
  127. package/lib/orchestrator/compare.d.ts.map +1 -1
  128. package/lib/orchestrator/compare.js +12 -0
  129. package/lib/orchestrator/compare.js.map +1 -1
  130. package/lib/orchestrator/config.d.ts.map +1 -1
  131. package/lib/orchestrator/config.js +0 -2
  132. package/lib/orchestrator/config.js.map +1 -1
  133. package/lib/orchestrator/doctor.d.ts.map +1 -1
  134. package/lib/orchestrator/doctor.js +5 -3
  135. package/lib/orchestrator/doctor.js.map +1 -1
  136. package/lib/orchestrator/experiment-contracts.d.ts +19 -0
  137. package/lib/orchestrator/experiment-contracts.d.ts.map +1 -1
  138. package/lib/orchestrator/experiment-evaluation.d.ts.map +1 -1
  139. package/lib/orchestrator/experiment-evaluation.js +17 -0
  140. package/lib/orchestrator/experiment-evaluation.js.map +1 -1
  141. package/lib/orchestrator/experiment-report.d.ts +19 -0
  142. package/lib/orchestrator/experiment-report.d.ts.map +1 -1
  143. package/lib/orchestrator/experiment-runs.d.ts +38 -0
  144. package/lib/orchestrator/experiment-runs.d.ts.map +1 -1
  145. package/lib/orchestrator/grader-experiment.d.ts +12 -0
  146. package/lib/orchestrator/grader-experiment.d.ts.map +1 -1
  147. package/lib/orchestrator/grading.d.ts +2 -1
  148. package/lib/orchestrator/grading.d.ts.map +1 -1
  149. package/lib/orchestrator/grading.js +16 -4
  150. package/lib/orchestrator/grading.js.map +1 -1
  151. package/lib/orchestrator/index.d.ts +1 -1
  152. package/lib/orchestrator/index.d.ts.map +1 -1
  153. package/lib/orchestrator/index.js +1 -1
  154. package/lib/orchestrator/index.js.map +1 -1
  155. package/lib/orchestrator/list-runs.d.ts +7 -1
  156. package/lib/orchestrator/list-runs.d.ts.map +1 -1
  157. package/lib/orchestrator/list-runs.js +50 -6
  158. package/lib/orchestrator/list-runs.js.map +1 -1
  159. package/lib/orchestrator/loader.d.ts.map +1 -1
  160. package/lib/orchestrator/loader.js +2 -0
  161. package/lib/orchestrator/loader.js.map +1 -1
  162. package/lib/orchestrator/orchestrator.d.ts +3 -0
  163. package/lib/orchestrator/orchestrator.d.ts.map +1 -1
  164. package/lib/orchestrator/orchestrator.js +42 -14
  165. package/lib/orchestrator/orchestrator.js.map +1 -1
  166. package/lib/orchestrator/owned-lock.d.ts.map +1 -1
  167. package/lib/orchestrator/owned-lock.js +52 -38
  168. package/lib/orchestrator/owned-lock.js.map +1 -1
  169. package/lib/orchestrator/regrade.d.ts +6 -0
  170. package/lib/orchestrator/regrade.d.ts.map +1 -1
  171. package/lib/orchestrator/regrade.js +8 -4
  172. package/lib/orchestrator/regrade.js.map +1 -1
  173. package/lib/orchestrator/run-execution.d.ts +2 -1
  174. package/lib/orchestrator/run-execution.d.ts.map +1 -1
  175. package/lib/orchestrator/run-execution.js +20 -5
  176. package/lib/orchestrator/run-execution.js.map +1 -1
  177. package/lib/orchestrator/suite-compare.d.ts +34 -0
  178. package/lib/orchestrator/suite-compare.d.ts.map +1 -1
  179. package/lib/orchestrator/suite-summary.d.ts +68 -0
  180. package/lib/orchestrator/suite-summary.d.ts.map +1 -1
  181. package/lib/orchestrator/suite-summary.js +45 -7
  182. package/lib/orchestrator/suite-summary.js.map +1 -1
  183. package/lib/previewer/artifacts.d.ts +25 -0
  184. package/lib/previewer/artifacts.d.ts.map +1 -0
  185. package/lib/previewer/artifacts.js +180 -0
  186. package/lib/previewer/artifacts.js.map +1 -0
  187. package/lib/previewer/server.d.ts +105 -0
  188. package/lib/previewer/server.d.ts.map +1 -0
  189. package/lib/previewer/server.js +542 -0
  190. package/lib/previewer/server.js.map +1 -0
  191. package/package.json +2 -2
  192. package/skills/lux/SKILL.md +7 -3
  193. package/skills/lux/references/cli.md +25 -2
  194. package/skills/lux/references/eval-authoring.md +158 -4
  195. package/skills/lux-answerer/SKILL.md +1 -1
  196. package/src/answerers/persona/index.ts +21 -4
  197. package/src/answerers/terminal/index.ts +1 -1
  198. package/src/assertions/authoring.ts +81 -0
  199. package/src/assertions/core/helpers.ts +3 -6
  200. package/src/assertions/core/index.ts +2 -0
  201. package/src/assertions/core/resource-checks.ts +35 -0
  202. package/src/authoring.ts +1 -0
  203. package/src/cli/program.ts +14 -93
  204. package/src/cli/render/reporter.ts +18 -3
  205. package/src/cli/run-options.ts +1 -0
  206. package/src/cli/view.ts +168 -0
  207. package/src/core/action-events.ts +59 -0
  208. package/src/core/agent-activity.ts +236 -0
  209. package/src/core/agent-events.ts +17 -0
  210. package/src/core/artifact-evidence.ts +47 -4
  211. package/src/core/assertion.ts +41 -22
  212. package/src/core/case-authoring-state.ts +65 -0
  213. package/src/core/case.ts +57 -6
  214. package/src/core/driver-metrics.ts +51 -0
  215. package/src/core/driver.ts +3 -0
  216. package/src/core/events.ts +14 -2
  217. package/src/core/experiment.ts +2 -0
  218. package/src/core/index.ts +4 -0
  219. package/src/core/lifecycle-fixtures.ts +7 -2
  220. package/src/core/lifecycle-hooks.ts +16 -2
  221. package/src/core/run.ts +9 -0
  222. package/src/core/wire.ts +5 -1
  223. package/src/drivers/claude-code/parse-events.ts +98 -12
  224. package/src/drivers/codex/app-events.ts +116 -16
  225. package/src/drivers/codex/index.ts +16 -14
  226. package/src/drivers/subprocess/index.ts +6 -0
  227. package/src/index.ts +50 -6
  228. package/src/orchestrator/annotation-store.ts +5 -1
  229. package/src/orchestrator/case-extensions.ts +44 -0
  230. package/src/orchestrator/compare.ts +18 -0
  231. package/src/orchestrator/config.ts +0 -2
  232. package/src/orchestrator/doctor.ts +5 -3
  233. package/src/orchestrator/experiment-evaluation.ts +23 -0
  234. package/src/orchestrator/grading.ts +21 -2
  235. package/src/orchestrator/index.ts +1 -0
  236. package/src/orchestrator/list-runs.ts +66 -5
  237. package/src/orchestrator/loader.ts +2 -0
  238. package/src/orchestrator/orchestrator.ts +86 -40
  239. package/src/orchestrator/owned-lock.ts +54 -34
  240. package/src/orchestrator/regrade.ts +8 -7
  241. package/src/orchestrator/run-execution.ts +24 -3
  242. package/src/orchestrator/suite-summary.ts +101 -8
  243. package/src/previewer/artifacts.ts +221 -0
  244. package/src/previewer/server.ts +765 -0
  245. package/viewer/app.css +2075 -0
  246. package/viewer/app.js +606 -0
  247. package/viewer/artifact-view.js +48 -0
  248. package/viewer/charts.js +86 -0
  249. package/viewer/detail.js +1214 -0
  250. package/viewer/format.js +129 -0
  251. package/viewer/index.html +151 -0
  252. package/lib/answerers/scripted/index.d.ts +0 -8
  253. package/lib/answerers/scripted/index.d.ts.map +0 -1
  254. package/lib/answerers/scripted/index.js +0 -163
  255. package/lib/answerers/scripted/index.js.map +0 -1
  256. package/src/answerers/scripted/index.ts +0 -186
@@ -0,0 +1,236 @@
1
+ import { z } from "zod";
2
+ import { ActionProgressPayloadSchema } from "./action-events.js";
3
+ import { AgentCompletedPayloadSchema, AgentSpawnPayloadSchema } from "./agent-events.js";
4
+ import type { AgentAttribution, ReadyState, RunEvent } from "./events.js";
5
+ import { ToolCallPayloadSchema, ToolResultPayloadSchema } from "./tool-events.js";
6
+ import { TokenUsageSchema } from "./usage.js";
7
+
8
+ export const AgentActivitySchema = z.object({
9
+ agent: z.object({
10
+ id: z.string(),
11
+ role: z.enum(["main", "subagent"]),
12
+ parentId: z.exactOptional(z.string()),
13
+ spawnToolUseId: z.exactOptional(z.string()),
14
+ name: z.exactOptional(z.string()),
15
+ model: z.exactOptional(z.string()),
16
+ }),
17
+ startedAt: z.number(),
18
+ endedAt: z.number(),
19
+ durationMs: z.number().nonnegative(),
20
+ status: z.enum(["completed", "failed", "cancelled", "unknown"]),
21
+ eventCount: z.number().int().nonnegative(),
22
+ usage: TokenUsageSchema.extend({ total: z.number().int().nonnegative() }),
23
+ toolCalls: z.object({
24
+ total: z.number().int().nonnegative(),
25
+ succeeded: z.number().int().nonnegative(),
26
+ failed: z.number().int().nonnegative(),
27
+ unresolved: z.number().int().nonnegative(),
28
+ }),
29
+ actions: z.object({
30
+ total: z.number().int().nonnegative(),
31
+ mutations: z.number().int().nonnegative(),
32
+ durableMutations: z.number().int().nonnegative(),
33
+ timeToFirstActionMs: z.exactOptional(z.number().nonnegative()),
34
+ timeToFirstMutationMs: z.exactOptional(z.number().nonnegative()),
35
+ timeToFirstDurableMutationMs: z.exactOptional(z.number().nonnegative()),
36
+ }),
37
+ });
38
+ export type AgentActivity = z.infer<typeof AgentActivitySchema>;
39
+
40
+ const legacyAttribution = (
41
+ event: Extract<RunEvent, { type: "progress" }>,
42
+ ready: ReadyState,
43
+ ): AgentAttribution => {
44
+ const payload = event.payload;
45
+ const parentToolUseId =
46
+ typeof payload === "object" && payload !== null && "parentToolUseId" in payload
47
+ ? payload.parentToolUseId
48
+ : undefined;
49
+ const spawnToolUseId =
50
+ typeof parentToolUseId === "string" && parentToolUseId.length > 0 ? parentToolUseId : undefined;
51
+ return spawnToolUseId
52
+ ? {
53
+ id: `subagent:${spawnToolUseId}`,
54
+ role: "subagent" as const,
55
+ parentId: ready.agentId,
56
+ spawnToolUseId,
57
+ }
58
+ : { id: ready.agentId, role: "main" as const, ...(ready.model ? { model: ready.model } : {}) };
59
+ };
60
+
61
+ type MutableActivity = AgentActivity & { calls: Map<string, "pending" | "succeeded" | "failed"> };
62
+ type ProgressEvent = Extract<RunEvent, { type: "progress" }>;
63
+ type SpawnInfo = { ts: number; name?: string; model?: string };
64
+
65
+ const initialActivity = (agent: AgentAttribution, startedAt: number): MutableActivity => ({
66
+ agent,
67
+ startedAt,
68
+ endedAt: startedAt,
69
+ durationMs: 0,
70
+ status: "unknown",
71
+ eventCount: 0,
72
+ usage: { input: 0, output: 0, cacheRead: 0, cacheCreation: 0, total: 0 },
73
+ toolCalls: { total: 0, succeeded: 0, failed: 0, unresolved: 0 },
74
+ actions: { total: 0, mutations: 0, durableMutations: 0 },
75
+ calls: new Map(),
76
+ });
77
+
78
+ const collectSpawnInfo = (events: RunEvent[]): Map<string, SpawnInfo> => {
79
+ const spawns = new Map<string, SpawnInfo>();
80
+ for (const event of events) {
81
+ if (event.type !== "progress" || event.kind !== "tool-call") continue;
82
+ const call = ToolCallPayloadSchema.safeParse(event.payload).data;
83
+ if (!call) continue;
84
+ const input = z
85
+ .object({
86
+ name: z.string().min(1).optional(),
87
+ subagent_type: z.string().min(1).optional(),
88
+ agent: z.string().min(1).optional(),
89
+ role: z.string().min(1).optional(),
90
+ model: z.string().min(1).optional(),
91
+ })
92
+ .loose()
93
+ .safeParse(call.input).data;
94
+ const name = input?.name ?? input?.subagent_type ?? input?.agent ?? input?.role;
95
+ spawns.set(call.toolUseId, {
96
+ ts: event.ts,
97
+ ...(name ? { name } : {}),
98
+ ...(input?.model ? { model: input.model } : {}),
99
+ });
100
+ }
101
+ return spawns;
102
+ };
103
+
104
+ const applyLifecycle = (
105
+ event: ProgressEvent,
106
+ activities: Map<string, MutableActivity>,
107
+ ): boolean => {
108
+ if (event.kind === "agent-spawn") {
109
+ const spawn = AgentSpawnPayloadSchema.safeParse(event.payload).data;
110
+ if (spawn && !activities.has(spawn.agent.id)) {
111
+ activities.set(spawn.agent.id, initialActivity(spawn.agent, event.ts));
112
+ }
113
+ return true;
114
+ }
115
+ if (event.kind !== "agent-completed") return false;
116
+ const completed = AgentCompletedPayloadSchema.safeParse(event.payload).data;
117
+ const activity = completed ? activities.get(completed.agentId) : undefined;
118
+ if (activity && completed) {
119
+ activity.endedAt = Math.max(activity.endedAt, event.ts);
120
+ activity.durationMs = Math.max(0, activity.endedAt - activity.startedAt);
121
+ activity.status = completed.status;
122
+ }
123
+ return true;
124
+ };
125
+
126
+ const activityFor = (
127
+ event: ProgressEvent,
128
+ ready: ReadyState,
129
+ driverStartedAt: number,
130
+ spawns: Map<string, SpawnInfo>,
131
+ activities: Map<string, MutableActivity>,
132
+ ): MutableActivity => {
133
+ const agent = event.agent ?? legacyAttribution(event, ready);
134
+ const existing = activities.get(agent.id);
135
+ if (existing) return existing;
136
+ const spawn = agent.spawnToolUseId ? spawns.get(agent.spawnToolUseId) : undefined;
137
+ const name = agent.name ?? spawn?.name;
138
+ const model = agent.model ?? spawn?.model;
139
+ const attributedAgent = {
140
+ ...agent,
141
+ ...(name ? { name } : {}),
142
+ ...(model ? { model } : {}),
143
+ };
144
+ const startedAt =
145
+ agent.role === "subagent" && agent.spawnToolUseId
146
+ ? (spawn?.ts ?? event.ts)
147
+ : driverStartedAt || event.ts;
148
+ return activities.get(agent.id) ?? initialActivity(attributedAgent, startedAt);
149
+ };
150
+
151
+ const addAction = (event: ProgressEvent, activity: MutableActivity): void => {
152
+ const action = ActionProgressPayloadSchema.safeParse(event.payload).data;
153
+ if (!action) return;
154
+ const elapsed = Math.max(0, event.ts - activity.startedAt);
155
+ activity.actions.total++;
156
+ activity.actions.timeToFirstActionMs ??= elapsed;
157
+ if (action.category === "mutation" || action.category === "durable-mutation") {
158
+ activity.actions.mutations++;
159
+ activity.actions.timeToFirstMutationMs ??= elapsed;
160
+ }
161
+ if (action.category === "durable-mutation") {
162
+ activity.actions.durableMutations++;
163
+ activity.actions.timeToFirstDurableMutationMs ??= elapsed;
164
+ }
165
+ };
166
+
167
+ const addUsage = (event: ProgressEvent, activity: MutableActivity): void => {
168
+ const usage = TokenUsageSchema.safeParse(event.payload).data;
169
+ if (!usage) return;
170
+ activity.usage.input += usage.input;
171
+ activity.usage.output += usage.output;
172
+ activity.usage.cacheRead += usage.cacheRead;
173
+ activity.usage.cacheCreation += usage.cacheCreation;
174
+ activity.usage.total += usage.input + usage.output + usage.cacheRead + usage.cacheCreation;
175
+ };
176
+
177
+ const addWork = (event: ProgressEvent, activity: MutableActivity): void => {
178
+ activity.eventCount++;
179
+ activity.endedAt = Math.max(activity.endedAt, event.ts);
180
+ activity.durationMs = Math.max(0, activity.endedAt - activity.startedAt);
181
+ if (event.kind === "tool-call") {
182
+ const call = ToolCallPayloadSchema.safeParse(event.payload).data;
183
+ if (call && !activity.calls.has(call.toolUseId)) {
184
+ activity.calls.set(call.toolUseId, "pending");
185
+ activity.toolCalls.total++;
186
+ }
187
+ return;
188
+ }
189
+ if (event.kind === "tool-result") {
190
+ const result = ToolResultPayloadSchema.safeParse(event.payload).data;
191
+ if (result && activity.calls.has(result.toolUseId)) {
192
+ activity.calls.set(result.toolUseId, result.isError ? "failed" : "succeeded");
193
+ }
194
+ return;
195
+ }
196
+ if (event.kind === "action") addAction(event, activity);
197
+ if (event.kind === "usage") addUsage(event, activity);
198
+ };
199
+
200
+ export const summarizeAgentActivity = (events: RunEvent[], ready: ReadyState): AgentActivity[] => {
201
+ const driverStart = events.find(
202
+ (event): event is Extract<RunEvent, { type: "lifecycle" }> =>
203
+ event.type === "lifecycle" && event.stage === "start-driver",
204
+ );
205
+ const driverStartedAt = driverStart?.ts ?? 0;
206
+ const spawns = collectSpawnInfo(events);
207
+ const activities = new Map<string, MutableActivity>();
208
+ for (const event of events) {
209
+ if (event.type !== "progress") continue;
210
+ if (applyLifecycle(event, activities)) continue;
211
+ const activity = activityFor(event, ready, driverStartedAt, spawns, activities);
212
+ addWork(event, activity);
213
+ activities.set(activity.agent.id, activity);
214
+ }
215
+
216
+ if (!activities.has(ready.agentId)) {
217
+ activities.set(
218
+ ready.agentId,
219
+ initialActivity(
220
+ {
221
+ id: ready.agentId,
222
+ role: "main",
223
+ ...(ready.model ? { model: ready.model } : {}),
224
+ },
225
+ driverStartedAt,
226
+ ),
227
+ );
228
+ }
229
+ return [...activities.values()]
230
+ .map(({ calls, ...activity }) => {
231
+ for (const outcome of calls.values())
232
+ activity.toolCalls[outcome === "pending" ? "unresolved" : outcome]++;
233
+ return AgentActivitySchema.parse(activity);
234
+ })
235
+ .sort((a, b) => a.startedAt - b.startedAt || a.agent.id.localeCompare(b.agent.id));
236
+ };
@@ -0,0 +1,17 @@
1
+ import { z } from "zod";
2
+ import { AgentAttributionSchema } from "./events.js";
3
+
4
+ export const AGENT_SPAWN_KIND = "agent-spawn" as const;
5
+ export const AGENT_COMPLETED_KIND = "agent-completed" as const;
6
+
7
+ export const AgentSpawnPayloadSchema = z.object({
8
+ agent: AgentAttributionSchema.extend({ role: z.literal("subagent") }),
9
+ prompt: z.exactOptional(z.string()),
10
+ });
11
+ export type AgentSpawnPayload = z.infer<typeof AgentSpawnPayloadSchema>;
12
+
13
+ export const AgentCompletedPayloadSchema = z.object({
14
+ agentId: z.string().min(1),
15
+ status: z.enum(["completed", "failed", "cancelled", "unknown"]),
16
+ });
17
+ export type AgentCompletedPayload = z.infer<typeof AgentCompletedPayloadSchema>;
@@ -7,12 +7,15 @@ import type { Run } from "./run.js";
7
7
  const DEFAULT_ARTIFACT_READ_BYTES = 5 * 1024 * 1024;
8
8
  const MAX_ARTIFACT_READ_BYTES = 10 * 1024 * 1024;
9
9
 
10
+ export type RunArtifactIssueReason = "absent" | "unreadable";
11
+
10
12
  export type RunArtifactInspection = {
11
13
  target: string;
12
14
  root: string;
13
15
  artifact?: Artifact;
14
16
  stats?: Stats;
15
17
  issue?: string;
18
+ reason?: RunArtifactIssueReason;
16
19
  };
17
20
 
18
21
  type RunArtifactEvidence = {
@@ -82,6 +85,7 @@ export const inspectRunArtifact = async (
82
85
  target: authoredPath,
83
86
  root: runArtifactsRoot(run),
84
87
  issue: `${authoredPath} contains a null byte`,
88
+ reason: "unreadable",
85
89
  };
86
90
  }
87
91
  const matching = run.artifacts.filter((candidate) => candidate.path === authoredPath);
@@ -91,6 +95,7 @@ export const inspectRunArtifact = async (
91
95
  target: resolve(root, authoredPath),
92
96
  root,
93
97
  issue: `${authoredPath} was not captured in the frozen artifact index`,
98
+ reason: "absent",
94
99
  };
95
100
  }
96
101
  if (matching.some((candidate) => candidate.root !== undefined && !isAbsolute(candidate.root))) {
@@ -98,6 +103,7 @@ export const inspectRunArtifact = async (
98
103
  target: authoredPath,
99
104
  root: defaultArtifactsRoot(run),
100
105
  issue: `${authoredPath} declares a non-absolute artifact root`,
106
+ reason: "unreadable",
101
107
  };
102
108
  }
103
109
  const artifact = matching[0];
@@ -108,6 +114,7 @@ export const inspectRunArtifact = async (
108
114
  target: authoredPath,
109
115
  root: defaultArtifactsRoot(run),
110
116
  issue: `${authoredPath} is ambiguous across multiple artifact roots`,
117
+ reason: "unreadable",
111
118
  };
112
119
  }
113
120
  // A non-default root is trusted only for an exact artifact-index entry.
@@ -116,7 +123,12 @@ export const inspectRunArtifact = async (
116
123
  const root = matchingRoots[0] ?? defaultArtifactsRoot(run);
117
124
  const target = resolve(root, authoredPath);
118
125
  if (pathEscapesRoot(root, target)) {
119
- return { target, root, issue: `${authoredPath} escapes the captured artifact root` };
126
+ return {
127
+ target,
128
+ root,
129
+ issue: `${authoredPath} escapes the captured artifact root`,
130
+ reason: "unreadable",
131
+ };
120
132
  }
121
133
 
122
134
  const path = relative(root, target);
@@ -134,7 +146,13 @@ export const inspectRunArtifact = async (
134
146
  } catch (error) {
135
147
  const code = (error as NodeJS.ErrnoException).code;
136
148
  if (code === "ENOENT" || code === "ENOTDIR") {
137
- return { target, root, ...identified, issue: `${authoredPath} not found` };
149
+ return {
150
+ target,
151
+ root,
152
+ ...identified,
153
+ issue: `${authoredPath} not found`,
154
+ reason: "absent",
155
+ };
138
156
  }
139
157
  throw error;
140
158
  }
@@ -144,16 +162,17 @@ export const inspectRunArtifact = async (
144
162
  root,
145
163
  ...identified,
146
164
  issue: `${authoredPath} resolves through a symlink`,
165
+ reason: "unreadable",
147
166
  };
148
167
  }
149
168
  if (index < components.length - 1 && !stats.isDirectory()) {
150
- return { target, root, ...identified, issue: `${authoredPath} not found` };
169
+ return { target, root, ...identified, issue: `${authoredPath} not found`, reason: "absent" };
151
170
  }
152
171
  if (index === components.length - 1) {
153
172
  return { target, root, ...identified, stats };
154
173
  }
155
174
  }
156
- return { target, root, ...identified, issue: `${authoredPath} not found` };
175
+ return { target, root, ...identified, issue: `${authoredPath} not found`, reason: "absent" };
157
176
  };
158
177
 
159
178
  export type ReadRunArtifactTextOptions = {
@@ -247,3 +266,27 @@ export const readRunArtifactText = async (
247
266
  ): Promise<string> => {
248
267
  return (await readRunArtifactBytes(run, authoredPath, options)).toString("utf8");
249
268
  };
269
+
270
+ /**
271
+ * Read optional artifact evidence. Returns null when the artifact was never
272
+ * captured, and still throws when one exists but is unsafe to consume, so a
273
+ * traversal or symlink violation is never mistaken for missing evidence.
274
+ */
275
+ export const readRunArtifactTextIfPresent = async (
276
+ run: RunArtifactEvidence,
277
+ authoredPath: string,
278
+ options: ReadRunArtifactTextOptions = {},
279
+ ): Promise<string | null> => {
280
+ const inspected = await inspectRunArtifact(run, authoredPath);
281
+ if (inspected.reason === "absent") return null;
282
+ if (!inspected.stats?.isFile()) {
283
+ throw new Error(
284
+ inspected.issue ??
285
+ (inspected.stats ? `${authoredPath} is not a file` : `${authoredPath} not found`),
286
+ );
287
+ }
288
+ const maxBytes = options.maxBytes ?? DEFAULT_ARTIFACT_READ_BYTES;
289
+ return (
290
+ await readInspectedFileBytes(inspected.target, inspected.stats, maxBytes, authoredPath)
291
+ ).toString("utf8");
292
+ };
@@ -42,34 +42,23 @@ export interface AssertionImpl<C extends Assertion = Assertion> {
42
42
  * `AssertionImpl`, plugins must describe their case configuration so Lux can
43
43
  * reject invalid plans before starting a subject run.
44
44
  */
45
- export type AssertionPlugin<C extends Assertion = Assertion> = AssertionImpl<C> & {
45
+ export type AssertionPlugin<
46
+ C extends Assertion = Assertion,
47
+ Input extends Assertion = C,
48
+ > = AssertionImpl<C> & {
46
49
  configSchema: z.ZodObject & z.ZodType<C>;
50
+ /** Type-only input accepted while authoring before schema defaults apply. */
51
+ readonly __input?: Input;
47
52
  /** Author-maintained identity for evaluator/schema behavior. */
48
53
  pluginVersion: string;
49
54
  };
50
55
 
51
56
  type SchemaAssertion<S extends z.ZodObject> = z.output<S> extends Assertion ? z.output<S> : never;
52
-
53
- type IsUnion<T, Whole = T> = T extends Whole ? ([Whole] extends [T] ? false : true) : never;
54
-
55
- type SingleStringLiteral<T> = T extends string
56
- ? string extends T
57
- ? never
58
- : true extends IsUnion<T>
59
- ? never
60
- : T
61
- : never;
57
+ type SchemaAssertionInput<S extends z.ZodObject> =
58
+ z.input<S> extends Assertion ? z.input<S> : never;
62
59
 
63
60
  type LiteralAssertionType<S extends z.ZodObject> =
64
- SchemaAssertion<S> extends { type: infer OutputType }
65
- ? z.input<S> extends { type: infer InputType }
66
- ? SingleStringLiteral<OutputType> extends infer Literal extends string
67
- ? [InputType, Literal] extends [Literal, InputType]
68
- ? Literal
69
- : never
70
- : never
71
- : never
72
- : never;
61
+ S["shape"]["type"] extends z.ZodLiteral<infer Literal extends string> ? Literal : never;
73
62
 
74
63
  export type AssertionPluginDefinition<S extends z.ZodObject> = {
75
64
  type: LiteralAssertionType<S>;
@@ -111,12 +100,34 @@ export const assertionPluginIssue = (definition: {
111
100
  * Define a schema-validated assertion plugin while inferring the config passed
112
101
  * to `evaluate` from the schema.
113
102
  */
103
+ export const baseAssertionFields = {
104
+ id: z.string().min(1).optional(),
105
+ weight: z.number().positive().optional(),
106
+ threshold: z.number().min(0).max(1).optional(),
107
+ name: z.string().optional(),
108
+ } as const;
109
+
114
110
  export const defineAssertion = <S extends z.ZodObject>(
115
111
  definition: AssertionPluginDefinition<S>,
116
- ): AssertionPlugin<SchemaAssertion<S>> => {
112
+ ): AssertionPlugin<SchemaAssertion<S>, SchemaAssertionInput<S>> => {
117
113
  const issue = assertionPluginIssue(definition);
118
114
  if (issue) throw new TypeError(issue);
119
- return definition as unknown as AssertionPlugin<SchemaAssertion<S>>;
115
+ return definition as unknown as AssertionPlugin<SchemaAssertion<S>, SchemaAssertionInput<S>>;
116
+ };
117
+
118
+ export const defineAssertionRegistry = <const Plugins extends readonly AssertionPlugin[]>(
119
+ ...plugins: Plugins
120
+ ): Plugins => {
121
+ const types = new Set<string>();
122
+ for (const plugin of plugins) {
123
+ const issue = assertionPluginIssue(plugin);
124
+ if (issue) throw new TypeError(issue);
125
+ if (types.has(plugin.type)) {
126
+ throw new TypeError(`assertion registry has duplicate type '${plugin.type}'`);
127
+ }
128
+ types.add(plugin.type);
129
+ }
130
+ return plugins;
120
131
  };
121
132
 
122
133
  export const GradingAssertionEntrySchema = z.object({
@@ -137,11 +148,19 @@ export const GradingAssertionEntrySchema = z.object({
137
148
  });
138
149
  export type GradingAssertionEntry = z.infer<typeof GradingAssertionEntrySchema>;
139
150
 
151
+ const SkippedGradingAssertionSchema = z.object({
152
+ id: z.string(),
153
+ type: z.string(),
154
+ name: z.string(),
155
+ reason: z.literal("judge-disabled"),
156
+ });
157
+
140
158
  export const GradingReportSchema = z.object({
141
159
  schemaVersion: z.literal(1),
142
160
  casePassed: z.boolean().nullable(),
143
161
  caseScore: z.number().nullable(),
144
162
  assertions: z.array(GradingAssertionEntrySchema),
163
+ skippedAssertions: z.array(SkippedGradingAssertionSchema).optional(),
145
164
  /**
146
165
  * Present when the subject run or grading did not complete. Assertions may
147
166
  * still contain partial diagnostics, but the report is not a clean verdict.
@@ -0,0 +1,65 @@
1
+ import type { AssertionPlugin } from "./assertion.js";
2
+ import type { EvalCase } from "./case.js";
3
+ import type { LifecycleHooksPlugin } from "./lifecycle-hooks.js";
4
+
5
+ export type EvalCaseExtensions = {
6
+ assertions: readonly AssertionPlugin[];
7
+ fixtures: readonly LifecycleHooksPlugin[];
8
+ };
9
+
10
+ const CASE_EXTENSIONS_KEY = Symbol.for("@prismatic-io/lux/eval-case-extensions");
11
+ const extensionState = globalThis as typeof globalThis & { [key: symbol]: unknown };
12
+ const sharedCaseExtensions = extensionState[CASE_EXTENSIONS_KEY];
13
+ // A project-local package and the CLI can load separate Lux module instances.
14
+ const caseExtensions =
15
+ sharedCaseExtensions instanceof WeakMap
16
+ ? (sharedCaseExtensions as WeakMap<object, EvalCaseExtensions>)
17
+ : new WeakMap<object, EvalCaseExtensions>();
18
+ extensionState[CASE_EXTENSIONS_KEY] = caseExtensions;
19
+
20
+ const uniqueBy = <T>(values: readonly T[], keyOf: (value: T) => string, kind: string): T[] => {
21
+ const unique = new Map<string, T>();
22
+ for (const value of values) {
23
+ const key = keyOf(value);
24
+ const existing = unique.get(key);
25
+ if (existing && existing !== value) {
26
+ throw new TypeError(`case has conflicting ${kind} definitions for '${key}'`);
27
+ }
28
+ unique.set(key, value);
29
+ }
30
+ return [...unique.values()];
31
+ };
32
+
33
+ export const collectEvalCaseExtensions = (
34
+ input: EvalCase,
35
+ assertions: readonly AssertionPlugin[] = [],
36
+ fixtures: readonly LifecycleHooksPlugin[] = [],
37
+ ): EvalCaseExtensions => {
38
+ const availableAssertions = uniqueBy(assertions, (plugin) => plugin.type, "assertion");
39
+ const byType = new Map(availableAssertions.map((plugin) => [plugin.type, plugin]));
40
+ return {
41
+ assertions: uniqueBy(
42
+ (input.assertions ?? []).flatMap((assertion) => {
43
+ const plugin = byType.get(assertion.type);
44
+ return plugin ? [plugin] : [];
45
+ }),
46
+ (plugin) => plugin.type,
47
+ "assertion",
48
+ ),
49
+ fixtures: uniqueBy(fixtures, (fixture) => fixture.name, "fixture"),
50
+ };
51
+ };
52
+
53
+ export const setEvalCaseExtensions = (evalCase: EvalCase, extensions: EvalCaseExtensions): void => {
54
+ if (extensions.assertions.length > 0 || extensions.fixtures.length > 0) {
55
+ caseExtensions.set(evalCase, extensions);
56
+ }
57
+ };
58
+
59
+ export const evalCaseExtensions = (evalCase: EvalCase): EvalCaseExtensions =>
60
+ caseExtensions.get(evalCase) ?? { assertions: [], fixtures: [] };
61
+
62
+ export const transferEvalCaseExtensions = (source: EvalCase, target: EvalCase): void => {
63
+ const extensions = caseExtensions.get(source);
64
+ if (extensions) caseExtensions.set(target, extensions);
65
+ };
package/src/core/case.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  import { z } from "zod";
2
+ import type { AssertionPlugin } from "./assertion.js";
3
+ import { collectEvalCaseExtensions, setEvalCaseExtensions } from "./case-authoring-state.js";
4
+ import type { LifecycleHooksPlugin } from "./lifecycle-hooks.js";
2
5
  import { formatZodIssues } from "./zod.js";
3
6
 
4
7
  export const FixturesRefSchema = z.union([
@@ -67,14 +70,62 @@ export const EvalCaseSchema = EvalCaseObjectSchema
67
70
  });
68
71
  export type EvalCase = z.infer<typeof EvalCaseSchema>;
69
72
 
70
- export const defineEvalCase = (input: EvalCase): EvalCase => {
71
- const parsed = EvalCaseSchema.safeParse(input);
72
- if (!parsed.success) {
73
- throw new Error(`Invalid eval case: ${formatZodIssues(parsed.error)}`);
74
- }
75
- return parsed.data;
73
+ type AssertionPlanFields = Pick<Assertion, "id" | "name" | "threshold" | "weight">;
74
+ type AssertionInputOf<Plugin> =
75
+ Plugin extends AssertionPlugin<Assertion, infer Input> ? Input & AssertionPlanFields : never;
76
+
77
+ export type EvalCaseInput<AllowedAssertion extends Assertion = Assertion> = Omit<
78
+ EvalCase,
79
+ "assertions"
80
+ > & {
81
+ assertions?: AllowedAssertion[];
82
+ };
83
+
84
+ export type EvalCaseAuthoringOptions<
85
+ Plugins extends readonly AssertionPlugin[] = readonly AssertionPlugin[],
86
+ > = {
87
+ assertions?: Plugins;
88
+ fixtures?: readonly LifecycleHooksPlugin[];
89
+ };
90
+
91
+ export type EvalCaseDefinition<AllowedAssertion extends Assertion = Assertion> = {
92
+ (input: EvalCaseInput<AllowedAssertion>): EvalCase;
93
+ /**
94
+ * Create a case definition with selectively activated lifecycle fixtures.
95
+ * Further calls to `extend` append fixtures in setup order.
96
+ */
97
+ extend<const Plugins extends readonly AssertionPlugin[] = readonly []>(
98
+ options: EvalCaseAuthoringOptions<Plugins>,
99
+ ): EvalCaseDefinition<AllowedAssertion | AssertionInputOf<Plugins[number]>>;
76
100
  };
77
101
 
102
+ export const createEvalCaseDefinition = <AllowedAssertion extends Assertion = Assertion>(
103
+ inheritedAssertions: readonly AssertionPlugin[] = [],
104
+ inheritedFixtures: readonly LifecycleHooksPlugin[] = [],
105
+ ): EvalCaseDefinition<AllowedAssertion> => {
106
+ const define = ((input: EvalCaseInput<AllowedAssertion>): EvalCase => {
107
+ const parsed = EvalCaseSchema.safeParse(input);
108
+ if (!parsed.success) {
109
+ throw new Error(`Invalid eval case: ${formatZodIssues(parsed.error)}`);
110
+ }
111
+ const extensions = collectEvalCaseExtensions(
112
+ parsed.data,
113
+ inheritedAssertions,
114
+ inheritedFixtures,
115
+ );
116
+ setEvalCaseExtensions(parsed.data, extensions);
117
+ return parsed.data;
118
+ }) as EvalCaseDefinition<AllowedAssertion>;
119
+ define.extend = (({ assertions = [], fixtures = [] }: EvalCaseAuthoringOptions) =>
120
+ createEvalCaseDefinition(
121
+ [...inheritedAssertions, ...assertions],
122
+ [...inheritedFixtures, ...fixtures],
123
+ )) as EvalCaseDefinition<AllowedAssertion>["extend"];
124
+ return define;
125
+ };
126
+
127
+ export const defineEvalCase = createEvalCaseDefinition();
128
+
78
129
  const PersistedFixturesRefSchema = z.union([
79
130
  z.string(),
80
131
  z.looseObject({ kind: z.literal("dir"), path: z.string() }),
@@ -0,0 +1,51 @@
1
+ import { z } from "zod";
2
+
3
+ export const DriverMetricUnitSchema = z.enum([
4
+ "number",
5
+ "count",
6
+ "milliseconds",
7
+ "bytes",
8
+ "tokens",
9
+ "usd",
10
+ "ratio",
11
+ "percent",
12
+ "score",
13
+ ]);
14
+ export type DriverMetricUnit = z.infer<typeof DriverMetricUnitSchema>;
15
+
16
+ export const DriverMetricDescriptorSchema = z
17
+ .object({
18
+ unit: DriverMetricUnitSchema.default("number"),
19
+ label: z.string().min(1).max(120).optional(),
20
+ better: z.enum(["lower", "higher", "neutral"]).default("neutral"),
21
+ })
22
+ .strict();
23
+ export type DriverMetricDescriptor = z.infer<typeof DriverMetricDescriptorSchema>;
24
+
25
+ export const DriverMetricSchema = DriverMetricDescriptorSchema.extend({
26
+ value: z.number().finite(),
27
+ }).strict();
28
+ export type DriverMetric = z.infer<typeof DriverMetricSchema>;
29
+
30
+ export const DriverMetricsSchema = z.record(z.string().min(1).max(120), DriverMetricSchema);
31
+ export type DriverMetrics = z.infer<typeof DriverMetricsSchema>;
32
+
33
+ const ReportedDriverMetricSchema = z.union([z.number().finite(), DriverMetricSchema]);
34
+ export const ReportedDriverMetricsSchema = z.record(
35
+ z.string().min(1).max(120),
36
+ ReportedDriverMetricSchema,
37
+ );
38
+ export type ReportedDriverMetrics = z.input<typeof ReportedDriverMetricsSchema>;
39
+
40
+ export const normalizeDriverMetrics = (value: unknown): DriverMetrics => {
41
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
42
+ return Object.fromEntries(
43
+ Object.entries(value).flatMap(([name, metric]) => {
44
+ if (name.length === 0 || name.length > 120) return [];
45
+ const parsed = DriverMetricSchema.safeParse(
46
+ typeof metric === "number" ? { value: metric } : metric,
47
+ ).data;
48
+ return parsed ? [[name, parsed]] : [];
49
+ }),
50
+ );
51
+ };
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import type { EventHandler } from "./emitter.js";
3
3
  import type { Answer, Done, ErrorEvent, Interrupt, Progress, ReadyState } from "./events.js";
4
+ import type { FixtureValueBinding } from "./lifecycle-hooks.js";
4
5
 
5
6
  export const ArtifactSchema = z.object({
6
7
  path: z.string(),
@@ -28,6 +29,8 @@ export type StartContext = {
28
29
  caseId?: string;
29
30
  prompt: string;
30
31
  fixtures?: ResolvedFixtures | undefined;
32
+ /** Authored fixture values selected for this case. */
33
+ fixtureValues?: readonly FixtureValueBinding[] | undefined;
31
34
  /** Root containing persisted runs. Drivers may use it as an isolation boundary. */
32
35
  runsRoot?: string | undefined;
33
36
  runDir: string;