@inflexa-ai/harness 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/dist/agents/conversation-agent.d.ts +9 -15
  2. package/dist/agents/conversation-agent.js +32 -31
  3. package/dist/agents/sandbox/cheminformatics-agent.js +1 -1
  4. package/dist/agents/sandbox/chromatin-agent.js +1 -1
  5. package/dist/agents/sandbox/dna-methylation-agent.js +1 -1
  6. package/dist/agents/sandbox/drug-repurposing-agent.js +5 -10
  7. package/dist/agents/sandbox/enrichment-agent.js +1 -1
  8. package/dist/agents/sandbox/genomic-variant-agent.js +1 -11
  9. package/dist/agents/sandbox/immune-profiling-agent.js +1 -1
  10. package/dist/agents/sandbox/index.js +0 -3
  11. package/dist/agents/sandbox/multi-omics-integration-agent.js +1 -1
  12. package/dist/agents/sandbox/network-agent.js +1 -1
  13. package/dist/agents/sandbox/pkpd-clinical-response-agent.js +3 -6
  14. package/dist/agents/sandbox/scientific-executor.js +1 -1
  15. package/dist/agents/sandbox/shared.d.ts +2 -2
  16. package/dist/agents/sandbox/shared.js +8 -14
  17. package/dist/agents/sandbox/translational-safety-agent.js +4 -7
  18. package/dist/agents/sandbox/types.d.ts +5 -5
  19. package/dist/agents/sandbox/types.js +4 -4
  20. package/dist/app/chat-turn.js +4 -1
  21. package/dist/app/message-assembly.d.ts +5 -2
  22. package/dist/app/message-assembly.js +7 -2
  23. package/dist/app/run-activity.d.ts +13 -0
  24. package/dist/app/run-activity.js +51 -0
  25. package/dist/config/resource-limits.d.ts +0 -7
  26. package/dist/config/resource-limits.js +0 -1
  27. package/dist/contracts/target-dossier.d.ts +10 -10
  28. package/dist/execution/dbos-run-launcher.js +1 -26
  29. package/dist/execution/report-runner.js +1 -1
  30. package/dist/execution/run-authorizer.d.ts +1 -1
  31. package/dist/execution/run-authorizer.js +1 -1
  32. package/dist/execution/run-event-parts.d.ts +44 -0
  33. package/dist/execution/run-event-parts.js +0 -0
  34. package/dist/execution/run-event-stream.d.ts +91 -0
  35. package/dist/execution/run-event-stream.js +212 -0
  36. package/dist/execution/run-launcher.d.ts +2 -18
  37. package/dist/execution/run-launcher.js +2 -4
  38. package/dist/execution/run-synthesis.js +1 -1
  39. package/dist/index.d.ts +7 -4
  40. package/dist/index.js +23 -6
  41. package/dist/input-sanitization.d.ts +14 -4
  42. package/dist/input-sanitization.js +27 -4
  43. package/dist/lib/storage.js +4 -1
  44. package/dist/loop/budget-exceeded.d.ts +18 -2
  45. package/dist/loop/budget-exceeded.js +49 -2
  46. package/dist/loop/run-agent.d.ts +22 -1
  47. package/dist/loop/run-agent.js +51 -5
  48. package/dist/loop/run-to-terminal.d.ts +0 -2
  49. package/dist/loop/run-to-terminal.js +8 -1
  50. package/dist/memory/ai-sdk-message-storage.d.ts +37 -4
  51. package/dist/memory/ai-sdk-message-storage.js +45 -4
  52. package/dist/memory/content-to-cortex.js +19 -3
  53. package/dist/memory/message-backfill.js +1 -1
  54. package/dist/memory/reconstruct-cards.d.ts +1 -1
  55. package/dist/memory/reconstruct-cards.js +7 -5
  56. package/dist/memory/thread-history.js +72 -8
  57. package/dist/prompts/conversation.d.ts +1 -1
  58. package/dist/prompts/conversation.js +41 -15
  59. package/dist/prompts/literature-reviewer.d.ts +1 -1
  60. package/dist/prompts/literature-reviewer.js +16 -14
  61. package/dist/prompts/planner.d.ts +3 -0
  62. package/dist/prompts/planner.js +16 -21
  63. package/dist/prompts/sandbox/drug-repurposing-agent.d.ts +1 -1
  64. package/dist/prompts/sandbox/drug-repurposing-agent.js +31 -22
  65. package/dist/prompts/sandbox/multi-omics-integration-agent.d.ts +1 -1
  66. package/dist/prompts/sandbox/multi-omics-integration-agent.js +9 -10
  67. package/dist/prompts/sandbox/pkpd-clinical-response-agent.d.ts +1 -1
  68. package/dist/prompts/sandbox/pkpd-clinical-response-agent.js +8 -7
  69. package/dist/prompts/sandbox/translational-safety-agent.d.ts +1 -1
  70. package/dist/prompts/sandbox/translational-safety-agent.js +20 -14
  71. package/dist/prompts/sandbox-standards.d.ts +4 -4
  72. package/dist/prompts/sandbox-standards.js +5 -5
  73. package/dist/prompts/synthesis-agent.d.ts +1 -1
  74. package/dist/prompts/synthesis-agent.js +7 -7
  75. package/dist/providers/errors.d.ts +7 -0
  76. package/dist/providers/errors.js +88 -3
  77. package/dist/runtime/assemble.d.ts +5 -10
  78. package/dist/runtime/assemble.js +0 -4
  79. package/dist/runtime/boot.d.ts +2 -2
  80. package/dist/runtime/boot.js +1 -1
  81. package/dist/runtime/dbos.d.ts +4 -5
  82. package/dist/runtime/dbos.js +6 -7
  83. package/dist/runtime/pools.d.ts +15 -1
  84. package/dist/runtime/pools.js +15 -1
  85. package/dist/sandbox/mount-plan.d.ts +1 -1
  86. package/dist/sandbox/sandbox-error.d.ts +1 -1
  87. package/dist/sandbox/sandbox-error.js +1 -1
  88. package/dist/sandbox/sandbox-step-translate.d.ts +15 -2
  89. package/dist/sandbox/sandbox-step-translate.js +17 -2
  90. package/dist/sandbox/types.d.ts +1 -1
  91. package/dist/schemas/validate-plan.d.ts +1 -1
  92. package/dist/schemas/workflow-state.d.ts +2 -2
  93. package/dist/schemas/workflow-state.js +2 -2
  94. package/dist/state/data-profile.d.ts +53 -1
  95. package/dist/state/data-profile.js +66 -6
  96. package/dist/state/index.d.ts +6 -6
  97. package/dist/state/index.js +4 -4
  98. package/dist/state/init.js +37 -2
  99. package/dist/state/plans.js +1 -1
  100. package/dist/state/runs.d.ts +49 -0
  101. package/dist/state/runs.js +150 -0
  102. package/dist/tasks/data-profile-activity.d.ts +87 -0
  103. package/dist/tasks/data-profile-activity.js +56 -0
  104. package/dist/tasks/data-profile.js +66 -4
  105. package/dist/tools/__fixtures__/tool-context.js +1 -0
  106. package/dist/tools/ad-hoc-router.d.ts +31 -0
  107. package/dist/tools/ad-hoc-router.js +132 -0
  108. package/dist/tools/analysis-invocation.d.ts +4 -0
  109. package/dist/tools/analysis-invocation.js +17 -0
  110. package/dist/tools/bio/chembl.d.ts +1 -1
  111. package/dist/tools/bio/chembl.js +51 -54
  112. package/dist/tools/bio/comptox.js +35 -33
  113. package/dist/tools/bio/drug-gene-interactions.d.ts +76 -0
  114. package/dist/tools/bio/drug-gene-interactions.js +175 -0
  115. package/dist/tools/bio/gene-disease-evidence.d.ts +61 -0
  116. package/dist/tools/bio/gene-disease-evidence.js +187 -0
  117. package/dist/tools/bio/gene-preclinical-profile.d.ts +58 -0
  118. package/dist/tools/bio/gene-preclinical-profile.js +125 -0
  119. package/dist/tools/bio/index.d.ts +23 -16
  120. package/dist/tools/bio/index.js +29 -22
  121. package/dist/tools/bio/keys.d.ts +16 -9
  122. package/dist/tools/bio/keys.js +22 -13
  123. package/dist/tools/bio/lookup-annotation.d.ts +48 -0
  124. package/dist/tools/bio/lookup-annotation.js +173 -0
  125. package/dist/tools/bio/opentargets.d.ts +7 -11
  126. package/dist/tools/bio/opentargets.js +24 -35
  127. package/dist/tools/bio/pubchem.js +6 -4
  128. package/dist/tools/bio/pubmed.d.ts +15 -7
  129. package/dist/tools/bio/pubmed.js +79 -20
  130. package/dist/tools/bio/search-clinical-trials.d.ts +20 -1
  131. package/dist/tools/bio/search-clinical-trials.js +26 -5
  132. package/dist/tools/bio/search-faers.js +9 -3
  133. package/dist/tools/bio/search-gene.d.ts +0 -1
  134. package/dist/tools/bio/search-gene.js +4 -5
  135. package/dist/tools/bio/search-geo-datasets.js +10 -4
  136. package/dist/tools/bio/search-interactions.d.ts +8 -4
  137. package/dist/tools/bio/search-interactions.js +28 -12
  138. package/dist/tools/bio/target-safety.d.ts +59 -0
  139. package/dist/tools/bio/target-safety.js +191 -0
  140. package/dist/tools/define-tool.d.ts +6 -2
  141. package/dist/tools/define-tool.js +1 -1
  142. package/dist/tools/execute-analysis.d.ts +44 -0
  143. package/dist/tools/execute-analysis.js +252 -0
  144. package/dist/tools/iterate-report.js +7 -6
  145. package/dist/tools/lib/dgidb-client.d.ts +49 -0
  146. package/dist/tools/lib/dgidb-client.js +168 -0
  147. package/dist/tools/lib/disgenet-client.d.ts +56 -0
  148. package/dist/tools/lib/disgenet-client.js +59 -0
  149. package/dist/tools/lib/drugbank-client.d.ts +83 -0
  150. package/dist/tools/lib/drugbank-client.js +95 -0
  151. package/dist/tools/lib/ensembl-client.d.ts +4 -1
  152. package/dist/tools/lib/ensembl-client.js +5 -3
  153. package/dist/tools/lib/gwas-catalog-client.d.ts +60 -0
  154. package/dist/tools/lib/gwas-catalog-client.js +136 -0
  155. package/dist/tools/lib/pharmgkb-client.d.ts +22 -0
  156. package/dist/tools/lib/pharmgkb-client.js +32 -0
  157. package/dist/tools/lib/pubmed-client.d.ts +57 -4
  158. package/dist/tools/lib/pubmed-client.js +57 -3
  159. package/dist/tools/lib/string-client.d.ts +25 -4
  160. package/dist/tools/lib/string-client.js +22 -5
  161. package/dist/tools/research/generate-plan.d.ts +16 -9
  162. package/dist/tools/research/generate-plan.js +116 -70
  163. package/dist/tools/research/inspect-data-profile.d.ts +31 -6
  164. package/dist/tools/research/inspect-data-profile.js +23 -7
  165. package/dist/tools/research/inspect-run.d.ts +33 -15
  166. package/dist/tools/research/inspect-run.js +155 -66
  167. package/dist/tools/research/literature-reviewer.js +7 -11
  168. package/dist/workflows/execute-analysis.d.ts +68 -5
  169. package/dist/workflows/execute-analysis.js +76 -7
  170. package/dist/workflows/sandbox-step.js +1 -11
  171. package/dist/workflows/target-assessment/decisions/index.d.ts +1 -1
  172. package/dist/workflows/target-assessment/phase4-assemble.d.ts +1 -1
  173. package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +2 -2
  174. package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +1 -1
  175. package/package.json +1 -1
  176. package/dist/agents/sandbox/ephemeral-executor.d.ts +0 -9
  177. package/dist/agents/sandbox/ephemeral-executor.js +0 -20
  178. package/dist/execution/ephemeral-runner.d.ts +0 -98
  179. package/dist/execution/ephemeral-runner.js +0 -189
  180. package/dist/prompts/sandbox/ephemeral-executor.d.ts +0 -1
  181. package/dist/prompts/sandbox/ephemeral-executor.js +0 -66
  182. package/dist/tools/bio/check-safety-panel.d.ts +0 -35
  183. package/dist/tools/bio/check-safety-panel.js +0 -76
  184. package/dist/tools/bio/get-impc-ko-profile.d.ts +0 -9
  185. package/dist/tools/bio/get-impc-ko-profile.js +0 -23
  186. package/dist/tools/bio/lookup-go-term.d.ts +0 -31
  187. package/dist/tools/bio/lookup-go-term.js +0 -116
  188. package/dist/tools/bio/search-bgee-expression.d.ts +0 -10
  189. package/dist/tools/bio/search-bgee-expression.js +0 -30
  190. package/dist/tools/bio/search-clinvar.d.ts +0 -13
  191. package/dist/tools/bio/search-clinvar.js +0 -28
  192. package/dist/tools/bio/search-dgidb.d.ts +0 -53
  193. package/dist/tools/bio/search-dgidb.js +0 -213
  194. package/dist/tools/bio/search-disgenet.d.ts +0 -30
  195. package/dist/tools/bio/search-disgenet.js +0 -84
  196. package/dist/tools/bio/search-drugbank.d.ts +0 -38
  197. package/dist/tools/bio/search-drugbank.js +0 -109
  198. package/dist/tools/bio/search-gwas-catalog.d.ts +0 -33
  199. package/dist/tools/bio/search-gwas-catalog.js +0 -172
  200. package/dist/tools/bio/search-pathway.d.ts +0 -12
  201. package/dist/tools/bio/search-pathway.js +0 -32
  202. package/dist/tools/bio/search-pharmgkb.d.ts +0 -17
  203. package/dist/tools/bio/search-pharmgkb.js +0 -52
  204. package/dist/tools/execute-plan.d.ts +0 -64
  205. package/dist/tools/execute-plan.js +0 -211
  206. package/dist/tools/run-ephemeral.d.ts +0 -33
  207. package/dist/tools/run-ephemeral.js +0 -65
@@ -38,21 +38,31 @@ export function tryStartDataProfile(pool, analysisId) {
38
38
  return tryMutation("dataProfile.tryStartDataProfile", async () => {
39
39
  const result = await pool.query({
40
40
  text: `UPDATE cortex_analysis_state
41
- SET data_profile_status = 'running', data_profile_started_at = $1
41
+ SET data_profile_status = 'running', data_profile_started_at = $1,
42
+ data_profile_workflow_id = NULL
42
43
  WHERE analysis_id = $2 AND (data_profile_status = 'pending' OR data_profile_status IS NULL) AND ${SEEDED}`,
43
44
  values: [now, analysisId],
44
45
  });
45
46
  return (result.rowCount ?? 0) > 0;
46
47
  });
47
48
  }
48
- /** Claim a `failed` row back into `running` (the deliberate-retry route). Carries {@link SEEDED}. */
49
+ /**
50
+ * Claim a `failed` row back into `running` (the deliberate-retry route). Carries {@link SEEDED}.
51
+ *
52
+ * Clears `data_profile_workflow_id`, because the claim IS the moment a new attempt takes the row
53
+ * and the prior attempt's workflow is finished. Left in place it would leave the row `running`
54
+ * while naming a stream that has already drained, so a consumer would subscribe and observe
55
+ * nothing without knowing why; NULL says "not addressable yet", which is exactly true until the
56
+ * new body records its own id (see {@link recordDataProfileWorkflowId}).
57
+ */
49
58
  export function tryRetryDataProfile(pool, analysisId) {
50
59
  const now = new Date().toISOString();
51
60
  return tryMutation("dataProfile.tryRetryDataProfile", async () => {
52
61
  const result = await pool.query({
53
62
  text: `UPDATE cortex_analysis_state
54
63
  SET data_profile_status = 'running', data_profile_started_at = $1,
55
- data_profile_error = NULL, data_profile_completed_at = NULL
64
+ data_profile_error = NULL, data_profile_completed_at = NULL,
65
+ data_profile_workflow_id = NULL
56
66
  WHERE analysis_id = $2 AND data_profile_status = 'failed' AND ${SEEDED}`,
57
67
  values: [now, analysisId],
58
68
  });
@@ -63,6 +73,11 @@ export function tryRetryDataProfile(pool, analysisId) {
63
73
  * Claim a `completed` row back into `running` (the re-profile route). `data_profile_result`
64
74
  * is deliberately preserved so a consumer can keep serving the prior profile while the new
65
75
  * one runs. Carries {@link SEEDED}.
76
+ *
77
+ * `data_profile_workflow_id` is NOT preserved alongside it, and the asymmetry is the point: the
78
+ * result is CONTENT that stays valid until replaced, while the id is a POINTER to a live stream
79
+ * whose workflow is now finished. Keeping it would leave the row `running` while naming a drained
80
+ * stream — see {@link tryRetryDataProfile} for the same reasoning.
66
81
  */
67
82
  export function tryRerunDataProfile(pool, analysisId) {
68
83
  const now = new Date().toISOString();
@@ -70,13 +85,56 @@ export function tryRerunDataProfile(pool, analysisId) {
70
85
  const result = await pool.query({
71
86
  text: `UPDATE cortex_analysis_state
72
87
  SET data_profile_status = 'running', data_profile_started_at = $1,
73
- data_profile_error = NULL, data_profile_completed_at = NULL
88
+ data_profile_error = NULL, data_profile_completed_at = NULL,
89
+ data_profile_workflow_id = NULL
74
90
  WHERE analysis_id = $2 AND data_profile_status = 'completed' AND ${SEEDED}`,
75
91
  values: [now, analysisId],
76
92
  });
77
93
  return (result.rowCount ?? 0) > 0;
78
94
  });
79
95
  }
96
+ /**
97
+ * Record the DBOS workflow id of the attempt that owns a claimed-`running` row, so a
98
+ * consumer resolves which durable event stream carries this profile's activity from the
99
+ * ledger row alone — no durability-engine query, no id reconstructed from the workflow-id
100
+ * string format. The workflow BODY calls this, never the trigger: the claim CAS runs
101
+ * before the workflow id is minted, so only the body can report the id of the attempt
102
+ * that actually started.
103
+ *
104
+ * `AND data_profile_status = 'running'` buys exactly one thing: a write that lands late
105
+ * cannot stamp a row that has already settled, which would otherwise point a consumer at
106
+ * a workflow for a profile that is already finished. What it does NOT buy is
107
+ * disambiguation between two attempts that each believe they are the running one — the
108
+ * stale-expiry claim admits precisely that, since a row whose `data_profile_started_at`
109
+ * has aged past the timeout can be claimed by a second attempt while the first body is
110
+ * still alive but has not completed its first step, and that first step then overwrites
111
+ * the second's id.
112
+ *
113
+ * That residue is deliberately left open rather than closed. Its worst outcome is already
114
+ * a specified-normal state: a consumer subscribed to a superseded workflow reads a stream
115
+ * that has already drained, so it observes no activity — indistinguishable from "running,
116
+ * nothing reported yet". A missing activity line for one profile is acceptable; a wrong
117
+ * one would not be. And closing it costs more than it buys: either the three claim
118
+ * functions widen their `Result<boolean>` returns to carry the id — their callers branch
119
+ * on truthiness (`if (!retried)`), so an object return makes every such branch
120
+ * always-taken, a silent behaviour change in the embedder's recovery path — or the
121
+ * embedder mints the workflow ids, putting a harness-internal construction in a
122
+ * consumer's hands.
123
+ *
124
+ * `ok(true)` when this call stamped the row; `ok(false)` when the CAS refused it — a
125
+ * normal in-band outcome, not an error.
126
+ */
127
+ export function recordDataProfileWorkflowId(pool, analysisId, workflowId) {
128
+ return tryMutation("dataProfile.recordDataProfileWorkflowId", async () => {
129
+ const result = await pool.query({
130
+ text: `UPDATE cortex_analysis_state
131
+ SET data_profile_workflow_id = $1
132
+ WHERE analysis_id = $2 AND data_profile_status = 'running'`,
133
+ values: [workflowId, analysisId],
134
+ });
135
+ return (result.rowCount ?? 0) > 0;
136
+ });
137
+ }
80
138
  /**
81
139
  * Stamp a claimed-`running` row `completed` and record its result — a terminal
82
140
  * write CAS'd on `data_profile_status = 'running'`.
@@ -213,7 +271,8 @@ export function clearDataProfile(pool, analysisId) {
213
271
  text: `UPDATE cortex_analysis_state
214
272
  SET data_profile_status = NULL, data_profile_error = NULL,
215
273
  data_profile_started_at = NULL, data_profile_completed_at = NULL,
216
- data_profile_result = NULL, seed_input_file_ids = NULL
274
+ data_profile_result = NULL, data_profile_workflow_id = NULL,
275
+ seed_input_file_ids = NULL
217
276
  WHERE analysis_id = $1 AND data_profile_status IS DISTINCT FROM 'running'`,
218
277
  values: [analysisId],
219
278
  });
@@ -242,7 +301,7 @@ export function loadDataProfileStatus(pool, analysisId) {
242
301
  const result = await pool.query({
243
302
  text: `SELECT data_profile_status, data_profile_error,
244
303
  data_profile_started_at, data_profile_completed_at,
245
- data_profile_result, seed_input_file_ids
304
+ data_profile_result, data_profile_workflow_id, seed_input_file_ids
246
305
  FROM cortex_analysis_state WHERE analysis_id = $1`,
247
306
  values: [analysisId],
248
307
  });
@@ -255,6 +314,7 @@ export function loadDataProfileStatus(pool, analysisId) {
255
314
  startedAt: row.data_profile_started_at ?? null,
256
315
  completedAt: row.data_profile_completed_at ?? null,
257
316
  result: row.data_profile_result ?? null,
317
+ workflowId: row.data_profile_workflow_id ?? null,
258
318
  seedInputFileIds: row.seed_input_file_ids ?? null,
259
319
  };
260
320
  });
@@ -11,16 +11,16 @@ export type { Querier } from "./db.js";
11
11
  export { upsertAnalysis, resolveAnalysisBilling, suspendAnalysis, resumeAnalysis, loadAnalysisStatus } from "./analyses.js";
12
12
  export { upsertArtifact, upsertArtifacts, queryInputArtifacts, queryStepArtifactPaths, queryUnsyncedStepArtifacts, updateArtifactId, updateFileIds, countArtifactsForRun, } from "./artifacts.js";
13
13
  export type { RegisterArtifactInput, InputArtifactMeta, StepArtifactRef } from "./artifacts.js";
14
- export { insertRun, RunDedupCollisionError, updateRunStatus, promoteFailedToPartial, setRunMandate, // oss-core-managed-ok: run-mandate ledger (nullable; OSS leaves null)
15
- setRunSynthesisOutcome, queryRun, queryActiveRun, queryRunsByAnalysis, queryRunsByThread, } from "./runs.js";
16
- export type { InsertRunInput } from "./runs.js";
14
+ export { insertRun, reserveRunById, RunDedupCollisionError, RunIdentityCollisionError, updateRunStatus, promoteFailedToPartial, setRunMandate, // oss-core-managed-ok: run-mandate ledger (nullable; OSS leaves null)
15
+ setRunSynthesisOutcome, queryRun, queryActiveRun, queryNonTerminalRunsByAnalysis, queryRunsForInspection, queryRunsByAnalysis, queryRunsByThread, } from "./runs.js";
16
+ export type { InsertRunInput, RunPage, RunReservation } from "./runs.js";
17
17
  export { insertStepExecution, seedStepExecutions, sweepPendingStepExecutions, updateStepExecution, queryStepsByRun } from "./step-executions.js";
18
18
  export type { InsertStepExecutionInput, SeedStepExecutionRow, UpdateStepExecutionInput } from "./step-executions.js";
19
19
  export { setSandboxRef, setActiveExecId, clearSandboxRef, queryActiveSandboxes, reconcileReapedSandbox } from "./active-sandboxes.js";
20
20
  export type { ActiveSandboxRow } from "./active-sandboxes.js";
21
- export { insertPlan, loadPlan } from "./plans.js";
22
- export type { InsertPlanInput } from "./plans.js";
23
- export { tryStartDataProfile, tryRetryDataProfile, tryRerunDataProfile, completeDataProfile, failDataProfile, expireStaleDataProfile, reconcileOrphanedDataProfile, clearDataProfile, loadDataProfileStatus, loadSeedInputFileIds, } from "./data-profile.js";
21
+ export { insertPlan, upsertPlan, loadPlan } from "./plans.js";
22
+ export type { InsertPlanInput, UpsertPlanInput } from "./plans.js";
23
+ export { tryStartDataProfile, tryRetryDataProfile, tryRerunDataProfile, completeDataProfile, failDataProfile, expireStaleDataProfile, reconcileOrphanedDataProfile, clearDataProfile, loadDataProfileStatus, loadSeedInputFileIds, recordDataProfileWorkflowId, } from "./data-profile.js";
24
24
  export type { DataProfileStatus, DataProfileResult, DataProfileInputFile, DataProfileFile, DataProfileQualityAssessment } from "./data-profile.js";
25
25
  export { queryRunCountsByAnalyses, queryThreadCountsByAnalyses, queryDataProfileStatusByAnalyses } from "./analyses-metrics.js";
26
26
  export { AnalysisStateRowSchema, ArtifactRole, ArtifactRowSchema, RunStatus, StepExecutionStatus, SynthesisStatus, CortexRunRowSchema, StepExecutionRowSchema, CortexPlanRowSchema, } from "./schema.js";
@@ -9,12 +9,12 @@
9
9
  export { initCortexState } from "./init.js";
10
10
  export { upsertAnalysis, resolveAnalysisBilling, suspendAnalysis, resumeAnalysis, loadAnalysisStatus } from "./analyses.js";
11
11
  export { upsertArtifact, upsertArtifacts, queryInputArtifacts, queryStepArtifactPaths, queryUnsyncedStepArtifacts, updateArtifactId, updateFileIds, countArtifactsForRun, } from "./artifacts.js";
12
- export { insertRun, RunDedupCollisionError, updateRunStatus, promoteFailedToPartial, setRunMandate, // oss-core-managed-ok: run-mandate ledger (nullable; OSS leaves null)
13
- setRunSynthesisOutcome, queryRun, queryActiveRun, queryRunsByAnalysis, queryRunsByThread, } from "./runs.js";
12
+ export { insertRun, reserveRunById, RunDedupCollisionError, RunIdentityCollisionError, updateRunStatus, promoteFailedToPartial, setRunMandate, // oss-core-managed-ok: run-mandate ledger (nullable; OSS leaves null)
13
+ setRunSynthesisOutcome, queryRun, queryActiveRun, queryNonTerminalRunsByAnalysis, queryRunsForInspection, queryRunsByAnalysis, queryRunsByThread, } from "./runs.js";
14
14
  export { insertStepExecution, seedStepExecutions, sweepPendingStepExecutions, updateStepExecution, queryStepsByRun } from "./step-executions.js";
15
15
  export { setSandboxRef, setActiveExecId, clearSandboxRef, queryActiveSandboxes, reconcileReapedSandbox } from "./active-sandboxes.js";
16
- export { insertPlan, loadPlan } from "./plans.js";
17
- export { tryStartDataProfile, tryRetryDataProfile, tryRerunDataProfile, completeDataProfile, failDataProfile, expireStaleDataProfile, reconcileOrphanedDataProfile, clearDataProfile, loadDataProfileStatus, loadSeedInputFileIds, } from "./data-profile.js";
16
+ export { insertPlan, upsertPlan, loadPlan } from "./plans.js";
17
+ export { tryStartDataProfile, tryRetryDataProfile, tryRerunDataProfile, completeDataProfile, failDataProfile, expireStaleDataProfile, reconcileOrphanedDataProfile, clearDataProfile, loadDataProfileStatus, loadSeedInputFileIds, recordDataProfileWorkflowId, } from "./data-profile.js";
18
18
  export { queryRunCountsByAnalyses, queryThreadCountsByAnalyses, queryDataProfileStatusByAnalyses } from "./analyses-metrics.js";
19
19
  export { AnalysisStateRowSchema, ArtifactRole, ArtifactRowSchema, RunStatus, StepExecutionStatus, SynthesisStatus, CortexRunRowSchema, StepExecutionRowSchema, CortexPlanRowSchema, } from "./schema.js";
20
20
  export { insertAssessment, updateProgress, setDossier, markFailed, getAssessment, listAssessmentsByOrg, softDeleteAssessment, TargetAssessmentStatusSchema, TargetAssessmentErrorSchema, TargetAssessmentRowSchema, } from "./target-assessments.js";
@@ -27,10 +27,22 @@ CREATE TABLE IF NOT EXISTS cortex_analysis_state (
27
27
  data_profile_started_at TEXT,
28
28
  data_profile_completed_at TEXT,
29
29
  data_profile_result JSONB,
30
+ data_profile_workflow_id TEXT,
30
31
  created_at TEXT NOT NULL,
31
32
  updated_at TEXT NOT NULL
32
33
  );
33
34
 
35
+ -- DBOS workflow id of the profile attempt that owns the row — what a consumer
36
+ -- subscribes to in order to observe that profile's activity. Written by the
37
+ -- workflow body, not the trigger: the ledger claim happens before the workflow id
38
+ -- is minted, so only the body knows the id of the attempt that actually started.
39
+ -- Nullable, and absence is a normal state rather than a defect, with two ordinary
40
+ -- causes: a row claimed whose body has not yet recorded its id, and a row written
41
+ -- before this column existed. Both mean "this profile's stream is not
42
+ -- addressable", which is true in each case — hence no backfill.
43
+ ALTER TABLE cortex_analysis_state
44
+ ADD COLUMN IF NOT EXISTS data_profile_workflow_id TEXT;
45
+
34
46
  -- File registry — artifact_id (provenance) + file_id (S3), cross-run.
35
47
  CREATE TABLE IF NOT EXISTS cortex_artifacts (
36
48
  analysis_id TEXT NOT NULL,
@@ -207,12 +219,20 @@ CREATE INDEX IF NOT EXISTS idx_cortex_regulatory_chunks_metadata
207
219
  -- workflow and sandbox agent loops use the DBOS step cache, never this
208
220
  -- table (see the harness-thread-store spec). The (thread_id, seq) primary key already serves
209
221
  -- (thread_id, seq DESC) reads, so no separate index is needed.
222
+ --
223
+ -- message_envelope is JSON, not JSONB, and must stay that way. JSONB stores a
224
+ -- re-sorted parse tree rather than the document it was given, so a tool call's
225
+ -- input — and every other free-form payload, which has no schema to be
226
+ -- normalized back against on read — comes back key-reordered. Those bytes are
227
+ -- re-sent to the provider on the next turn, where a reordered prefix misses the
228
+ -- prompt cache. JSON stores the text verbatim. Nothing indexes this column, and
229
+ -- the JSON operators retractLastTurn uses work the same on either type.
210
230
  CREATE TABLE IF NOT EXISTS messages (
211
231
  thread_id TEXT NOT NULL,
212
232
  seq BIGINT NOT NULL,
213
233
  role TEXT,
214
234
  content_jsonb JSONB,
215
- message_envelope JSONB,
235
+ message_envelope JSON,
216
236
  tokens INTEGER NOT NULL,
217
237
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
218
238
  PRIMARY KEY (thread_id, seq)
@@ -411,7 +431,22 @@ export async function initCortexState(pool, injected) {
411
431
  // carry the NOT NULL floor, which would reject a clear. Idempotent:
412
432
  // dropping an absent NOT NULL no-ops, so this is a no-op on fresh DBs.
413
433
  "ALTER TABLE cortex_analysis_state ALTER COLUMN data_profile_status DROP NOT NULL",
414
- "ALTER TABLE messages ADD COLUMN IF NOT EXISTS message_envelope JSONB",
434
+ "ALTER TABLE messages ADD COLUMN IF NOT EXISTS message_envelope JSON",
435
+ // Demote an existing JSONB envelope column to JSON (see the CREATE
436
+ // TABLE comment). Type-guarded because ALTER ... TYPE rewrites the
437
+ // whole table and this runs at every boot; scoped to
438
+ // current_schema() so parallel test schemas never see each other.
439
+ // Rows already written as JSONB carry over as they stand — their
440
+ // original key order is not recoverable.
441
+ `DO $$ BEGIN
442
+ IF EXISTS (
443
+ SELECT 1 FROM information_schema.columns
444
+ WHERE table_schema = current_schema() AND table_name = 'messages'
445
+ AND column_name = 'message_envelope' AND data_type = 'jsonb'
446
+ ) THEN
447
+ ALTER TABLE messages ALTER COLUMN message_envelope TYPE JSON USING message_envelope::text::json;
448
+ END IF;
449
+ END $$`,
415
450
  // Grant key an 'always' records the standing grant under. Nullable
416
451
  // and unbackfilled: every new row writes it (command when absent), and
417
452
  // orphaned legacy pending rows are swept to 'expired' before any answer
@@ -6,7 +6,7 @@ import { randomUUID } from "node:crypto";
6
6
  import { tryMutation, tryQuery } from "../lib/db-result.js";
7
7
  /**
8
8
  * `cortex_plans.plan_id` shape contract — `pln-` followed by 8 lowercase hex
9
- * chars. The chat trigger (`tools/execute-plan.ts`) enforces the same regex on
9
+ * chars. The chat trigger (`tools/execute-analysis.ts`) enforces the same regex on
10
10
  * its tool input; `upsertPlan` re-asserts it because it accepts a caller-derived
11
11
  * id rather than minting one.
12
12
  */
@@ -22,6 +22,19 @@ export interface InsertRunInput {
22
22
  mandateJti?: string | null;
23
23
  mandateExpiresAt?: string | null;
24
24
  }
25
+ export interface RunReservation {
26
+ readonly row: CortexRunRow;
27
+ readonly inserted: boolean;
28
+ }
29
+ /** Bounded run rows paired with the true count before limit/offset. */
30
+ export interface RunPage {
31
+ readonly runs: CortexRunRow[];
32
+ readonly total: number;
33
+ }
34
+ export declare class RunIdentityCollisionError extends Error {
35
+ readonly runId: string;
36
+ constructor(runId: string);
37
+ }
25
38
  /**
26
39
  * Raised when `insertRun` collides with the partial-unique index
27
40
  * `idx_cortex_runs_active_plan` — i.e., an active run already exists for
@@ -43,12 +56,32 @@ export declare class RunDedupCollisionError extends Error {
43
56
  * the err channel as a `DbError`.
44
57
  */
45
58
  export declare function insertRun(pool: Querier, input: InsertRunInput): ResultAsync<void, DbError>;
59
+ /** Reserve a deterministic run id, treating same-analysis PK conflict as redelivery. */
60
+ export declare function reserveRunById(pool: Querier, input: InsertRunInput): ResultAsync<RunReservation, DbError>;
46
61
  /**
47
62
  * Recover the active run row for `(analysisId, planId)` after `insertRun`
48
63
  * raises `RunDedupCollisionError`. The partial-unique index guarantees at
49
64
  * most one match.
50
65
  */
51
66
  export declare function queryActiveRun(pool: Querier, analysisId: string, planId: string | null): ResultAsync<CortexRunRow | null, DbError>;
67
+ /**
68
+ * Every NON-TERMINAL run of an analysis, newest-first and deliberately unbounded.
69
+ *
70
+ * The counterpart to {@link queryRunsByAnalysis}, which answers "the newest N runs" — a windowed
71
+ * question, and the wrong one for a host driving live surfaces. Ordering by `started_at DESC` and
72
+ * capping means the run most worth watching is the first to fall out: a long analysis still going
73
+ * while ten short ones start and finish after it leaves the window entirely, so a host reading only
74
+ * that window loses its progress AND never learns it terminated.
75
+ *
76
+ * Unbounded is safe here in a way it is not for the general listing: this result is capped by the
77
+ * analysis's live concurrency, not by its history, and a run leaves the set permanently when it
78
+ * finishes.
79
+ *
80
+ * The status set is the one {@link queryActiveRun} uses — non-terminal means still running, or
81
+ * suspended awaiting funds. Keep the two in step: a new non-terminal status added to one and not the
82
+ * other makes a live run invisible to exactly one of its readers.
83
+ */
84
+ export declare function queryActiveRunsByAnalysis(pool: Querier, analysisId: string): ResultAsync<CortexRunRow[], DbError>;
52
85
  export declare function updateRunStatus(pool: Querier, runId: string, status: "running" | "completed" | "partial" | "failed" | "canceled" | "suspended_insufficient_funds", error?: string | null): ResultAsync<void, DbError>;
53
86
  export declare function promoteFailedToPartial(pool: Querier, runId: string): ResultAsync<boolean, DbError>;
54
87
  export declare function queryRun(pool: Querier, runId: string): ResultAsync<CortexRunRow | null, DbError>;
@@ -56,6 +89,22 @@ export declare function queryRunsByAnalysis(pool: Querier, analysisId: string, {
56
89
  limit?: number;
57
90
  offset?: number;
58
91
  }): ResultAsync<CortexRunRow[], DbError>;
92
+ /**
93
+ * Read the analysis's non-terminal run activity without coupling it to a
94
+ * conversation thread. The total is counted before the detail limit so a
95
+ * bounded prompt projection can disclose omitted rows.
96
+ */
97
+ export declare function queryNonTerminalRunsByAnalysis(pool: Querier, analysisId: string, limit?: number): ResultAsync<RunPage, DbError>;
98
+ /**
99
+ * Read one stable inspection page: running first, then suspended, then
100
+ * terminal history. This is deliberately separate from
101
+ * {@link queryRunsByAnalysis}, whose chronological order is an embedder/UI
102
+ * contract.
103
+ */
104
+ export declare function queryRunsForInspection(pool: Querier, analysisId: string, { limit, offset }?: {
105
+ limit?: number;
106
+ offset?: number;
107
+ }): ResultAsync<RunPage, DbError>;
59
108
  export declare function queryRunsByThread(pool: Querier, analysisId: string, threadId: string, { limit, offset }?: {
60
109
  limit?: number;
61
110
  offset?: number;
@@ -10,6 +10,14 @@
10
10
  * `err(constraint_violation)` via `tryMutation`.
11
11
  */
12
12
  import { tryMutation, tryQuery } from "../lib/db-result.js";
13
+ export class RunIdentityCollisionError extends Error {
14
+ runId;
15
+ constructor(runId) {
16
+ super(`run id ${runId} already belongs to another analysis`);
17
+ this.runId = runId;
18
+ this.name = "RunIdentityCollisionError";
19
+ }
20
+ }
13
21
  /**
14
22
  * Raised when `insertRun` collides with the partial-unique index
15
23
  * `idx_cortex_runs_active_plan` — i.e., an active run already exists for
@@ -65,6 +73,45 @@ export function insertRun(pool, input) {
65
73
  return e;
66
74
  });
67
75
  }
76
+ /** Reserve a deterministic run id, treating same-analysis PK conflict as redelivery. */
77
+ export function reserveRunById(pool, input) {
78
+ const now = new Date().toISOString();
79
+ return tryMutation("runs.reserveRunById", async () => {
80
+ const result = await pool.query({
81
+ text: `INSERT INTO cortex_runs
82
+ (run_id, analysis_id, thread_id, workflow_name,
83
+ status, started_at, plan_id, mandate_jti, mandate_expires_at)
84
+ VALUES ($1, $2, $3, $4, 'running', $5, $6, $7, $8)
85
+ ON CONFLICT (run_id) DO NOTHING
86
+ RETURNING run_id`,
87
+ values: [
88
+ input.runId,
89
+ input.analysisId,
90
+ input.threadId ?? null,
91
+ input.workflowName,
92
+ now,
93
+ input.planId ?? null,
94
+ input.mandateJti ?? null,
95
+ input.mandateExpiresAt ?? null,
96
+ ],
97
+ });
98
+ return (result.rowCount ?? 0) > 0;
99
+ }).andThen((inserted) => tryQuery("runs.reserveRunById.reload", async () => {
100
+ const result = await pool.query({
101
+ text: `SELECT run_id, analysis_id, thread_id, workflow_name,
102
+ status, started_at, completed_at, error, synthesis_status, synthesis_reason, parts,
103
+ mandate_jti, mandate_expires_at, plan_id
104
+ FROM cortex_runs
105
+ WHERE run_id = $1`,
106
+ values: [input.runId],
107
+ });
108
+ return result.rows[0] ?? null;
109
+ }).map((row) => {
110
+ if (!row || row.analysis_id !== input.analysisId)
111
+ throw new RunIdentityCollisionError(input.runId);
112
+ return { row: mapRunRow(row), inserted };
113
+ }));
114
+ }
68
115
  function isPartialUniqueViolation(err) {
69
116
  if (err === null || typeof err !== "object")
70
117
  return false;
@@ -92,6 +139,38 @@ export function queryActiveRun(pool, analysisId, planId) {
92
139
  return row ? mapRunRow(row) : null;
93
140
  });
94
141
  }
142
+ /**
143
+ * Every NON-TERMINAL run of an analysis, newest-first and deliberately unbounded.
144
+ *
145
+ * The counterpart to {@link queryRunsByAnalysis}, which answers "the newest N runs" — a windowed
146
+ * question, and the wrong one for a host driving live surfaces. Ordering by `started_at DESC` and
147
+ * capping means the run most worth watching is the first to fall out: a long analysis still going
148
+ * while ten short ones start and finish after it leaves the window entirely, so a host reading only
149
+ * that window loses its progress AND never learns it terminated.
150
+ *
151
+ * Unbounded is safe here in a way it is not for the general listing: this result is capped by the
152
+ * analysis's live concurrency, not by its history, and a run leaves the set permanently when it
153
+ * finishes.
154
+ *
155
+ * The status set is the one {@link queryActiveRun} uses — non-terminal means still running, or
156
+ * suspended awaiting funds. Keep the two in step: a new non-terminal status added to one and not the
157
+ * other makes a live run invisible to exactly one of its readers.
158
+ */
159
+ export function queryActiveRunsByAnalysis(pool, analysisId) {
160
+ return tryQuery("runs.queryActiveRunsByAnalysis", async () => {
161
+ const result = await pool.query({
162
+ text: `SELECT run_id, analysis_id, thread_id, workflow_name,
163
+ status, started_at, completed_at, error, synthesis_status, synthesis_reason, parts,
164
+ mandate_jti, mandate_expires_at, plan_id -- oss-core-managed-ok: run-mandate ledger (nullable; OSS leaves null)
165
+ FROM cortex_runs
166
+ WHERE analysis_id = $1
167
+ AND status IN ('running','suspended_insufficient_funds')
168
+ ORDER BY started_at DESC`,
169
+ values: [analysisId],
170
+ });
171
+ return result.rows.map(mapRunRow);
172
+ });
173
+ }
95
174
  export function updateRunStatus(pool, runId, status, error) {
96
175
  const now = new Date().toISOString();
97
176
  const isTerminal = status === "completed" || status === "partial" || status === "failed" || status === "canceled";
@@ -142,6 +221,77 @@ export function queryRunsByAnalysis(pool, analysisId, { limit = 50, offset = 0 }
142
221
  return result.rows.map(mapRunRow);
143
222
  });
144
223
  }
224
+ /**
225
+ * Read the analysis's non-terminal run activity without coupling it to a
226
+ * conversation thread. The total is counted before the detail limit so a
227
+ * bounded prompt projection can disclose omitted rows.
228
+ */
229
+ export function queryNonTerminalRunsByAnalysis(pool, analysisId, limit = 20) {
230
+ return tryQuery("runs.queryNonTerminalRunsByAnalysis", async () => {
231
+ const countResult = await pool.query({
232
+ text: `SELECT COUNT(*)::int AS total
233
+ FROM cortex_runs
234
+ WHERE analysis_id = $1
235
+ AND status IN ('running','suspended_insufficient_funds')`,
236
+ values: [analysisId],
237
+ });
238
+ const rowsResult = await pool.query({
239
+ text: `SELECT run_id, analysis_id, thread_id, workflow_name,
240
+ status, started_at, completed_at, error, synthesis_status, synthesis_reason, parts,
241
+ mandate_jti, mandate_expires_at, plan_id
242
+ FROM cortex_runs
243
+ WHERE analysis_id = $1
244
+ AND status IN ('running','suspended_insufficient_funds')
245
+ ORDER BY CASE status
246
+ WHEN 'running' THEN 0
247
+ WHEN 'suspended_insufficient_funds' THEN 1
248
+ ELSE 2
249
+ END,
250
+ started_at DESC,
251
+ run_id DESC
252
+ LIMIT $2`,
253
+ values: [analysisId, limit],
254
+ });
255
+ return {
256
+ runs: rowsResult.rows.map(mapRunRow),
257
+ total: Number(countResult.rows[0]?.total ?? 0),
258
+ };
259
+ });
260
+ }
261
+ /**
262
+ * Read one stable inspection page: running first, then suspended, then
263
+ * terminal history. This is deliberately separate from
264
+ * {@link queryRunsByAnalysis}, whose chronological order is an embedder/UI
265
+ * contract.
266
+ */
267
+ export function queryRunsForInspection(pool, analysisId, { limit = 50, offset = 0 } = {}) {
268
+ return tryQuery("runs.queryRunsForInspection", async () => {
269
+ const countResult = await pool.query({
270
+ text: "SELECT COUNT(*)::int AS total FROM cortex_runs WHERE analysis_id = $1",
271
+ values: [analysisId],
272
+ });
273
+ const rowsResult = await pool.query({
274
+ text: `SELECT run_id, analysis_id, thread_id, workflow_name,
275
+ status, started_at, completed_at, error, synthesis_status, synthesis_reason, parts,
276
+ mandate_jti, mandate_expires_at, plan_id
277
+ FROM cortex_runs
278
+ WHERE analysis_id = $1
279
+ ORDER BY CASE status
280
+ WHEN 'running' THEN 0
281
+ WHEN 'suspended_insufficient_funds' THEN 1
282
+ ELSE 2
283
+ END,
284
+ started_at DESC,
285
+ run_id DESC
286
+ LIMIT $2 OFFSET $3`,
287
+ values: [analysisId, limit, offset],
288
+ });
289
+ return {
290
+ runs: rowsResult.rows.map(mapRunRow),
291
+ total: Number(countResult.rows[0]?.total ?? 0),
292
+ };
293
+ });
294
+ }
145
295
  export function queryRunsByThread(pool, analysisId, threadId, { limit = 50, offset = 0 } = {}) {
146
296
  return tryQuery("runs.queryRunsByThread", async () => {
147
297
  const result = await pool.query({
@@ -0,0 +1,87 @@
1
+ /**
2
+ * The data-profile workflow's activity emissions, as a pure factory over a raw
3
+ * stream write.
4
+ *
5
+ * Extracted from the workflow body for the same reason `createLineageCollector`
6
+ * and `buildDriftSignature` are: a DBOS workflow body is not reachable from a
7
+ * unit test — it needs a launched engine, a sandbox, a scripted model, and an
8
+ * embedder — so the decidable part is lifted out and tested on its own. What is
9
+ * decidable here is the whole observable contract of this capability: which
10
+ * phase each moment of a profile reports, and in what words. Those phrases are
11
+ * what a user reads, so they are the one part of the work that must not be left
12
+ * to drift.
13
+ *
14
+ * What stays in the body is only control flow — where each call sits relative to
15
+ * the ledger writes. That is not extractable, and the ordering property it
16
+ * carries (exactly one terminal emission) is guaranteed structurally instead:
17
+ * there is exactly one `complete` call site and it is the last statement of the
18
+ * success path, after the terminal ledger write and the authorization revoke, so
19
+ * anything that throws on the way there reaches the failure path having emitted
20
+ * no terminal at all.
21
+ */
22
+ import type { Logger } from "../lib/logger.js";
23
+ /**
24
+ * Writes one part to the run-event stream. Injected rather than imported so this
25
+ * module stays free of the durability engine and so a test can record what a
26
+ * profile would have reported without launching one.
27
+ */
28
+ export type ActivityWrite = (part: unknown) => Promise<void>;
29
+ /**
30
+ * The profile's activity channel. One method per moment the body reports, rather
31
+ * than a generic `emit(phase, text)`: the phrases are normative, and a call site
32
+ * that passes its own string is a call site that can drift from the spec.
33
+ *
34
+ * Every method resolves even when the underlying write fails — see
35
+ * {@link createProfileActivityEmitter}.
36
+ *
37
+ * Five of the contract's phases have no method here, and their absence is a
38
+ * decision rather than an oversight: `generating-metadata` and
39
+ * `generating-summary` describe the sandbox-step's post-agent pipeline, which a
40
+ * profile does not run (its agent delivers everything through `submit_profile`);
41
+ * `persisting` is defined as step bytes uploading to an artifact store, and a
42
+ * profile's durable products are the vector index and the ledger row, so it would
43
+ * describe nothing; `retrying` needs a retry loop, and a profile failure is
44
+ * terminal with recovery being a fresh attempt; `warning` needs a non-fatal
45
+ * user-facing warning channel, and the body's two soft conditions (an empty
46
+ * manifest, a fallback file description) are logged because neither warrants
47
+ * interrupting the activity line.
48
+ */
49
+ export interface ProfileActivityEmitter {
50
+ /** Reported before the sandbox is created — the longest wait in a profile, and the one that precedes the agent entirely. */
51
+ sandboxInit(): Promise<void>;
52
+ /** Reported once the sandbox is ready, covering the gap before the agent's first tool call. */
53
+ agentStarting(): Promise<void>;
54
+ /** Reported per tool call the profiler agent makes, phrased by the shared translator. */
55
+ forTool(name: string, input: unknown): Promise<void>;
56
+ /** Reported for the vector-store pass that indexes per-file descriptions. */
57
+ indexing(): Promise<void>;
58
+ /** The success terminal. Emitted only after the terminal ledger write has landed. */
59
+ complete(): Promise<void>;
60
+ /** The failure terminal, carrying the same user-safe reason the ledger receives. */
61
+ failed(reason: string): Promise<void>;
62
+ }
63
+ /** Identity the emitted parts carry — the workflow's synthetic frame, not a real run. */
64
+ export interface ProfileActivityFrame {
65
+ /**
66
+ * The constant literal the data-profile workflow uses as its run id. It is the
67
+ * same string for every analysis, so it identifies nothing; consumers are
68
+ * required not to key on it (see the data-profile-observation spec). It is
69
+ * carried because the part contract requires the field and because this is the
70
+ * frame the workflow already uses for its sandbox identity and scratch path.
71
+ */
72
+ readonly runId: string;
73
+ /** The constant literal step id of the profile's single synthetic step. */
74
+ readonly stepId: string;
75
+ }
76
+ /**
77
+ * Build the profile's activity channel over `write`.
78
+ *
79
+ * Every emission is awaited by its caller in body order, because `DBOS.writeStream`
80
+ * allocates a function id and an unawaited write would race the next real operation
81
+ * for the counter, desynchronising the recorded sequence on replay.
82
+ *
83
+ * A failing write is swallowed and logged, never propagated: observation is a
84
+ * diagnostic channel, and a dropped frame must not fail a profile that is otherwise
85
+ * succeeding. This mirrors the sandbox-step producer's stance on the same write.
86
+ */
87
+ export declare function createProfileActivityEmitter(write: ActivityWrite, frame: ProfileActivityFrame, logger: Logger): ProfileActivityEmitter;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * The data-profile workflow's activity emissions, as a pure factory over a raw
3
+ * stream write.
4
+ *
5
+ * Extracted from the workflow body for the same reason `createLineageCollector`
6
+ * and `buildDriftSignature` are: a DBOS workflow body is not reachable from a
7
+ * unit test — it needs a launched engine, a sandbox, a scripted model, and an
8
+ * embedder — so the decidable part is lifted out and tested on its own. What is
9
+ * decidable here is the whole observable contract of this capability: which
10
+ * phase each moment of a profile reports, and in what words. Those phrases are
11
+ * what a user reads, so they are the one part of the work that must not be left
12
+ * to drift.
13
+ *
14
+ * What stays in the body is only control flow — where each call sits relative to
15
+ * the ledger writes. That is not extractable, and the ordering property it
16
+ * carries (exactly one terminal emission) is guaranteed structurally instead:
17
+ * there is exactly one `complete` call site and it is the last statement of the
18
+ * success path, after the terminal ledger write and the authorization revoke, so
19
+ * anything that throws on the way there reaches the failure path having emitted
20
+ * no terminal at all.
21
+ */
22
+ import { activityForTool, stepPartId } from "../sandbox/sandbox-step-translate.js";
23
+ /**
24
+ * Build the profile's activity channel over `write`.
25
+ *
26
+ * Every emission is awaited by its caller in body order, because `DBOS.writeStream`
27
+ * allocates a function id and an unawaited write would race the next real operation
28
+ * for the counter, desynchronising the recorded sequence on replay.
29
+ *
30
+ * A failing write is swallowed and logged, never propagated: observation is a
31
+ * diagnostic channel, and a dropped frame must not fail a profile that is otherwise
32
+ * succeeding. This mirrors the sandbox-step producer's stance on the same write.
33
+ */
34
+ export function createProfileActivityEmitter(write, frame, logger) {
35
+ // One reconciling id for the profile's single step, so the run-stream fold
36
+ // collapses every phase transition latest-wins onto one frame rather than
37
+ // accumulating them. Minted through the shared construction because that
38
+ // sameness across producers is what makes the fold correct.
39
+ const id = stepPartId("step-activity", frame.runId, frame.stepId);
40
+ const emit = async (phase, activity) => {
41
+ try {
42
+ await write({ type: "data-step-activity", id, runId: frame.runId, stepId: frame.stepId, phase, activity });
43
+ }
44
+ catch (err) {
45
+ logger.warn("run-event write failed (non-fatal)", { phase, ...logger.errorFields(err) });
46
+ }
47
+ };
48
+ return {
49
+ sandboxInit: () => emit("sandbox-init", "Starting sandbox"),
50
+ agentStarting: () => emit("executing", "Running data-profiler"),
51
+ forTool: (name, input) => emit("executing", activityForTool(name, input)),
52
+ indexing: () => emit("indexing", "Indexing input descriptions for search"),
53
+ complete: () => emit("complete", "Profile complete"),
54
+ failed: (reason) => emit("failed", reason),
55
+ };
56
+ }