@inflexa-ai/harness 0.28.1 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/dist/agents/conversation-agent.js +7 -4
  2. package/dist/agents/report-session-agent.js +1 -1
  3. package/dist/agents/sandbox/data-profiler.js +0 -1
  4. package/dist/agents/sandbox/immune-profiling-agent.js +3 -1
  5. package/dist/agents/sandbox/network-agent.js +0 -1
  6. package/dist/agents/sandbox/types.d.ts +1 -1
  7. package/dist/agents/sandbox/types.js +1 -1
  8. package/dist/app/data-profile-orientation.d.ts +52 -27
  9. package/dist/app/data-profile-orientation.js +331 -92
  10. package/dist/app/data-profile-view.d.ts +70 -0
  11. package/dist/app/data-profile-view.js +123 -0
  12. package/dist/app/synthesize-run.js +1 -1
  13. package/dist/config/resource-limits.d.ts +1 -1
  14. package/dist/config/resource-limits.js +1 -1
  15. package/dist/contracts/data-profile.d.ts +276 -25
  16. package/dist/contracts/index.d.ts +3 -1
  17. package/dist/contracts/index.js +1 -0
  18. package/dist/contracts/profile-vocabulary.d.ts +103 -0
  19. package/dist/contracts/profile-vocabulary.js +449 -0
  20. package/dist/contracts/schemas/chat-parts.d.ts +12 -12
  21. package/dist/contracts/severity.d.ts +2 -2
  22. package/dist/contracts/target-dossier.d.ts +91 -91
  23. package/dist/data/safety-panel-schema.d.ts +4 -4
  24. package/dist/execution/input-signature.d.ts +16 -3
  25. package/dist/execution/input-signature.js +0 -0
  26. package/dist/execution/post-step-pipeline.js +1 -1
  27. package/dist/execution/staged-input.d.ts +4 -4
  28. package/dist/index.d.ts +7 -2
  29. package/dist/index.js +14 -0
  30. package/dist/input-scan/clustering.d.ts +63 -0
  31. package/dist/input-scan/clustering.js +208 -0
  32. package/dist/input-scan/companions.d.ts +30 -0
  33. package/dist/input-scan/companions.js +138 -0
  34. package/dist/input-scan/decoder/container_readout.py +171 -0
  35. package/dist/input-scan/detect-sets.d.ts +24 -0
  36. package/dist/input-scan/detect-sets.js +279 -0
  37. package/dist/input-scan/enrich.d.ts +62 -19
  38. package/dist/input-scan/enrich.js +132 -187
  39. package/dist/input-scan/formats.d.ts +13 -5
  40. package/dist/input-scan/formats.js +13 -5
  41. package/dist/input-scan/index.d.ts +10 -4
  42. package/dist/input-scan/index.js +7 -4
  43. package/dist/input-scan/markers.d.ts +21 -0
  44. package/dist/input-scan/markers.js +89 -0
  45. package/dist/input-scan/menu.d.ts +127 -0
  46. package/dist/input-scan/menu.js +0 -0
  47. package/dist/input-scan/quarantine.d.ts +17 -0
  48. package/dist/input-scan/quarantine.js +69 -0
  49. package/dist/input-scan/readout-budget.d.ts +32 -0
  50. package/dist/input-scan/readout-budget.js +61 -0
  51. package/dist/input-scan/readout.d.ts +33 -0
  52. package/dist/input-scan/readout.js +336 -0
  53. package/dist/input-scan/scan.d.ts +5 -4
  54. package/dist/input-scan/scan.js +5 -5
  55. package/dist/input-scan/set-types.d.ts +209 -0
  56. package/dist/input-scan/set-types.js +15 -0
  57. package/dist/input-scan/templates.d.ts +93 -0
  58. package/dist/input-scan/templates.js +369 -0
  59. package/dist/input-scan/tokens.d.ts +94 -0
  60. package/dist/input-scan/tokens.js +267 -0
  61. package/dist/input-scan/tuning.d.ts +55 -0
  62. package/dist/input-scan/tuning.js +55 -0
  63. package/dist/input-scan/types.d.ts +2 -2
  64. package/dist/loop/run-agent.d.ts +13 -1
  65. package/dist/loop/run-agent.js +2 -1
  66. package/dist/prompts/SOUL.d.ts +2 -2
  67. package/dist/prompts/SOUL.js +17 -1
  68. package/dist/prompts/conversation.d.ts +1 -1
  69. package/dist/prompts/conversation.js +45 -71
  70. package/dist/prompts/planner.js +57 -10
  71. package/dist/prompts/report-session.d.ts +1 -1
  72. package/dist/prompts/report-session.js +3 -2
  73. package/dist/prompts/sandbox/bulk-transcriptomics-agent.d.ts +1 -1
  74. package/dist/prompts/sandbox/bulk-transcriptomics-agent.js +3 -1
  75. package/dist/prompts/sandbox/data-profiler.d.ts +10 -1
  76. package/dist/prompts/sandbox/data-profiler.js +197 -53
  77. package/dist/prompts/sandbox/dna-methylation-agent.d.ts +1 -1
  78. package/dist/prompts/sandbox/dna-methylation-agent.js +18 -12
  79. package/dist/prompts/sandbox/immune-profiling-agent.d.ts +1 -1
  80. package/dist/prompts/sandbox/immune-profiling-agent.js +6 -2
  81. package/dist/prompts/sandbox/metabolomics-agent.d.ts +1 -1
  82. package/dist/prompts/sandbox/metabolomics-agent.js +8 -4
  83. package/dist/prompts/sandbox/microbiome-agent.d.ts +1 -1
  84. package/dist/prompts/sandbox/microbiome-agent.js +12 -6
  85. package/dist/prompts/sandbox/network-agent.d.ts +1 -1
  86. package/dist/prompts/sandbox/network-agent.js +9 -6
  87. package/dist/prompts/sandbox/spatial-omics-agent.d.ts +1 -1
  88. package/dist/prompts/sandbox/spatial-omics-agent.js +4 -2
  89. package/dist/prompts/sandbox-standards.d.ts +1 -1
  90. package/dist/prompts/sandbox-standards.js +10 -7
  91. package/dist/providers/ai-sdk.js +34 -1
  92. package/dist/providers/reasoning.d.ts +65 -0
  93. package/dist/providers/reasoning.js +81 -0
  94. package/dist/providers/types.d.ts +14 -0
  95. package/dist/schemas/data-profile-schemas.d.ts +947 -84
  96. package/dist/schemas/data-profile-schemas.js +300 -115
  97. package/dist/schemas/run-synthesis.d.ts +4 -4
  98. package/dist/state/data-profile.d.ts +1 -1
  99. package/dist/state/index.d.ts +1 -1
  100. package/dist/state/vector-store.d.ts +13 -0
  101. package/dist/state/vector-store.js +8 -0
  102. package/dist/tasks/data-profile-absorb.d.ts +78 -0
  103. package/dist/tasks/data-profile-absorb.js +285 -0
  104. package/dist/tasks/data-profile-index.d.ts +34 -25
  105. package/dist/tasks/data-profile-index.js +160 -59
  106. package/dist/tasks/data-profile-resolve.d.ts +72 -0
  107. package/dist/tasks/data-profile-resolve.js +639 -0
  108. package/dist/tasks/data-profile.d.ts +16 -5
  109. package/dist/tasks/data-profile.js +270 -131
  110. package/dist/tools/bio/chembl.d.ts +1 -1
  111. package/dist/tools/bio/chembl.js +15 -10
  112. package/dist/tools/bio/comptox.js +5 -1
  113. package/dist/tools/bio/drug-gene-interactions.js +5 -2
  114. package/dist/tools/bio/gene-disease-evidence.d.ts +19 -9
  115. package/dist/tools/bio/gene-disease-evidence.js +78 -22
  116. package/dist/tools/bio/gene-preclinical-profile.d.ts +33 -13
  117. package/dist/tools/bio/gene-preclinical-profile.js +63 -22
  118. package/dist/tools/bio/lookup-annotation.d.ts +1 -0
  119. package/dist/tools/bio/lookup-annotation.js +28 -10
  120. package/dist/tools/bio/opentargets.d.ts +10 -2
  121. package/dist/tools/bio/opentargets.js +42 -13
  122. package/dist/tools/bio/pubchem.js +5 -2
  123. package/dist/tools/bio/pubmed.js +9 -3
  124. package/dist/tools/bio/search-clinical-trials.d.ts +25 -25
  125. package/dist/tools/bio/search-clinical-trials.js +56 -18
  126. package/dist/tools/bio/search-faers.d.ts +26 -6
  127. package/dist/tools/bio/search-faers.js +70 -18
  128. package/dist/tools/bio/search-gene.d.ts +19 -3
  129. package/dist/tools/bio/search-gene.js +69 -12
  130. package/dist/tools/bio/search-geo-datasets.js +4 -1
  131. package/dist/tools/bio/search-interactions.js +6 -2
  132. package/dist/tools/bio/target-safety.d.ts +2 -2
  133. package/dist/tools/bio/target-safety.js +3 -2
  134. package/dist/tools/lib/cbioportal-client.js +25 -6
  135. package/dist/tools/lib/chembl-client.d.ts +3 -1
  136. package/dist/tools/lib/chembl-client.js +6 -1
  137. package/dist/tools/lib/gwas-catalog-client.d.ts +7 -6
  138. package/dist/tools/lib/gwas-catalog-client.js +18 -6
  139. package/dist/tools/lib/openfda-client.d.ts +7 -5
  140. package/dist/tools/lib/openfda-client.js +17 -9
  141. package/dist/tools/lib/opentargets-client.d.ts +20 -0
  142. package/dist/tools/lib/opentargets-client.js +45 -0
  143. package/dist/tools/lib/pathway-client.d.ts +7 -0
  144. package/dist/tools/lib/pathway-client.js +68 -1
  145. package/dist/tools/research/context7-docs.js +3 -3
  146. package/dist/tools/research/generate-analogy-report.js +2 -2
  147. package/dist/tools/research/generate-plan.d.ts +3 -0
  148. package/dist/tools/research/generate-plan.js +64 -5
  149. package/dist/tools/research/inspect-data-profile.d.ts +48 -22
  150. package/dist/tools/research/inspect-data-profile.js +73 -54
  151. package/dist/tools/research/resolve-citation.js +8 -3
  152. package/dist/tools/research/search-arxiv.js +8 -3
  153. package/dist/tools/research/search-semantic-scholar.js +10 -4
  154. package/dist/tools/workspace/scan-inputs.d.ts +4 -4
  155. package/dist/tools/workspace/scan-inputs.js +13 -14
  156. package/dist/tools/workspace/workspace-search.d.ts +11 -2
  157. package/dist/tools/workspace/workspace-search.js +44 -16
  158. package/dist/workflows/sandbox-step.d.ts +6 -0
  159. package/dist/workflows/sandbox-step.js +31 -14
  160. package/dist/workflows/target-assessment/assemblers/safety.d.ts +1 -1
  161. package/dist/workflows/target-assessment/phase4-assemble.d.ts +38 -38
  162. package/dist/workflows/target-assessment/schemas.d.ts +12 -12
  163. package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +6 -6
  164. package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +6 -6
  165. package/dist/workflows/target-assessment/synthesis/index.d.ts +12 -12
  166. package/package.json +5 -3
  167. package/dist/input-scan/coverage.d.ts +0 -22
  168. package/dist/input-scan/coverage.js +0 -85
@@ -35,6 +35,12 @@ import { defineTool } from "../define-tool.js";
35
35
  import { createListAvailablePackagesTool } from "../sandbox/list-available-packages.js";
36
36
  import { createListAvailableRefsTool } from "../sandbox/list-available-refs.js";
37
37
  import { createReportBlockerToolFor } from "../sandbox/report-blocker.js";
38
+ import { searchGeoDatasetsTool } from "../bio/search-geo-datasets.js";
39
+ import { createNcbiTools } from "../bio/keys.js";
40
+ import { queryDocsTool, resolveLibraryIdTool } from "./context7-docs.js";
41
+ import { searchArxivTool } from "./search-arxiv.js";
42
+ import { createSearchGithubReposTool } from "./search-github-repos.js";
43
+ import { createSearchSemanticScholarTool } from "./search-semantic-scholar.js";
38
44
  import { DATA_PROFILE_ORIENTATION_MAX_CHARS, buildDataProfileOrientation } from "../../app/data-profile-orientation.js";
39
45
  import { DEFAULT_SANDBOX_MAX_STEPS } from "../../config/resource-limits.js";
40
46
  import { plannerPrompt } from "../../prompts/planner.js";
@@ -49,10 +55,17 @@ import { insertPlan, loadDataProfileStatus, loadPlan } from "../../state/index.j
49
55
  /** Sub-agent identity for the planner — provenance only. */
50
56
  const PLANNER_AGENT_ID = "planner";
51
57
  /**
52
- * Budget for the planner's internal loop: one draft/submit attempt plus
53
- * correction retries and headroom.
58
+ * Budget for the planner's internal loop: the research phase, one draft/submit
59
+ * attempt, correction retries, and headroom.
60
+ *
61
+ * The number is deliberately far above what a plan costs. A well-grounded plan
62
+ * still costs two or three calls, because the prompt gates the research phase on
63
+ * what the seed does not already answer. What the ceiling must never do is stop
64
+ * a planner that is searching correctly. A capped run takes the forced wrap-up
65
+ * path and submits the plan that it had, not the plan that it was building.
66
+ * Thus the wall-clock guard, not this number, is what bounds the worst case.
54
67
  */
55
- const PLANNER_MAX_ITERATIONS = 13;
68
+ const PLANNER_MAX_ITERATIONS = 200;
56
69
  /** Wall-clock guard for a single plan-generation invocation. */
57
70
  const PLAN_TIMEOUT_MS = 600_000;
58
71
  // ── Diagnostic bounds ───────────────────────────────────────────────
@@ -71,7 +84,7 @@ const MAX_LOGGED_ISSUE_CHARS = 240;
71
84
  const MAX_LOGGED_REJECTIONS = 6;
72
85
  /** Excerpt of the planner's last words — the one artifact that explains a run ending on prose. */
73
86
  const MAX_LOGGED_PROSE_CHARS = 800;
74
- /** Tool-call trace length. The planner's whole budget is ~16 turns, so this rarely truncates. */
87
+ /** Tool-call trace length. A long research phase passes it, and `toolCallsTruncated` marks the record when it does. */
75
88
  const MAX_LOGGED_TOOL_CALLS = 48;
76
89
  /** Trim to `max`, marking the cut so a truncated value never reads as a complete one. */
77
90
  function bounded(value, max) {
@@ -629,6 +642,44 @@ const OUTCOME_LEVEL = {
629
642
  invalid_parent_plan: "warn",
630
643
  parent_plan_load_failed: "error",
631
644
  };
645
+ /**
646
+ * The search tools of the planner — built one time, not per invocation.
647
+ *
648
+ * The set answers the two questions that a seed cannot. "What did a study of
649
+ * this kind do before?" reaches the public dataset records and the literature.
650
+ * "What does this package actually give me?" reaches the developer docs. The
651
+ * planner reads its reference and package inventories from the seed, thus the
652
+ * docs pair is what turns a package name into a real function.
653
+ *
654
+ * A tool here never writes and never computes. Thus the worst outcome of a
655
+ * needless call is latency, and the prompt is what bounds that.
656
+ */
657
+ function buildPlannerSearchTools(deps) {
658
+ const bioKeys = deps.bioKeys;
659
+ const ncbi = createNcbiTools(bioKeys);
660
+ return [
661
+ // Comparable public studies — the design, the platform, and the sample
662
+ // count that a plan for the same assay must answer to.
663
+ searchGeoDatasetsTool,
664
+ // Prior methods, in three corpora that do not overlap: the biomedical
665
+ // literature, the statistics and machine-learning preprints, and the
666
+ // cross-field index that ranks a method paper best.
667
+ ncbi.pubmed,
668
+ searchArxivTool,
669
+ createSearchSemanticScholarTool({ ...(bioKeys.semanticScholar === undefined ? {} : { apiKey: bioKeys.semanticScholar }) }),
670
+ // The same approach as running code — a pipeline that someone published.
671
+ createSearchGithubReposTool({ githubToken: bioKeys.github }),
672
+ // The API of a staged package, so a step names a function that exists.
673
+ resolveLibraryIdTool,
674
+ queryDocsTool,
675
+ // The environment itself. The seed already carries a rendered census of
676
+ // both stores. These two tools are for the narrow second look: one
677
+ // collection of the reference store, or one package name that a step is
678
+ // about to import.
679
+ createListAvailableRefsTool(deps.refStorePath === undefined ? {} : { refStorePath: deps.refStorePath }),
680
+ createListAvailablePackagesTool(deps.packagesFile === undefined ? {} : { packagesFile: deps.packagesFile }),
681
+ ];
682
+ }
632
683
  /** Build the `generate_plan` tool bound to its provider and pool. */
633
684
  export function createGeneratePlanTool(deps) {
634
685
  const baseLogger = (deps.logger ?? createNoopLogger()).named("generate-plan");
@@ -806,11 +857,19 @@ export function createGeneratePlanTool(deps) {
806
857
  parentPlanId: input.parentPlanId ?? null,
807
858
  };
808
859
  const innerTools = buildInnerTools(holder, trace, persistCtx, deps.pool, deps.resourcePolicy, logger);
860
+ // Built here rather than at construction: a `describeCall` hook reads no
861
+ // dep, thus the tool must stay constructible from an empty bag. The tool
862
+ // definitions are identical across invocations, thus the request prefix
863
+ // that the cache keys on does not move.
864
+ const searchTools = buildPlannerSearchTools(deps);
809
865
  const planner = {
810
866
  id: PLANNER_AGENT_ID,
811
867
  systemPrompt: composeSystemPrompt(plannerInstructions(deps.resourcePolicy)),
812
868
  model: deps.conversation.model,
813
- tools: innerTools.terminal,
869
+ // The search tools come first, and the terminal tools come last.
870
+ // The order is the order of the prompt, and it is stable across
871
+ // every invocation, thus the cached request prefix holds.
872
+ tools: [...searchTools, ...innerTools.terminal],
814
873
  maxIterations: PLANNER_MAX_ITERATIONS,
815
874
  };
816
875
  // Merge the outer abort signal with a wall-clock timeout — either
@@ -8,13 +8,16 @@
8
8
  * **There is no data-profile file.** The profiler's `runs/data-profile/` scratch tree
9
9
  * is deleted when profiling completes; the profile's only durable home is the
10
10
  * `cortex_analysis_state` row this tool reads. That makes this the single authoritative
11
- * source for dataset facts (organism, domain, design, per-file type/format/dimensions),
12
- * and it is why the record is served in full rather than summarized: an agent that
13
- * cannot pull a fact here has no fallback but to re-derive it from the raw bytes.
11
+ * source for dataset facts — the identity fields, the resolved groups with their slots,
12
+ * the dimensions with their observations, and the partition census — and it is why the
13
+ * record is served in full rather than summarized: an agent that cannot pull a fact here
14
+ * has no fallback but to re-derive it from the raw bytes.
14
15
  *
15
- * Bounded by construction. Like `inspect_run`, the per-file scope is explicitly
16
- * paged and always reports `total` and `hasMore`: an elided tail is a fact the
17
- * model can see and act on, never a silent truncation.
16
+ * Bounded by construction. Like `inspect_run`, the member scope is explicitly paged and
17
+ * always reports `total` and `hasMore`: an elided tail is a fact the model can see and
18
+ * act on, never a silent truncation. The annotated-member count and the dataset's file
19
+ * count are reported as two distinct figures, because a profile annotating eight members
20
+ * of several thousand files is correct and a single number would read as a tiny dataset.
18
21
  *
19
22
  * Lifecycle states are data variants, not errors (the `defineTool` contract): a missing,
20
23
  * in-flight, failed, or stale profile is an ordinary, expected outcome the model must be
@@ -50,45 +53,68 @@ interface OverviewOutput extends ProfileEnvelope {
50
53
  readonly cellType?: string | null;
51
54
  readonly condition?: string | null;
52
55
  readonly experimentalDesign?: string;
53
- readonly qualityAssessment?: DataProfileResult["qualityAssessment"];
56
+ /** Dataset-wide findings the agent wrote, from whichever field the snapshot carries them in. */
57
+ readonly caveats?: string[];
54
58
  readonly accessions?: string[];
55
- /** How many per-file records `scope:"files"` would page through — NOT the dataset's size. */
59
+ /** How many member records `scope:"files"` would page through — NOT the dataset's size. */
56
60
  readonly describedFileCount: number;
57
61
  /**
58
62
  * How many files the dataset holds, or `null` when the snapshot cannot say (one
59
- * written before kinds and coverage existed). Reported separately from
60
- * `describedFileCount` because a profile describing eight files of several thousand
61
- * is correct under this capability, and a single count would read as a dataset of
62
- * eight files.
63
+ * written before any structure record existed). Reported separately from
64
+ * `describedFileCount` because a profile describing eight members of several thousand
65
+ * files is correct under this capability, and a single count would read as a dataset
66
+ * of eight files.
63
67
  */
64
68
  readonly datasetFileCount: number | null;
65
- /** How many kinds `scope:"kinds"` would return; `null` on a pre-kinds snapshot. */
66
- readonly kindCount: number | null;
69
+ /** How many groups `scope:"groups"` would return; `null` on a snapshot that carries no structure. */
70
+ readonly groupCount: number | null;
67
71
  /** Present when the two counts differ — says where the rest of the dataset is described. */
68
72
  readonly structureNote?: string;
73
+ /**
74
+ * The census, on a snapshot resolved under the partition: kept files, per-group
75
+ * counts, the unclassified sweep, and the quarantine with its reasons.
76
+ */
77
+ readonly partition?: DataProfileResult["partition"];
69
78
  readonly coverage?: DataProfileResult["coverage"];
70
79
  }
71
80
  /**
72
- * The dataset's structure. A snapshot predating kinds reports `available: false`
73
- * rather than an empty list: "this profile predates the structure" and "this dataset
74
- * has no structure" are different facts, and only one of them is true.
81
+ * The dataset's structure. A snapshot carrying none reports `available: false` rather
82
+ * than an empty list: "this profile predates the structure" and "this dataset has no
83
+ * structure" are different facts, and only one of them is true.
84
+ *
85
+ * A snapshot written under the previous model is served through `kinds`/`axes`, labelled
86
+ * — the structure exists, and an agent must not be told the dataset has none.
75
87
  */
76
- interface KindsOutput extends ProfileEnvelope {
77
- readonly scope: "kinds";
88
+ interface GroupsOutput extends ProfileEnvelope {
89
+ readonly scope: "groups";
78
90
  readonly available: boolean;
91
+ /** Authored under the previous model — `kinds`/`axes` rather than resolved groups. */
92
+ readonly legacy?: boolean;
93
+ readonly groups?: DataProfileResult["groups"];
94
+ readonly dimensions?: DataProfileResult["dimensions"];
95
+ readonly probes?: DataProfileResult["probes"];
96
+ readonly partition?: DataProfileResult["partition"];
97
+ readonly recipe?: DataProfileResult["recipe"];
79
98
  readonly kinds?: DataProfileResult["kinds"];
80
99
  readonly axes?: DataProfileResult["axes"];
81
100
  readonly coverage?: DataProfileResult["coverage"];
82
101
  readonly message?: string;
83
102
  }
103
+ /**
104
+ * The members the profile describes individually — annotated members on a resolved
105
+ * snapshot, the notable-file records on an older one. Never the dataset: `datasetFileCount`
106
+ * on the overview is what says how big that is.
107
+ */
84
108
  interface FilesOutput extends ProfileEnvelope {
85
109
  readonly scope: "files";
86
110
  readonly page: number;
87
111
  readonly pageSize: number;
88
- /** Every per-file record the profile holds — not just this page. */
112
+ /** Every member record the profile holds — not just this page. */
89
113
  readonly total: number;
90
114
  /** True when records remain past this page. Truncation is always stated, never silent. */
91
115
  readonly hasMore: boolean;
116
+ /** How many files the dataset holds, so a page of 20 never reads as the whole of it. */
117
+ readonly datasetFileCount: number | null;
92
118
  readonly files: DataProfileFile[];
93
119
  }
94
120
  /**
@@ -133,9 +159,9 @@ interface FailedOutput {
133
159
  readonly failedAt: string | null;
134
160
  readonly message: string;
135
161
  }
136
- export type InspectDataProfileOutput = OverviewOutput | KindsOutput | FilesOutput | AbsentOutput | PendingOutput | FailedOutput;
162
+ export type InspectDataProfileOutput = OverviewOutput | GroupsOutput | FilesOutput | AbsentOutput | PendingOutput | FailedOutput;
137
163
  export declare function createInspectDataProfileTool(pool: Pool): import("../define-tool.js").Tool<{
138
- scope?: "overview" | "files" | "kinds" | undefined;
164
+ scope?: "overview" | "groups" | "files" | undefined;
139
165
  page?: number | undefined;
140
166
  pageSize?: number | undefined;
141
167
  }, InspectDataProfileOutput>;
@@ -8,13 +8,16 @@
8
8
  * **There is no data-profile file.** The profiler's `runs/data-profile/` scratch tree
9
9
  * is deleted when profiling completes; the profile's only durable home is the
10
10
  * `cortex_analysis_state` row this tool reads. That makes this the single authoritative
11
- * source for dataset facts (organism, domain, design, per-file type/format/dimensions),
12
- * and it is why the record is served in full rather than summarized: an agent that
13
- * cannot pull a fact here has no fallback but to re-derive it from the raw bytes.
11
+ * source for dataset facts — the identity fields, the resolved groups with their slots,
12
+ * the dimensions with their observations, and the partition census — and it is why the
13
+ * record is served in full rather than summarized: an agent that cannot pull a fact here
14
+ * has no fallback but to re-derive it from the raw bytes.
14
15
  *
15
- * Bounded by construction. Like `inspect_run`, the per-file scope is explicitly
16
- * paged and always reports `total` and `hasMore`: an elided tail is a fact the
17
- * model can see and act on, never a silent truncation.
16
+ * Bounded by construction. Like `inspect_run`, the member scope is explicitly paged and
17
+ * always reports `total` and `hasMore`: an elided tail is a fact the model can see and
18
+ * act on, never a silent truncation. The annotated-member count and the dataset's file
19
+ * count are reported as two distinct figures, because a profile annotating eight members
20
+ * of several thousand files is correct and a single number would read as a tiny dataset.
18
21
  *
19
22
  * Lifecycle states are data variants, not errors (the `defineTool` contract): a missing,
20
23
  * in-flight, failed, or stale profile is an ordinary, expected outcome the model must be
@@ -28,6 +31,7 @@
28
31
  */
29
32
  import { ok } from "neverthrow";
30
33
  import { z } from "zod";
34
+ import { profileCaveats, profileDatasetFileCount, profileFileRecords, profileGroups } from "../../app/data-profile-view.js";
31
35
  import { scopeResource } from "../../auth/types.js";
32
36
  import { unwrapOrThrow } from "../../lib/result.js";
33
37
  import { loadDataProfileStatus } from "../../state/index.js";
@@ -37,31 +41,19 @@ import { defineTool } from "../define-tool.js";
37
41
  const DEFAULT_PAGE_SIZE = 20;
38
42
  /** Ceiling on `pageSize` — a page is a context-window budget, not a dump. */
39
43
  const MAX_PAGE_SIZE = 100;
40
- /**
41
- * How many files the dataset holds, as far as the snapshot can establish it.
42
- *
43
- * Coverage counts the scanned tree, so it is the authority where present. Summed kind
44
- * counts are the fallback. A snapshot with neither returns `null` rather than the
45
- * described-file count, which would be a dataset size the row cannot support.
46
- */
47
- function datasetFileCount(result) {
48
- if (result.coverage)
49
- return result.coverage.total;
50
- if (result.kinds && result.kinds.length > 0)
51
- return result.kinds.reduce((sum, kind) => sum + kind.count, 0);
52
- return null;
53
- }
54
44
  /** Serve each per-file record with a path that resolves in the frame of the caller. */
55
45
  function rootFilePaths(files, analysisId) {
56
46
  return files.map((file) => ({ ...file, path: toAnalysisRootPath(analysisId, file.path) }));
57
47
  }
58
48
  /**
59
- * Serve each kind with a glob that resolves in the frame of the caller.
49
+ * Serve each group with a display pattern that resolves in the frame of the caller.
60
50
  *
61
- * A `pathPattern` is a path with a wildcard segment, thus it takes the same
62
- * projection as a file path. The wildcard characters survive it: the projection
63
- * only re-roots the pattern.
51
+ * A pattern is a path with varying segments, thus it takes the same projection as a file
52
+ * path. Its placeholders survive it: the projection only re-roots the pattern.
64
53
  */
54
+ function rootGroupPatterns(groups, analysisId) {
55
+ return groups.map((group) => ({ ...group, displayPattern: toAnalysisRootPath(analysisId, group.displayPattern) }));
56
+ }
65
57
  function rootKindPatterns(kinds, analysisId) {
66
58
  return kinds.map((kind) => ({ ...kind, pathPattern: toAnalysisRootPath(analysisId, kind.pathPattern) }));
67
59
  }
@@ -103,20 +95,21 @@ export function createInspectDataProfileTool(pool) {
103
95
  id: "inspect_data_profile",
104
96
  description: "Read this analysis's data profile — the AUTHORITATIVE record of what the input dataset is: " +
105
97
  "organism and taxon id, scientific domain and subtype, tissue, cell type, condition, experimental design, " +
106
- "dataset-wide quality concerns and strengths, public accessions, the KINDS of file the dataset is made of " +
107
- "and what varies across them, and per-file data type, format, row/column dimensions, tags and warnings for " +
108
- "the files described individually. " +
98
+ "dataset-wide caveats, public accessions, the GROUPS of file the dataset is made of, the DIMENSIONS that vary " +
99
+ "across it with the evidence behind each, and the file census (kept, unclassified, quarantined). " +
109
100
  "There is NO data-profile file in the workspace — this tool is the only way to read it. Do not search for one, " +
110
101
  "and do not rediscover these facts by listing or reading the raw input files. " +
111
102
  "Call it before you reason about the data (planning, writing analysis code, interpreting results). " +
112
- "scope:'overview' (the default) returns the dataset-level orientation, how many files are described individually, " +
113
- "and how many files the dataset holds — those two differ on purpose, because a large dataset is described by its " +
114
- "kinds rather than file by file. " +
115
- "scope:'kinds' returns those kinds with their counts, path patterns, and axes — this is where a dataset of thousands " +
116
- "of files is described. A profile taken before kinds existed reports the scope unavailable. " +
117
- "scope:'files' returns the individually described files, paged: page (1-based, default 1) and pageSize (default 20, max 100), " +
103
+ "scope:'overview' (the default) returns the dataset-level orientation plus the partition accounting — how many " +
104
+ "files were kept, how many landed in no group, how many were quarantined — and how many members are described " +
105
+ "individually. That last figure and the dataset's file count differ on purpose: a large dataset is described by " +
106
+ "its groups, not file by file. " +
107
+ "scope:'groups' returns the groups with their derived counts, display patterns, and slots, plus the dimensions " +
108
+ "with their observations, reconciliations, and probe outcomes — this is where a dataset of thousands of files is " +
109
+ "described. A profile written under the previous model returns its kinds and axes, labelled as such. " +
110
+ "scope:'files' returns the individually annotated members, paged: page (1-based, default 1) and pageSize (default 20, max 100), " +
118
111
  "always with the true total and hasMore, so you can see exactly what you have not read yet. " +
119
- "For the paths of files no record describes, list the workspace tree. " +
112
+ "For the paths of files no record describes, use scope:'groups' for the structure and list the workspace tree for the paths. " +
120
113
  "The state field says what you got: 'ready'; 'stale' (a profile is returned but may not describe the current " +
121
114
  "inputs — staleReason says why); 'pending' (profiling is still running); 'failed'; or 'absent' (never profiled, " +
122
115
  "or the analysis has no input files). " +
@@ -126,19 +119,19 @@ export function createInspectDataProfileTool(pool) {
126
119
  "needs re-running rather than diagnosing the data itself.",
127
120
  inputSchema: z.object({
128
121
  scope: z
129
- .enum(["overview", "kinds", "files"])
122
+ .enum(["overview", "groups", "files"])
130
123
  .optional()
131
- .describe("'overview' (default): dataset-level facts, described-file count, and dataset file count. " +
132
- "'kinds': the sets the dataset is made of and what varies across them. " +
133
- "'files': paged records for the individually described files."),
134
- page: z.number().int().min(1).optional().describe("1-based page of per-file records. Only used when scope is 'files'. Default 1."),
124
+ .describe("'overview' (default): dataset-level facts, the partition accounting, the annotated-member count, and the dataset file count. " +
125
+ "'groups': the groups the dataset is made of, with their slots, and the dimensions that vary across it. " +
126
+ "'files': paged records for the individually annotated members."),
127
+ page: z.number().int().min(1).optional().describe("1-based page of member records. Only used when scope is 'files'. Default 1."),
135
128
  pageSize: z
136
129
  .number()
137
130
  .int()
138
131
  .min(1)
139
132
  .max(MAX_PAGE_SIZE)
140
133
  .optional()
141
- .describe(`Per-file records per page. Only used when scope is 'files'. Default ${DEFAULT_PAGE_SIZE}, max ${MAX_PAGE_SIZE}.`),
134
+ .describe(`Member records per page. Only used when scope is 'files'. Default ${DEFAULT_PAGE_SIZE}, max ${MAX_PAGE_SIZE}.`),
142
135
  }),
143
136
  // `execute` defaults both fields internally, and the hook applies the same
144
137
  // defaults. Without them the ordinary call, which names no field at all,
@@ -160,8 +153,8 @@ export function createInspectDataProfileTool(pool) {
160
153
  if (status.status === "failed") {
161
154
  // A failure that DID leave a prior profile on the row never reaches
162
155
  // here — it is served as `stale`, where `stalenessReasons` names the
163
- // failed re-profile and compares the input sets through the single
164
- // shared predicate. This branch is the case with nothing to compare.
156
+ // failed re-profile. This branch is the case with no prior profile to
157
+ // serve at all.
165
158
  return ok({
166
159
  state: "failed",
167
160
  error: status.error,
@@ -188,14 +181,16 @@ export function createInspectDataProfileTool(pool) {
188
181
  ? { state: "stale", staleReason: reasons.join("; "), profiledAt: result.profiledAt }
189
182
  : { state: "ready", profiledAt: result.profiledAt };
190
183
  const scope = input.scope ?? "overview";
184
+ const describedFiles = profileFileRecords(result);
185
+ const groups = profileGroups(result);
191
186
  if (scope === "overview") {
192
- const dataset = datasetFileCount(result);
193
- const described = result.files.length;
187
+ const dataset = profileDatasetFileCount(result);
188
+ const described = describedFiles.length;
194
189
  const structureNote = dataset === null
195
190
  ? "This profile predates the dataset-structure record, so only the individually described files are known; " +
196
191
  "list the workspace tree for the rest."
197
192
  : dataset !== described
198
- ? `${described} of ${dataset} files are described individually. Call scope:'kinds' for what the rest are, ` +
193
+ ? `${described} of ${dataset} files are described individually. Call scope:'groups' for what the rest are, ` +
199
194
  "and list the workspace tree for their paths."
200
195
  : undefined;
201
196
  return ok({
@@ -209,40 +204,63 @@ export function createInspectDataProfileTool(pool) {
209
204
  cellType: result.cellType,
210
205
  condition: result.condition,
211
206
  experimentalDesign: result.experimentalDesign,
212
- qualityAssessment: result.qualityAssessment,
207
+ caveats: profileCaveats(result),
213
208
  accessions: result.accessions,
214
209
  describedFileCount: described,
215
210
  datasetFileCount: dataset,
216
- kindCount: result.kinds ? result.kinds.length : null,
211
+ // Presence, not truthiness: a completed profile over a tree the scan kept
212
+ // nothing of carries `groups: []`, and "zero groups" is a finding — reading
213
+ // it as "no structure record" would report the analysis unprofiled.
214
+ groupCount: result.groups !== undefined || result.kinds !== undefined ? groups.length : null,
217
215
  ...(structureNote ? { structureNote } : {}),
216
+ ...(result.partition ? { partition: result.partition } : {}),
218
217
  ...(result.coverage ? { coverage: result.coverage } : {}),
219
218
  });
220
219
  }
221
- if (scope === "kinds") {
222
- if (!result.kinds) {
220
+ if (scope === "groups") {
221
+ if (result.groups !== undefined) {
222
+ return ok({
223
+ ...envelope,
224
+ scope: "groups",
225
+ available: true,
226
+ groups: rootGroupPatterns(result.groups, resourceId),
227
+ ...(result.dimensions ? { dimensions: result.dimensions } : {}),
228
+ ...(result.probes ? { probes: result.probes } : {}),
229
+ ...(result.partition ? { partition: result.partition } : {}),
230
+ ...(result.recipe ? { recipe: result.recipe } : {}),
231
+ });
232
+ }
233
+ if (result.kinds === undefined) {
223
234
  return ok({
224
235
  ...envelope,
225
- scope: "kinds",
236
+ scope: "groups",
226
237
  available: false,
227
- message: "This profile was taken before the dataset-structure record existed, so it carries no kinds. " +
238
+ message: "This profile was taken before the dataset-structure record existed, so it carries no groups. " +
228
239
  "That is a fact about the profile, not about the dataset: use scope:'files' and the workspace " +
229
240
  "listing tools, or re-profile the analysis.",
230
241
  });
231
242
  }
243
+ // The structure exists; only its vocabulary is older. Serving it labelled beats
244
+ // reporting the scope unavailable, which would tell an agent the dataset has no
245
+ // structure when the row plainly holds some.
232
246
  return ok({
233
247
  ...envelope,
234
- scope: "kinds",
248
+ scope: "groups",
235
249
  available: true,
250
+ legacy: true,
236
251
  kinds: rootKindPatterns(result.kinds, resourceId),
237
252
  ...(result.axes ? { axes: result.axes } : {}),
238
253
  ...(result.coverage ? { coverage: result.coverage } : {}),
254
+ message: "Authored under the previous model: these are kinds and axes, not resolved groups and dimensions. " +
255
+ "Counts and patterns were the profiler's own claims rather than a computed partition, and there is no " +
256
+ "unclassified sweep or quarantine accounting on a profile of that era.",
239
257
  });
240
258
  }
241
259
  const pageSize = input.pageSize ?? DEFAULT_PAGE_SIZE;
242
260
  const page = input.page ?? 1;
243
- const total = result.files.length;
261
+ const total = describedFiles.length;
244
262
  const start = (page - 1) * pageSize;
245
- const files = result.files.slice(start, start + pageSize);
263
+ const files = describedFiles.slice(start, start + pageSize);
246
264
  return ok({
247
265
  ...envelope,
248
266
  scope: "files",
@@ -250,6 +268,7 @@ export function createInspectDataProfileTool(pool) {
250
268
  pageSize,
251
269
  total,
252
270
  hasMore: start + files.length < total,
271
+ datasetFileCount: profileDatasetFileCount(result),
253
272
  files: rootFilePaths(files, resourceId),
254
273
  });
255
274
  },
@@ -4,10 +4,15 @@ import { defineTool } from "../define-tool.js";
4
4
  export function createResolveCitationTool(resolver) {
5
5
  return defineTool({
6
6
  id: "resolve_citation",
7
- description: "Verify one supplied citation against applicable bibliographic authorities. " +
8
- "Use this for 'does this reference exist and does its metadata agree?', not for topical literature discovery. " +
7
+ description: "Verify one supplied citation against the applicable bibliographic authorities — the DOI registry, Crossref, PubMed, arXiv and Semantic " +
8
+ "Scholar — and report where they agree. " +
9
+ "Use this for 'does this reference exist and does its metadata agree?', not for topical literature discovery: pubmed, " +
10
+ "search_semantic_scholar and search_arxiv are the discovery tools.\n" +
11
+ "ACCEPTED IDENTIFIERS: a DOI ('10.1038/nature12373'), a PMID ('23945592'), an arXiv ID ('2301.00001'), or raw bibliographic text. Each " +
12
+ "authority that the input cannot address is marked not_applicable rather than queried.\n" +
9
13
  "Returns source-labelled records, field comparisons, conflicts, coverage, and one of verified, metadata_mismatch, " +
10
- "not_found, unverifiable, or inconclusive. Source failure never becomes not_found.",
14
+ "not_found, unverifiable, or inconclusive. Source failure never becomes not_found — an unreachable authority reports unavailable, and " +
15
+ "not_found means the authorities that answered hold no such record.",
11
16
  inputSchema: CitationInputSchema,
12
17
  describeCall: ({ citation }) => `verify ${citation}`,
13
18
  execute: async (input, ctx) => ok(await resolver.resolveOne(input, { signal: ctx.signal })),
@@ -7,9 +7,14 @@ export { parseArxivAtom } from "../../literature/sources/arxiv.js";
7
7
  const source = createArxivSource({ maxRetries: 3, retryDelayMs: 1_000, timeoutMs: 90_000 });
8
8
  export const searchArxivTool = defineTool({
9
9
  id: "search_arxiv",
10
- description: "Search arXiv for preprints in ML, physics, math, control theory, " +
11
- "economics, and related quantitative fields. Returns id, title, " +
12
- "abstract, authors, publication date, arXiv categories, and URLs.",
10
+ description: "Search arXiv — the open preprint server of Cornell University — for work in machine learning, physics, math, quantitative biology, control " +
11
+ "theory, economics and the related quantitative fields. Returns id, title, abstract, authors, publication date, arXiv categories, and URLs.\n" +
12
+ "It carries PREPRINTS, thus a paper here may not be peer reviewed, and it is where a method appears months before a journal holds it. For the " +
13
+ "clinical and biomedical literature use pubmed, which indexes MEDLINE; for cross-domain coverage of any field use search_semantic_scholar.\n" +
14
+ "ACCEPTED IDENTIFIERS: free text in `query`, and arXiv category codes in `categories` ('cs.LG', 'q-bio.GN', 'math.OC'). An arXiv ID " +
15
+ "('2301.00001') also matches as free text.\n" +
16
+ "ABSENCE IS NORMAL: an empty papers array means arXiv holds nothing for the query, and `success: false` with an `error` string means the service " +
17
+ "could not be reached. Report either one and continue, do not retry unchanged.",
13
18
  inputSchema: z.object({
14
19
  query: z.string().describe('Free-text query. Example: "adaptive control feedback stabilization".'),
15
20
  categories: z
@@ -13,10 +13,16 @@ export function createSearchSemanticScholarTool(options = {}) {
13
13
  });
14
14
  return defineTool({
15
15
  id: "search_semantic_scholar",
16
- description: "Search Semantic Scholar for academic papers across all sciences " +
17
- "(biology, ML, physics, math, economics, engineering, etc.). Returns " +
18
- "paper id, title, abstract, year, venue, citation count, authors, and " +
19
- "external IDs (DOI, ArXiv). Best for cross-domain analogical search.",
16
+ description: "Search Semantic Scholar — the cross-disciplinary literature corpus of the Allen Institute for AI, which indexes over 200M papers across " +
17
+ "biology, medicine, machine learning, physics, math, economics and engineering. Returns paper id, title, abstract, year, venue, citation " +
18
+ "count, authors, and external IDs (DOI, arXiv). Its citation counts are what rank a field by influence, and its breadth is what makes it the " +
19
+ "tool for a cross-domain analogical search.\n" +
20
+ "For the clinical and biomedical literature alone, pubmed indexes MEDLINE more precisely; for preprints in the quantitative fields use " +
21
+ "search_arxiv.\n" +
22
+ "ACCEPTED IDENTIFIERS: free text in `query`. A DOI or an arXiv ID also matches as free text, and each result carries its DOI and arXiv ID " +
23
+ "back for a citation.\n" +
24
+ "ABSENCE IS NORMAL: an empty papers array means the corpus holds nothing for the query, and `success: false` with an `error` string means the " +
25
+ "service could not be reached (its unkeyed rate limit lands here). Report either one and continue, do not retry unchanged.",
20
26
  inputSchema: z.object({
21
27
  query: z
22
28
  .string()
@@ -7,9 +7,9 @@
7
7
  * with the observed shapes, or wants a directory the injected manifest summarised, can
8
8
  * get the evidence to group differently.
9
9
  *
10
- * Dependency-bearing factory. The walk and the shape observation run in-process over
11
- * the workspace read seam; only the per-shape header readout reaches into the sandbox
12
- * (see `input-scan/enrich.ts`).
10
+ * Dependency-bearing factory. The walk, the shape observation, and every
11
+ * prefix-sufficient header readout run in-process over the workspace read seam; only a
12
+ * footer-indexed container reaches into the sandbox (see `input-scan/enrich.ts`).
13
13
  */
14
14
  import type { SandboxClient } from "../../sandbox/client.js";
15
15
  import type { SandboxRef } from "../../sandbox/types.js";
@@ -17,7 +17,7 @@ import type { WorkspaceFilesystem } from "../../workspace/filesystem.js";
17
17
  export interface ScanInputsDeps {
18
18
  readonly workspaceFs: WorkspaceFilesystem;
19
19
  readonly analysisId: string;
20
- /** Omit to run the host-side scan with no header readout (no sandbox wired). */
20
+ /** Omit to skip the container readouts; the prefix readouts run regardless. */
21
21
  readonly sandboxClient?: SandboxClient;
22
22
  readonly sandbox?: SandboxRef;
23
23
  readonly workflowId: string;
@@ -7,9 +7,9 @@
7
7
  * with the observed shapes, or wants a directory the injected manifest summarised, can
8
8
  * get the evidence to group differently.
9
9
  *
10
- * Dependency-bearing factory. The walk and the shape observation run in-process over
11
- * the workspace read seam; only the per-shape header readout reaches into the sandbox
12
- * (see `input-scan/enrich.ts`).
10
+ * Dependency-bearing factory. The walk, the shape observation, and every
11
+ * prefix-sufficient header readout run in-process over the workspace read seam; only a
12
+ * footer-indexed container reaches into the sandbox (see `input-scan/enrich.ts`).
13
13
  */
14
14
  import { ok } from "neverthrow";
15
15
  import { z } from "zod";
@@ -38,17 +38,16 @@ export function createScanInputsTool(deps) {
38
38
  root: path.replace(/^\/+/, "").replace(/\/+$/, ""),
39
39
  signal: ctx.signal,
40
40
  });
41
- const shapes = deps.sandboxClient && deps.sandbox
42
- ? await enrichShapes({
43
- shapes: scan.manifest.shapes,
44
- sandboxClient: deps.sandboxClient,
45
- sandbox: deps.sandbox,
46
- mountRoot: `/${deps.analysisId}`,
47
- execId: `${deps.workflowId}:${deps.stepId}:${deps.nextFunctionId()}`,
48
- deadlineMs: deps.deadlineMs(),
49
- emit: ctx.emit,
50
- })
51
- : scan.manifest.shapes;
41
+ const shapes = await enrichShapes({
42
+ shapes: scan.manifest.shapes,
43
+ session: ctx.session,
44
+ fs: deps.workspaceFs,
45
+ ...(deps.sandboxClient && deps.sandbox ? { sandboxClient: deps.sandboxClient, sandbox: deps.sandbox } : {}),
46
+ mountRoot: `/${deps.analysisId}`,
47
+ execId: `${deps.workflowId}:${deps.stepId}:${deps.nextFunctionId()}`,
48
+ deadlineMs: deps.deadlineMs(),
49
+ emit: ctx.emit,
50
+ });
52
51
  const manifest = { ...scan.manifest, shapes };
53
52
  return ok({
54
53
  path: manifest.root,
@@ -12,14 +12,23 @@
12
12
  */
13
13
  import type { Pool } from "pg";
14
14
  import type { EmbeddingProvider } from "../../providers/types.js";
15
+ /**
16
+ * The workspace path a hit addresses, or `undefined` for a pattern entry. Resolved here,
17
+ * once, across every index era — group/dimension/kind entries and previous-era entity
18
+ * entries (`entity` in metadata) are patterns; everything else is a file whose path is
19
+ * stamped in metadata or, on rows older than the stamp, is the id minus the resource
20
+ * segment. The consumer branches on presence, nothing else.
21
+ */
22
+ export declare function fileTargetOf(id: string, metadata: unknown): string | undefined;
15
23
  export declare function createWorkspaceSearchTool(pool: Pool, embedding: EmbeddingProvider): import("../define-tool.js").Tool<{
16
24
  query: string;
17
25
  limit: number;
18
- type?: "output" | "input" | "synthesis" | "summary" | "input-kind" | undefined;
26
+ type?: "output" | "input" | "synthesis" | "summary" | "input-group" | "input-dimension" | "input-kind" | undefined;
19
27
  }, {
20
28
  results: {
29
+ metadata: unknown;
30
+ path?: string | undefined;
21
31
  id: string;
22
32
  score: number;
23
- metadata: unknown;
24
33
  }[];
25
34
  }>;