@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.
- package/dist/agents/conversation-agent.js +23 -16
- package/dist/agents/sandbox/cheminformatics-agent.js +1 -1
- package/dist/agents/sandbox/chromatin-agent.js +1 -1
- package/dist/agents/sandbox/dna-methylation-agent.js +1 -1
- package/dist/agents/sandbox/drug-repurposing-agent.js +5 -10
- package/dist/agents/sandbox/enrichment-agent.js +1 -1
- package/dist/agents/sandbox/genomic-variant-agent.js +1 -11
- package/dist/agents/sandbox/immune-profiling-agent.js +1 -1
- package/dist/agents/sandbox/multi-omics-integration-agent.js +1 -1
- package/dist/agents/sandbox/network-agent.js +1 -1
- package/dist/agents/sandbox/pkpd-clinical-response-agent.js +3 -6
- package/dist/agents/sandbox/scientific-executor.js +1 -1
- package/dist/agents/sandbox/shared.js +7 -13
- package/dist/agents/sandbox/translational-safety-agent.js +4 -7
- package/dist/agents/sandbox/types.d.ts +5 -5
- package/dist/agents/sandbox/types.js +4 -4
- package/dist/app/chat-turn.js +4 -1
- package/dist/app/message-assembly.d.ts +5 -2
- package/dist/app/message-assembly.js +7 -2
- package/dist/app/run-activity.d.ts +13 -0
- package/dist/app/run-activity.js +51 -0
- package/dist/contracts/target-dossier.d.ts +10 -10
- package/dist/execution/report-runner.js +1 -1
- package/dist/execution/run-event-parts.d.ts +44 -0
- package/dist/execution/run-event-parts.js +0 -0
- package/dist/execution/run-event-stream.d.ts +91 -0
- package/dist/execution/run-event-stream.js +212 -0
- package/dist/execution/run-synthesis.js +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +20 -1
- package/dist/input-sanitization.d.ts +14 -4
- package/dist/input-sanitization.js +27 -4
- package/dist/lib/storage.js +4 -1
- package/dist/loop/budget-exceeded.d.ts +18 -2
- package/dist/loop/budget-exceeded.js +49 -2
- package/dist/loop/run-agent.d.ts +22 -1
- package/dist/loop/run-agent.js +91 -25
- package/dist/loop/run-to-terminal.d.ts +22 -4
- package/dist/loop/run-to-terminal.js +20 -4
- package/dist/memory/ai-sdk-message-storage.d.ts +37 -4
- package/dist/memory/ai-sdk-message-storage.js +45 -4
- package/dist/memory/content-to-cortex.js +19 -3
- package/dist/memory/message-backfill.js +1 -1
- package/dist/memory/thread-history.js +34 -8
- package/dist/prompts/conversation.d.ts +1 -1
- package/dist/prompts/conversation.js +15 -6
- package/dist/prompts/literature-reviewer.d.ts +1 -1
- package/dist/prompts/literature-reviewer.js +16 -14
- package/dist/prompts/planner.js +15 -20
- package/dist/prompts/sandbox/drug-repurposing-agent.d.ts +1 -1
- package/dist/prompts/sandbox/drug-repurposing-agent.js +31 -22
- package/dist/prompts/sandbox/multi-omics-integration-agent.d.ts +1 -1
- package/dist/prompts/sandbox/multi-omics-integration-agent.js +9 -10
- package/dist/prompts/sandbox/pkpd-clinical-response-agent.d.ts +1 -1
- package/dist/prompts/sandbox/pkpd-clinical-response-agent.js +8 -7
- package/dist/prompts/sandbox/translational-safety-agent.d.ts +1 -1
- package/dist/prompts/sandbox/translational-safety-agent.js +20 -14
- package/dist/prompts/synthesis-agent.d.ts +1 -1
- package/dist/prompts/synthesis-agent.js +7 -7
- package/dist/providers/errors.d.ts +7 -0
- package/dist/providers/errors.js +88 -3
- package/dist/runtime/pools.d.ts +15 -1
- package/dist/runtime/pools.js +15 -1
- package/dist/sandbox/sandbox-step-translate.d.ts +15 -2
- package/dist/sandbox/sandbox-step-translate.js +17 -2
- package/dist/schemas/workflow-state.d.ts +2 -2
- package/dist/schemas/workflow-state.js +2 -2
- package/dist/state/data-profile.d.ts +53 -1
- package/dist/state/data-profile.js +66 -6
- package/dist/state/index.d.ts +3 -3
- package/dist/state/index.js +2 -2
- package/dist/state/init.js +37 -2
- package/dist/state/runs.d.ts +39 -0
- package/dist/state/runs.js +103 -0
- package/dist/tasks/data-profile-activity.d.ts +87 -0
- package/dist/tasks/data-profile-activity.js +56 -0
- package/dist/tasks/data-profile.js +66 -4
- package/dist/tools/bio/chembl.d.ts +1 -1
- package/dist/tools/bio/chembl.js +51 -54
- package/dist/tools/bio/comptox.js +35 -33
- package/dist/tools/bio/drug-gene-interactions.d.ts +76 -0
- package/dist/tools/bio/drug-gene-interactions.js +175 -0
- package/dist/tools/bio/gene-disease-evidence.d.ts +61 -0
- package/dist/tools/bio/gene-disease-evidence.js +187 -0
- package/dist/tools/bio/gene-preclinical-profile.d.ts +58 -0
- package/dist/tools/bio/gene-preclinical-profile.js +125 -0
- package/dist/tools/bio/index.d.ts +23 -16
- package/dist/tools/bio/index.js +29 -22
- package/dist/tools/bio/keys.d.ts +16 -9
- package/dist/tools/bio/keys.js +22 -13
- package/dist/tools/bio/lookup-annotation.d.ts +48 -0
- package/dist/tools/bio/lookup-annotation.js +173 -0
- package/dist/tools/bio/opentargets.d.ts +7 -11
- package/dist/tools/bio/opentargets.js +24 -35
- package/dist/tools/bio/pubchem.js +6 -4
- package/dist/tools/bio/pubmed.d.ts +15 -7
- package/dist/tools/bio/pubmed.js +79 -20
- package/dist/tools/bio/search-clinical-trials.d.ts +20 -1
- package/dist/tools/bio/search-clinical-trials.js +26 -5
- package/dist/tools/bio/search-faers.js +9 -3
- package/dist/tools/bio/search-gene.d.ts +0 -1
- package/dist/tools/bio/search-gene.js +4 -5
- package/dist/tools/bio/search-geo-datasets.js +10 -4
- package/dist/tools/bio/search-interactions.d.ts +8 -4
- package/dist/tools/bio/search-interactions.js +28 -12
- package/dist/tools/bio/target-safety.d.ts +59 -0
- package/dist/tools/bio/target-safety.js +191 -0
- package/dist/tools/execute-analysis.d.ts +1 -0
- package/dist/tools/execute-analysis.js +5 -5
- package/dist/tools/lib/dgidb-client.d.ts +49 -0
- package/dist/tools/lib/dgidb-client.js +168 -0
- package/dist/tools/lib/disgenet-client.d.ts +56 -0
- package/dist/tools/lib/disgenet-client.js +59 -0
- package/dist/tools/lib/drugbank-client.d.ts +83 -0
- package/dist/tools/lib/drugbank-client.js +95 -0
- package/dist/tools/lib/ensembl-client.d.ts +4 -1
- package/dist/tools/lib/ensembl-client.js +5 -3
- package/dist/tools/lib/gwas-catalog-client.d.ts +60 -0
- package/dist/tools/lib/gwas-catalog-client.js +136 -0
- package/dist/tools/lib/pharmgkb-client.d.ts +22 -0
- package/dist/tools/lib/pharmgkb-client.js +32 -0
- package/dist/tools/lib/pubmed-client.d.ts +57 -4
- package/dist/tools/lib/pubmed-client.js +57 -3
- package/dist/tools/lib/string-client.d.ts +25 -4
- package/dist/tools/lib/string-client.js +22 -5
- package/dist/tools/research/generate-plan.d.ts +16 -9
- package/dist/tools/research/generate-plan.js +405 -85
- package/dist/tools/research/inspect-data-profile.d.ts +31 -6
- package/dist/tools/research/inspect-data-profile.js +23 -7
- package/dist/tools/research/inspect-run.d.ts +33 -15
- package/dist/tools/research/inspect-run.js +155 -66
- package/dist/tools/research/literature-reviewer.js +7 -11
- package/dist/workflows/execute-analysis.d.ts +63 -0
- package/dist/workflows/execute-analysis.js +73 -2
- package/dist/workflows/sandbox-step.js +1 -11
- package/dist/workflows/target-assessment/decisions/index.d.ts +1 -1
- package/dist/workflows/target-assessment/phase4-assemble.d.ts +1 -1
- package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +2 -2
- package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +1 -1
- package/package.json +1 -1
- package/dist/tools/bio/check-safety-panel.d.ts +0 -35
- package/dist/tools/bio/check-safety-panel.js +0 -76
- package/dist/tools/bio/get-impc-ko-profile.d.ts +0 -9
- package/dist/tools/bio/get-impc-ko-profile.js +0 -23
- package/dist/tools/bio/lookup-go-term.d.ts +0 -31
- package/dist/tools/bio/lookup-go-term.js +0 -116
- package/dist/tools/bio/search-bgee-expression.d.ts +0 -10
- package/dist/tools/bio/search-bgee-expression.js +0 -30
- package/dist/tools/bio/search-clinvar.d.ts +0 -13
- package/dist/tools/bio/search-clinvar.js +0 -28
- package/dist/tools/bio/search-dgidb.d.ts +0 -53
- package/dist/tools/bio/search-dgidb.js +0 -213
- package/dist/tools/bio/search-disgenet.d.ts +0 -30
- package/dist/tools/bio/search-disgenet.js +0 -84
- package/dist/tools/bio/search-drugbank.d.ts +0 -38
- package/dist/tools/bio/search-drugbank.js +0 -109
- package/dist/tools/bio/search-gwas-catalog.d.ts +0 -33
- package/dist/tools/bio/search-gwas-catalog.js +0 -172
- package/dist/tools/bio/search-pathway.d.ts +0 -12
- package/dist/tools/bio/search-pathway.js +0 -32
- package/dist/tools/bio/search-pharmgkb.d.ts +0 -17
- package/dist/tools/bio/search-pharmgkb.js +0 -52
|
@@ -40,9 +40,12 @@ its output. Work with the tools you have.
|
|
|
40
40
|
adverse event reports for drugs targeting the pathways/genes in scope.
|
|
41
41
|
Compare observed AE patterns with the molecular findings.
|
|
42
42
|
|
|
43
|
-
4. **Target safety assessment** — use \`
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
4. **Target safety assessment** — use \`target_safety\` to screen targets
|
|
44
|
+
against both the curated secondary-pharmacology panel and the curated
|
|
45
|
+
Open Targets liabilities in one call. Cross-reference with omics
|
|
46
|
+
findings to identify on-target toxicity risks. Absence of a match is
|
|
47
|
+
NOT evidence of safety: the panel is finite and Open Targets carries
|
|
48
|
+
only curated liabilities.
|
|
46
49
|
|
|
47
50
|
5. **In-vitro and in-vivo toxicology** (conditional — see above) —
|
|
48
51
|
\`comptox({dataset:"toxcast"})\` for ToxCast/Tox21 high-throughput bioactivity data
|
|
@@ -56,24 +59,27 @@ its output. Work with the tools you have.
|
|
|
56
59
|
product composition data.
|
|
57
60
|
|
|
58
61
|
6. **CYP and DDI analysis** — assess CYP expression patterns in omics
|
|
59
|
-
data,
|
|
60
|
-
|
|
62
|
+
data, then use \`drug_gene_interactions\` with
|
|
63
|
+
\`sources:["pharmgkb"]\` for the pharmacogenomic annotations (graded
|
|
64
|
+
1A…4, exact symbol match only), and flag potential DDI risks when
|
|
65
|
+
multiple drugs are in scope.
|
|
61
66
|
|
|
62
67
|
7. **CTCAE grading** — when clinical safety lab data is available, apply
|
|
63
68
|
CTCAE v5 grading criteria to lab values and correlate with treatment
|
|
64
69
|
groups.
|
|
65
70
|
|
|
66
71
|
8. **Preclinical KO and tissue-of-action grounding** —
|
|
67
|
-
\`
|
|
68
|
-
|
|
69
|
-
|
|
72
|
+
\`gene_preclinical_profile\` (single human gene symbol) returns both
|
|
73
|
+
halves at once. The knockout half gives the mouse-KO phenotype
|
|
74
|
+
profile, organ systems, sex-dimorphism flag, and pre-weaning
|
|
75
|
+
viability (lethal / subviable / viable, per zygosity) — use it to
|
|
70
76
|
surface loss-of-function safety signals before assuming a target is
|
|
71
|
-
tractable.
|
|
72
|
-
expression
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
retry.
|
|
77
|
+
tractable. The expression half gives cross-species baseline
|
|
78
|
+
expression — use it to check whether a target is normally expressed
|
|
79
|
+
in tissues that drive your safety hypothesis and whether the model
|
|
80
|
+
organism is a credible surrogate; pass \`species\` to widen beyond
|
|
81
|
+
human and mouse. Empty/null fields are valid "no data" outcomes — do
|
|
82
|
+
NOT retry.
|
|
77
83
|
|
|
78
84
|
## Workflow Pattern
|
|
79
85
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const synthesisAgentPrompt = "# Run Synthesizer\n\nYou are a cross-step synthesizer for a completed bioinformatics analysis\nrun. You receive the plan's analytical narrative and a block of per-step\nsummaries (markdown). Your job is to distill them into a concise,\nliterature-grounded **interpretation** \u2014 not a catalog of what each step\nproduced.\n\nYou are task-oriented \u2014 you do NOT interact with the user. You receive\nthe run's context, investigate what you need via your research sub-agent,\nand return a structured synthesis.\n\n## CRITICAL \u2014 Read This First\n\n**You communicate results EXCLUSIVELY by calling tools. This is not optional.**\n\nA response that contains text, prose, JSON, markdown, code blocks, or any\nother non-tool-call content is a **failure** \u2014 the orchestrator does not\nread your text and has no way to use it. The only way a synthesis reaches\nthe user is via `submit_synthesis`. The only way a blocker reaches the\nuser is via `report_blocker`.\n\nEvery session MUST end with exactly one call to `submit_synthesis` or\n`report_blocker`. Do NOT respond with text before any tool call.\n\n## Your Mission\n\nThe reader of your synthesis is a scientist who ALREADY has the step\nsummaries. They do not need you to repeat what each step found \u2014 they\ncan read that themselves. What they need from you is:\n\n1. **Interpretation** \u2014 what do the results mean taken together?\n2. **Conclusions** \u2014 what can we confidently conclude? What is novel vs\n confirmatory? What is actionable from a translational perspective?\n3. **Limitations** \u2014 what caveats apply? Where should the reader be\n skeptical? What confounds exist?\n4. **Evidence** \u2014 key literature references that support, challenge, or\n contextualize the findings.\n\nThink like a senior reviewer writing the Discussion section of a paper,\nnot like a scribe summarizing the Results section.\n\n## Your Tools\n\n### literature_reviewer (research sub-agent \u2014 delegate focused research)\nA task-oriented sub-agent with access to bio-lookup tools (search_gene,\nsearch_pathway, lookup_go_term, search_interactions, pubmed,\nsearch_bgee_expression, get_impc_ko_profile). Delegate to it when you need\nevidence \u2014 you have no bio-lookup tools of your own.\n\nDelegations must be **focused**:\n- ONE well-scoped research brief per call (e.g., \"Investigate FOXM1 and\n MKI67 in the context of proliferation signatures in bulk RNA-seq of\n breast tumor vs normal. Assess novelty and find supporting PMIDs.\")\n- Include the experimental context the sub-agent needs (conditions, modality,\n what the computational finding showed).\n- Do NOT send the whole summary block and ask for \"a literature review\".\n The sub-agent will produce shallow results on diffuse briefs.\n\nTypical usage: 1-3 delegations per run. Multiple small focused briefs\nbeat one giant one. You may delegate again after an initial pass if a\ntheme becomes apparent and needs its own evidence sweep.\n\n### validate_synthesis (non-terminal \u2014 dry run)\nCheck a candidate synthesis without submitting it. Takes the payload in\nANY shape \u2014 a partial, malformed, or wrong-typed draft is reported, not\nrejected \u2014 and returns `{valid, issues}` covering both schema problems\nand the cross-field invariants below. Call it as often as you need: it\nrecords nothing and never ends the session. Use it before every\n`submit_synthesis` so you spend your terminal call on a payload you\nalready know is clean.\n\n### submit_synthesis (terminal \u2014 success path)\nPersist the synthesis. Re-validates the payload; returns either\n`{accepted: true}` (DONE \u2014 stop) or `{accepted: false, issues}` (fix\nspecific fields per issue path and call again, OR switch to\n`report_blocker` if the synthesis cannot be made valid). A rejected\nsubmission leaves an already-accepted synthesis untouched; a later\naccepted submission supersedes the earlier one.\n\n### report_blocker (terminal \u2014 cannot produce a synthesis)\nCall ONLY when the run produced no synthesizable content \u2014 every summary is\nempty, or the summaries are contradictory to the point of incoherence.\n\"No findings worth surfacing\" is NOT a blocker: with non-empty summaries you\ncan always submit a synthesis (overview + conclusions) with an empty\n`findings[]`. Pass a short reason. STOP after calling.\n\n## Canonical Flow\n\n1. Read the analytical narrative + all step summaries end-to-end.\n Identify the 3-5 most important conclusions, any novel or\n contradicted findings, and cross-step themes.\n2. Delegate 1-3 focused research briefs to `literature_reviewer` \u2014\n targeting novel findings, contradictions, and themes that need\n literature grounding.\n3. Draft the synthesis payload with an interpretive focus.\n4. Call `validate_synthesis` on the draft. Fix the cited issue paths\n and re-validate until it comes back `{valid: true}`.\n5. Call `submit_synthesis` with the validated payload. If it is still\n rejected, fix the cited issue paths and call again. If unfixable,\n call `report_blocker`.\n\n## Writing the Synthesis\n\nThe `submit_synthesis` arg schema defines every field, its enum values,\nand its size expectations \u2014 read it, and follow it. This section is the\ncraft the schema cannot carry.\n\n**An empty `findings[]` is a valid submission.** `findings` is SELECTIVE,\nnot required \u2014 a run whose summaries yield nothing individually notable is\nstill synthesized: write the `overview` and `conclusions` from the\nsummaries and submit with `findings: []`. \"Nothing worth a finding\" is a\nreason to submit a lean synthesis, never a reason to `report_blocker`.\n\n**`conclusions` is the \"Discussion\" of the synthesis** \u2014 the part the\nreader will actually want to read. Be opinionated and interpretive, not\ndescriptive. Say where computational predictions agree or disagree with\nclinical evidence, and be honest about the translational distance: is this\ninteresting biology, or a validated target with a measurable biomarker and\na known drug?\n\n**Assigning `preclinical-validation`** \u2014 cite IMPC mouse-KO phenotypes\n(`get_impc_ko_profile`) for loss-of-function consequences and\ncross-species baseline expression (`search_bgee_expression`) for\ntissue-of-action context, when the literature reviewer's report includes\nthem.\n\n**What a good `limitation` looks like** \u2014 it names what it affects and\nwhy it matters:\n- \"Signatures derive from a single cohort (n=57); cross-validation in\n independent NASH datasets was not performed.\"\n- \"CMap-style scoring cannot distinguish targeted pathway reversal from\n non-specific cytotoxic transcriptional disruption.\"\n- \"Biomarker candidates identified computationally; analytical validation\n (assay performance, reproducibility) and clinical validation (outcome\n association in independent cohorts) not performed.\"\n- \"Target druggability not assessed \u2014 pathway importance does not imply\n tractability.\"\n\n## Cross-Field Invariants (reported by validate_synthesis, enforced at submit_synthesis)\n\n- `finding.stepId` must match a stepId present in the input summaries.\n- Every `theme.findings[]` entry must reference a finding that exists\n in `findings[]` (same stepId + title).\n- Every `keyReferences[].pmid` must appear in at least one\n `finding.references[]` entry \u2014 the cross-run list summarizes what's\n already cited per finding.\n- Every `pmid` you write must come from a `literature_reviewer` response.\n Inventing PMIDs is a terminal failure \u2014 the synthesis is discarded on audit.\n\n## Investigation Strategy\n\n**Which findings need delegation?**\n- Findings labeled high-confidence in the summary but potentially novel \u2014\n needs literature check to confirm or challenge novelty.\n- Findings that could map to a known disease or pathway context \u2014 needs\n a PubMed sweep for the gene+condition combination.\n- Candidate themes spanning 2+ steps \u2014 delegate a theme-level brief to\n get references that tie the findings together.\n\n**Which findings do NOT need delegation?**\n- Trivial / expected results (QC passed, dimensions as expected).\n- Technical findings with no biological claim (batch effect detected,\n normalization applied).\n\n**Composing a delegation brief** \u2014 include:\n1. The specific genes/pathways/features to investigate (from the summary).\n2. The experimental context (conditions, modality, organism if known).\n3. What the computational finding showed (one sentence).\n4. What you want back (novelty assessment? supporting PMIDs? pathway\n context? interaction partners?).\n\nExample:\n> \"Investigate SPP1, TREM2, and APOE in the context of microglia\n> activation in Alzheimer's disease single-cell RNA-seq. Finding:\n> these three genes form a co-expression module enriched in\n> disease-associated microglia. Return: novelty assessment, 3-5 key\n> PMIDs supporting or challenging the DAM signature, and any known\n> interactions among the three.\"\n\n## Do NOT\n\n- Respond with plain text (even to explain your reasoning \u2014 nothing is read).\n- Invent PMIDs, citations, or gene facts. Only report what the reviewer returned.\n- Delegate a blanket brief (\"review all the findings\") \u2014 delegate targeted briefs.\n- **Reiterate step results.** The reader has the step summaries. Do not\n repeat gene lists, sample counts, table dimensions, method parameters,\n or per-step results in the synthesis. Refer to findings by their\n biological meaning, not their technical details.\n- **Create one finding per step.** That is a catalog, not a synthesis.\n Cross-reference across steps and surface only what matters.\n- **Write a long overview.** The conclusions field is where interpretation goes.\n- **Include methodology findings.** Gene intersection sizes, normalization\n choices, platform coverage \u2014 these are technical housekeeping, not\n synthesis findings.\n- **Include expected or confirmatory findings.** Canonical pathway\n enrichments, QC metrics, and well-established biology do not warrant a\n finding \u2014 unless the confirmation itself is noteworthy (e.g., a new\n dataset independently confirming a contested result).\n- **Inflate translational relevance.** A differentially expressed gene is\n NOT automatically a therapeutic target. A survival association is NOT\n automatically a predictive biomarker (it may be prognostic only) \u2014\n predictive requires evidence of treatment x marker interaction,\n prognostic only requires association with outcome.\n- Force findings into themes. If findings don't converge, return fewer (or zero) themes.\n- **Call `report_blocker` because no finding seems worth surfacing.** With\n non-empty summaries, submit a synthesis with an empty `findings[]` instead \u2014\n a blocker is only for empty or incoherent summaries.\n- Call `submit_synthesis` before delegating at least once when findings need\n literature grounding. (A run with only technical/QC findings may skip delegation\n and submit directly, but state that clearly in the overview.)\n- Reference stepIds that aren't in the summaries, or theme-findings that\n aren't in `findings[]`.\n- Keep calling `submit_synthesis` after `accepted: true`. It is not an\n error \u2014 a later accepted submission simply supersedes the earlier one \u2014\n but the synthesis is already recorded, so stop immediately.\n- Use `submit_synthesis` as a validation probe. Probing is what\n `validate_synthesis` is for; submit only what you already validated.\n- Submit a placeholder or stub payload to \"see what happens\". A degenerate\n payload that passes the schema is recorded as the run's result.\n- End the session without a terminal tool call. That is a failure mode.\n";
|
|
1
|
+
export declare const synthesisAgentPrompt = "# Run Synthesizer\n\nYou are a cross-step synthesizer for a completed bioinformatics analysis\nrun. You receive the plan's analytical narrative and a block of per-step\nsummaries (markdown). Your job is to distill them into a concise,\nliterature-grounded **interpretation** \u2014 not a catalog of what each step\nproduced.\n\nYou are task-oriented \u2014 you do NOT interact with the user. You receive\nthe run's context, investigate what you need via your research sub-agent,\nand return a structured synthesis.\n\n## CRITICAL \u2014 Read This First\n\n**You communicate results EXCLUSIVELY by calling tools. This is not optional.**\n\nA response that contains text, prose, JSON, markdown, code blocks, or any\nother non-tool-call content is a **failure** \u2014 the orchestrator does not\nread your text and has no way to use it. The only way a synthesis reaches\nthe user is via `submit_synthesis`. The only way a blocker reaches the\nuser is via `report_blocker`.\n\nEvery session MUST end with exactly one call to `submit_synthesis` or\n`report_blocker`. Do NOT respond with text before any tool call.\n\n## Your Mission\n\nThe reader of your synthesis is a scientist who ALREADY has the step\nsummaries. They do not need you to repeat what each step found \u2014 they\ncan read that themselves. What they need from you is:\n\n1. **Interpretation** \u2014 what do the results mean taken together?\n2. **Conclusions** \u2014 what can we confidently conclude? What is novel vs\n confirmatory? What is actionable from a translational perspective?\n3. **Limitations** \u2014 what caveats apply? Where should the reader be\n skeptical? What confounds exist?\n4. **Evidence** \u2014 key literature references that support, challenge, or\n contextualize the findings.\n\nThink like a senior reviewer writing the Discussion section of a paper,\nnot like a scribe summarizing the Results section.\n\n## Your Tools\n\n### literature_reviewer (research sub-agent \u2014 delegate focused research)\nA task-oriented sub-agent with access to bio-lookup tools (search_gene,\nlookup_annotation, search_interactions, pubmed,\ngene_preclinical_profile). Delegate to it when you need\nevidence \u2014 you have no bio-lookup tools of your own.\n\nDelegations must be **focused**:\n- ONE well-scoped research brief per call (e.g., \"Investigate FOXM1 and\n MKI67 in the context of proliferation signatures in bulk RNA-seq of\n breast tumor vs normal. Assess novelty and find supporting PMIDs.\")\n- Include the experimental context the sub-agent needs (conditions, modality,\n what the computational finding showed).\n- Do NOT send the whole summary block and ask for \"a literature review\".\n The sub-agent will produce shallow results on diffuse briefs.\n\nTypical usage: 1-3 delegations per run. Multiple small focused briefs\nbeat one giant one. You may delegate again after an initial pass if a\ntheme becomes apparent and needs its own evidence sweep.\n\n### validate_synthesis (non-terminal \u2014 dry run)\nCheck a candidate synthesis without submitting it. Takes the payload in\nANY shape \u2014 a partial, malformed, or wrong-typed draft is reported, not\nrejected \u2014 and returns `{valid, issues}` covering both schema problems\nand the cross-field invariants below. Call it as often as you need: it\nrecords nothing and never ends the session. Use it before every\n`submit_synthesis` so you spend your terminal call on a payload you\nalready know is clean.\n\n### submit_synthesis (terminal \u2014 success path)\nPersist the synthesis. Re-validates the payload; returns either\n`{accepted: true}` (DONE \u2014 stop) or `{accepted: false, issues}` (fix\nspecific fields per issue path and call again, OR switch to\n`report_blocker` if the synthesis cannot be made valid). A rejected\nsubmission leaves an already-accepted synthesis untouched; a later\naccepted submission supersedes the earlier one.\n\n### report_blocker (terminal \u2014 cannot produce a synthesis)\nCall ONLY when the run produced no synthesizable content \u2014 every summary is\nempty, or the summaries are contradictory to the point of incoherence.\n\"No findings worth surfacing\" is NOT a blocker: with non-empty summaries you\ncan always submit a synthesis (overview + conclusions) with an empty\n`findings[]`. Pass a short reason. STOP after calling.\n\n## Canonical Flow\n\n1. Read the analytical narrative + all step summaries end-to-end.\n Identify the 3-5 most important conclusions, any novel or\n contradicted findings, and cross-step themes.\n2. Delegate 1-3 focused research briefs to `literature_reviewer` \u2014\n targeting novel findings, contradictions, and themes that need\n literature grounding.\n3. Draft the synthesis payload with an interpretive focus.\n4. Call `validate_synthesis` on the draft. Fix the cited issue paths\n and re-validate until it comes back `{valid: true}`.\n5. Call `submit_synthesis` with the validated payload. If it is still\n rejected, fix the cited issue paths and call again. If unfixable,\n call `report_blocker`.\n\n## Writing the Synthesis\n\nThe `submit_synthesis` arg schema defines every field, its enum values,\nand its size expectations \u2014 read it, and follow it. This section is the\ncraft the schema cannot carry.\n\n**An empty `findings[]` is a valid submission.** `findings` is SELECTIVE,\nnot required \u2014 a run whose summaries yield nothing individually notable is\nstill synthesized: write the `overview` and `conclusions` from the\nsummaries and submit with `findings: []`. \"Nothing worth a finding\" is a\nreason to submit a lean synthesis, never a reason to `report_blocker`.\n\n**`conclusions` is the \"Discussion\" of the synthesis** \u2014 the part the\nreader will actually want to read. Be opinionated and interpretive, not\ndescriptive. Say where computational predictions agree or disagree with\nclinical evidence, and be honest about the translational distance: is this\ninteresting biology, or a validated target with a measurable biomarker and\na known drug?\n\n**Assigning `preclinical-validation`** \u2014 cite IMPC mouse-KO phenotypes for\nloss-of-function consequences and cross-species baseline expression for\ntissue-of-action context (both come back from\n`gene_preclinical_profile`), when the literature reviewer's report\nincludes them.\n\n**What a good `limitation` looks like** \u2014 it names what it affects and\nwhy it matters:\n- \"Signatures derive from a single cohort (n=57); cross-validation in\n independent NASH datasets was not performed.\"\n- \"CMap-style scoring cannot distinguish targeted pathway reversal from\n non-specific cytotoxic transcriptional disruption.\"\n- \"Biomarker candidates identified computationally; analytical validation\n (assay performance, reproducibility) and clinical validation (outcome\n association in independent cohorts) not performed.\"\n- \"Target druggability not assessed \u2014 pathway importance does not imply\n tractability.\"\n\n## Cross-Field Invariants (reported by validate_synthesis, enforced at submit_synthesis)\n\n- `finding.stepId` must match a stepId present in the input summaries.\n- Every `theme.findings[]` entry must reference a finding that exists\n in `findings[]` (same stepId + title).\n- Every `keyReferences[].pmid` must appear in at least one\n `finding.references[]` entry \u2014 the cross-run list summarizes what's\n already cited per finding.\n- Every `pmid` you write must come from a `literature_reviewer` response.\n Inventing PMIDs is a terminal failure \u2014 the synthesis is discarded on audit.\n\n## Investigation Strategy\n\n**Which findings need delegation?**\n- Findings labeled high-confidence in the summary but potentially novel \u2014\n needs literature check to confirm or challenge novelty.\n- Findings that could map to a known disease or pathway context \u2014 needs\n a PubMed sweep for the gene+condition combination.\n- Candidate themes spanning 2+ steps \u2014 delegate a theme-level brief to\n get references that tie the findings together.\n\n**Which findings do NOT need delegation?**\n- Trivial / expected results (QC passed, dimensions as expected).\n- Technical findings with no biological claim (batch effect detected,\n normalization applied).\n\n**Composing a delegation brief** \u2014 include:\n1. The specific genes/pathways/features to investigate (from the summary).\n2. The experimental context (conditions, modality, organism if known).\n3. What the computational finding showed (one sentence).\n4. What you want back (novelty assessment? supporting PMIDs? pathway\n context? interaction partners?).\n\nExample:\n> \"Investigate SPP1, TREM2, and APOE in the context of microglia\n> activation in Alzheimer's disease single-cell RNA-seq. Finding:\n> these three genes form a co-expression module enriched in\n> disease-associated microglia. Return: novelty assessment, 3-5 key\n> PMIDs supporting or challenging the DAM signature, and any known\n> interactions among the three.\"\n\n## Do NOT\n\n- Respond with plain text (even to explain your reasoning \u2014 nothing is read).\n- Invent PMIDs, citations, or gene facts. Only report what the reviewer returned.\n- Delegate a blanket brief (\"review all the findings\") \u2014 delegate targeted briefs.\n- **Reiterate step results.** The reader has the step summaries. Do not\n repeat gene lists, sample counts, table dimensions, method parameters,\n or per-step results in the synthesis. Refer to findings by their\n biological meaning, not their technical details.\n- **Create one finding per step.** That is a catalog, not a synthesis.\n Cross-reference across steps and surface only what matters.\n- **Write a long overview.** The conclusions field is where interpretation goes.\n- **Include methodology findings.** Gene intersection sizes, normalization\n choices, platform coverage \u2014 these are technical housekeeping, not\n synthesis findings.\n- **Include expected or confirmatory findings.** Canonical pathway\n enrichments, QC metrics, and well-established biology do not warrant a\n finding \u2014 unless the confirmation itself is noteworthy (e.g., a new\n dataset independently confirming a contested result).\n- **Inflate translational relevance.** A differentially expressed gene is\n NOT automatically a therapeutic target. A survival association is NOT\n automatically a predictive biomarker (it may be prognostic only) \u2014\n predictive requires evidence of treatment x marker interaction,\n prognostic only requires association with outcome.\n- Force findings into themes. If findings don't converge, return fewer (or zero) themes.\n- **Call `report_blocker` because no finding seems worth surfacing.** With\n non-empty summaries, submit a synthesis with an empty `findings[]` instead \u2014\n a blocker is only for empty or incoherent summaries.\n- Call `submit_synthesis` before delegating at least once when findings need\n literature grounding. (A run with only technical/QC findings may skip delegation\n and submit directly, but state that clearly in the overview.)\n- Reference stepIds that aren't in the summaries, or theme-findings that\n aren't in `findings[]`.\n- Keep calling `submit_synthesis` after `accepted: true`. It is not an\n error \u2014 a later accepted submission simply supersedes the earlier one \u2014\n but the synthesis is already recorded, so stop immediately.\n- Use `submit_synthesis` as a validation probe. Probing is what\n `validate_synthesis` is for; submit only what you already validated.\n- Submit a placeholder or stub payload to \"see what happens\". A degenerate\n payload that passes the schema is recorded as the run's result.\n- End the session without a terminal tool call. That is a failure mode.\n";
|
|
@@ -44,8 +44,8 @@ not like a scribe summarizing the Results section.
|
|
|
44
44
|
|
|
45
45
|
### literature_reviewer (research sub-agent — delegate focused research)
|
|
46
46
|
A task-oriented sub-agent with access to bio-lookup tools (search_gene,
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
lookup_annotation, search_interactions, pubmed,
|
|
48
|
+
gene_preclinical_profile). Delegate to it when you need
|
|
49
49
|
evidence — you have no bio-lookup tools of your own.
|
|
50
50
|
|
|
51
51
|
Delegations must be **focused**:
|
|
@@ -119,11 +119,11 @@ clinical evidence, and be honest about the translational distance: is this
|
|
|
119
119
|
interesting biology, or a validated target with a measurable biomarker and
|
|
120
120
|
a known drug?
|
|
121
121
|
|
|
122
|
-
**Assigning \`preclinical-validation\`** — cite IMPC mouse-KO phenotypes
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
them.
|
|
122
|
+
**Assigning \`preclinical-validation\`** — cite IMPC mouse-KO phenotypes for
|
|
123
|
+
loss-of-function consequences and cross-species baseline expression for
|
|
124
|
+
tissue-of-action context (both come back from
|
|
125
|
+
\`gene_preclinical_profile\`), when the literature reviewer's report
|
|
126
|
+
includes them.
|
|
127
127
|
|
|
128
128
|
**What a good \`limitation\` looks like** — it names what it affects and
|
|
129
129
|
why it matters:
|
|
@@ -77,6 +77,13 @@ export interface ProviderErrorClassification {
|
|
|
77
77
|
*/
|
|
78
78
|
readonly retryable: boolean;
|
|
79
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Walk the `cause` chain for the first numeric HTTP status. The single status
|
|
82
|
+
* walker: `classifyProviderError` keys on what this returns, so a caller asking
|
|
83
|
+
* "did the provider layer have a status to classify on?" must ask it here rather
|
|
84
|
+
* than re-implement the traversal.
|
|
85
|
+
*/
|
|
86
|
+
export declare function extractStatus(err: unknown): number | undefined;
|
|
80
87
|
/**
|
|
81
88
|
* Classify a provider failure by origin.
|
|
82
89
|
*
|
package/dist/providers/errors.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* re-authenticating fixes it — which is a different remedy from every other
|
|
16
16
|
* non-retryable 4xx, so it is a different kind.
|
|
17
17
|
*/
|
|
18
|
+
import { redactSecrets } from "../input-sanitization.js";
|
|
18
19
|
/**
|
|
19
20
|
* Structural guard for an already-constructed `ProviderError` value. A
|
|
20
21
|
* `ProviderError` is a plain object, not an `Error`, so it cannot be
|
|
@@ -71,7 +72,67 @@ export function toProviderError(e, workload) {
|
|
|
71
72
|
cause: e,
|
|
72
73
|
};
|
|
73
74
|
}
|
|
74
|
-
|
|
75
|
+
// The `provider` arm composes like its three siblings rather than forwarding
|
|
76
|
+
// the SDK message verbatim. It has to: when a 4xx body does not parse against
|
|
77
|
+
// the configured provider's error schema, the AI SDK falls back to
|
|
78
|
+
// `response.statusText`, so the "detail" is a bare HTTP reason phrase —
|
|
79
|
+
// `"Bad Request"` — that names neither the call nor the cause, while the
|
|
80
|
+
// status, the workload, and the body the SDK captured are all still on the
|
|
81
|
+
// `cause` chain right here.
|
|
82
|
+
//
|
|
83
|
+
// Composed strictly AFTER `classifyProviderError` has returned, so message
|
|
84
|
+
// text can never become a classification input: the harness-providers spec
|
|
85
|
+
// fixes classification on the HTTP status alone.
|
|
86
|
+
const status = extractStatus(e);
|
|
87
|
+
const lead = status === undefined ? `Provider call failed for ${workload}` : `Provider call failed for ${workload} (HTTP ${status})`;
|
|
88
|
+
// A transport that carries no reason phrase (an HTTP/2 hop yields
|
|
89
|
+
// `statusText === ""`) leaves `detail` empty; appending it regardless would
|
|
90
|
+
// trail a bare `": "` on an otherwise complete message.
|
|
91
|
+
const reason = detail.trim();
|
|
92
|
+
const diagnosed = reason ? `${lead}: ${reason}` : lead;
|
|
93
|
+
const body = extractResponseBody(e);
|
|
94
|
+
return {
|
|
95
|
+
type: "provider",
|
|
96
|
+
retryable,
|
|
97
|
+
// The excerpt trails deliberately — workload and status lead, so a
|
|
98
|
+
// downstream truncation (`profileFailureReason` cuts the line at 200)
|
|
99
|
+
// eats the least diagnostic content first.
|
|
100
|
+
message: body ? `${diagnosed} — response body: ${excerptResponseBody(body)}` : diagnosed,
|
|
101
|
+
cause: e,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Max characters of a captured provider response body carried in a message.
|
|
106
|
+
*
|
|
107
|
+
* Derived, not picked: the tightest downstream consumer (`profileFailureReason`,
|
|
108
|
+
* `tasks/data-profile.ts`) truncates the whole line at 200, and the lead this
|
|
109
|
+
* excerpt trails — a workload label plus `HTTP <status>` plus a reason phrase —
|
|
110
|
+
* runs to roughly 80. 120 is therefore the largest round value that cannot evict
|
|
111
|
+
* that lead.
|
|
112
|
+
*
|
|
113
|
+
* Deliberately NOT shared with that consumer's `PROFILE_ERROR_MAX_LEN`: that one
|
|
114
|
+
* bounds a `varchar` ledger column, this one bounds a message many non-ledger
|
|
115
|
+
* consumers read, and coupling them would make either one unmovable.
|
|
116
|
+
*/
|
|
117
|
+
const PROVIDER_BODY_EXCERPT_MAX_LEN = 120;
|
|
118
|
+
/**
|
|
119
|
+
* Single-lined, secret-redacted, length-capped rendering of a captured response body. Bounding happens
|
|
120
|
+
* here rather than at a consumer because most `ProviderError` consumers truncate nothing at all, and the
|
|
121
|
+
* one that does would let an unbounded body crowd out the workload and status ahead of it.
|
|
122
|
+
*
|
|
123
|
+
* The body is an UNTRUSTED upstream/proxy payload, and this excerpt lands verbatim in the ledger's
|
|
124
|
+
* `data_profile_error` (surfaced to the UI) and the logs, whereas the composed lead does not — so
|
|
125
|
+
* secrets are redacted here and nowhere else on the message. Redaction runs on the FULL body before
|
|
126
|
+
* truncation: a token straddling the cut would otherwise be sliced into an unmatchable fragment that
|
|
127
|
+
* leaks. `redactSecrets` matches only prefixed formats (API keys, JWTs, bearer tokens, connection
|
|
128
|
+
* strings) and is safe on biological data, so a legitimate error body passes through intact.
|
|
129
|
+
*/
|
|
130
|
+
function excerptResponseBody(body) {
|
|
131
|
+
const singleLine = redactSecrets(body).replace(/\s+/g, " ").trim();
|
|
132
|
+
// Slice by CODE POINT, not UTF-16 unit: a raw byte body can carry astral characters, and a plain
|
|
133
|
+
// `.slice` could bisect a surrogate pair into a lone half that renders as U+FFFD.
|
|
134
|
+
const chars = [...singleLine];
|
|
135
|
+
return chars.length > PROVIDER_BODY_EXCERPT_MAX_LEN ? chars.slice(0, PROVIDER_BODY_EXCERPT_MAX_LEN - 1).join("") + "…" : singleLine;
|
|
75
136
|
}
|
|
76
137
|
/** Max links walked on the `cause` chain looking for a structured status. */
|
|
77
138
|
const MAX_CAUSE_HOPS = 5;
|
|
@@ -88,8 +149,13 @@ const CONNECTION_ERROR_CODES = new Set([
|
|
|
88
149
|
"UND_ERR_SOCKET",
|
|
89
150
|
]);
|
|
90
151
|
const CONNECTION_ERROR_PATTERN = /fetch failed|socket hang up|network error|connection (?:refused|reset|error|closed)|econnrefused|enotfound|etimedout|terminated/i;
|
|
91
|
-
/**
|
|
92
|
-
|
|
152
|
+
/**
|
|
153
|
+
* Walk the `cause` chain for the first numeric HTTP status. The single status
|
|
154
|
+
* walker: `classifyProviderError` keys on what this returns, so a caller asking
|
|
155
|
+
* "did the provider layer have a status to classify on?" must ask it here rather
|
|
156
|
+
* than re-implement the traversal.
|
|
157
|
+
*/
|
|
158
|
+
export function extractStatus(err) {
|
|
93
159
|
let cursor = err;
|
|
94
160
|
for (let i = 0; i < MAX_CAUSE_HOPS && cursor; i++) {
|
|
95
161
|
const e = cursor;
|
|
@@ -101,6 +167,25 @@ function extractStatus(err) {
|
|
|
101
167
|
}
|
|
102
168
|
return undefined;
|
|
103
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* Walk the `cause` chain for the first captured provider response body.
|
|
172
|
+
*
|
|
173
|
+
* This is what recovers a proxy-minted or otherwise non-conforming error body:
|
|
174
|
+
* the AI SDK keeps the raw bytes on `APICallError.responseBody` even when its
|
|
175
|
+
* own parse against the provider's error schema fails and its `message` degrades
|
|
176
|
+
* to the HTTP reason phrase. A blank body is treated as absent — an empty
|
|
177
|
+
* `— response body:` segment says less than no segment at all.
|
|
178
|
+
*/
|
|
179
|
+
function extractResponseBody(err) {
|
|
180
|
+
let cursor = err;
|
|
181
|
+
for (let i = 0; i < MAX_CAUSE_HOPS && cursor; i++) {
|
|
182
|
+
const e = cursor;
|
|
183
|
+
if (typeof e.responseBody === "string" && e.responseBody.trim() !== "")
|
|
184
|
+
return e.responseBody;
|
|
185
|
+
cursor = e.cause;
|
|
186
|
+
}
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
104
189
|
/** Does any link of the `cause` chain look like a transport-level failure? */
|
|
105
190
|
function looksLikeConnectionError(err) {
|
|
106
191
|
let cursor = err;
|
package/dist/runtime/pools.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Per-pod Postgres pool sizing — the single source of
|
|
2
|
+
* Per-pod Postgres pool sizing and acquisition bounds — the single source of
|
|
3
|
+
* truth.
|
|
3
4
|
*
|
|
4
5
|
* Cortex runs two pools per pod against the same cluster (see the postgres-storage-backend spec): the
|
|
5
6
|
* application pool (`lib/storage.ts::createPool()`) and the DBOS system-DB
|
|
@@ -21,5 +22,18 @@ export declare const DEFAULT_APP_POOL_SIZE = 12;
|
|
|
21
22
|
* overlap during DBOS recovery, replication / monitoring users.
|
|
22
23
|
*/
|
|
23
24
|
export declare const SAFETY_MARGIN = 5;
|
|
25
|
+
/**
|
|
26
|
+
* How long a caller may wait for a connection from the app pool before the
|
|
27
|
+
* acquisition fails.
|
|
28
|
+
*
|
|
29
|
+
* The driver waits forever by default, which leaves a caller on a saturated
|
|
30
|
+
* pool with no failure mode at all — only an indefinite hang, indistinguishable
|
|
31
|
+
* from work still in progress and therefore able to stall the caller's own
|
|
32
|
+
* in-flight guard permanently. A bound turns that silent stall into a surfaced
|
|
33
|
+
* error the caller can degrade on. 30s is long enough that a healthy pool under
|
|
34
|
+
* burst never trips it, and short enough that a wedged caller surfaces well
|
|
35
|
+
* inside a human's patience.
|
|
36
|
+
*/
|
|
37
|
+
export declare const APP_POOL_ACQUIRE_TIMEOUT_MS = 30000;
|
|
24
38
|
/** Resolve the app pool size from the optional `DB_POOL_MAX` override. */
|
|
25
39
|
export declare function resolveAppPoolSize(dbPoolMax: string | undefined): number;
|
package/dist/runtime/pools.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Per-pod Postgres pool sizing — the single source of
|
|
2
|
+
* Per-pod Postgres pool sizing and acquisition bounds — the single source of
|
|
3
|
+
* truth.
|
|
3
4
|
*
|
|
4
5
|
* Cortex runs two pools per pod against the same cluster (see the postgres-storage-backend spec): the
|
|
5
6
|
* application pool (`lib/storage.ts::createPool()`) and the DBOS system-DB
|
|
@@ -21,6 +22,19 @@ export const DEFAULT_APP_POOL_SIZE = 12;
|
|
|
21
22
|
* overlap during DBOS recovery, replication / monitoring users.
|
|
22
23
|
*/
|
|
23
24
|
export const SAFETY_MARGIN = 5;
|
|
25
|
+
/**
|
|
26
|
+
* How long a caller may wait for a connection from the app pool before the
|
|
27
|
+
* acquisition fails.
|
|
28
|
+
*
|
|
29
|
+
* The driver waits forever by default, which leaves a caller on a saturated
|
|
30
|
+
* pool with no failure mode at all — only an indefinite hang, indistinguishable
|
|
31
|
+
* from work still in progress and therefore able to stall the caller's own
|
|
32
|
+
* in-flight guard permanently. A bound turns that silent stall into a surfaced
|
|
33
|
+
* error the caller can degrade on. 30s is long enough that a healthy pool under
|
|
34
|
+
* burst never trips it, and short enough that a wedged caller surfaces well
|
|
35
|
+
* inside a human's patience.
|
|
36
|
+
*/
|
|
37
|
+
export const APP_POOL_ACQUIRE_TIMEOUT_MS = 30_000;
|
|
24
38
|
/** Resolve the app pool size from the optional `DB_POOL_MAX` override. */
|
|
25
39
|
export function resolveAppPoolSize(dbPoolMax) {
|
|
26
40
|
if (dbPoolMax === undefined || dbPoolMax === "")
|
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure translation between the agent-loop / sandbox event stream and the
|
|
3
3
|
* typed per-step parts the frontend renders. No DBOS, no per-run state — just the
|
|
4
|
-
* label mapping, the file-tree delta
|
|
5
|
-
*
|
|
4
|
+
* reconciliation-id construction, the label mapping, the file-tree delta
|
|
5
|
+
* extraction/fold, and the chat-data-part narrowing a workflow body composes
|
|
6
|
+
* into its emitters.
|
|
6
7
|
*/
|
|
7
8
|
import type { ChatDataPart, EmitFn } from "../loop/types.js";
|
|
9
|
+
/**
|
|
10
|
+
* Stable reconciliation id for a step's reconciling parts
|
|
11
|
+
* (`data-step-activity`, `data-step-file-tree`). One id per (runId, stepId)
|
|
12
|
+
* so the run-stream fold collapses every phase transition latest-wins onto
|
|
13
|
+
* a single frame. The client keys these by `stepId`, but the server fold
|
|
14
|
+
* keys by part `id`, so the id MUST be unique per step within the run.
|
|
15
|
+
*
|
|
16
|
+
* Every producer of these parts mints its ids here: the id is a
|
|
17
|
+
* reconciliation contract, and the fold collapses correctly only while all
|
|
18
|
+
* producers construct it identically.
|
|
19
|
+
*/
|
|
20
|
+
export declare function stepPartId(kind: string, runId: string, stepId: string): string;
|
|
8
21
|
export declare function activityForTool(name: string, input?: unknown): string;
|
|
9
22
|
/** On-change working-tree delta the sandbox executor posts per exec (paths
|
|
10
23
|
* only; the executor skips directories). */
|
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure translation between the agent-loop / sandbox event stream and the
|
|
3
3
|
* typed per-step parts the frontend renders. No DBOS, no per-run state — just the
|
|
4
|
-
* label mapping, the file-tree delta
|
|
5
|
-
*
|
|
4
|
+
* reconciliation-id construction, the label mapping, the file-tree delta
|
|
5
|
+
* extraction/fold, and the chat-data-part narrowing a workflow body composes
|
|
6
|
+
* into its emitters.
|
|
6
7
|
*/
|
|
8
|
+
/**
|
|
9
|
+
* Stable reconciliation id for a step's reconciling parts
|
|
10
|
+
* (`data-step-activity`, `data-step-file-tree`). One id per (runId, stepId)
|
|
11
|
+
* so the run-stream fold collapses every phase transition latest-wins onto
|
|
12
|
+
* a single frame. The client keys these by `stepId`, but the server fold
|
|
13
|
+
* keys by part `id`, so the id MUST be unique per step within the run.
|
|
14
|
+
*
|
|
15
|
+
* Every producer of these parts mints its ids here: the id is a
|
|
16
|
+
* reconciliation contract, and the fold collapses correctly only while all
|
|
17
|
+
* producers construct it identically.
|
|
18
|
+
*/
|
|
19
|
+
export function stepPartId(kind, runId, stepId) {
|
|
20
|
+
return `${kind}-${runId}-${stepId}`;
|
|
21
|
+
}
|
|
7
22
|
/** Friendly live-activity label per sandbox tool id. Falls back to the raw
|
|
8
23
|
* tool name so a newly added tool still reads sensibly. */
|
|
9
24
|
const TOOL_ACTIVITY = {
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
/**
|
|
9
9
|
* A step of an analysis plan. `PlanStepSchema` (schemas/plan-schemas.ts) narrows
|
|
10
|
-
* this into the planner's output contract and emits it as the `
|
|
11
|
-
*
|
|
10
|
+
* this into the planner's output contract and emits it as the `submit_plan`
|
|
11
|
+
* arg schema — so each field's meaning belongs in `.describe()`,
|
|
12
12
|
* not in a comment the model never sees.
|
|
13
13
|
*/
|
|
14
14
|
export declare const AnalysisStepSchema: z.ZodObject<{
|
|
@@ -8,8 +8,8 @@ import { z } from "zod";
|
|
|
8
8
|
// ── Plan structures ─────────────────────────────────────────────────
|
|
9
9
|
/**
|
|
10
10
|
* A step of an analysis plan. `PlanStepSchema` (schemas/plan-schemas.ts) narrows
|
|
11
|
-
* this into the planner's output contract and emits it as the `
|
|
12
|
-
*
|
|
11
|
+
* this into the planner's output contract and emits it as the `submit_plan`
|
|
12
|
+
* arg schema — so each field's meaning belongs in `.describe()`,
|
|
13
13
|
* not in a comment the model never sees.
|
|
14
14
|
*/
|
|
15
15
|
export const AnalysisStepSchema = z.object({
|
|
@@ -101,6 +101,13 @@ export interface DataProfileStatus {
|
|
|
101
101
|
startedAt: string | null;
|
|
102
102
|
completedAt: string | null;
|
|
103
103
|
result: DataProfileResult | null;
|
|
104
|
+
/**
|
|
105
|
+
* The DBOS workflow id of the profile attempt that owns this row — the durable
|
|
106
|
+
* event stream a consumer subscribes to for this profile's activity. `null` means
|
|
107
|
+
* this profile's stream is not addressable (see
|
|
108
|
+
* {@link recordDataProfileWorkflowId}).
|
|
109
|
+
*/
|
|
110
|
+
workflowId: string | null;
|
|
104
111
|
seedInputFileIds: string[] | null;
|
|
105
112
|
}
|
|
106
113
|
/**
|
|
@@ -117,14 +124,59 @@ export interface DataProfileStatus {
|
|
|
117
124
|
* unseeded — neither is an error, both stay in the ok channel.
|
|
118
125
|
*/
|
|
119
126
|
export declare function tryStartDataProfile(pool: Querier, analysisId: string): ResultAsync<boolean, DbError>;
|
|
120
|
-
/**
|
|
127
|
+
/**
|
|
128
|
+
* Claim a `failed` row back into `running` (the deliberate-retry route). Carries {@link SEEDED}.
|
|
129
|
+
*
|
|
130
|
+
* Clears `data_profile_workflow_id`, because the claim IS the moment a new attempt takes the row
|
|
131
|
+
* and the prior attempt's workflow is finished. Left in place it would leave the row `running`
|
|
132
|
+
* while naming a stream that has already drained, so a consumer would subscribe and observe
|
|
133
|
+
* nothing without knowing why; NULL says "not addressable yet", which is exactly true until the
|
|
134
|
+
* new body records its own id (see {@link recordDataProfileWorkflowId}).
|
|
135
|
+
*/
|
|
121
136
|
export declare function tryRetryDataProfile(pool: Querier, analysisId: string): ResultAsync<boolean, DbError>;
|
|
122
137
|
/**
|
|
123
138
|
* Claim a `completed` row back into `running` (the re-profile route). `data_profile_result`
|
|
124
139
|
* is deliberately preserved so a consumer can keep serving the prior profile while the new
|
|
125
140
|
* one runs. Carries {@link SEEDED}.
|
|
141
|
+
*
|
|
142
|
+
* `data_profile_workflow_id` is NOT preserved alongside it, and the asymmetry is the point: the
|
|
143
|
+
* result is CONTENT that stays valid until replaced, while the id is a POINTER to a live stream
|
|
144
|
+
* whose workflow is now finished. Keeping it would leave the row `running` while naming a drained
|
|
145
|
+
* stream — see {@link tryRetryDataProfile} for the same reasoning.
|
|
126
146
|
*/
|
|
127
147
|
export declare function tryRerunDataProfile(pool: Querier, analysisId: string): ResultAsync<boolean, DbError>;
|
|
148
|
+
/**
|
|
149
|
+
* Record the DBOS workflow id of the attempt that owns a claimed-`running` row, so a
|
|
150
|
+
* consumer resolves which durable event stream carries this profile's activity from the
|
|
151
|
+
* ledger row alone — no durability-engine query, no id reconstructed from the workflow-id
|
|
152
|
+
* string format. The workflow BODY calls this, never the trigger: the claim CAS runs
|
|
153
|
+
* before the workflow id is minted, so only the body can report the id of the attempt
|
|
154
|
+
* that actually started.
|
|
155
|
+
*
|
|
156
|
+
* `AND data_profile_status = 'running'` buys exactly one thing: a write that lands late
|
|
157
|
+
* cannot stamp a row that has already settled, which would otherwise point a consumer at
|
|
158
|
+
* a workflow for a profile that is already finished. What it does NOT buy is
|
|
159
|
+
* disambiguation between two attempts that each believe they are the running one — the
|
|
160
|
+
* stale-expiry claim admits precisely that, since a row whose `data_profile_started_at`
|
|
161
|
+
* has aged past the timeout can be claimed by a second attempt while the first body is
|
|
162
|
+
* still alive but has not completed its first step, and that first step then overwrites
|
|
163
|
+
* the second's id.
|
|
164
|
+
*
|
|
165
|
+
* That residue is deliberately left open rather than closed. Its worst outcome is already
|
|
166
|
+
* a specified-normal state: a consumer subscribed to a superseded workflow reads a stream
|
|
167
|
+
* that has already drained, so it observes no activity — indistinguishable from "running,
|
|
168
|
+
* nothing reported yet". A missing activity line for one profile is acceptable; a wrong
|
|
169
|
+
* one would not be. And closing it costs more than it buys: either the three claim
|
|
170
|
+
* functions widen their `Result<boolean>` returns to carry the id — their callers branch
|
|
171
|
+
* on truthiness (`if (!retried)`), so an object return makes every such branch
|
|
172
|
+
* always-taken, a silent behaviour change in the embedder's recovery path — or the
|
|
173
|
+
* embedder mints the workflow ids, putting a harness-internal construction in a
|
|
174
|
+
* consumer's hands.
|
|
175
|
+
*
|
|
176
|
+
* `ok(true)` when this call stamped the row; `ok(false)` when the CAS refused it — a
|
|
177
|
+
* normal in-band outcome, not an error.
|
|
178
|
+
*/
|
|
179
|
+
export declare function recordDataProfileWorkflowId(pool: Querier, analysisId: string, workflowId: string): ResultAsync<boolean, DbError>;
|
|
128
180
|
/**
|
|
129
181
|
* Stamp a claimed-`running` row `completed` and record its result — a terminal
|
|
130
182
|
* write CAS'd on `data_profile_status = 'running'`.
|
|
@@ -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
|
-
/**
|
|
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,
|
|
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
|
});
|
package/dist/state/index.d.ts
CHANGED
|
@@ -12,15 +12,15 @@ export { upsertAnalysis, resolveAnalysisBilling, suspendAnalysis, resumeAnalysis
|
|
|
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
14
|
export { insertRun, reserveRunById, RunDedupCollisionError, RunIdentityCollisionError, 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, RunReservation } from "./runs.js";
|
|
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
21
|
export { insertPlan, upsertPlan, loadPlan } from "./plans.js";
|
|
22
22
|
export type { InsertPlanInput, UpsertPlanInput } from "./plans.js";
|
|
23
|
-
export { tryStartDataProfile, tryRetryDataProfile, tryRerunDataProfile, completeDataProfile, failDataProfile, expireStaleDataProfile, reconcileOrphanedDataProfile, clearDataProfile, loadDataProfileStatus, loadSeedInputFileIds, } from "./data-profile.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";
|