@inflexa-ai/harness 0.14.2 → 0.15.1

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 (128) hide show
  1. package/README.md +2 -1
  2. package/dist/agents/conversation-agent.d.ts +7 -0
  3. package/dist/agents/conversation-agent.js +5 -3
  4. package/dist/agents/sandbox/drug-repurposing-agent.js +5 -1
  5. package/dist/agents/sandbox/immune-profiling-agent.js +4 -1
  6. package/dist/app/synthesize-run.d.ts +3 -0
  7. package/dist/app/synthesize-run.js +2 -1
  8. package/dist/billing/noop-usage-recorder.d.ts +7 -0
  9. package/dist/billing/noop-usage-recorder.js +10 -0
  10. package/dist/billing/usage-recorder.d.ts +63 -0
  11. package/dist/billing/usage-recorder.js +15 -0
  12. package/dist/contracts/chat-events.d.ts +44 -3
  13. package/dist/contracts/chat-parts.d.ts +38 -2
  14. package/dist/contracts/chat-parts.js +1 -1
  15. package/dist/contracts/data-profile.d.ts +26 -0
  16. package/dist/contracts/data-profile.js +26 -0
  17. package/dist/contracts/index.d.ts +3 -1
  18. package/dist/contracts/index.js +1 -0
  19. package/dist/contracts/message.d.ts +21 -1
  20. package/dist/contracts/part-registry.js +1 -0
  21. package/dist/contracts/schemas/chat-events.d.ts +48 -2
  22. package/dist/contracts/schemas/chat-events.js +8 -1
  23. package/dist/contracts/schemas/chat-parts.d.ts +46 -3
  24. package/dist/contracts/schemas/chat-parts.js +13 -0
  25. package/dist/contracts/schemas/index.d.ts +2 -1
  26. package/dist/contracts/schemas/index.js +2 -1
  27. package/dist/contracts/schemas/usage.d.ts +11 -0
  28. package/dist/contracts/schemas/usage.js +18 -0
  29. package/dist/contracts/usage.d.ts +51 -0
  30. package/dist/contracts/usage.js +41 -0
  31. package/dist/execution/artifact-metadata.d.ts +3 -0
  32. package/dist/execution/artifact-metadata.js +1 -0
  33. package/dist/execution/dbos-workflow-purger.d.ts +30 -0
  34. package/dist/execution/dbos-workflow-purger.js +193 -0
  35. package/dist/execution/post-step-pipeline.d.ts +3 -0
  36. package/dist/execution/post-step-pipeline.js +2 -0
  37. package/dist/execution/reconcile-manifest.js +28 -15
  38. package/dist/execution/report-runner.d.ts +16 -0
  39. package/dist/execution/report-runner.js +4 -0
  40. package/dist/execution/run-synthesis.d.ts +3 -0
  41. package/dist/execution/run-synthesis.js +2 -0
  42. package/dist/execution/step-summary.d.ts +3 -0
  43. package/dist/execution/step-summary.js +1 -0
  44. package/dist/execution/workflow-purger.d.ts +38 -0
  45. package/dist/execution/workflow-purger.js +24 -0
  46. package/dist/index.d.ts +24 -2
  47. package/dist/index.js +27 -0
  48. package/dist/lib/metrics.js +2 -1
  49. package/dist/loop/metrics.d.ts +18 -0
  50. package/dist/loop/metrics.js +18 -1
  51. package/dist/loop/run-agent.d.ts +35 -0
  52. package/dist/loop/run-agent.js +149 -23
  53. package/dist/loop/run-to-terminal.d.ts +5 -2
  54. package/dist/loop/run-to-terminal.js +36 -2
  55. package/dist/loop/tool-detail.d.ts +54 -0
  56. package/dist/loop/tool-detail.js +105 -0
  57. package/dist/loop/tool-outcome.d.ts +21 -0
  58. package/dist/loop/tool-outcome.js +25 -0
  59. package/dist/loop/types.d.ts +15 -1
  60. package/dist/memory/content-to-cortex.d.ts +28 -4
  61. package/dist/memory/content-to-cortex.js +70 -14
  62. package/dist/memory/thread-history.d.ts +21 -1
  63. package/dist/memory/thread-history.js +29 -7
  64. package/dist/memory/thread-store.d.ts +65 -8
  65. package/dist/memory/thread-store.js +81 -17
  66. package/dist/prompts/sandbox/bulk-transcriptomics-agent.d.ts +1 -1
  67. package/dist/prompts/sandbox/bulk-transcriptomics-agent.js +17 -13
  68. package/dist/prompts/sandbox/chromatin-agent.d.ts +1 -1
  69. package/dist/prompts/sandbox/chromatin-agent.js +45 -54
  70. package/dist/prompts/sandbox/genomic-variant-agent.d.ts +1 -1
  71. package/dist/prompts/sandbox/genomic-variant-agent.js +39 -37
  72. package/dist/prompts/sandbox-standards.d.ts +1 -1
  73. package/dist/prompts/sandbox-standards.js +21 -2
  74. package/dist/provenance/collector.d.ts +5 -3
  75. package/dist/provenance/collector.js +5 -3
  76. package/dist/providers/ai-sdk.js +91 -16
  77. package/dist/providers/streaming-chat.js +5 -0
  78. package/dist/providers/types.d.ts +23 -0
  79. package/dist/providers/types.js +1 -0
  80. package/dist/reference-data/catalog.js +536 -0
  81. package/dist/runtime/assemble.d.ts +25 -8
  82. package/dist/runtime/assemble.js +7 -4
  83. package/dist/sandbox/sandbox-step-translate.d.ts +22 -1
  84. package/dist/sandbox/sandbox-step-translate.js +23 -36
  85. package/dist/schemas/run-synthesis.d.ts +2 -2
  86. package/dist/state/index.d.ts +2 -0
  87. package/dist/state/index.js +3 -0
  88. package/dist/state/init.js +28 -0
  89. package/dist/state/purge-analysis.d.ts +74 -0
  90. package/dist/state/purge-analysis.js +205 -0
  91. package/dist/state/vector-store.d.ts +6 -0
  92. package/dist/state/vector-store.js +22 -2
  93. package/dist/tasks/data-profile-activity.d.ts +14 -2
  94. package/dist/tasks/data-profile-activity.js +1 -1
  95. package/dist/tasks/data-profile.d.ts +3 -0
  96. package/dist/tasks/data-profile.js +9 -3
  97. package/dist/tools/bio/pubmed.js +15 -0
  98. package/dist/tools/bio/search-gene.js +12 -0
  99. package/dist/tools/define-tool.d.ts +41 -3
  100. package/dist/tools/define-tool.js +4 -1
  101. package/dist/tools/detail-resolver.d.ts +35 -0
  102. package/dist/tools/detail-resolver.js +39 -0
  103. package/dist/tools/execute-analysis.js +3 -0
  104. package/dist/tools/iterate-report.d.ts +3 -0
  105. package/dist/tools/iterate-report.js +3 -0
  106. package/dist/tools/memory/update-working-memory.js +8 -0
  107. package/dist/tools/research/generate-analogy-report.d.ts +3 -0
  108. package/dist/tools/research/generate-analogy-report.js +6 -0
  109. package/dist/tools/research/generate-plan.d.ts +3 -0
  110. package/dist/tools/research/generate-plan.js +6 -0
  111. package/dist/tools/research/inspect-run.js +4 -0
  112. package/dist/tools/research/literature-reviewer.d.ts +3 -0
  113. package/dist/tools/research/literature-reviewer.js +6 -0
  114. package/dist/tools/workspace/edit-file.js +2 -0
  115. package/dist/tools/workspace/execute-command.js +25 -0
  116. package/dist/tools/workspace/grep.js +3 -0
  117. package/dist/tools/workspace/list-files.js +2 -0
  118. package/dist/tools/workspace/read-file.js +7 -0
  119. package/dist/tools/workspace/workspace-search.js +1 -0
  120. package/dist/tools/workspace/write-file.js +3 -0
  121. package/dist/workflows/execute-analysis.d.ts +7 -0
  122. package/dist/workflows/execute-analysis.js +31 -2
  123. package/dist/workflows/execute-target-assessment.d.ts +3 -0
  124. package/dist/workflows/sandbox-step.d.ts +23 -0
  125. package/dist/workflows/sandbox-step.js +33 -2
  126. package/dist/workflows/target-assessment/lib/clinical-consequence-annotator.d.ts +3 -0
  127. package/dist/workflows/target-assessment/lib/clinical-consequence-annotator.js +1 -0
  128. package/package.json +1 -1
@@ -2,6 +2,7 @@
2
2
  * Zod schemas for Cortex-native chat-stream events — validation at boundaries.
3
3
  */
4
4
  import { z } from "zod";
5
+ import { TokenUsageRollupSchema } from "./usage.js";
5
6
  export const EventSourceSchema = z.object({
6
7
  agentId: z.string(),
7
8
  callPath: z.array(z.string()),
@@ -11,22 +12,28 @@ export const TextDeltaEventSchema = z.object({
11
12
  text: z.string(),
12
13
  source: EventSourceSchema,
13
14
  });
15
+ /** Mirrors the `ToolOutcome` union — three states, never a boolean. */
16
+ export const ToolOutcomeSchema = z.enum(["ok", "error", "denied"]);
14
17
  export const ToolStartedEventSchema = z.object({
15
18
  type: z.literal("tool-started"),
16
19
  toolUseId: z.string(),
17
20
  name: z.string(),
21
+ detail: z.string().optional(),
18
22
  source: EventSourceSchema,
19
23
  });
20
24
  export const ToolFinishedEventSchema = z.object({
21
25
  type: z.literal("tool-finished"),
22
26
  toolUseId: z.string(),
23
27
  name: z.string(),
24
- isError: z.boolean(),
28
+ outcome: ToolOutcomeSchema,
29
+ detail: z.string().optional(),
25
30
  source: EventSourceSchema,
26
31
  });
27
32
  export const FinishEventSchema = z.object({
28
33
  type: z.literal("finish"),
29
34
  source: EventSourceSchema,
35
+ usage: TokenUsageRollupSchema.optional(),
36
+ turnUsage: TokenUsageRollupSchema.optional(),
30
37
  });
31
38
  export const ChatErrorEventSchema = z.object({
32
39
  type: z.literal("error"),
@@ -104,8 +104,8 @@ export declare const FileReferencePartSchema: z.ZodObject<{
104
104
  }, z.core.$strip>>;
105
105
  }, z.core.$strip>;
106
106
  export declare const AskStatusSchema: z.ZodEnum<{
107
- pending: "pending";
108
107
  aborted: "aborted";
108
+ pending: "pending";
109
109
  resolved: "resolved";
110
110
  rejected: "rejected";
111
111
  expired: "expired";
@@ -117,8 +117,8 @@ export declare const AskPartSchema: z.ZodObject<{
117
117
  command: z.ZodString;
118
118
  detail: z.ZodOptional<z.ZodString>;
119
119
  status: z.ZodEnum<{
120
- pending: "pending";
121
120
  aborted: "aborted";
121
+ pending: "pending";
122
122
  resolved: "resolved";
123
123
  rejected: "rejected";
124
124
  expired: "expired";
@@ -279,6 +279,21 @@ export declare const StepSummaryPartSchema: z.ZodObject<{
279
279
  agentId: z.ZodString;
280
280
  markdown: z.ZodString;
281
281
  }, z.core.$strip>;
282
+ export declare const StepUsagePartSchema: z.ZodObject<{
283
+ type: z.ZodLiteral<"data-step-usage">;
284
+ id: z.ZodString;
285
+ runId: z.ZodString;
286
+ stepId: z.ZodString;
287
+ agentId: z.ZodString;
288
+ modelId: z.ZodString;
289
+ usage: z.ZodObject<{
290
+ inputTokens: z.ZodOptional<z.ZodNumber>;
291
+ outputTokens: z.ZodOptional<z.ZodNumber>;
292
+ cacheCreationInputTokens: z.ZodOptional<z.ZodNumber>;
293
+ cacheReadInputTokens: z.ZodOptional<z.ZodNumber>;
294
+ reasoningTokens: z.ZodOptional<z.ZodNumber>;
295
+ }, z.core.$strip>;
296
+ }, z.core.$strip>;
282
297
  export declare const StepBlockedPartSchema: z.ZodObject<{
283
298
  type: z.ZodLiteral<"data-step-blocked">;
284
299
  id: z.ZodString;
@@ -314,6 +329,13 @@ export declare const RunCompletedPartSchema: z.ZodObject<{
314
329
  }>;
315
330
  }, z.core.$strip>>;
316
331
  note: z.ZodOptional<z.ZodString>;
332
+ usage: z.ZodOptional<z.ZodObject<{
333
+ inputTokens: z.ZodOptional<z.ZodNumber>;
334
+ outputTokens: z.ZodOptional<z.ZodNumber>;
335
+ cacheCreationInputTokens: z.ZodOptional<z.ZodNumber>;
336
+ cacheReadInputTokens: z.ZodOptional<z.ZodNumber>;
337
+ reasoningTokens: z.ZodOptional<z.ZodNumber>;
338
+ }, z.core.$strip>>;
317
339
  }, z.core.$strip>;
318
340
  export declare const SynthesizedFindingSchema: z.ZodObject<{
319
341
  stepId: z.ZodString;
@@ -512,8 +534,8 @@ export declare const CortexChatPartSchema: z.ZodDiscriminatedUnion<[z.ZodObject<
512
534
  command: z.ZodString;
513
535
  detail: z.ZodOptional<z.ZodString>;
514
536
  status: z.ZodEnum<{
515
- pending: "pending";
516
537
  aborted: "aborted";
538
+ pending: "pending";
517
539
  resolved: "resolved";
518
540
  rejected: "rejected";
519
541
  expired: "expired";
@@ -607,6 +629,20 @@ export declare const CortexChatPartSchema: z.ZodDiscriminatedUnion<[z.ZodObject<
607
629
  stepId: z.ZodString;
608
630
  agentId: z.ZodString;
609
631
  markdown: z.ZodString;
632
+ }, z.core.$strip>, z.ZodObject<{
633
+ type: z.ZodLiteral<"data-step-usage">;
634
+ id: z.ZodString;
635
+ runId: z.ZodString;
636
+ stepId: z.ZodString;
637
+ agentId: z.ZodString;
638
+ modelId: z.ZodString;
639
+ usage: z.ZodObject<{
640
+ inputTokens: z.ZodOptional<z.ZodNumber>;
641
+ outputTokens: z.ZodOptional<z.ZodNumber>;
642
+ cacheCreationInputTokens: z.ZodOptional<z.ZodNumber>;
643
+ cacheReadInputTokens: z.ZodOptional<z.ZodNumber>;
644
+ reasoningTokens: z.ZodOptional<z.ZodNumber>;
645
+ }, z.core.$strip>;
610
646
  }, z.core.$strip>, z.ZodObject<{
611
647
  type: z.ZodLiteral<"data-step-blocked">;
612
648
  id: z.ZodString;
@@ -691,6 +727,13 @@ export declare const CortexChatPartSchema: z.ZodDiscriminatedUnion<[z.ZodObject<
691
727
  }>;
692
728
  }, z.core.$strip>>;
693
729
  note: z.ZodOptional<z.ZodString>;
730
+ usage: z.ZodOptional<z.ZodObject<{
731
+ inputTokens: z.ZodOptional<z.ZodNumber>;
732
+ outputTokens: z.ZodOptional<z.ZodNumber>;
733
+ cacheCreationInputTokens: z.ZodOptional<z.ZodNumber>;
734
+ cacheReadInputTokens: z.ZodOptional<z.ZodNumber>;
735
+ reasoningTokens: z.ZodOptional<z.ZodNumber>;
736
+ }, z.core.$strip>>;
694
737
  }, z.core.$strip>, z.ZodObject<{
695
738
  type: z.ZodLiteral<"data-run-failed">;
696
739
  runId: z.ZodString;
@@ -2,6 +2,7 @@
2
2
  * Zod schemas for Cortex chat data parts — validation at boundaries.
3
3
  */
4
4
  import { z } from "zod";
5
+ import { TokenUsageRollupSchema } from "./usage.js";
5
6
  // ── Presentation ────────────────────────────────────────────────────
6
7
  export const PlanStepSchema = z.object({
7
8
  id: z.string(),
@@ -181,6 +182,16 @@ export const StepSummaryPartSchema = z.object({
181
182
  agentId: z.string(),
182
183
  markdown: z.string(),
183
184
  });
185
+ // ── Step Usage ──────────────────────────────────────────────────────
186
+ export const StepUsagePartSchema = z.object({
187
+ type: z.literal("data-step-usage"),
188
+ id: z.string(),
189
+ runId: z.string(),
190
+ stepId: z.string(),
191
+ agentId: z.string(),
192
+ modelId: z.string(),
193
+ usage: TokenUsageRollupSchema,
194
+ });
184
195
  export const StepBlockedPartSchema = z.object({
185
196
  type: z.literal("data-step-blocked"),
186
197
  id: z.string(),
@@ -203,6 +214,7 @@ export const RunCompletedPartSchema = z.object({
203
214
  artifactCount: z.number(),
204
215
  findings: z.array(RunCompletedFindingSchema),
205
216
  note: z.string().optional(),
217
+ usage: TokenUsageRollupSchema.optional(),
206
218
  });
207
219
  // ── Run Synthesis ──────────────────────────────────────────────────
208
220
  export const SynthesizedFindingSchema = z.object({
@@ -295,6 +307,7 @@ export const CortexChatPartSchema = z.discriminatedUnion("type", [
295
307
  StepFileTreePartSchema,
296
308
  StepOutputPartSchema,
297
309
  StepSummaryPartSchema,
310
+ StepUsagePartSchema,
298
311
  StepBlockedPartSchema,
299
312
  RunSynthesisPartSchema,
300
313
  SynthesisProgressPartSchema,
@@ -1,2 +1,3 @@
1
- export { PlanStepSchema, PresentationContentSchema, PresentationPartSchema, PlanPartSchema, RunCardPartSchema, FileReferenceEntrySchema, FileReferencePartSchema, RunStartedPartSchema, StepStatusSchema, DagStepStateSchema, DagStatePartSchema, StepPhaseSchema, StepActivityPartSchema, FileTreeEntrySchema, StepFileTreePartSchema, StepOutputFileSchema, StepOutputPartSchema, StepSummaryPartSchema, StepBlockedPartSchema, SynthesizedFindingSchema, BiologicalThemeSchema, RunSynthesisPartSchema, SynthesisPhaseSchema, SynthesisProgressPartSchema, RunCompletedFindingSchema, RunCompletedPartSchema, RunFailedPartSchema, PreviewPartSchema, DataPreviewFailedPartSchema, CortexChatPartSchema, } from "./chat-parts.js";
1
+ export { PlanStepSchema, PresentationContentSchema, PresentationPartSchema, PlanPartSchema, RunCardPartSchema, FileReferenceEntrySchema, FileReferencePartSchema, RunStartedPartSchema, StepStatusSchema, DagStepStateSchema, DagStatePartSchema, StepPhaseSchema, StepActivityPartSchema, FileTreeEntrySchema, StepFileTreePartSchema, StepOutputFileSchema, StepOutputPartSchema, StepSummaryPartSchema, StepUsagePartSchema, StepBlockedPartSchema, SynthesizedFindingSchema, BiologicalThemeSchema, RunSynthesisPartSchema, SynthesisPhaseSchema, SynthesisProgressPartSchema, RunCompletedFindingSchema, RunCompletedPartSchema, RunFailedPartSchema, PreviewPartSchema, DataPreviewFailedPartSchema, CortexChatPartSchema, } from "./chat-parts.js";
2
2
  export { EventSourceSchema, TextDeltaEventSchema, ToolStartedEventSchema, ToolFinishedEventSchema, FinishEventSchema, ChatErrorEventSchema, CortexChatEventSchema, } from "./chat-events.js";
3
+ export { TokenUsageRollupSchema } from "./usage.js";
@@ -1,2 +1,3 @@
1
- export { PlanStepSchema, PresentationContentSchema, PresentationPartSchema, PlanPartSchema, RunCardPartSchema, FileReferenceEntrySchema, FileReferencePartSchema, RunStartedPartSchema, StepStatusSchema, DagStepStateSchema, DagStatePartSchema, StepPhaseSchema, StepActivityPartSchema, FileTreeEntrySchema, StepFileTreePartSchema, StepOutputFileSchema, StepOutputPartSchema, StepSummaryPartSchema, StepBlockedPartSchema, SynthesizedFindingSchema, BiologicalThemeSchema, RunSynthesisPartSchema, SynthesisPhaseSchema, SynthesisProgressPartSchema, RunCompletedFindingSchema, RunCompletedPartSchema, RunFailedPartSchema, PreviewPartSchema, DataPreviewFailedPartSchema, CortexChatPartSchema, } from "./chat-parts.js";
1
+ export { PlanStepSchema, PresentationContentSchema, PresentationPartSchema, PlanPartSchema, RunCardPartSchema, FileReferenceEntrySchema, FileReferencePartSchema, RunStartedPartSchema, StepStatusSchema, DagStepStateSchema, DagStatePartSchema, StepPhaseSchema, StepActivityPartSchema, FileTreeEntrySchema, StepFileTreePartSchema, StepOutputFileSchema, StepOutputPartSchema, StepSummaryPartSchema, StepUsagePartSchema, StepBlockedPartSchema, SynthesizedFindingSchema, BiologicalThemeSchema, RunSynthesisPartSchema, SynthesisPhaseSchema, SynthesisProgressPartSchema, RunCompletedFindingSchema, RunCompletedPartSchema, RunFailedPartSchema, PreviewPartSchema, DataPreviewFailedPartSchema, CortexChatPartSchema, } from "./chat-parts.js";
2
2
  export { EventSourceSchema, TextDeltaEventSchema, ToolStartedEventSchema, ToolFinishedEventSchema, FinishEventSchema, ChatErrorEventSchema, CortexChatEventSchema, } from "./chat-events.js";
3
+ export { TokenUsageRollupSchema } from "./usage.js";
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Zod schema for the token-usage rollup — validation at boundaries.
3
+ */
4
+ import { z } from "zod";
5
+ export declare const TokenUsageRollupSchema: z.ZodObject<{
6
+ inputTokens: z.ZodOptional<z.ZodNumber>;
7
+ outputTokens: z.ZodOptional<z.ZodNumber>;
8
+ cacheCreationInputTokens: z.ZodOptional<z.ZodNumber>;
9
+ cacheReadInputTokens: z.ZodOptional<z.ZodNumber>;
10
+ reasoningTokens: z.ZodOptional<z.ZodNumber>;
11
+ }, z.core.$strip>;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Zod schema for the token-usage rollup — validation at boundaries.
3
+ */
4
+ import { z } from "zod";
5
+ /**
6
+ * One reported count. Token counts are whole tokens and cannot run negative, so
7
+ * the schema rejects fractions and negatives rather than passing them through as
8
+ * plain numbers. Optional throughout: absent means "not reported", never zero.
9
+ */
10
+ const tokenCount = z.number().int().nonnegative().optional();
11
+ export const TokenUsageRollupSchema = z.object({
12
+ inputTokens: tokenCount,
13
+ outputTokens: tokenCount,
14
+ cacheCreationInputTokens: tokenCount,
15
+ cacheReadInputTokens: tokenCount,
16
+ reasoningTokens: tokenCount,
17
+ });
18
+ const _assertSchemaFields = true;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Token-usage rollups on the Cortex wire vocabulary.
3
+ *
4
+ * A rollup is the sum of what providers reported across some set of LLM calls
5
+ * — one agent loop, one whole turn, one run step. It is declared here, and not
6
+ * imported from the provider seam, because `contracts/` is the sole
7
+ * Cortex↔consumer wire contract and must stay free of the harness's own
8
+ * dependencies: a frontend importing these types should not inherit a
9
+ * typecheck dependency on the AI SDK.
10
+ *
11
+ * Every field is optional and absent means "not reported" — never zero. A set
12
+ * of calls that reported nothing carries no rollup at all rather than an
13
+ * all-zero one, so a consumer can tell "spent nothing" from "was told nothing".
14
+ */
15
+ export interface TokenUsageRollup {
16
+ /** Total billed prefix, cache reads included. */
17
+ readonly inputTokens?: number;
18
+ readonly outputTokens?: number;
19
+ /** Prefix tokens written into the provider's prompt cache. */
20
+ readonly cacheCreationInputTokens?: number;
21
+ /** Prefix tokens served from the provider's prompt cache. */
22
+ readonly cacheReadInputTokens?: number;
23
+ /**
24
+ * Exactly what the provider reported — never derived from, nor reconciled
25
+ * against, `outputTokens`. Whether reasoning tokens sit inside the output
26
+ * total varies by provider, so arithmetic between the two is a guess.
27
+ */
28
+ readonly reasoningTokens?: number;
29
+ }
30
+ /**
31
+ * The rollup's fields as values — the single enumeration a fold can iterate.
32
+ *
33
+ * This shape is declared four times over: here, on the provider seam's
34
+ * `ChatUsage`, on the loop's mutable `AgentRunUsage`, and on the Zod schema
35
+ * beside this file. Nothing but this list joins them, and it is deliberately
36
+ * the *only* thing that does: `contracts/` is the Cortex↔consumer wire
37
+ * vocabulary and must not import the loop, the providers, or the AI SDK, so a
38
+ * shared base type is not available to it. What is available is a
39
+ * dependency-free constant every side may import — the loop folds by iterating
40
+ * it (`loop/metrics.ts:addChatUsage`), and the declarations that cannot be
41
+ * expressed in terms of it are pinned to it by key-set assertions at their own
42
+ * sites, which is why adding a sixth count in one place fails `tsc` instead of
43
+ * silently going unfolded.
44
+ *
45
+ * `satisfies` rejects a name that is not a field; the assertion below rejects a
46
+ * field that is not named. Both directions are needed — either alone leaves one
47
+ * kind of drift compiling.
48
+ */
49
+ export declare const TOKEN_USAGE_FIELDS: readonly ["inputTokens", "outputTokens", "cacheCreationInputTokens", "cacheReadInputTokens", "reasoningTokens"];
50
+ /** One token-usage field name. */
51
+ export type TokenUsageField = (typeof TOKEN_USAGE_FIELDS)[number];
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Token-usage rollups on the Cortex wire vocabulary.
3
+ *
4
+ * A rollup is the sum of what providers reported across some set of LLM calls
5
+ * — one agent loop, one whole turn, one run step. It is declared here, and not
6
+ * imported from the provider seam, because `contracts/` is the sole
7
+ * Cortex↔consumer wire contract and must stay free of the harness's own
8
+ * dependencies: a frontend importing these types should not inherit a
9
+ * typecheck dependency on the AI SDK.
10
+ *
11
+ * Every field is optional and absent means "not reported" — never zero. A set
12
+ * of calls that reported nothing carries no rollup at all rather than an
13
+ * all-zero one, so a consumer can tell "spent nothing" from "was told nothing".
14
+ */
15
+ /**
16
+ * The rollup's fields as values — the single enumeration a fold can iterate.
17
+ *
18
+ * This shape is declared four times over: here, on the provider seam's
19
+ * `ChatUsage`, on the loop's mutable `AgentRunUsage`, and on the Zod schema
20
+ * beside this file. Nothing but this list joins them, and it is deliberately
21
+ * the *only* thing that does: `contracts/` is the Cortex↔consumer wire
22
+ * vocabulary and must not import the loop, the providers, or the AI SDK, so a
23
+ * shared base type is not available to it. What is available is a
24
+ * dependency-free constant every side may import — the loop folds by iterating
25
+ * it (`loop/metrics.ts:addChatUsage`), and the declarations that cannot be
26
+ * expressed in terms of it are pinned to it by key-set assertions at their own
27
+ * sites, which is why adding a sixth count in one place fails `tsc` instead of
28
+ * silently going unfolded.
29
+ *
30
+ * `satisfies` rejects a name that is not a field; the assertion below rejects a
31
+ * field that is not named. Both directions are needed — either alone leaves one
32
+ * kind of drift compiling.
33
+ */
34
+ export const TOKEN_USAGE_FIELDS = [
35
+ "inputTokens",
36
+ "outputTokens",
37
+ "cacheCreationInputTokens",
38
+ "cacheReadInputTokens",
39
+ "reasoningTokens",
40
+ ];
41
+ const _assertFieldsComplete = true;
@@ -25,6 +25,7 @@ import type { LoopMessage } from "../loop/types.js";
25
25
  import type { AgentChat } from "../providers/types.js";
26
26
  import type { WorkspaceFilesystem } from "../workspace/filesystem.js";
27
27
  import type { Logger } from "../lib/logger.js";
28
+ import type { UsageRecorder } from "../billing/usage-recorder.js";
28
29
  export interface ArtifactForMetadata {
29
30
  /** Path used to update `cortex_artifacts` after metadata generation. */
30
31
  readonly dbPath: string;
@@ -36,6 +37,8 @@ export interface ArtifactForMetadata {
36
37
  export interface GenerateFileMetadataOptions {
37
38
  /** Operational logging seam; omitted falls back to no-op. */
38
39
  readonly logger?: Logger;
40
+ /** LLM usage-accounting seam for the describer loop; omitted falls back to the no-op recorder. */
41
+ readonly usageRecorder?: UsageRecorder;
39
42
  readonly provider: AgentChat;
40
43
  readonly session: AgentSession;
41
44
  readonly artifacts: readonly ArtifactForMetadata[];
@@ -182,6 +182,7 @@ export async function generateFileMetadata(opts) {
182
182
  signal,
183
183
  emit: () => { },
184
184
  runStep: passthroughStep,
185
+ usageRecorder: opts.usageRecorder,
185
186
  });
186
187
  }
187
188
  catch (err) {
@@ -0,0 +1,30 @@
1
+ /**
2
+ * DBOS realization of the `WorkflowPurger` seam — the single production adapter,
3
+ * shared by every embedder. Host-neutral: it references only DBOS, which the
4
+ * harness already depends on.
5
+ *
6
+ * It is built on `DBOSClient`, not the static `DBOS` facade. The facade's
7
+ * workflow-management calls require a launched engine and throw without one,
8
+ * which would make a purge impossible from exactly the process that needs it
9
+ * most: a headless command holding nothing but a pool. `DBOSClient` accepts a
10
+ * pre-built pool and needs no launch, so one realization serves a booted host and
11
+ * a headless one alike and no embedder has to choose between two purgers. The
12
+ * engine's system database is the same database as the application pool, so the
13
+ * injected pool already reaches the ledger.
14
+ *
15
+ * The engine owns the deletion itself, including the breadth-first walk over
16
+ * `parent_workflow_id` that reaches child step workflows. Its single
17
+ * `DELETE FROM dbos.workflow_status WHERE workflow_uuid = ANY(...)` is all the
18
+ * dependent tables need: step outputs, streams, inputs, events, notifications,
19
+ * and queue rows each hold an `ON DELETE CASCADE` foreign key to
20
+ * `workflow_status`, so they go with the status row.
21
+ */
22
+ import type { Pool } from "pg";
23
+ import type { Logger } from "../lib/logger.js";
24
+ import type { WorkflowPurger } from "./workflow-purger.js";
25
+ export interface DbosWorkflowPurgerDeps {
26
+ /** Pool onto the engine's system database — the same database the application pool holds. */
27
+ readonly pool: Pool;
28
+ readonly logger?: Logger;
29
+ }
30
+ export declare function createDbosWorkflowPurger({ pool, logger: injected }: DbosWorkflowPurgerDeps): WorkflowPurger;
@@ -0,0 +1,193 @@
1
+ /**
2
+ * DBOS realization of the `WorkflowPurger` seam — the single production adapter,
3
+ * shared by every embedder. Host-neutral: it references only DBOS, which the
4
+ * harness already depends on.
5
+ *
6
+ * It is built on `DBOSClient`, not the static `DBOS` facade. The facade's
7
+ * workflow-management calls require a launched engine and throw without one,
8
+ * which would make a purge impossible from exactly the process that needs it
9
+ * most: a headless command holding nothing but a pool. `DBOSClient` accepts a
10
+ * pre-built pool and needs no launch, so one realization serves a booted host and
11
+ * a headless one alike and no embedder has to choose between two purgers. The
12
+ * engine's system database is the same database as the application pool, so the
13
+ * injected pool already reaches the ledger.
14
+ *
15
+ * The engine owns the deletion itself, including the breadth-first walk over
16
+ * `parent_workflow_id` that reaches child step workflows. Its single
17
+ * `DELETE FROM dbos.workflow_status WHERE workflow_uuid = ANY(...)` is all the
18
+ * dependent tables need: step outputs, streams, inputs, events, notifications,
19
+ * and queue rows each hold an `ON DELETE CASCADE` foreign key to
20
+ * `workflow_status`, so they go with the status row.
21
+ */
22
+ import { DBOSClient } from "@dbos-inc/dbos-sdk";
23
+ import { errAsync, okAsync } from "neverthrow";
24
+ import { createNoopLogger } from "../lib/console-logger.js";
25
+ import { tryMutation, tryQuery } from "../lib/db-result.js";
26
+ /**
27
+ * The schema the engine keeps its ledger in. The raw SQL below names it literally,
28
+ * `DBOSClient` defaults to it, and the schema probe asks after this same name — the
29
+ * SDK accepts a `systemDatabaseSchemaName`, so moving a deployment's ledger has to
30
+ * move all three together.
31
+ */
32
+ const SYSTEM_SCHEMA = "dbos";
33
+ /**
34
+ * SQLSTATE `undefined_table` — the answer both when the engine never created its
35
+ * schema and when `workflow_status` is gone from beneath a schema that exists. Only
36
+ * the first is nothing to purge, so the code on its own settles nothing.
37
+ */
38
+ const UNDEFINED_TABLE = "42P01";
39
+ function isUndefinedTable(e) {
40
+ const cause = e.cause;
41
+ return cause !== null && typeof cause === "object" && "code" in cause && cause.code === UNDEFINED_TABLE;
42
+ }
43
+ /**
44
+ * Route the client's own diagnostics — pool errors and idle-client errors — onto
45
+ * the injected sink. Left unset, the SDK builds a console logger of its own, and
46
+ * a host whose UI owns stdout discards those records entirely.
47
+ *
48
+ * The SDK renders every entry to a string before delegating, and splits a failure
49
+ * across both parameters: the text arrives as the entry, the stack — with any
50
+ * `cause` chain already folded into it — as `metadata.stack`. So the entry is the
51
+ * record's message at every level, `error` included, and engine wording stays
52
+ * queryable in one place instead of the message at three levels and a field at the
53
+ * fourth. The stack rides beside it under the key `defaultErrorFields` uses.
54
+ * `errorFields` is not the tool here: it normalizes a thrown value, and an entry
55
+ * the SDK has already rendered takes its `String(...)` branch, which emits a bare
56
+ * message and drops the one stack the SDK bothered to hand over.
57
+ */
58
+ function asEngineLogger(logger) {
59
+ const render = (entry) => (typeof entry === "string" ? entry : String(entry));
60
+ return {
61
+ debug: (entry) => logger.debug(render(entry)),
62
+ info: (entry) => logger.info(render(entry)),
63
+ warn: (entry) => logger.warn(render(entry)),
64
+ error: (entry, metadata) => logger.error(render(entry), metadata?.stack === undefined ? undefined : { stack: metadata.stack }),
65
+ };
66
+ }
67
+ export function createDbosWorkflowPurger({ pool, logger: injected }) {
68
+ const logger = (injected ?? createNoopLogger()).named("workflow-purger");
69
+ // One client per factory, not per call. Creating a client does no I/O, but the
70
+ // client registers `error` and `connect` handlers on the pool it is handed, so a
71
+ // client per purge would accumulate handler pairs on a long-lived pool. Its
72
+ // `destroy()` ends that pool, so a client over an injected pool must never be
73
+ // destroyed either — which leaves a per-call client nothing to release. Creation
74
+ // is lazy so building a purger stays free for a host that never purges.
75
+ let pending;
76
+ const engine = () => {
77
+ // `create` never initializes the system database, and initialization is the
78
+ // only path that reads the connection string once a pool is supplied — so the
79
+ // url is unreachable from this client rather than merely untaken, and there is
80
+ // none to thread through the seam.
81
+ pending ??= DBOSClient.create({ systemDatabaseUrl: "", systemDatabasePool: pool, logger: asEngineLogger(logger) });
82
+ return pending;
83
+ };
84
+ /**
85
+ * Whether the engine's schema exists at all — the only thing that separates a
86
+ * ledger the engine never created from one that is broken. It costs a round trip
87
+ * only on the failure path that consults it, and a positive answer is memoized for
88
+ * the purger's lifetime, since nothing drops the schema once the engine has made
89
+ * it. A negative answer is deliberately not memoized: a purger can be built before
90
+ * the launch that creates the schema, and caching absence would go on reading a
91
+ * ledger that has since broken as one that was never there.
92
+ */
93
+ let schemaConfirmed = false;
94
+ const systemSchemaExists = () => {
95
+ if (schemaConfirmed)
96
+ return okAsync(true);
97
+ return tryQuery("workflowPurger.systemSchemaPresent", async () => {
98
+ const { rows } = await pool.query({
99
+ text: `SELECT EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = $1) AS present`,
100
+ values: [SYSTEM_SCHEMA],
101
+ });
102
+ return rows[0]?.present ?? false;
103
+ }).map((present) => {
104
+ schemaConfirmed = present;
105
+ return present;
106
+ });
107
+ };
108
+ /**
109
+ * Recover a ledger the engine never created into `fallback`. A missing table
110
+ * beneath a schema that does exist stays on the error channel along with every
111
+ * other failure — an SDK rename, a manual drop, or a half-applied migration
112
+ * reaches the caller as one, rather than as a purge that reclaimed nothing.
113
+ */
114
+ function whenLedgerAbsent(fallback) {
115
+ return (e) => isUndefinedTable(e) ? systemSchemaExists().andThen((present) => (present ? errAsync(e) : okAsync(fallback))) : errAsync(e);
116
+ }
117
+ /**
118
+ * How many ledger rows the delete is about to take, read immediately before it,
119
+ * because the engine's delete reports no count of its own. This is a read only:
120
+ * the walk it mirrors selects the same set the delete targets — requested ids that
121
+ * exist, plus their descendants when asked — while the delete itself and its
122
+ * cascades stay engine-owned.
123
+ */
124
+ const countDoomed = (workflowIds, includeDescendants) => tryQuery("workflowPurger.countDoomed", async () => {
125
+ const { rows } = await pool.query(includeDescendants
126
+ ? {
127
+ // The non-recursive term unnests the requested ids rather than
128
+ // reading them out of the ledger, so a descendant is still
129
+ // reached when the id it descends from has already been
130
+ // removed; the outer count then keeps only rows that exist.
131
+ text: `WITH RECURSIVE doomed AS (
132
+ SELECT unnest($1::text[]) AS workflow_uuid
133
+ UNION
134
+ SELECT child.workflow_uuid
135
+ FROM dbos.workflow_status child
136
+ JOIN doomed ON child.parent_workflow_id = doomed.workflow_uuid
137
+ )
138
+ SELECT COUNT(*)::int AS total
139
+ FROM dbos.workflow_status
140
+ WHERE workflow_uuid IN (SELECT workflow_uuid FROM doomed)`,
141
+ values: [workflowIds],
142
+ }
143
+ : {
144
+ text: `SELECT COUNT(*)::int AS total FROM dbos.workflow_status WHERE workflow_uuid = ANY($1)`,
145
+ values: [workflowIds],
146
+ });
147
+ return rows[0]?.total ?? 0;
148
+ });
149
+ return {
150
+ findByIdPrefix(prefix) {
151
+ return tryQuery("workflowPurger.findByIdPrefix", async () => {
152
+ const { rows } = await pool.query({
153
+ // `starts_with` rather than a parameterized LIKE pattern: a namespace
154
+ // carrying `_` or `%` would match as a wildcard there and sweep in
155
+ // workflows belonging to a different analysis.
156
+ text: `SELECT workflow_uuid FROM dbos.workflow_status WHERE starts_with(workflow_uuid, $1)`,
157
+ values: [prefix],
158
+ });
159
+ return rows.map((row) => row.workflow_uuid);
160
+ }).orElse(whenLedgerAbsent([]));
161
+ },
162
+ cancel(workflowIds) {
163
+ if (workflowIds.length === 0)
164
+ return okAsync(undefined);
165
+ return tryMutation("workflowPurger.cancel", async () => {
166
+ const client = await engine();
167
+ // Descendants too: the delete side reaches them, so roots-only
168
+ // cancellation would leave child step executors writing behind it.
169
+ await client.cancelWorkflows([...workflowIds], { cancelChildren: true });
170
+ })
171
+ .map(() => {
172
+ logger.debug("cancelled workflows", { requested: workflowIds.length });
173
+ })
174
+ .orElse(whenLedgerAbsent(undefined));
175
+ },
176
+ deleteWorkflows(workflowIds, includeDescendants = false) {
177
+ if (workflowIds.length === 0)
178
+ return okAsync(0);
179
+ const ids = [...workflowIds];
180
+ return countDoomed(ids, includeDescendants)
181
+ .andThen((doomed) => tryMutation("workflowPurger.deleteWorkflows", async () => {
182
+ const client = await engine();
183
+ await client.deleteWorkflows(ids, includeDescendants);
184
+ return doomed;
185
+ }))
186
+ .map((deleted) => {
187
+ logger.debug("deleted workflows", { deleted, includeDescendants });
188
+ return deleted;
189
+ })
190
+ .orElse(whenLedgerAbsent(0));
191
+ },
192
+ };
193
+ }
@@ -17,6 +17,7 @@ import type { ResolveWorkspaceRoot } from "../workspace/paths.js";
17
17
  import type { ArtifactManifestEntry } from "../schemas/artifact-manifest.js";
18
18
  import type { StepSummary } from "../schemas/step-summary.js";
19
19
  import type { Logger } from "../lib/logger.js";
20
+ import type { UsageRecorder } from "../billing/usage-recorder.js";
20
21
  import type { ArtifactRegistry } from "./artifact-registry.js";
21
22
  import { type FileMetadataEntry } from "./artifact-metadata.js";
22
23
  import type { PostStepArtifacts, PostStepContext, StepOutputs } from "../workflows/sandbox-step.js";
@@ -28,6 +29,8 @@ export interface PostStepPipelineDeps {
28
29
  readonly pool: Pool;
29
30
  /** Operational logging seam; omitted falls back to no-op. */
30
31
  readonly logger?: Logger;
32
+ /** LLM usage-accounting seam for the metadata + summary loops; omitted falls back to the no-op recorder. */
33
+ readonly usageRecorder?: UsageRecorder;
31
34
  /** Non-streaming chat — the metadata + summary sub-agent loops. */
32
35
  readonly provider: AgentChat;
33
36
  /** Write-side embedder for the vector index. */
@@ -36,6 +36,7 @@ export async function generateStepFileMetadata(deps, postCtx, manifest) {
36
36
  }));
37
37
  const result = await coreGenerateFileMetadata({
38
38
  provider: deps.provider,
39
+ usageRecorder: deps.usageRecorder,
39
40
  session,
40
41
  artifacts: artifactsForMeta,
41
42
  resourceId: input.analysisId,
@@ -61,6 +62,7 @@ export async function generateStepSummaryAndWrite(deps, postCtx, manifest) {
61
62
  const { input, session, transcript, writePrefix } = postCtx;
62
63
  const summary = await coreGenerateStepSummary({
63
64
  provider: deps.provider,
65
+ usageRecorder: deps.usageRecorder,
64
66
  session,
65
67
  modelId: deps.model,
66
68
  messages: transcript,