@inflexa-ai/harness 0.13.0 → 0.14.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 (162) hide show
  1. package/dist/agents/conversation-agent.js +23 -16
  2. package/dist/agents/sandbox/cheminformatics-agent.js +1 -1
  3. package/dist/agents/sandbox/chromatin-agent.js +1 -1
  4. package/dist/agents/sandbox/dna-methylation-agent.js +1 -1
  5. package/dist/agents/sandbox/drug-repurposing-agent.js +5 -10
  6. package/dist/agents/sandbox/enrichment-agent.js +1 -1
  7. package/dist/agents/sandbox/genomic-variant-agent.js +1 -11
  8. package/dist/agents/sandbox/immune-profiling-agent.js +1 -1
  9. package/dist/agents/sandbox/multi-omics-integration-agent.js +1 -1
  10. package/dist/agents/sandbox/network-agent.js +1 -1
  11. package/dist/agents/sandbox/pkpd-clinical-response-agent.js +3 -6
  12. package/dist/agents/sandbox/scientific-executor.js +1 -1
  13. package/dist/agents/sandbox/shared.js +7 -13
  14. package/dist/agents/sandbox/translational-safety-agent.js +4 -7
  15. package/dist/agents/sandbox/types.d.ts +5 -5
  16. package/dist/agents/sandbox/types.js +4 -4
  17. package/dist/app/chat-turn.js +4 -1
  18. package/dist/app/message-assembly.d.ts +5 -2
  19. package/dist/app/message-assembly.js +7 -2
  20. package/dist/app/run-activity.d.ts +13 -0
  21. package/dist/app/run-activity.js +51 -0
  22. package/dist/contracts/target-dossier.d.ts +10 -10
  23. package/dist/execution/report-runner.js +1 -1
  24. package/dist/execution/run-event-parts.d.ts +44 -0
  25. package/dist/execution/run-event-parts.js +0 -0
  26. package/dist/execution/run-event-stream.d.ts +91 -0
  27. package/dist/execution/run-event-stream.js +212 -0
  28. package/dist/execution/run-synthesis.js +1 -1
  29. package/dist/index.d.ts +5 -2
  30. package/dist/index.js +20 -1
  31. package/dist/input-sanitization.d.ts +14 -4
  32. package/dist/input-sanitization.js +27 -4
  33. package/dist/lib/storage.js +4 -1
  34. package/dist/loop/budget-exceeded.d.ts +18 -2
  35. package/dist/loop/budget-exceeded.js +49 -2
  36. package/dist/loop/run-agent.d.ts +22 -1
  37. package/dist/loop/run-agent.js +91 -25
  38. package/dist/loop/run-to-terminal.d.ts +22 -4
  39. package/dist/loop/run-to-terminal.js +20 -4
  40. package/dist/memory/ai-sdk-message-storage.d.ts +37 -4
  41. package/dist/memory/ai-sdk-message-storage.js +45 -4
  42. package/dist/memory/content-to-cortex.js +19 -3
  43. package/dist/memory/message-backfill.js +1 -1
  44. package/dist/memory/thread-history.js +34 -8
  45. package/dist/prompts/conversation.d.ts +1 -1
  46. package/dist/prompts/conversation.js +15 -6
  47. package/dist/prompts/literature-reviewer.d.ts +1 -1
  48. package/dist/prompts/literature-reviewer.js +16 -14
  49. package/dist/prompts/planner.js +15 -20
  50. package/dist/prompts/sandbox/drug-repurposing-agent.d.ts +1 -1
  51. package/dist/prompts/sandbox/drug-repurposing-agent.js +31 -22
  52. package/dist/prompts/sandbox/multi-omics-integration-agent.d.ts +1 -1
  53. package/dist/prompts/sandbox/multi-omics-integration-agent.js +9 -10
  54. package/dist/prompts/sandbox/pkpd-clinical-response-agent.d.ts +1 -1
  55. package/dist/prompts/sandbox/pkpd-clinical-response-agent.js +8 -7
  56. package/dist/prompts/sandbox/translational-safety-agent.d.ts +1 -1
  57. package/dist/prompts/sandbox/translational-safety-agent.js +20 -14
  58. package/dist/prompts/synthesis-agent.d.ts +1 -1
  59. package/dist/prompts/synthesis-agent.js +7 -7
  60. package/dist/providers/errors.d.ts +7 -0
  61. package/dist/providers/errors.js +88 -3
  62. package/dist/runtime/pools.d.ts +15 -1
  63. package/dist/runtime/pools.js +15 -1
  64. package/dist/sandbox/sandbox-step-translate.d.ts +15 -2
  65. package/dist/sandbox/sandbox-step-translate.js +17 -2
  66. package/dist/schemas/workflow-state.d.ts +2 -2
  67. package/dist/schemas/workflow-state.js +2 -2
  68. package/dist/state/data-profile.d.ts +53 -1
  69. package/dist/state/data-profile.js +66 -6
  70. package/dist/state/index.d.ts +3 -3
  71. package/dist/state/index.js +2 -2
  72. package/dist/state/init.js +37 -2
  73. package/dist/state/runs.d.ts +39 -0
  74. package/dist/state/runs.js +103 -0
  75. package/dist/tasks/data-profile-activity.d.ts +87 -0
  76. package/dist/tasks/data-profile-activity.js +56 -0
  77. package/dist/tasks/data-profile.js +66 -4
  78. package/dist/tools/bio/chembl.d.ts +1 -1
  79. package/dist/tools/bio/chembl.js +51 -54
  80. package/dist/tools/bio/comptox.js +35 -33
  81. package/dist/tools/bio/drug-gene-interactions.d.ts +76 -0
  82. package/dist/tools/bio/drug-gene-interactions.js +175 -0
  83. package/dist/tools/bio/gene-disease-evidence.d.ts +61 -0
  84. package/dist/tools/bio/gene-disease-evidence.js +187 -0
  85. package/dist/tools/bio/gene-preclinical-profile.d.ts +58 -0
  86. package/dist/tools/bio/gene-preclinical-profile.js +125 -0
  87. package/dist/tools/bio/index.d.ts +23 -16
  88. package/dist/tools/bio/index.js +29 -22
  89. package/dist/tools/bio/keys.d.ts +16 -9
  90. package/dist/tools/bio/keys.js +22 -13
  91. package/dist/tools/bio/lookup-annotation.d.ts +48 -0
  92. package/dist/tools/bio/lookup-annotation.js +173 -0
  93. package/dist/tools/bio/opentargets.d.ts +7 -11
  94. package/dist/tools/bio/opentargets.js +24 -35
  95. package/dist/tools/bio/pubchem.js +6 -4
  96. package/dist/tools/bio/pubmed.d.ts +15 -7
  97. package/dist/tools/bio/pubmed.js +79 -20
  98. package/dist/tools/bio/search-clinical-trials.d.ts +20 -1
  99. package/dist/tools/bio/search-clinical-trials.js +26 -5
  100. package/dist/tools/bio/search-faers.js +9 -3
  101. package/dist/tools/bio/search-gene.d.ts +0 -1
  102. package/dist/tools/bio/search-gene.js +4 -5
  103. package/dist/tools/bio/search-geo-datasets.js +10 -4
  104. package/dist/tools/bio/search-interactions.d.ts +8 -4
  105. package/dist/tools/bio/search-interactions.js +28 -12
  106. package/dist/tools/bio/target-safety.d.ts +59 -0
  107. package/dist/tools/bio/target-safety.js +191 -0
  108. package/dist/tools/execute-analysis.d.ts +1 -0
  109. package/dist/tools/execute-analysis.js +5 -5
  110. package/dist/tools/lib/dgidb-client.d.ts +49 -0
  111. package/dist/tools/lib/dgidb-client.js +168 -0
  112. package/dist/tools/lib/disgenet-client.d.ts +56 -0
  113. package/dist/tools/lib/disgenet-client.js +59 -0
  114. package/dist/tools/lib/drugbank-client.d.ts +83 -0
  115. package/dist/tools/lib/drugbank-client.js +95 -0
  116. package/dist/tools/lib/ensembl-client.d.ts +4 -1
  117. package/dist/tools/lib/ensembl-client.js +5 -3
  118. package/dist/tools/lib/gwas-catalog-client.d.ts +60 -0
  119. package/dist/tools/lib/gwas-catalog-client.js +136 -0
  120. package/dist/tools/lib/pharmgkb-client.d.ts +22 -0
  121. package/dist/tools/lib/pharmgkb-client.js +32 -0
  122. package/dist/tools/lib/pubmed-client.d.ts +57 -4
  123. package/dist/tools/lib/pubmed-client.js +57 -3
  124. package/dist/tools/lib/string-client.d.ts +25 -4
  125. package/dist/tools/lib/string-client.js +22 -5
  126. package/dist/tools/research/generate-plan.d.ts +16 -9
  127. package/dist/tools/research/generate-plan.js +405 -85
  128. package/dist/tools/research/inspect-data-profile.d.ts +31 -6
  129. package/dist/tools/research/inspect-data-profile.js +23 -7
  130. package/dist/tools/research/inspect-run.d.ts +33 -15
  131. package/dist/tools/research/inspect-run.js +155 -66
  132. package/dist/tools/research/literature-reviewer.js +7 -11
  133. package/dist/workflows/execute-analysis.d.ts +63 -0
  134. package/dist/workflows/execute-analysis.js +73 -2
  135. package/dist/workflows/sandbox-step.js +1 -11
  136. package/dist/workflows/target-assessment/decisions/index.d.ts +1 -1
  137. package/dist/workflows/target-assessment/phase4-assemble.d.ts +1 -1
  138. package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +2 -2
  139. package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +1 -1
  140. package/package.json +1 -1
  141. package/dist/tools/bio/check-safety-panel.d.ts +0 -35
  142. package/dist/tools/bio/check-safety-panel.js +0 -76
  143. package/dist/tools/bio/get-impc-ko-profile.d.ts +0 -9
  144. package/dist/tools/bio/get-impc-ko-profile.js +0 -23
  145. package/dist/tools/bio/lookup-go-term.d.ts +0 -31
  146. package/dist/tools/bio/lookup-go-term.js +0 -116
  147. package/dist/tools/bio/search-bgee-expression.d.ts +0 -10
  148. package/dist/tools/bio/search-bgee-expression.js +0 -30
  149. package/dist/tools/bio/search-clinvar.d.ts +0 -13
  150. package/dist/tools/bio/search-clinvar.js +0 -28
  151. package/dist/tools/bio/search-dgidb.d.ts +0 -53
  152. package/dist/tools/bio/search-dgidb.js +0 -213
  153. package/dist/tools/bio/search-disgenet.d.ts +0 -30
  154. package/dist/tools/bio/search-disgenet.js +0 -84
  155. package/dist/tools/bio/search-drugbank.d.ts +0 -38
  156. package/dist/tools/bio/search-drugbank.js +0 -109
  157. package/dist/tools/bio/search-gwas-catalog.d.ts +0 -33
  158. package/dist/tools/bio/search-gwas-catalog.js +0 -172
  159. package/dist/tools/bio/search-pathway.d.ts +0 -12
  160. package/dist/tools/bio/search-pathway.js +0 -32
  161. package/dist/tools/bio/search-pharmgkb.d.ts +0 -17
  162. package/dist/tools/bio/search-pharmgkb.js +0 -52
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The two input-only rules of the run-event read seam: narrowing a raw durable
3
+ * stream value to the chat data-part contract, and the latest-wins reconciling
4
+ * fold applied before delivery.
5
+ *
6
+ * They live apart from the reader because they are the only part of the seam
7
+ * whose correctness is a function of its argument alone — no database, no
8
+ * durability engine, no clock — and keeping them here is what lets that be
9
+ * proven directly instead of inferred from a driven subscription.
10
+ */
11
+ import type { CortexChatPart } from "../contracts/chat-parts.js";
12
+ /**
13
+ * Narrow one raw stream value to a chat data part, or `null` when it is not one.
14
+ *
15
+ * A run's streams carry more than the contract: a sandbox step also writes its
16
+ * loop orchestration under envelopes (`data-loop-event`, `data-sandbox-event`)
17
+ * that belong to the workflow layer and have no registry entry. The seam's
18
+ * contract is the `CortexChatPart` union, so anything the registry does not
19
+ * classify is not deliverable — which also covers a part written by a producer
20
+ * newer than the reader.
21
+ *
22
+ * The registry is keyed by the discriminant alone, so this validates `type` and
23
+ * trusts the producer for the payload. There is no per-part schema to validate
24
+ * against: `contracts/chat-parts.ts` declares interfaces, not Zod objects. Both
25
+ * ends of this stream are typed against that same union inside this package, so
26
+ * the discriminant is the boundary that can actually drift (a producer emitting
27
+ * something else entirely) and the one worth checking.
28
+ */
29
+ export declare function parseRunEventPart(value: unknown): CortexChatPart | null;
30
+ /**
31
+ * Collapse a batch of parts so each reconciling id appears once, carrying its
32
+ * latest value, and every other part survives untouched in write order.
33
+ *
34
+ * `reconciling` in `PART_REGISTRY` already means "a later emission under this id
35
+ * supersedes the earlier one" — `data-step-activity` mints a stable id per
36
+ * `(runId, stepId)` precisely so a fold collapses every phase transition. Reading
37
+ * the flag rather than holding a list of part types here keeps one source of
38
+ * truth: a part type added to the registry later folds correctly without this
39
+ * module changing.
40
+ *
41
+ * A survivor keeps the position of its LAST occurrence, so relative order among
42
+ * the parts that are delivered is the order they were written.
43
+ */
44
+ export declare function foldRunEventParts(parts: readonly CortexChatPart[]): CortexChatPart[];
@@ -0,0 +1,91 @@
1
+ /**
2
+ * RunEventStream — the read side of the durable per-run event stream.
3
+ *
4
+ * Every analysis run already writes a rich, typed event stream: the parent
5
+ * `executeAnalysis` workflow writes the run-level parts, and each `sandbox-step`
6
+ * child writes its own step-level parts to its OWN stream, because a workflow can
7
+ * only write the stream it owns. This seam reads all of them back as one channel,
8
+ * so an embedder can show a user what a run is actually doing without
9
+ * reconstructing progress from the durability engine's internal step-cache tables
10
+ * — which record a step only when it *completes*, and so describe the thing that
11
+ * just finished rather than the thing in flight.
12
+ *
13
+ * The seam quarantines the engine the same way `RunLauncher` does for starting
14
+ * workflows: the delivered values are the harness's own `contracts/` part types,
15
+ * and no engine type appears in the exported signature.
16
+ *
17
+ * It is a push subscription rather than a snapshot reader because the engine's
18
+ * read primitive is an async generator with no offset parameter — "give me what
19
+ * is new since X" is not expressible on the public API, and expressing it would
20
+ * mean reaching into the SDK internals this seam exists to remove. A merged async
21
+ * iterator was the other candidate and was rejected because the set of sources
22
+ * grows during iteration (children join mid-run), which costs materially more
23
+ * code than invoking a handler; a callback also matches the `EmitFn` idiom the
24
+ * producers already speak.
25
+ *
26
+ * `subscribe` resolves rather than returning a `Result` because it has no failure
27
+ * to report. Observation is a diagnostic channel: a throwing handler and a
28
+ * failing stream are both logged and contained, mirroring the `safeEmit` stance
29
+ * on the write side, where a dropped UI frame must never fail a step. A read side
30
+ * that tore itself down on one bad part would be strictly less robust than the
31
+ * writer it observes.
32
+ */
33
+ import type { Pool } from "pg";
34
+ import type { CortexChatPart } from "../contracts/chat-parts.js";
35
+ import type { Logger } from "../lib/logger.js";
36
+ /**
37
+ * Construction-time dependencies. The pool is the application pool the run
38
+ * ledger lives in; the logger is where every contained failure is reported, and
39
+ * defaults to silence so an embedder that wired none sees nothing rather than
40
+ * having console forced on it.
41
+ */
42
+ export interface RunEventStreamDeps {
43
+ readonly pool: Pool;
44
+ readonly logger?: Logger;
45
+ }
46
+ /**
47
+ * Receives one part. May be async — the subscription awaits it, so a handler is
48
+ * never invoked concurrently with itself and back-pressure is the caller's to
49
+ * exert by taking its time. A throw is logged and swallowed.
50
+ */
51
+ export type RunEventPartHandler = (part: CortexChatPart) => void | Promise<void>;
52
+ /** Call-time parameters of one subscription. */
53
+ export interface RunEventSubscribeOptions {
54
+ /** The run to observe. Also the parent workflow's id. */
55
+ readonly runId: string;
56
+ /** Where each delivered part goes. */
57
+ readonly onPart: RunEventPartHandler;
58
+ /** Aborting stops delivery and settles the returned promise. */
59
+ readonly signal: AbortSignal;
60
+ }
61
+ /**
62
+ * The run-event read seam. One method, because a run-scoped subscription is the
63
+ * whole capability: a general-purpose stream reader would leak the engine's model
64
+ * back into the surface this exists to quarantine.
65
+ */
66
+ export interface RunEventStream {
67
+ /**
68
+ * Deliver every part the run produces — parent and children alike — to
69
+ * `onPart`, resolving when the run is terminal and every stream opened has
70
+ * drained, or promptly when the signal aborts.
71
+ *
72
+ * Parts from one workflow arrive in the order that workflow wrote them. No
73
+ * order is promised ACROSS the parent and its children: those workflows
74
+ * execute concurrently and the producers establish no cross-stream clock, so
75
+ * a total order could only be synthesised from timestamps that were never
76
+ * meant to be one. Consumers do not need it — every part is addressed by
77
+ * `runId`/`stepId` and the reconciling fold is per id.
78
+ */
79
+ subscribe(options: RunEventSubscribeOptions): Promise<void>;
80
+ }
81
+ /**
82
+ * Build the DBOS-backed realization of {@link RunEventStream}.
83
+ *
84
+ * Every stream is read from its beginning, and each workflow is opened at most
85
+ * once per subscription. Replay is not a cost worked around here, it is what
86
+ * makes attaching mid-run correct: a subscriber joining at minute ten receives
87
+ * the run's whole history and folding it yields the true current state.
88
+ * Re-opening a workflow would redeliver that history and duplicate every part
89
+ * the fold does not reconcile.
90
+ */
91
+ export declare function createRunEventStream(deps: RunEventStreamDeps): RunEventStream;
@@ -0,0 +1,212 @@
1
+ /**
2
+ * RunEventStream — the read side of the durable per-run event stream.
3
+ *
4
+ * Every analysis run already writes a rich, typed event stream: the parent
5
+ * `executeAnalysis` workflow writes the run-level parts, and each `sandbox-step`
6
+ * child writes its own step-level parts to its OWN stream, because a workflow can
7
+ * only write the stream it owns. This seam reads all of them back as one channel,
8
+ * so an embedder can show a user what a run is actually doing without
9
+ * reconstructing progress from the durability engine's internal step-cache tables
10
+ * — which record a step only when it *completes*, and so describe the thing that
11
+ * just finished rather than the thing in flight.
12
+ *
13
+ * The seam quarantines the engine the same way `RunLauncher` does for starting
14
+ * workflows: the delivered values are the harness's own `contracts/` part types,
15
+ * and no engine type appears in the exported signature.
16
+ *
17
+ * It is a push subscription rather than a snapshot reader because the engine's
18
+ * read primitive is an async generator with no offset parameter — "give me what
19
+ * is new since X" is not expressible on the public API, and expressing it would
20
+ * mean reaching into the SDK internals this seam exists to remove. A merged async
21
+ * iterator was the other candidate and was rejected because the set of sources
22
+ * grows during iteration (children join mid-run), which costs materially more
23
+ * code than invoking a handler; a callback also matches the `EmitFn` idiom the
24
+ * producers already speak.
25
+ *
26
+ * `subscribe` resolves rather than returning a `Result` because it has no failure
27
+ * to report. Observation is a diagnostic channel: a throwing handler and a
28
+ * failing stream are both logged and contained, mirroring the `safeEmit` stance
29
+ * on the write side, where a dropped UI frame must never fail a step. A read side
30
+ * that tore itself down on one bad part would be strictly less robust than the
31
+ * writer it observes.
32
+ */
33
+ import { DBOS } from "@dbos-inc/dbos-sdk";
34
+ import { sleep } from "../lib/async-utils.js";
35
+ import { createNoopLogger } from "../lib/console-logger.js";
36
+ import { describeDbError } from "../lib/db-result.js";
37
+ import { queryStepsByRun } from "../state/step-executions.js";
38
+ import { foldRunEventParts, parseRunEventPart } from "./run-event-parts.js";
39
+ /** Durable stream key every run producer writes its parts under. */
40
+ const RUN_EVENT_STREAM_KEY = "events";
41
+ /**
42
+ * How often the run's step ledger is re-read for children that have started
43
+ * since the last look.
44
+ *
45
+ * A child records its own workflow id as it begins, so discovery can only be as
46
+ * timely as that write and there is nothing to subscribe to before it lands.
47
+ * Polling is the honest mechanism: the parent's dag snapshot marks a step running
48
+ * before the child's body has written its row, so an event-driven re-check would
49
+ * race the ledger. One second matches the engine's own stream-polling fallback,
50
+ * and lagging a child's first writes costs nothing — a stream subscribed late is
51
+ * still read from its beginning.
52
+ */
53
+ const CHILD_DISCOVERY_INTERVAL_MS = 1_000;
54
+ /**
55
+ * Build the DBOS-backed realization of {@link RunEventStream}.
56
+ *
57
+ * Every stream is read from its beginning, and each workflow is opened at most
58
+ * once per subscription. Replay is not a cost worked around here, it is what
59
+ * makes attaching mid-run correct: a subscriber joining at minute ten receives
60
+ * the run's whole history and folding it yields the true current state.
61
+ * Re-opening a workflow would redeliver that history and duplicate every part
62
+ * the fold does not reconcile.
63
+ */
64
+ export function createRunEventStream(deps) {
65
+ const baseLogger = (deps.logger ?? createNoopLogger()).named("run-event-stream");
66
+ return {
67
+ async subscribe({ runId, onPart, signal }) {
68
+ const logger = baseLogger.with({ runId });
69
+ // Open generators, so an abort can ask each to wind down. The engine's
70
+ // reader exposes no cancellation: while it waits on a quiet-but-active
71
+ // workflow it is suspended inside an await, where a queued `return()` is
72
+ // only honoured once it next reaches a yield. Teardown is therefore
73
+ // best-effort by construction, and the subscription's own promise is
74
+ // raced against the signal rather than waiting on it.
75
+ const open = new Set();
76
+ let resolveAbort;
77
+ const aborted = new Promise((resolve) => {
78
+ resolveAbort = resolve;
79
+ });
80
+ const onAbort = () => {
81
+ resolveAbort();
82
+ for (const generator of open) {
83
+ void generator.return(undefined).catch(() => {
84
+ /* the read is already being abandoned */
85
+ });
86
+ }
87
+ };
88
+ if (signal.aborted)
89
+ onAbort();
90
+ else
91
+ signal.addEventListener("abort", onAbort, { once: true });
92
+ // Delivery is serialized through one queue across every stream. That
93
+ // gives the fold a batch to collapse — whatever accumulated while the
94
+ // previous handler call was in flight is reconciled before it is handed
95
+ // over, which is what spares a mid-run subscriber the superseded
96
+ // intermediates it replayed. The queue is FIFO and the fold keeps
97
+ // survivors in place, so a single stream's write order survives it.
98
+ const pending = [];
99
+ let delivering = false;
100
+ const deliver = async () => {
101
+ if (delivering)
102
+ return;
103
+ delivering = true;
104
+ try {
105
+ while (pending.length > 0 && !signal.aborted) {
106
+ const batch = pending.splice(0, pending.length);
107
+ for (const part of foldRunEventParts(batch)) {
108
+ if (signal.aborted)
109
+ return;
110
+ try {
111
+ await onPart(part);
112
+ }
113
+ catch (err) {
114
+ logger.warn("run-event handler threw", { partType: part.type, ...logger.errorFields(err) });
115
+ }
116
+ }
117
+ }
118
+ }
119
+ finally {
120
+ delivering = false;
121
+ }
122
+ };
123
+ const readWorkflowStream = async (workflowId) => {
124
+ const streamLogger = logger.with({ workflowId });
125
+ const generator = DBOS.readStream(workflowId, RUN_EVENT_STREAM_KEY);
126
+ open.add(generator);
127
+ try {
128
+ for (;;) {
129
+ const next = await generator.next();
130
+ // Checked after the read, not before: `next()` is what notices
131
+ // the workflow going inactive, and an aborted subscription
132
+ // must not enqueue anything the drain will never take.
133
+ if (next.done === true || signal.aborted)
134
+ return;
135
+ const part = parseRunEventPart(next.value);
136
+ if (part === null) {
137
+ streamLogger.debug("dropped a stream value outside the chat-part contract");
138
+ continue;
139
+ }
140
+ pending.push(part);
141
+ void deliver();
142
+ }
143
+ }
144
+ catch (err) {
145
+ // One stream's failure is contained here so the parent and every
146
+ // other child keep arriving.
147
+ streamLogger.error("run-event stream read failed", streamLogger.errorFields(err));
148
+ }
149
+ finally {
150
+ open.delete(generator);
151
+ void generator.return(undefined).catch(() => {
152
+ /* already finished, or abandoned under abort */
153
+ });
154
+ }
155
+ };
156
+ const subscribed = new Set([runId]);
157
+ const childReads = [];
158
+ // Children are addressed by the id the workflow bodies already persist.
159
+ // Deriving them from the scheduler's `${runId}-${idx}` naming would
160
+ // hard-code a scheme that belongs to the scheduler, and the engine's own
161
+ // child bookkeeping is internals of the kind this seam removes.
162
+ const discoverChildren = async () => {
163
+ (await queryStepsByRun(deps.pool, runId)).match((rows) => {
164
+ for (const row of rows) {
165
+ const childWorkflowId = row.childWorkflowId;
166
+ if (childWorkflowId === null || subscribed.has(childWorkflowId))
167
+ continue;
168
+ subscribed.add(childWorkflowId);
169
+ childReads.push(readWorkflowStream(childWorkflowId));
170
+ }
171
+ }, (error) => {
172
+ logger.warn("child discovery failed", { err: describeDbError(error) });
173
+ });
174
+ };
175
+ let parentDrained = false;
176
+ const parentRead = readWorkflowStream(runId).finally(() => {
177
+ parentDrained = true;
178
+ });
179
+ // The parent's stream draining IS the run reaching terminal: the engine
180
+ // ends a read exactly when the workflow is no longer active. One further
181
+ // pass runs after that — a child whose id landed in the ledger late is
182
+ // still replayed from zero — and then discovery stops.
183
+ const discovery = (async () => {
184
+ for (;;) {
185
+ if (signal.aborted)
186
+ return;
187
+ await discoverChildren();
188
+ if (signal.aborted || parentDrained)
189
+ return;
190
+ await Promise.race([sleep(CHILD_DISCOVERY_INTERVAL_MS), aborted, parentRead]);
191
+ }
192
+ })();
193
+ const settled = (async () => {
194
+ await discovery;
195
+ await parentRead;
196
+ await Promise.all(childReads);
197
+ })().catch((err) => {
198
+ // Defensive: every stream read and every discovery pass contains its
199
+ // own failure, so arriving here is a defect in this seam rather than
200
+ // a runtime condition. Logged rather than left to surface as an
201
+ // unhandled rejection after an abort already settled the caller.
202
+ logger.error("run-event subscription failed", logger.errorFields(err));
203
+ });
204
+ try {
205
+ await Promise.race([settled, aborted]);
206
+ }
207
+ finally {
208
+ signal.removeEventListener("abort", onAbort);
209
+ }
210
+ },
211
+ };
212
+ }
@@ -369,9 +369,9 @@ export async function generateRunSynthesis(input) {
369
369
  signal,
370
370
  emit,
371
371
  runStep: passthroughStep,
372
+ resolved: () => holder.outcome !== null,
372
373
  };
373
374
  await runToTerminal(agent, [{ role: "user", content: prompt }], input.session, loopDeps, {
374
- resolved: () => holder.outcome !== null,
375
375
  tools: [submitTool, blockerTool],
376
376
  nudge: "You ended without calling a terminal tool. You MUST call " +
377
377
  "submit_synthesis with the final synthesis now, or report_blocker " +
package/dist/index.d.ts CHANGED
@@ -54,6 +54,7 @@ export { createThreadStore } from "./memory/thread-store.js";
54
54
  export type { ThreadStore, Thread, CreateThreadInput, ListThreadsInput, ThreadPage } from "./memory/thread-store.js";
55
55
  export { createThreadHistory } from "./memory/thread-history.js";
56
56
  export type { ThreadHistory, StoredMessage, MessagePage, RetractOutcome } from "./memory/thread-history.js";
57
+ export { syntheticUserMessage, syntheticRecordMessage, isSyntheticUserMessage, isSyntheticRecordMessage } from "./memory/ai-sdk-message-storage.js";
57
58
  export { contentToCortexMessages } from "./memory/content-to-cortex.js";
58
59
  export { createCardResolver } from "./memory/reconstruct-cards.js";
59
60
  export type { ToolCardResolver, StoredToolCallForCard } from "./memory/reconstruct-cards.js";
@@ -88,9 +89,11 @@ export { deliverExecEvent, execEventTopic } from "./sandbox/deliver-exec-event.j
88
89
  export { ExecEventMessageSchema } from "./sandbox/types.js";
89
90
  export type { ExecEventMessage, DoneMarker } from "./sandbox/types.js";
90
91
  export { registerExecuteAnalysis } from "./workflows/execute-analysis.js";
91
- export type { ExecuteAnalysisDeps, ExecuteAnalysisInput, ExecuteAnalysisResult, RunProvenanceEvent } from "./workflows/execute-analysis.js";
92
+ export type { ExecuteAnalysisDeps, ExecuteAnalysisInput, ExecuteAnalysisResult, RunObservation, RunProvenanceEvent, RunStepObservation, StepRuntimeStatus, } from "./workflows/execute-analysis.js";
92
93
  export { registerSandboxStep } from "./workflows/sandbox-step.js";
93
94
  export type { SandboxStepDeps, SandboxStepInput, SandboxStepResult, SandboxAgentBuildContext } from "./workflows/sandbox-step.js";
95
+ export { createRunEventStream } from "./execution/run-event-stream.js";
96
+ export type { RunEventStream, RunEventStreamDeps, RunEventSubscribeOptions, RunEventPartHandler } from "./execution/run-event-stream.js";
94
97
  export { createSandboxAgents, SANDBOX_AGENT_META } from "./agents/sandbox/index.js";
95
98
  export type { SandboxAgentDeps, SandboxStepCoords, AgentMeta } from "./agents/sandbox/index.js";
96
99
  export { AnalysisPlanSchema } from "./schemas/workflow-state.js";
@@ -101,7 +104,7 @@ export type { ValidatePlanOptions } from "./schemas/validate-plan.js";
101
104
  export type { ValidationResult } from "./schemas/validate-plan.js";
102
105
  export { upsertPlan, loadPlan } from "./state/plans.js";
103
106
  export type { UpsertPlanInput } from "./state/plans.js";
104
- export { insertRun, reserveRunById, queryActiveRun, updateRunStatus, queryRun, queryRunsByAnalysis, RunDedupCollisionError, RunIdentityCollisionError, } from "./state/runs.js";
107
+ export { insertRun, reserveRunById, queryActiveRun, queryActiveRunsByAnalysis, updateRunStatus, queryRun, queryRunsByAnalysis, RunDedupCollisionError, RunIdentityCollisionError, } from "./state/runs.js";
105
108
  export type { InsertRunInput, RunReservation } from "./state/runs.js";
106
109
  export { queryStepsByRun } from "./state/step-executions.js";
107
110
  export type { CortexRunRow, StepExecutionRow, RunStatus } from "./state/schema.js";
package/dist/index.js CHANGED
@@ -81,6 +81,16 @@ export { createStreamingChat } from "./providers/streaming-chat.js";
81
81
  export { prepareChatTurn } from "./app/chat-turn.js";
82
82
  export { createThreadStore } from "./memory/thread-store.js";
83
83
  export { createThreadHistory } from "./memory/thread-history.js";
84
+ // The synthetic-message primitives. A host appends a record of out-of-band work (an analysis run's
85
+ // outcome) into a thread through `appendTurn`, and needs a message the model reads but that is not
86
+ // user input — which is exactly what the marker denotes. The constructor is exported rather than
87
+ // the marker key so a host can never hand-assemble a message that the turn-boundary predicates
88
+ // (`isGenuineUserStart` and its SQL twin) would then fail to recognise; `isSyntheticUserMessage`
89
+ // lets a host renderer tell such a message apart from a genuine user turn on the way back out.
90
+ // `syntheticRecordMessage` is the one a HOST reaches for: equally non-turn-opening, but rendered —
91
+ // the display reconstruction emits it as a `system` message rather than dropping it as loop
92
+ // machinery, so an appended run outcome is visible in the transcript instead of silently absent.
93
+ export { syntheticUserMessage, syntheticRecordMessage, isSyntheticUserMessage, isSyntheticRecordMessage } from "./memory/ai-sdk-message-storage.js";
84
94
  export { contentToCortexMessages } from "./memory/content-to-cortex.js";
85
95
  export { createCardResolver } from "./memory/reconstruct-cards.js";
86
96
  export { PART_REGISTRY, isTransient, isReconciling, isSidebarPart } from "./contracts/part-registry.js";
@@ -138,6 +148,15 @@ export { ExecEventMessageSchema } from "./sandbox/types.js";
138
148
  // these, but an embedder running only the run engine registers them directly.
139
149
  export { registerExecuteAnalysis } from "./workflows/execute-analysis.js";
140
150
  export { registerSandboxStep } from "./workflows/sandbox-step.js";
151
+ // Run-event read seam — the fine-grained durable channel beside `observeRun`.
152
+ // The two are a deliberate pair, not a duplication: `observeRun` is the coarse,
153
+ // in-process run/step snapshot a workflow hands its host synchronously, and it
154
+ // defers sub-step detail to the run's durable event stream. This is that stream,
155
+ // read back — the per-tool-call activity, file trees, and step summaries a run
156
+ // writes, fanned in from the parent workflow and every sandbox-step child.
157
+ // Nothing in the signature belongs to the durability engine, so an embedder
158
+ // consumes run events without depending on it.
159
+ export { createRunEventStream } from "./execution/run-event-stream.js";
141
160
  // Sandbox-agent catalog. `buildAgent` maps a `SandboxAgentBuildContext` onto
142
161
  // `SandboxAgentDeps`, then selects the per-step agent by id from this record;
143
162
  // `SANDBOX_AGENT_META` is the planner-facing meta the same ids key.
@@ -155,7 +174,7 @@ export { validatePlan } from "./schemas/validate-plan.js";
155
174
  // caller-derived id and inserts-if-absent (the deterministic-id intake path);
156
175
  // the run-state calls mirror the chat trigger's reserve → dedup → status flow.
157
176
  export { upsertPlan, loadPlan } from "./state/plans.js";
158
- export { insertRun, reserveRunById, queryActiveRun, updateRunStatus, queryRun, queryRunsByAnalysis, RunDedupCollisionError, RunIdentityCollisionError, } from "./state/runs.js";
177
+ export { insertRun, reserveRunById, queryActiveRun, queryActiveRunsByAnalysis, updateRunStatus, queryRun, queryRunsByAnalysis, RunDedupCollisionError, RunIdentityCollisionError, } from "./state/runs.js";
159
178
  export { queryStepsByRun } from "./state/step-executions.js";
160
179
  // Host-neutral reference-data catalog and shared installation receipts. Each
161
180
  // artifact names the third-party upstream it is fetched from; embedders own all
@@ -1,9 +1,10 @@
1
1
  /**
2
- * Input sanitization — two pure, always-on functions applied once to the
3
- * incoming user message by the chat route (change 6). Never applied to
4
- * assistant messages or tool results.
2
+ * Input sanitization — pure, always-on text functions, not a plug-in pipeline.
3
+ * What runs is exactly what is declared here.
5
4
  *
6
- * Two pure functions, not a plug-in pipeline. What runs is exactly these two.
5
+ * `normalizeUnicode` and `redactSecrets` are applied once to the incoming user
6
+ * message by the chat route (change 6), and never to assistant messages or tool
7
+ * results. `stripNulCharacters` is the exception — it applies to tool results.
7
8
  */
8
9
  /**
9
10
  * NFC-normalize and strip control characters.
@@ -13,6 +14,15 @@
13
14
  * that could smuggle formatting or terminal escapes.
14
15
  */
15
16
  export declare function normalizeUnicode(text: string): string;
17
+ /**
18
+ * Strip every NUL from `text`. Applied where a tool result is built — so the
19
+ * loop's in-memory message and the stored row agree byte-for-byte and the
20
+ * prompt-cache prefix survives the round trip — and again where the turn is
21
+ * written, which holds the invariant for writers that are not the loop. User
22
+ * input needs no separate treatment: `normalizeUnicode` already strips NUL along
23
+ * with the rest of the C0 range.
24
+ */
25
+ export declare function stripNulCharacters(text: string): string;
16
26
  /**
17
27
  * Replace structured secret formats with `[REDACTED: <name>]`.
18
28
  *
@@ -1,9 +1,10 @@
1
1
  /**
2
- * Input sanitization — two pure, always-on functions applied once to the
3
- * incoming user message by the chat route (change 6). Never applied to
4
- * assistant messages or tool results.
2
+ * Input sanitization — pure, always-on text functions, not a plug-in pipeline.
3
+ * What runs is exactly what is declared here.
5
4
  *
6
- * Two pure functions, not a plug-in pipeline. What runs is exactly these two.
5
+ * `normalizeUnicode` and `redactSecrets` are applied once to the incoming user
6
+ * message by the chat route (change 6), and never to assistant messages or tool
7
+ * results. `stripNulCharacters` is the exception — it applies to tool results.
7
8
  */
8
9
  /**
9
10
  * C0/C1 control characters and DEL, excluding `\t` (U+0009) and `\n`
@@ -21,6 +22,28 @@ const CONTROL_CHARS = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g;
21
22
  export function normalizeUnicode(text) {
22
23
  return text.normalize("NFC").replace(CONTROL_CHARS, "");
23
24
  }
25
+ /**
26
+ * NUL (U+0000) — the one character a stored message cannot carry. Postgres
27
+ * refuses it inside JSON: a `jsonb` column rejects the insert outright, and a
28
+ * `json` column accepts it but errors the moment a JSON operator reads that row
29
+ * ("U+0000 cannot be converted to text"), which is what `retractLastTurn`'s
30
+ * boundary predicate does. Either way one NUL byte poisons a thread, and it
31
+ * arrives through no fault of the model: tool results carry raw command output
32
+ * and file bytes, which is why they are otherwise left unsanitized.
33
+ */
34
+ // eslint-disable-next-line no-control-regex -- matching NUL is the intent; see above.
35
+ const NUL = /\u0000/g;
36
+ /**
37
+ * Strip every NUL from `text`. Applied where a tool result is built — so the
38
+ * loop's in-memory message and the stored row agree byte-for-byte and the
39
+ * prompt-cache prefix survives the round trip — and again where the turn is
40
+ * written, which holds the invariant for writers that are not the loop. User
41
+ * input needs no separate treatment: `normalizeUnicode` already strips NUL along
42
+ * with the rest of the C0 range.
43
+ */
44
+ export function stripNulCharacters(text) {
45
+ return text.replace(NUL, "");
46
+ }
24
47
  /**
25
48
  * Each entry redacts one structured, prefixed secret format. Every pattern
26
49
  * has a prefix (`AKIA`, `sk-`, `gh*_`, `eyJ`, `Bearer`, a DB scheme) that
@@ -7,7 +7,7 @@
7
7
  * the composition root and threaded as a constructor dep (see the harness-durable-runtime spec).
8
8
  */
9
9
  import pg from "pg";
10
- import { resolveAppPoolSize } from "../runtime/pools.js";
10
+ import { APP_POOL_ACQUIRE_TIMEOUT_MS, resolveAppPoolSize } from "../runtime/pools.js";
11
11
  function poolOptions(config) {
12
12
  return {
13
13
  host: config.host,
@@ -18,6 +18,9 @@ function poolOptions(config) {
18
18
  // Per-pod app pool; DBOS owns its system-DB connections separately
19
19
  // (see the postgres-storage-backend spec). The boot guard checks this fits `max_connections`.
20
20
  max: resolveAppPoolSize(config.poolMax),
21
+ // Acquisition is bounded so a saturated pool fails a caller instead of
22
+ // hanging it — see `APP_POOL_ACQUIRE_TIMEOUT_MS` for the invariant.
23
+ connectionTimeoutMillis: APP_POOL_ACQUIRE_TIMEOUT_MS,
21
24
  // `pg` accepts `ssl: false` to disable entirely, or an object with
22
25
  // `rejectUnauthorized` to tune TLS. We map sslmode to those forms.
23
26
  ssl: config.sslMode === "disable"
@@ -6,15 +6,31 @@
6
6
  * is wasteful because every subsequent call charged to the same budget will
7
7
  * also fail until the user tops up.
8
8
  *
9
- * Classification order (first hit wins):
9
+ * Classification order (first hit wins), and the order is load-bearing:
10
10
  * 1. Structured `statusCode === 402` (or `status === 402`) on any
11
11
  * link of the cause chain. The gateway emits 402 exclusively for
12
12
  * `budget_exceeded`, so a structured 402 is unambiguous.
13
- * 2. Pattern fallback: `/budget.?exceeded/i` on the stringified
13
+ * 2. A `ProviderError` on the value or its cause chain that was decided on
14
+ * a status: its `type` answers, and the text patterns are skipped.
15
+ * 3. Pattern fallback: `/budget.?exceeded/i` on the stringified
14
16
  * top-level error message. Covers stringified throws and paths
15
17
  * where no `statusCode` is attached (ops scripts, sandbox-server
16
18
  * surfacing upstream billing errors as text).
17
19
  *
20
+ * Step 2 exists because the text heuristic must never outrank a real
21
+ * classification. A `ProviderError` message carries an excerpt of the provider's
22
+ * response body, so a non-retryable 400 whose body merely mentions a budget would
23
+ * otherwise match step 3 — and the callers of this predicate act on it fatally
24
+ * (`sandbox-step` self-cancels the workflow, `execute-analysis` re-raises it as a
25
+ * budget throw). A phrase inside a quoted body is not evidence against the status
26
+ * the failure actually carried.
27
+ *
28
+ * "Decided on a status" is the load-bearing qualifier. `toProviderError` wraps
29
+ * every throwable, including ones `classifyProviderError` had no status for — it
30
+ * returns `provider` as a fall-through default there, which is an absence of
31
+ * classification, not a determination. Treating that as authoritative would
32
+ * silence step 3 in precisely the case it was written for.
33
+ *
18
34
  * Returns `false` for all other errors. Safe to call with any value;
19
35
  * non-Error inputs hit the stringified-pattern branch and return `false`
20
36
  * unless the string happens to match.
@@ -6,19 +6,36 @@
6
6
  * is wasteful because every subsequent call charged to the same budget will
7
7
  * also fail until the user tops up.
8
8
  *
9
- * Classification order (first hit wins):
9
+ * Classification order (first hit wins), and the order is load-bearing:
10
10
  * 1. Structured `statusCode === 402` (or `status === 402`) on any
11
11
  * link of the cause chain. The gateway emits 402 exclusively for
12
12
  * `budget_exceeded`, so a structured 402 is unambiguous.
13
- * 2. Pattern fallback: `/budget.?exceeded/i` on the stringified
13
+ * 2. A `ProviderError` on the value or its cause chain that was decided on
14
+ * a status: its `type` answers, and the text patterns are skipped.
15
+ * 3. Pattern fallback: `/budget.?exceeded/i` on the stringified
14
16
  * top-level error message. Covers stringified throws and paths
15
17
  * where no `statusCode` is attached (ops scripts, sandbox-server
16
18
  * surfacing upstream billing errors as text).
17
19
  *
20
+ * Step 2 exists because the text heuristic must never outrank a real
21
+ * classification. A `ProviderError` message carries an excerpt of the provider's
22
+ * response body, so a non-retryable 400 whose body merely mentions a budget would
23
+ * otherwise match step 3 — and the callers of this predicate act on it fatally
24
+ * (`sandbox-step` self-cancels the workflow, `execute-analysis` re-raises it as a
25
+ * budget throw). A phrase inside a quoted body is not evidence against the status
26
+ * the failure actually carried.
27
+ *
28
+ * "Decided on a status" is the load-bearing qualifier. `toProviderError` wraps
29
+ * every throwable, including ones `classifyProviderError` had no status for — it
30
+ * returns `provider` as a fall-through default there, which is an absence of
31
+ * classification, not a determination. Treating that as authoritative would
32
+ * silence step 3 in precisely the case it was written for.
33
+ *
18
34
  * Returns `false` for all other errors. Safe to call with any value;
19
35
  * non-Error inputs hit the stringified-pattern branch and return `false`
20
36
  * unless the string happens to match.
21
37
  */
38
+ import { extractStatus, isProviderError } from "../providers/errors.js";
22
39
  const BUDGET_EXCEEDED_PATTERNS = [/budget.?exceeded/i];
23
40
  /** Max depth walked on the cause chain looking for a structured statusCode. */
24
41
  const MAX_CAUSE_HOPS = 5;
@@ -33,9 +50,39 @@ function hasStatus402(err) {
33
50
  }
34
51
  return false;
35
52
  }
53
+ /**
54
+ * Find a `ProviderError` on the value or its cause chain. A `ResultError`
55
+ * thrown at a step boundary carries the structured value on `.cause`, which is
56
+ * the shape the callers of this predicate actually catch.
57
+ */
58
+ function findProviderError(err) {
59
+ let cursor = err;
60
+ for (let i = 0; i < MAX_CAUSE_HOPS && cursor; i++) {
61
+ if (isProviderError(cursor))
62
+ return cursor;
63
+ cursor = cursor.cause;
64
+ }
65
+ return undefined;
66
+ }
36
67
  export function isBudgetExceeded(err) {
37
68
  if (hasStatus402(err))
38
69
  return true;
70
+ const classified = findProviderError(err);
71
+ if (classified) {
72
+ if (classified.type === "budget")
73
+ return true;
74
+ // `auth` and `tenant-blocked` exist only because a status produced them, and
75
+ // a `provider` arm that carried a status was decided on it. None can be
76
+ // overturned by a phrase inside a response body the message merely quotes.
77
+ //
78
+ // A `provider` arm with NO status is the one exception: there
79
+ // `classifyProviderError` had nothing to key on and applied a fall-through
80
+ // default, so it made no determination to respect. That is exactly the shape
81
+ // a gateway reporting `budget_exceeded` as plain text arrives in, and the
82
+ // patterns below are the reason it is still caught.
83
+ if (classified.type !== "provider" || extractStatus(classified) !== undefined)
84
+ return false;
85
+ }
39
86
  const msg = err instanceof Error ? err.message : String(err ?? "");
40
87
  return BUDGET_EXCEEDED_PATTERNS.some((p) => p.test(msg));
41
88
  }