@bjoernrochel/sevro 0.1.0-rc.2 → 0.1.0-rc.21

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 (243) hide show
  1. package/CONTRIBUTING.md +170 -0
  2. package/LICENSE +63 -136
  3. package/README.md +83 -66
  4. package/docs/README.md +44 -0
  5. package/docs/acknowledgements.md +14 -0
  6. package/docs/architecture.md +146 -0
  7. package/docs/assets/README.md +27 -0
  8. package/docs/assets/sevro-logo.png +0 -0
  9. package/docs/assets/sevro-symbol.svg +1 -0
  10. package/docs/assets/sevro-wordmark.svg +1 -0
  11. package/docs/builtin-graders-v1.md +41 -13
  12. package/docs/codex-host.md +184 -52
  13. package/docs/contracts.md +19 -0
  14. package/docs/creating-cases.md +139 -0
  15. package/docs/development-cli.md +190 -30
  16. package/docs/documentation-quality.md +61 -0
  17. package/docs/evaluation-workflow.md +110 -0
  18. package/docs/evidence/code-mode-controls-71.md +64 -0
  19. package/docs/evidence/code-mode-programs-85.md +64 -0
  20. package/docs/evidence/codex-state-73/README.md +204 -0
  21. package/docs/evidence/codex-state-73/darrow-migration.patch +182 -0
  22. package/docs/evidence/codex-state-73/results.json +661 -0
  23. package/docs/evidence/local-http-74.md +104 -0
  24. package/docs/evidence/runtime-startup-30/README.md +185 -0
  25. package/docs/evidence/runtime-startup-30/after.json +319 -0
  26. package/docs/evidence/runtime-startup-30/baseline-timeout.jsonl +2 -0
  27. package/docs/evidence/runtime-startup-30/before.json +403 -0
  28. package/docs/evidence/runtime-startup-30/consumer-smoke.json +63 -0
  29. package/docs/evidence/runtime-startup-30/full-policy-diagnostic.jsonl +3 -0
  30. package/docs/evidence/runtime-startup-30/minimal-policy-diagnostic.jsonl +3 -0
  31. package/docs/evidence/runtime-startup-30/plugin-template-assessment.json +20 -0
  32. package/docs/evidence/runtime-startup-30/seedless-timeout.jsonl +2 -0
  33. package/docs/evidence/skill-reads-70/results.json +823 -0
  34. package/docs/evidence.md +21 -0
  35. package/docs/extension-protocol-v1.md +50 -16
  36. package/docs/fixture-cleanup-validation.md +3 -0
  37. package/docs/getting-started.md +102 -0
  38. package/docs/guide-evaluation.md +89 -0
  39. package/docs/guide-verification.json +239 -0
  40. package/docs/identity-v1.md +35 -1
  41. package/docs/installing.md +88 -0
  42. package/docs/licenses/BUSL-1.1-0.1.0-rc.2.txt +159 -0
  43. package/docs/licensing.md +42 -0
  44. package/docs/native-controls.md +76 -5
  45. package/docs/native-hosts.md +228 -0
  46. package/docs/reading-results.md +141 -0
  47. package/docs/recipes.md +489 -0
  48. package/docs/releases.md +81 -40
  49. package/docs/report-v1.md +29 -1
  50. package/docs/results-v1.md +180 -0
  51. package/docs/run-settings-v1.md +147 -0
  52. package/docs/running-evaluations.md +31 -0
  53. package/docs/runtime-v1.md +614 -0
  54. package/docs/specs/repository-guide.md +58 -0
  55. package/docs/troubleshooting.md +30 -0
  56. package/docs/typescript-quality.md +234 -0
  57. package/docs/usability-study.md +114 -0
  58. package/examples/basic/host.ts +2 -2
  59. package/examples/recipes/candidate.ts +26 -0
  60. package/examples/recipes/cases/coding-change.json +21 -0
  61. package/examples/recipes/cases/host-tools.json +21 -0
  62. package/examples/recipes/cases/mise.json +21 -0
  63. package/examples/recipes/cases/node.json +21 -0
  64. package/examples/recipes/cases/python-uv.json +21 -0
  65. package/examples/recipes/cases/semantic.json +17 -0
  66. package/examples/recipes/coding-change/check.ts +23 -0
  67. package/examples/recipes/extension-artifacts.ts +65 -0
  68. package/examples/recipes/extension-methods.ts +174 -0
  69. package/examples/recipes/extension.ts +37 -0
  70. package/examples/recipes/repository-skill/.agents/skills/startup-guide/SKILL.md +7 -0
  71. package/examples/recipes/repository-skill/.claude/skills/startup-guide/SKILL.md +7 -0
  72. package/examples/recipes/repository-skill/README.md +3 -0
  73. package/examples/recipes/run-inputs.ts +188 -0
  74. package/examples/recipes/run-options.ts +131 -0
  75. package/examples/recipes/run.ts +63 -0
  76. package/examples/recipes/runtime/empty.json +1 -0
  77. package/examples/recipes/runtime/goals.json +1 -0
  78. package/examples/recipes/runtime/mise.json +6 -0
  79. package/examples/recipes/runtime/path.json +1 -0
  80. package/examples/recipes/runtime/plugins.json +4 -0
  81. package/examples/recipes/runtime/python-uv.json +10 -0
  82. package/examples/recipes/semantic-judge.ts +29 -0
  83. package/examples/templates/coding/adapter.ts +17 -0
  84. package/examples/templates/coding/case.json +15 -0
  85. package/examples/templates/repository-skill/.agents/skills/startup-guide/SKILL.md +6 -0
  86. package/examples/templates/repository-skill/extension-data.ts +64 -0
  87. package/examples/templates/repository-skill/extension.ts +31 -0
  88. package/examples/templates/response/adapter.ts +12 -0
  89. package/examples/templates/response/case.json +13 -0
  90. package/package.json +39 -6
  91. package/schemas/report-v1.schema.json +9 -0
  92. package/schemas/run-evidence-v1.schema.json +2 -1
  93. package/schemas/runtime-v1.schema.json +193 -0
  94. package/src/advisory.ts +10 -5
  95. package/src/candidate-artifacts.ts +208 -0
  96. package/src/candidate-codex-state.ts +185 -0
  97. package/src/case-validation.ts +105 -0
  98. package/src/cli/cancellation.ts +20 -0
  99. package/src/cli/command.ts +81 -0
  100. package/src/cli/completion.ts +15 -0
  101. package/src/cli/demo.ts +59 -0
  102. package/src/cli/evaluation.ts +55 -0
  103. package/src/cli/extension.ts +79 -0
  104. package/src/cli/help.ts +222 -0
  105. package/src/cli/hosts.ts +59 -0
  106. package/src/cli/init-settings.ts +30 -0
  107. package/src/cli/init.ts +111 -0
  108. package/src/cli/invocation.ts +627 -0
  109. package/src/cli/loading.ts +151 -0
  110. package/src/cli/preparation.ts +163 -0
  111. package/src/cli/presentation.ts +91 -0
  112. package/src/cli/progress.ts +6 -0
  113. package/src/cli/run-manifest.ts +133 -0
  114. package/src/cli/run-options.ts +101 -0
  115. package/src/cli/run-overrides.ts +83 -0
  116. package/src/cli/settings.ts +160 -0
  117. package/src/cli/summary.ts +43 -0
  118. package/src/cli.ts +13 -843
  119. package/src/contract-types.ts +94 -0
  120. package/src/engine.ts +19 -2373
  121. package/src/evaluation/advisory.ts +150 -0
  122. package/src/evaluation/check-plan.ts +510 -0
  123. package/src/evaluation/collection.ts +88 -0
  124. package/src/evaluation/context.ts +10 -0
  125. package/src/evaluation/grading-host.ts +39 -0
  126. package/src/evaluation/grading.ts +258 -0
  127. package/src/evaluation/invocation-plan.ts +548 -0
  128. package/src/evaluation/plan.ts +736 -0
  129. package/src/evaluation/progress.ts +19 -0
  130. package/src/evaluation/resources.ts +241 -0
  131. package/src/evaluation/run.ts +206 -0
  132. package/src/{trial-scheduler.ts → evaluation/scheduler.ts} +3 -1
  133. package/src/evaluation/semantic.ts +312 -0
  134. package/src/evaluation/trial.ts +866 -0
  135. package/src/evaluation/types.ts +207 -0
  136. package/src/evidence/host-results.ts +92 -0
  137. package/src/evidence/retained-artifacts.ts +18 -0
  138. package/src/evidence/trial-writer.ts +203 -0
  139. package/src/evidence/usage.ts +17 -0
  140. package/src/extension-client.ts +228 -148
  141. package/src/extension-session.ts +326 -195
  142. package/src/fixtures/advisory.ts +338 -0
  143. package/src/fixtures/bin.ts +57 -0
  144. package/src/{fixture-cleanup.ts → fixtures/cleanup.ts} +13 -7
  145. package/src/fixtures/create.ts +187 -0
  146. package/src/fixtures/generated.ts +300 -0
  147. package/src/fixtures/git-hooks.ts +24 -0
  148. package/src/fixtures/preparation.ts +270 -0
  149. package/src/{repository-fixture.ts → fixtures/repository.ts} +207 -85
  150. package/src/fixtures/setup.ts +224 -0
  151. package/src/fixtures/tools.ts +88 -0
  152. package/src/fixtures/workspace-state.ts +212 -0
  153. package/src/generated/cli-result.cjs +2 -0
  154. package/src/generated/cli-result.d.cts +3 -0
  155. package/src/generated/extension.cjs +2 -0
  156. package/src/generated/extension.d.cts +3 -0
  157. package/src/generated/report.cjs +2 -0
  158. package/src/generated/report.d.cts +3 -0
  159. package/src/generated/run-evidence.cjs +2 -0
  160. package/src/generated/run-evidence.d.cts +3 -0
  161. package/src/generated/runtime.cjs +2 -0
  162. package/src/generated/runtime.d.cts +3 -0
  163. package/src/graders/artifact.ts +56 -23
  164. package/src/graders/git-head.ts +79 -75
  165. package/src/graders/output.ts +222 -140
  166. package/src/graders/semantic.ts +102 -43
  167. package/src/graders/shell.ts +379 -207
  168. package/src/host-diagnostic-redaction.ts +52 -0
  169. package/src/host-diagnostics.ts +319 -0
  170. package/src/hosts/claude-continuation.ts +63 -18
  171. package/src/hosts/claude-credential.ts +43 -62
  172. package/src/hosts/claude-events.ts +86 -51
  173. package/src/hosts/claude-keychain.ts +59 -0
  174. package/src/hosts/claude-native-goal.ts +57 -33
  175. package/src/hosts/claude-process.ts +111 -0
  176. package/src/hosts/claude-repository-invocation.ts +208 -117
  177. package/src/hosts/claude-settings.ts +240 -21
  178. package/src/hosts/claude-skill-activations.ts +166 -0
  179. package/src/hosts/claude-state.ts +403 -0
  180. package/src/hosts/claude-tool-calls.ts +171 -106
  181. package/src/hosts/claude.ts +561 -451
  182. package/src/hosts/codex-app-server.ts +326 -178
  183. package/src/hosts/codex-configuration.ts +44 -18
  184. package/src/hosts/codex-events.ts +97 -60
  185. package/src/hosts/codex-exec-controls.ts +516 -0
  186. package/src/hosts/codex-invocation.ts +70 -0
  187. package/src/hosts/codex-marketplace.ts +283 -0
  188. package/src/hosts/codex-native-calls.ts +546 -500
  189. package/src/hosts/codex-process.ts +202 -0
  190. package/src/hosts/codex-profile.ts +257 -28
  191. package/src/hosts/codex-session-files.ts +106 -0
  192. package/src/hosts/codex-state.ts +492 -0
  193. package/src/hosts/codex.ts +783 -832
  194. package/src/hosts/isolation-common.ts +94 -0
  195. package/src/hosts/isolation-roots.ts +54 -27
  196. package/src/hosts/isolation.ts +10 -0
  197. package/src/hosts/linux-sandbox.ts +159 -0
  198. package/src/hosts/mac-sandbox.ts +187 -61
  199. package/src/hosts/native-controls.ts +152 -62
  200. package/src/hosts/process-lifecycle.ts +37 -0
  201. package/src/hosts/protected-worktrees.ts +51 -19
  202. package/src/hosts/skill-hook-catalog.ts +131 -0
  203. package/src/hosts/skill-hook-handler.ts +374 -0
  204. package/src/hosts/skill-hooks.ts +580 -0
  205. package/src/hosts/workspace-fingerprint.ts +55 -35
  206. package/src/identity.ts +102 -48
  207. package/src/instrumentation.ts +109 -48
  208. package/src/native-transcript-state.ts +131 -0
  209. package/src/native-transcript-view.ts +90 -0
  210. package/src/native-transcripts.ts +286 -0
  211. package/src/process-lifecycle.ts +150 -0
  212. package/src/provenance.ts +161 -81
  213. package/src/prune.ts +215 -0
  214. package/src/report.ts +172 -140
  215. package/src/resolved-case.ts +33 -0
  216. package/src/results-lock.ts +29 -0
  217. package/src/results.ts +133 -86
  218. package/src/run-owner.ts +154 -63
  219. package/src/runtime/config.ts +456 -0
  220. package/src/runtime/discovery-apple.ts +90 -0
  221. package/src/runtime/discovery-providers.ts +63 -0
  222. package/src/runtime/discovery.ts +70 -0
  223. package/src/runtime/hook-processes.ts +97 -0
  224. package/src/runtime/hooks.ts +476 -0
  225. package/src/runtime/paths.ts +313 -0
  226. package/src/runtime/query.ts +85 -0
  227. package/src/runtime/seeds.ts +134 -0
  228. package/src/runtime/shared-cache.ts +143 -0
  229. package/src/runtime/state.ts +143 -0
  230. package/src/schema-types.ts +198 -0
  231. package/src/schema.ts +15 -19
  232. package/src/transcript-retention.ts +146 -0
  233. package/src/value-guards.ts +20 -0
  234. package/src/advisory-fixture.ts +0 -282
  235. package/src/fixture-bin.ts +0 -87
  236. package/src/fixture-setup.ts +0 -163
  237. package/src/generated-fixture.ts +0 -220
  238. package/src/git-hooks.ts +0 -63
  239. package/src/hosts/claude-nested-skills.ts +0 -218
  240. package/src/hosts/codex-command-output.ts +0 -399
  241. package/src/hosts/codex-executor-command.ts +0 -195
  242. package/src/hosts/codex-skill-reads.ts +0 -391
  243. package/src/preparation.ts +0 -211
@@ -1,3 +1,5 @@
1
+ import type { Stats } from "node:fs";
2
+ import { isRecord } from "../value-guards";
1
3
  import { lstat, readFile, realpath, stat } from "node:fs/promises";
2
4
  import { dirname, isAbsolute, join, relative, sep } from "node:path";
3
5
  import { parse } from "smol-toml";
@@ -9,8 +11,8 @@ export async function configurationRoot(root: string): Promise<string> {
9
11
  if (!(await stat(canonical)).isDirectory())
10
12
  throw new Error("not a directory");
11
13
  return canonical;
12
- } catch {
13
- throw new Error(`configuration root is unreadable: ${root}`);
14
+ } catch (cause) {
15
+ throw new Error(`configuration root is unreadable: ${root}`, { cause });
14
16
  }
15
17
  }
16
18
 
@@ -34,6 +36,15 @@ export async function codexAgentConcurrency(
34
36
  root: string,
35
37
  ): Promise<number | null> {
36
38
  const configPath = join(root, ".codex", "config.toml");
39
+ const source = await readConfiguration(root, configPath);
40
+ if (source === null) return null;
41
+ return parseAgentConcurrency(source, configPath);
42
+ }
43
+
44
+ async function readConfiguration(
45
+ root: string,
46
+ configPath: string,
47
+ ): Promise<string | null> {
37
48
  let source: string;
38
49
  let declared = false;
39
50
  try {
@@ -41,17 +52,7 @@ export async function codexAgentConcurrency(
41
52
  const info = await lstat(configPath);
42
53
  declared = true;
43
54
  const canonical = await realpath(configPath);
44
- const location = relative(root, canonical);
45
- if (
46
- !info.isFile() ||
47
- info.size > 64 * 1024 ||
48
- location === ".." ||
49
- location.startsWith(`..${sep}`) ||
50
- isAbsolute(location)
51
- )
52
- throw new Error(
53
- "configuration must be a contained regular file up to 64 KiB",
54
- );
55
+ validateConfigurationFile(info, relative(root, canonical));
55
56
  source = await readFile(configPath, "utf8");
56
57
  } catch (error) {
57
58
  if (!declared && (error as NodeJS.ErrnoException).code === "ENOENT")
@@ -60,18 +61,30 @@ export async function codexAgentConcurrency(
60
61
  cause: error,
61
62
  });
62
63
  }
64
+ return source;
65
+ }
66
+
67
+ function parseAgentConcurrency(
68
+ source: string,
69
+ configPath: string,
70
+ ): number | null {
63
71
  let config: Record<string, unknown>;
64
72
  try {
65
73
  config = parse(source, { integersAsBigInt: true });
66
- } catch {
67
- throw new Error(`Invalid Codex configuration: ${configPath}`);
74
+ } catch (cause) {
75
+ throw new Error(`Invalid Codex configuration: ${configPath}`, { cause });
68
76
  }
69
77
  const agents = config.agents;
70
78
  if (agents === undefined) return null;
71
- if (!agents || typeof agents !== "object" || Array.isArray(agents))
79
+ if (!isRecord(agents))
72
80
  throw new Error(`Expected an agents table in ${configPath}`);
73
- const limit = (agents as Record<string, unknown>)
74
- .max_concurrent_threads_per_session;
81
+ return concurrencyLimit(
82
+ agents.max_concurrent_threads_per_session,
83
+ configPath,
84
+ );
85
+ }
86
+
87
+ function concurrencyLimit(limit: unknown, configPath: string): number | null {
75
88
  if (limit === undefined) return null;
76
89
  if (
77
90
  typeof limit !== "bigint" ||
@@ -83,3 +96,16 @@ export async function codexAgentConcurrency(
83
96
  );
84
97
  return Number(limit);
85
98
  }
99
+
100
+ function validateConfigurationFile(info: Stats, location: string): void {
101
+ if (!info.isFile() || info.size > 64 * 1024 || escapedRoot(location))
102
+ throw new Error(
103
+ "configuration must be a contained regular file up to 64 KiB",
104
+ );
105
+ }
106
+
107
+ function escapedRoot(location: string): boolean {
108
+ return (
109
+ location === ".." || location.startsWith(`..${sep}`) || isAbsolute(location)
110
+ );
111
+ }
@@ -1,32 +1,89 @@
1
+ import { isRecord } from "../value-guards";
1
2
  const MAX_EVENT_STREAM_BYTES = 8 * 1024 * 1024;
2
3
 
3
4
  export class CodexEventError extends Error {
4
- constructor(message: string) {
5
- super(message);
5
+ constructor(message: string, options?: ErrorOptions) {
6
+ super(message, options);
6
7
  this.name = "CodexEventError";
7
8
  }
8
9
  }
9
10
 
10
- function record(value: unknown): value is Record<string, unknown> {
11
- return value !== null && typeof value === "object" && !Array.isArray(value);
12
- }
13
-
14
11
  function usage(
15
12
  value: unknown,
16
13
  ): { inputTokens: number; outputTokens: number } | null {
17
- if (!record(value)) return null;
14
+ if (!isRecord(value)) return null;
18
15
  const input = value.input_tokens;
19
16
  const output = value.output_tokens;
17
+ if (!nonnegativeInteger(input) || !nonnegativeInteger(output)) return null;
18
+ return { inputTokens: input, outputTokens: output };
19
+ }
20
+
21
+ function nonnegativeInteger(value: unknown): value is number {
22
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
23
+ }
24
+
25
+ interface TurnState {
26
+ threadId: string | null;
27
+ completed: boolean;
28
+ failed: boolean;
29
+ finalMessage: string | null;
30
+ tokenUsage: ReturnType<typeof usage>;
31
+ }
32
+
33
+ function parseEvent(line: string): Record<string, unknown> {
34
+ let parsed: unknown;
35
+ try {
36
+ parsed = JSON.parse(line);
37
+ } catch (cause) {
38
+ throw new CodexEventError("Codex event stream contains invalid JSONL", {
39
+ cause,
40
+ });
41
+ }
42
+ if (!isRecord(parsed) || typeof parsed.type !== "string")
43
+ throw new CodexEventError("Codex event stream contains an invalid event");
44
+ return parsed;
45
+ }
46
+
47
+ function startThread(state: TurnState, event: Record<string, unknown>): void {
20
48
  if (
21
- typeof input !== "number" ||
22
- !Number.isSafeInteger(input) ||
23
- input < 0 ||
24
- typeof output !== "number" ||
25
- !Number.isSafeInteger(output) ||
26
- output < 0
49
+ state.threadId !== null ||
50
+ typeof event.thread_id !== "string" ||
51
+ !event.thread_id
27
52
  )
28
- return null;
29
- return { inputTokens: input, outputTokens: output };
53
+ throw new CodexEventError("Codex event stream has an invalid thread start");
54
+ state.threadId = event.thread_id;
55
+ }
56
+
57
+ function completeTurn(state: TurnState, event: Record<string, unknown>): void {
58
+ if (!state.threadId || state.completed || state.failed)
59
+ throw new CodexEventError(
60
+ "Codex event stream has an invalid turn completion",
61
+ );
62
+ state.completed = true;
63
+ state.tokenUsage = usage(event.usage);
64
+ }
65
+
66
+ function completeItem(state: TurnState, item: unknown): void {
67
+ if (!isRecord(item) || item.type !== "agent_message" || state.completed)
68
+ return;
69
+ state.finalMessage = typeof item.text === "string" ? item.text : null;
70
+ }
71
+
72
+ function applyEvent(state: TurnState, event: Record<string, unknown>): void {
73
+ switch (event.type) {
74
+ case "thread.started":
75
+ startThread(state, event);
76
+ break;
77
+ case "item.completed":
78
+ completeItem(state, event.item);
79
+ break;
80
+ case "turn.completed":
81
+ completeTurn(state, event);
82
+ break;
83
+ case "turn.failed":
84
+ state.failed = true;
85
+ break;
86
+ }
30
87
  }
31
88
 
32
89
  export interface CodexEventSummary {
@@ -45,58 +102,38 @@ export function summarizeCodexEvents(
45
102
  ): CodexEventSummary {
46
103
  if (Buffer.byteLength(stream, "utf8") > MAX_EVENT_STREAM_BYTES)
47
104
  throw new CodexEventError("Codex event stream exceeds 8 MiB");
48
- let threadId: string | null = null;
49
- let completed = false;
50
- let failed = false;
51
- let finalMessage: string | null = null;
52
- let tokenUsage: ReturnType<typeof usage> = null;
53
- let usageComplete = false;
105
+ const state: TurnState = {
106
+ threadId: null,
107
+ completed: false,
108
+ failed: false,
109
+ finalMessage: null,
110
+ tokenUsage: null,
111
+ };
54
112
  for (const line of stream.split("\n")) {
55
113
  if (!line.trim()) continue;
56
- let parsed: unknown;
57
- try {
58
- parsed = JSON.parse(line);
59
- } catch {
60
- throw new CodexEventError("Codex event stream contains invalid JSONL");
61
- }
62
- if (!record(parsed) || typeof parsed.type !== "string")
63
- throw new CodexEventError("Codex event stream contains an invalid event");
64
- if (parsed.type === "thread.started") {
65
- if (
66
- threadId !== null ||
67
- typeof parsed.thread_id !== "string" ||
68
- !parsed.thread_id
69
- )
70
- throw new CodexEventError(
71
- "Codex event stream has an invalid thread start",
72
- );
73
- threadId = parsed.thread_id;
74
- }
75
- if (parsed.type === "item.completed" && record(parsed.item)) {
76
- if (parsed.item.type === "agent_message" && !completed) {
77
- finalMessage =
78
- typeof parsed.item.text === "string" ? parsed.item.text : null;
79
- }
80
- }
81
- if (parsed.type === "turn.completed") {
82
- if (!threadId || completed || failed)
83
- throw new CodexEventError(
84
- "Codex event stream has an invalid turn completion",
85
- );
86
- completed = true;
87
- usageComplete = usage(parsed.usage) !== null;
88
- tokenUsage = usage(parsed.usage);
89
- }
90
- if (parsed.type === "turn.failed") failed = true;
114
+ applyEvent(state, parseEvent(line));
91
115
  }
116
+ return summarizeTurn(state, exitCode);
117
+ }
118
+
119
+ function summarizeTurn(state: TurnState, exitCode: number): CodexEventSummary {
120
+ const { threadId, completed, failed, finalMessage, tokenUsage } = state;
92
121
  if (!threadId)
93
122
  throw new CodexEventError("Codex event stream has no thread start");
123
+ const complete = exitCode === 0 && completed && !failed;
94
124
  return {
95
125
  threadId,
96
- complete: exitCode === 0 && completed && !failed,
126
+ complete,
97
127
  finalMessage,
98
- inputTokens: tokenUsage?.inputTokens ?? null,
99
- outputTokens: tokenUsage?.outputTokens ?? null,
100
- usageComplete: exitCode === 0 && completed && !failed && usageComplete,
128
+ ...summarizeUsage(tokenUsage, complete),
101
129
  };
102
130
  }
131
+
132
+ function summarizeUsage(
133
+ tokenUsage: ReturnType<typeof usage>,
134
+ complete: boolean,
135
+ ) {
136
+ if (tokenUsage === null)
137
+ return { inputTokens: null, outputTokens: null, usageComplete: false };
138
+ return { ...tokenUsage, usageComplete: complete };
139
+ }