@inflexa-ai/harness 0.17.0 → 0.18.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 (221) hide show
  1. package/dist/agents/report-builder.d.ts +2 -0
  2. package/dist/agents/report-builder.js +2 -0
  3. package/dist/app/spawn-report-session.d.ts +85 -0
  4. package/dist/app/spawn-report-session.js +83 -0
  5. package/dist/contracts/chat-events.d.ts +9 -0
  6. package/dist/contracts/index.d.ts +6 -2
  7. package/dist/contracts/index.js +3 -1
  8. package/dist/contracts/organ-system.d.ts +49 -0
  9. package/dist/contracts/organ-system.js +67 -0
  10. package/dist/contracts/report-blocks.d.ts +564 -0
  11. package/dist/contracts/report-blocks.js +117 -0
  12. package/dist/contracts/report-reference.d.ts +459 -0
  13. package/dist/contracts/report-reference.js +256 -0
  14. package/dist/contracts/schemas/chat-events.d.ts +2 -0
  15. package/dist/contracts/schemas/chat-events.js +2 -0
  16. package/dist/contracts/severity.d.ts +15 -0
  17. package/dist/contracts/severity.js +10 -0
  18. package/dist/contracts/target-dossier.d.ts +8675 -12972
  19. package/dist/contracts/target-dossier.js +534 -591
  20. package/dist/data/safety-panel-schema.d.ts +18 -5
  21. package/dist/data/safety-panel-schema.js +4 -4
  22. package/dist/execution/artifact-metadata.js +1 -0
  23. package/dist/execution/report-runner.js +1 -3
  24. package/dist/execution/run-synthesis.js +2 -0
  25. package/dist/index.d.ts +6 -0
  26. package/dist/index.js +19 -0
  27. package/dist/lib/async-utils.d.ts +16 -4
  28. package/dist/lib/async-utils.js +20 -15
  29. package/dist/lib/host-concurrency.js +1 -0
  30. package/dist/loop/__fixtures__/scripted-provider.js +23 -2
  31. package/dist/loop/run-agent.js +58 -10
  32. package/dist/loop/tool-detail.d.ts +40 -0
  33. package/dist/loop/tool-detail.js +35 -3
  34. package/dist/loop/types.d.ts +13 -0
  35. package/dist/memory/thread-history.d.ts +10 -0
  36. package/dist/memory/thread-history.js +12 -1
  37. package/dist/prompts/target-assessment/briefs/executive-recommendation.d.ts +1 -1
  38. package/dist/prompts/target-assessment/briefs/executive-recommendation.js +5 -5
  39. package/dist/prompts/target-assessment/briefs/liability-bullets.d.ts +1 -1
  40. package/dist/prompts/target-assessment/briefs/liability-bullets.js +1 -1
  41. package/dist/prompts/target-assessment/briefs/target-organ-liabilities.d.ts +1 -1
  42. package/dist/prompts/target-assessment/briefs/target-organ-liabilities.js +11 -3
  43. package/dist/prompts/target-assessment/investigation/adversarial-critique.d.ts +1 -0
  44. package/dist/prompts/target-assessment/investigation/adversarial-critique.js +67 -0
  45. package/dist/prompts/target-assessment/investigation/claim-reverification.d.ts +1 -0
  46. package/dist/prompts/target-assessment/investigation/claim-reverification.js +49 -0
  47. package/dist/prompts/target-assessment/investigation/index.d.ts +3 -0
  48. package/dist/prompts/target-assessment/investigation/index.js +3 -0
  49. package/dist/prompts/target-assessment/investigation/mechanism-proposal.d.ts +1 -0
  50. package/dist/prompts/target-assessment/investigation/mechanism-proposal.js +49 -0
  51. package/dist/prompts/target-assessment/tox-voice/index.d.ts +1 -1
  52. package/dist/prompts/target-assessment/tox-voice/vocabulary.d.ts +8 -5
  53. package/dist/prompts/target-assessment/tox-voice/vocabulary.js +9 -20
  54. package/dist/providers/__fixtures__/session.d.ts +5 -4
  55. package/dist/providers/__fixtures__/session.js +4 -4
  56. package/dist/report-model/block-walk.d.ts +56 -0
  57. package/dist/report-model/block-walk.js +89 -0
  58. package/dist/report-model/draft-finish.d.ts +60 -0
  59. package/dist/report-model/draft-finish.js +56 -0
  60. package/dist/report-model/draft-operations.d.ts +125 -0
  61. package/dist/report-model/draft-operations.js +341 -0
  62. package/dist/report-model/draft-read.d.ts +59 -0
  63. package/dist/report-model/draft-read.js +109 -0
  64. package/dist/report-model/draft.d.ts +51 -0
  65. package/dist/report-model/draft.js +32 -0
  66. package/dist/report-model/fixture-resolver.d.ts +8 -0
  67. package/dist/report-model/fixture-resolver.js +252 -0
  68. package/dist/report-model/mint-snapshot.d.ts +30 -0
  69. package/dist/report-model/mint-snapshot.js +40 -0
  70. package/dist/report-model/reference-resolver.d.ts +113 -0
  71. package/dist/report-model/reference-resolver.js +60 -0
  72. package/dist/report-model/structural-validation.d.ts +22 -0
  73. package/dist/report-model/structural-validation.js +68 -0
  74. package/dist/report-model/validate.d.ts +40 -0
  75. package/dist/report-model/validate.js +80 -0
  76. package/dist/report-render/chart.d.ts +33 -0
  77. package/dist/report-render/chart.js +538 -0
  78. package/dist/report-render/page.d.ts +127 -0
  79. package/dist/report-render/page.js +307 -0
  80. package/dist/report-render/references.d.ts +32 -0
  81. package/dist/report-render/references.js +44 -0
  82. package/dist/report-render/render.d.ts +25 -0
  83. package/dist/report-render/render.js +125 -0
  84. package/dist/report-render/script-json.d.ts +13 -0
  85. package/dist/report-render/script-json.js +15 -0
  86. package/dist/report-render/types.d.ts +39 -0
  87. package/dist/report-render/types.js +10 -0
  88. package/dist/report-render/views/chart-view.d.ts +11 -0
  89. package/dist/report-render/views/chart-view.js +23 -0
  90. package/dist/report-render/views/page-view.d.ts +20 -0
  91. package/dist/report-render/views/page-view.js +49 -0
  92. package/dist/report-render/views/prose.d.ts +28 -0
  93. package/dist/report-render/views/prose.js +75 -0
  94. package/dist/report-render/views/references-view.d.ts +20 -0
  95. package/dist/report-render/views/references-view.js +64 -0
  96. package/dist/report-render/views/values.d.ts +42 -0
  97. package/dist/report-render/views/values.js +43 -0
  98. package/dist/runtime/boot.d.ts +3 -3
  99. package/dist/runtime/boot.js +15 -4
  100. package/dist/runtime/dbos.d.ts +4 -4
  101. package/dist/state/artifacts.d.ts +16 -0
  102. package/dist/state/artifacts.js +18 -0
  103. package/dist/state/init.js +31 -0
  104. package/dist/state/purge-analysis.js +1 -0
  105. package/dist/state/report-versions.d.ts +132 -0
  106. package/dist/state/report-versions.js +194 -0
  107. package/dist/tasks/data-profile.js +1 -0
  108. package/dist/tools/bio/chembl.js +1 -0
  109. package/dist/tools/bio/comptox.js +1 -0
  110. package/dist/tools/bio/drug-gene-interactions.js +1 -0
  111. package/dist/tools/bio/gene-disease-evidence.js +1 -0
  112. package/dist/tools/bio/gene-preclinical-profile.js +1 -0
  113. package/dist/tools/bio/keys.d.ts +3 -2
  114. package/dist/tools/bio/keys.js +3 -2
  115. package/dist/tools/bio/lookup-annotation.js +1 -0
  116. package/dist/tools/bio/opentargets.d.ts +3 -3
  117. package/dist/tools/bio/opentargets.js +4 -3
  118. package/dist/tools/bio/pubchem.js +1 -0
  119. package/dist/tools/bio/search-clinical-trials.js +1 -0
  120. package/dist/tools/bio/search-faers.js +1 -0
  121. package/dist/tools/bio/search-geo-datasets.js +1 -0
  122. package/dist/tools/bio/search-interactions.js +1 -0
  123. package/dist/tools/bio/target-safety.d.ts +4 -6
  124. package/dist/tools/bio/target-safety.js +7 -6
  125. package/dist/tools/define-tool.d.ts +19 -12
  126. package/dist/tools/define-tool.js +4 -2
  127. package/dist/tools/display/show-user.js +1 -0
  128. package/dist/tools/iterate-report.js +2 -0
  129. package/dist/tools/lib/chembl-client.d.ts +2 -0
  130. package/dist/tools/lib/chembl-client.js +1 -0
  131. package/dist/tools/lib/monarch-client.d.ts +42 -0
  132. package/dist/tools/lib/monarch-client.js +100 -0
  133. package/dist/tools/report/build-report.js +1 -0
  134. package/dist/tools/report/mint-preview-url.js +1 -0
  135. package/dist/tools/report/preview-snapshot.js +1 -0
  136. package/dist/tools/report/submit-report.js +1 -0
  137. package/dist/tools/report/version-fs.js +4 -0
  138. package/dist/tools/report-authoring/authoring-tools.d.ts +134 -0
  139. package/dist/tools/report-authoring/authoring-tools.js +352 -0
  140. package/dist/tools/research/context7-docs.js +2 -0
  141. package/dist/tools/research/generate-analogy-report.js +1 -0
  142. package/dist/tools/research/generate-plan.js +3 -0
  143. package/dist/tools/research/inspect-data-profile.js +4 -0
  144. package/dist/tools/research/literature-reviewer.js +1 -0
  145. package/dist/tools/research/search-arxiv.js +1 -0
  146. package/dist/tools/research/search-github-repos.js +1 -0
  147. package/dist/tools/research/search-semantic-scholar.js +1 -0
  148. package/dist/tools/sandbox/list-available-packages.js +23 -0
  149. package/dist/tools/sandbox/list-available-refs.js +36 -0
  150. package/dist/tools/sandbox/report-blocker.js +1 -0
  151. package/dist/tools/sandbox/skills.js +2 -0
  152. package/dist/tools/workspace/file-stat.js +1 -0
  153. package/dist/tools/workspace/show-file.js +2 -0
  154. package/dist/tools/workspace/show-plan.js +1 -0
  155. package/dist/workflows/execute-analysis.d.ts +5 -1
  156. package/dist/workflows/execute-analysis.js +5 -5
  157. package/dist/workflows/execute-target-assessment.d.ts +13 -2
  158. package/dist/workflows/execute-target-assessment.js +78 -16
  159. package/dist/workflows/target-assessment/assemblers/index.d.ts +1 -1
  160. package/dist/workflows/target-assessment/assemblers/index.js +1 -1
  161. package/dist/workflows/target-assessment/assemblers/literature.d.ts +9 -27
  162. package/dist/workflows/target-assessment/assemblers/literature.js +32 -48
  163. package/dist/workflows/target-assessment/assemblers/orchestrator.d.ts +17 -9
  164. package/dist/workflows/target-assessment/assemblers/orchestrator.js +195 -85
  165. package/dist/workflows/target-assessment/assemblers/safety.d.ts +70 -48
  166. package/dist/workflows/target-assessment/assemblers/safety.js +125 -42
  167. package/dist/workflows/target-assessment/collectors/index.d.ts +5 -2
  168. package/dist/workflows/target-assessment/collectors/index.js +48 -5
  169. package/dist/workflows/target-assessment/coverage.d.ts +43 -27
  170. package/dist/workflows/target-assessment/coverage.js +33 -29
  171. package/dist/workflows/target-assessment/fanout/index.js +1 -0
  172. package/dist/workflows/target-assessment/investigation/index.d.ts +97 -0
  173. package/dist/workflows/target-assessment/investigation/index.js +419 -0
  174. package/dist/workflows/target-assessment/lib/approval-precedents.d.ts +26 -5
  175. package/dist/workflows/target-assessment/lib/approval-precedents.js +95 -29
  176. package/dist/workflows/target-assessment/lib/citation-validator.js +1 -1
  177. package/dist/workflows/target-assessment/lib/clinical-consequence-annotator.d.ts +2 -2
  178. package/dist/workflows/target-assessment/lib/clinical-evidence-attribution.d.ts +2 -3
  179. package/dist/workflows/target-assessment/lib/clinical-evidence-attribution.js +0 -4
  180. package/dist/workflows/target-assessment/lib/compute-derived.d.ts +6 -5
  181. package/dist/workflows/target-assessment/lib/compute-derived.js +14 -5
  182. package/dist/workflows/target-assessment/lib/family-complex-supplement.d.ts +5 -6
  183. package/dist/workflows/target-assessment/lib/family-complex-supplement.js +2 -3
  184. package/dist/workflows/target-assessment/lib/fda-label-safety.d.ts +72 -0
  185. package/dist/workflows/target-assessment/lib/fda-label-safety.js +166 -0
  186. package/dist/workflows/target-assessment/lib/heterodimer-filter.d.ts +0 -9
  187. package/dist/workflows/target-assessment/lib/heterodimer-filter.js +0 -10
  188. package/dist/workflows/target-assessment/lib/hpo-organ-map.d.ts +34 -0
  189. package/dist/workflows/target-assessment/lib/hpo-organ-map.js +63 -0
  190. package/dist/workflows/target-assessment/lib/impc-organ-map.d.ts +19 -0
  191. package/dist/workflows/target-assessment/lib/impc-organ-map.js +39 -0
  192. package/dist/workflows/target-assessment/lib/meddra-organ-map.d.ts +24 -14
  193. package/dist/workflows/target-assessment/lib/meddra-organ-map.js +59 -47
  194. package/dist/workflows/target-assessment/lib/recommendation-sanitizer.js +1 -1
  195. package/dist/workflows/target-assessment/lib/regulatory-actions.d.ts +2 -3
  196. package/dist/workflows/target-assessment/lib/regulatory-actions.js +33 -4
  197. package/dist/workflows/target-assessment/lib/safety-corroboration.d.ts +44 -0
  198. package/dist/workflows/target-assessment/lib/safety-corroboration.js +273 -0
  199. package/dist/workflows/target-assessment/lib/target-identity-filter.d.ts +5 -7
  200. package/dist/workflows/target-assessment/lib/target-identity-filter.js +5 -7
  201. package/dist/workflows/target-assessment/metrics.d.ts +1 -6
  202. package/dist/workflows/target-assessment/metrics.js +1 -13
  203. package/dist/workflows/target-assessment/phase4-assemble.d.ts +2147 -807
  204. package/dist/workflows/target-assessment/phase4-assemble.js +19 -19
  205. package/dist/workflows/target-assessment/phase5-persist.d.ts +20 -21
  206. package/dist/workflows/target-assessment/phase5-persist.js +56 -173
  207. package/dist/workflows/target-assessment/schemas.d.ts +46 -1
  208. package/dist/workflows/target-assessment/schemas.js +19 -0
  209. package/dist/workflows/target-assessment/steps/fanout/polypharm-for-one-modulator-step.d.ts +1 -0
  210. package/dist/workflows/target-assessment/steps/fanout/polypharm-for-one-modulator-step.js +2 -0
  211. package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +26 -62
  212. package/dist/workflows/target-assessment/steps/phase2-aggregate.js +0 -4
  213. package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +27 -154
  214. package/dist/workflows/target-assessment/steps/phase3-aggregate.js +0 -6
  215. package/dist/workflows/target-assessment/steps/synthesis/diagnostics.d.ts +0 -6
  216. package/dist/workflows/target-assessment/steps/synthesis/diagnostics.js +0 -18
  217. package/dist/workflows/target-assessment/sub-workflows/per-modulator-polypharm.d.ts +1 -0
  218. package/dist/workflows/target-assessment/synthesis/index.d.ts +52 -5
  219. package/dist/workflows/target-assessment/synthesis/index.js +4 -1
  220. package/dist/workflows/target-assessment/synthesis-probes.d.ts +3 -3
  221. package/package.json +4 -1
@@ -0,0 +1,132 @@
1
+ /**
2
+ * The report version store -- the append-only record of report versions.
3
+ *
4
+ * A version is one immutable row. It holds the block document, the pinned
5
+ * snapshot, and the anchor. The store records a version and reads a version. It
6
+ * never updates a recorded row, thus a correction is a new version.
7
+ *
8
+ * A thread holds at most one version, and a named unique constraint on the thread
9
+ * id enforces it. The record reads nothing before the insert. A second record for
10
+ * one thread trips the constraint, and the store maps that trip to the typed
11
+ * refusal thread_already_holds_version.
12
+ *
13
+ * The store keeps the snapshot and the anchor as given, and it never mints a
14
+ * snapshot. A read parses the stored document and the stored snapshot with the
15
+ * current schemas. A row that fails a parse reads as a typed error, and it does
16
+ * not crash. An absent row reads as a normal absence.
17
+ *
18
+ * A version outlives its thread. The anchor columns sit on the row, thus a
19
+ * deleted thread leaves a recorded version in place. Only a purge of the analysis
20
+ * removes the version, through the analysis-id cascade.
21
+ */
22
+ import { type ResultAsync } from "neverthrow";
23
+ import type { Pool } from "pg";
24
+ import { type ReportDocument } from "../contracts/report-blocks.js";
25
+ import { type DbError } from "../lib/db-result.js";
26
+ import type { Logger } from "../lib/logger.js";
27
+ import type { ReportSnapshot } from "../report-model/reference-resolver.js";
28
+ /** One reduced schema issue -- the dotted path and the message, without the rest. */
29
+ export interface SchemaIssue {
30
+ readonly path: string;
31
+ readonly message: string;
32
+ }
33
+ /** The anchor and the payload of a version to record. */
34
+ export interface RecordVersionInput {
35
+ /** The block document. It rides as `unknown`, and the record parses it. */
36
+ readonly document: unknown;
37
+ /** The pinned snapshot, stored as given. */
38
+ readonly snapshot: ReportSnapshot;
39
+ readonly analysisId: string;
40
+ readonly threadId: string;
41
+ /** The parent conversation thread of the anchor, or `null` when a root has none. */
42
+ readonly parentThreadId: string | null;
43
+ /** The parent transcript position of the anchor, or `null` beside a null parent thread. */
44
+ readonly parentSeq: number | null;
45
+ /** The earlier version that this one reuses. The parent must belong to the same analysis. */
46
+ readonly parentVersionId?: string;
47
+ }
48
+ /** The stable reference of a freshly recorded version. */
49
+ export interface RecordedVersionRef {
50
+ readonly versionId: string;
51
+ }
52
+ /**
53
+ * A record the store refuses as data, before or in place of a row. Distinct from
54
+ * `DbError`: the request describes a version that the store will not write.
55
+ *
56
+ * The store parses the document and the snapshot before any row. The store
57
+ * refuses a malformed document or a malformed snapshot here as typed data, and
58
+ * no row lands.
59
+ *
60
+ * A second record for one thread is `thread_already_holds_version`. The unique
61
+ * constraint on the thread id trips, and the record maps that one constraint by
62
+ * its name. Every other constraint trip stays a `DbError`.
63
+ *
64
+ * A `parentVersionId` naming no row is not one of these -- the self foreign key
65
+ * refuses an unknown parent id, and `tryMutation` classifies that refusal as the
66
+ * `constraint_violation` variant of `DbError`.
67
+ */
68
+ export type RecordVersionError = {
69
+ readonly type: "malformed_document";
70
+ readonly op: string;
71
+ readonly issues: readonly SchemaIssue[];
72
+ } | {
73
+ readonly type: "malformed_snapshot";
74
+ readonly op: string;
75
+ readonly issues: readonly SchemaIssue[];
76
+ } | {
77
+ readonly type: "parent_analysis_mismatch";
78
+ readonly op: string;
79
+ readonly analysisId: string;
80
+ readonly parentVersionId: string;
81
+ readonly parentAnalysisId: string;
82
+ } | {
83
+ readonly type: "thread_already_holds_version";
84
+ readonly op: string;
85
+ readonly threadId: string;
86
+ } | DbError;
87
+ /**
88
+ * A stored row that a read cannot parse with the current schemas. It names the
89
+ * version id and the part that failed, and it carries the reduced issues.
90
+ */
91
+ export type VersionReadError = {
92
+ readonly type: "corrupt_version";
93
+ readonly op: string;
94
+ readonly versionId: string;
95
+ readonly part: "document" | "snapshot";
96
+ readonly issues: readonly SchemaIssue[];
97
+ };
98
+ /** One recorded version, as the store gives it back. */
99
+ export interface RecordedVersion {
100
+ readonly versionId: string;
101
+ readonly analysisId: string;
102
+ readonly threadId: string;
103
+ readonly parentThreadId: string | null;
104
+ readonly parentSeq: number | null;
105
+ /** The earlier version that this one reuses, or `null` when it has none. */
106
+ readonly parentVersionId: string | null;
107
+ readonly document: ReportDocument;
108
+ readonly snapshot: ReportSnapshot;
109
+ readonly createdAt: Date;
110
+ }
111
+ export interface ReportVersionStore {
112
+ /**
113
+ * Record a version. The record parses the document, refuses a malformed value
114
+ * as data with no row, and refuses a parent from a different analysis. It reads
115
+ * nothing before the insert. A second record for one thread refuses with
116
+ * thread_already_holds_version. The snapshot and the anchor store as given.
117
+ */
118
+ record(input: RecordVersionInput): ResultAsync<RecordedVersionRef, RecordVersionError>;
119
+ /** One version by its id, or `null` when no row holds it. */
120
+ getVersion(versionId: string): ResultAsync<RecordedVersion | null, DbError | VersionReadError>;
121
+ /** The one version of a thread, or `null` for a thread with no version. */
122
+ getThreadVersion(threadId: string): ResultAsync<RecordedVersion | null, DbError | VersionReadError>;
123
+ }
124
+ export interface ReportVersionStoreDeps {
125
+ readonly pool: Pool;
126
+ readonly logger?: Logger;
127
+ }
128
+ /**
129
+ * Create a `ReportVersionStore` bound to a Postgres pool. The
130
+ * `cortex_report_versions` table is provisioned by the state-init DDL.
131
+ */
132
+ export declare function createReportVersionStore({ pool }: ReportVersionStoreDeps): ReportVersionStore;
@@ -0,0 +1,194 @@
1
+ /**
2
+ * The report version store -- the append-only record of report versions.
3
+ *
4
+ * A version is one immutable row. It holds the block document, the pinned
5
+ * snapshot, and the anchor. The store records a version and reads a version. It
6
+ * never updates a recorded row, thus a correction is a new version.
7
+ *
8
+ * A thread holds at most one version, and a named unique constraint on the thread
9
+ * id enforces it. The record reads nothing before the insert. A second record for
10
+ * one thread trips the constraint, and the store maps that trip to the typed
11
+ * refusal thread_already_holds_version.
12
+ *
13
+ * The store keeps the snapshot and the anchor as given, and it never mints a
14
+ * snapshot. A read parses the stored document and the stored snapshot with the
15
+ * current schemas. A row that fails a parse reads as a typed error, and it does
16
+ * not crash. An absent row reads as a normal absence.
17
+ *
18
+ * A version outlives its thread. The anchor columns sit on the row, thus a
19
+ * deleted thread leaves a recorded version in place. Only a purge of the analysis
20
+ * removes the version, through the analysis-id cascade.
21
+ */
22
+ import { randomUUID } from "node:crypto";
23
+ import { err, errAsync, ok } from "neverthrow";
24
+ import { z } from "zod";
25
+ import { ReportDocumentSchema } from "../contracts/report-blocks.js";
26
+ import { tryMutation, tryQuery } from "../lib/db-result.js";
27
+ /**
28
+ * The name of the unique constraint on thread_id. Postgres reports it on a
29
+ * duplicate-thread refusal, and the record matches it to map the trip to the
30
+ * thread_already_holds_version refusal.
31
+ */
32
+ const THREAD_UNIQUE_CONSTRAINT = "cortex_report_versions_one_per_thread";
33
+ /**
34
+ * One pinned artifact of a stored snapshot. The value carries the content hash,
35
+ * an optional file type, and optional rows. The `z.ZodType<ReportSnapshot>`
36
+ * annotation below keeps this schema and the `ArtifactSnapshot` type of the
37
+ * reference model in step, thus a change to one shows as a compile error there.
38
+ */
39
+ const ArtifactSnapshotSchema = z.object({
40
+ hash: z.string(),
41
+ fileType: z.string().nullable().optional(),
42
+ rows: z.array(z.record(z.string(), z.union([z.string(), z.number()]))).optional(),
43
+ });
44
+ /**
45
+ * The stored-snapshot schema. The annotation ties the parse output to the
46
+ * `ReportSnapshot` type of the reference model, thus the store and the structural
47
+ * validation never disagree about the snapshot shape. The mint never fills `rows`,
48
+ * but the schema admits what the type admits.
49
+ */
50
+ const ReportSnapshotSchema = z.object({
51
+ artifacts: z.record(z.string(), ArtifactSnapshotSchema),
52
+ citations: z.array(z.string()).optional(),
53
+ });
54
+ function reduceIssues(error) {
55
+ return error.issues.map((issue) => ({
56
+ path: issue.path.map((segment) => String(segment)).join("."),
57
+ message: issue.message,
58
+ }));
59
+ }
60
+ const _assertRecordError = true;
61
+ const _assertReadError = true;
62
+ /**
63
+ * The full row projection every read shares. `parent_seq` is a bigint, thus the
64
+ * driver hands it back as text, and `rowToVersion` is the single place that turns
65
+ * it into a number.
66
+ */
67
+ const VERSION_COLUMNS = "version_id, analysis_id, thread_id, parent_thread_id, parent_seq::text AS parent_seq, parent_version_id, document, snapshot, created_at";
68
+ /**
69
+ * The insert. The row carries no ordinal, thus the statement writes the given
70
+ * columns directly. `created_at` takes the column default.
71
+ */
72
+ const INSERT_SQL = `INSERT INTO cortex_report_versions
73
+ (version_id, analysis_id, thread_id, parent_thread_id, parent_seq,
74
+ parent_version_id, document, snapshot)
75
+ VALUES ($1, $2, $3, $4, $5::bigint, $6, $7::jsonb, $8::jsonb)`;
76
+ function rowToVersion(row) {
77
+ const document = ReportDocumentSchema.safeParse(row.document);
78
+ if (!document.success) {
79
+ return err({ type: "corrupt_version", op: "report-versions.read", versionId: row.version_id, part: "document", issues: reduceIssues(document.error) });
80
+ }
81
+ const snapshot = ReportSnapshotSchema.safeParse(row.snapshot);
82
+ if (!snapshot.success) {
83
+ return err({ type: "corrupt_version", op: "report-versions.read", versionId: row.version_id, part: "snapshot", issues: reduceIssues(snapshot.error) });
84
+ }
85
+ return ok({
86
+ versionId: row.version_id,
87
+ analysisId: row.analysis_id,
88
+ threadId: row.thread_id,
89
+ parentThreadId: row.parent_thread_id,
90
+ parentSeq: row.parent_seq === null ? null : Number(row.parent_seq),
91
+ parentVersionId: row.parent_version_id,
92
+ document: document.data,
93
+ snapshot: snapshot.data,
94
+ createdAt: row.created_at,
95
+ });
96
+ }
97
+ /**
98
+ * Map an insert failure to a record refusal. A trip of the thread constraint is
99
+ * thread_already_holds_version. Every other DbError stays unchanged.
100
+ */
101
+ function mapInsertError(error, threadId) {
102
+ if (error.type === "constraint_violation" && error.constraint === THREAD_UNIQUE_CONSTRAINT) {
103
+ return { type: "thread_already_holds_version", op: "report-versions.record", threadId };
104
+ }
105
+ return error;
106
+ }
107
+ /**
108
+ * Create a `ReportVersionStore` bound to a Postgres pool. The
109
+ * `cortex_report_versions` table is provisioned by the state-init DDL.
110
+ */
111
+ export function createReportVersionStore({ pool }) {
112
+ /**
113
+ * The analysis scope of a parent version, or `null` when no such row exists.
114
+ * Absence is not a verdict here: an unknown parent id falls through to the
115
+ * insert, where the self foreign key refuses it as a `DbError`.
116
+ */
117
+ function parentAnalysis(parentVersionId) {
118
+ return tryQuery("report-versions.record.parentScope", () => pool.query({
119
+ text: "SELECT analysis_id FROM cortex_report_versions WHERE version_id = $1",
120
+ values: [parentVersionId],
121
+ })).map(({ rows }) => rows[0]?.analysis_id ?? null);
122
+ }
123
+ function record(input) {
124
+ const parsed = ReportDocumentSchema.safeParse(input.document);
125
+ if (!parsed.success) {
126
+ return errAsync({ type: "malformed_document", op: "report-versions.record", issues: reduceIssues(parsed.error) });
127
+ }
128
+ const document = parsed.data;
129
+ // The parse guards the row against a caller bug through a cast. The store
130
+ // stores the given value, thus the parse changes no value.
131
+ const parsedSnapshot = ReportSnapshotSchema.safeParse(input.snapshot);
132
+ if (!parsedSnapshot.success) {
133
+ return errAsync({ type: "malformed_snapshot", op: "report-versions.record", issues: reduceIssues(parsedSnapshot.error) });
134
+ }
135
+ const insert = () => {
136
+ const versionId = randomUUID();
137
+ return tryMutation("report-versions.record.insert", async () => {
138
+ await pool.query({
139
+ text: INSERT_SQL,
140
+ values: [
141
+ versionId,
142
+ input.analysisId,
143
+ input.threadId,
144
+ input.parentThreadId,
145
+ input.parentSeq,
146
+ input.parentVersionId ?? null,
147
+ JSON.stringify(document),
148
+ JSON.stringify(input.snapshot),
149
+ ],
150
+ });
151
+ return { versionId };
152
+ }).mapErr((error) => mapInsertError(error, input.threadId));
153
+ };
154
+ if (input.parentVersionId === undefined) {
155
+ return insert();
156
+ }
157
+ const parentVersionId = input.parentVersionId;
158
+ return parentAnalysis(parentVersionId).andThen((parentAnalysisId) => {
159
+ if (parentAnalysisId !== null && parentAnalysisId !== input.analysisId) {
160
+ return errAsync({
161
+ type: "parent_analysis_mismatch",
162
+ op: "report-versions.record",
163
+ analysisId: input.analysisId,
164
+ parentVersionId,
165
+ parentAnalysisId,
166
+ });
167
+ }
168
+ return insert();
169
+ });
170
+ }
171
+ function getVersion(versionId) {
172
+ return tryQuery("report-versions.getVersion", () => pool.query({
173
+ text: `SELECT ${VERSION_COLUMNS} FROM cortex_report_versions WHERE version_id = $1`,
174
+ values: [versionId],
175
+ })).andThen(({ rows }) => {
176
+ const row = rows[0];
177
+ if (!row)
178
+ return ok(null);
179
+ return rowToVersion(row);
180
+ });
181
+ }
182
+ function getThreadVersion(threadId) {
183
+ return tryQuery("report-versions.getThreadVersion", () => pool.query({
184
+ text: `SELECT ${VERSION_COLUMNS} FROM cortex_report_versions WHERE thread_id = $1`,
185
+ values: [threadId],
186
+ })).andThen(({ rows }) => {
187
+ const row = rows[0];
188
+ if (!row)
189
+ return ok(null);
190
+ return rowToVersion(row);
191
+ });
192
+ }
193
+ return { record, getVersion, getThreadVersion };
194
+ }
@@ -272,6 +272,7 @@ export async function runDataProfileBody(input, deps) {
272
272
  id: "submit_profile",
273
273
  description: "Submit the profiling results. Call this tool once after completing " + "all profiling work — it validates and records your findings.",
274
274
  inputSchema: ProfilerOutputSchema,
275
+ describeCall: "none",
275
276
  execute: async (input) => {
276
277
  capturedProfile = input;
277
278
  return ok({ status: "accepted" });
@@ -118,6 +118,7 @@ export const chemblTool = defineTool({
118
118
  "compounds here first. If ChEMBL misses the compound, resolve it via pubchem action='compound' and bridge back with pubchem action='crossrefs'.\n" +
119
119
  "An empty array is valid no-data, not an error — do not retry the same call.",
120
120
  inputSchema,
121
+ describeCall: "none",
121
122
  execute: async ({ action, query, searchType, chemblId, idType, activityType, limit }) => {
122
123
  switch (action) {
123
124
  case "compounds":
@@ -106,6 +106,7 @@ export function createComptoxTool(deps) {
106
106
  "This is environmental/industrial-chemical data: for drug-like compounds prefer PubChem or ChEMBL — dataset 'chemical' returns " +
107
107
  "`detail.pubchemCid` to bridge back.",
108
108
  inputSchema,
109
+ describeCall: "none",
109
110
  execute: async (input) => {
110
111
  const headers = getEpaCcteHeaders(deps.apiKey);
111
112
  const resolved = await resolveDtxsid(input.query, headers);
@@ -119,6 +119,7 @@ export function createDrugGeneInteractionsTool(deps) {
119
119
  "not be reached (a missing DRUGBANK_API_KEY lands here — tell the user and proceed with DGIdb). A `found: false` entry means the identifier " +
120
120
  "is not in DGIdb at all, usually a non-HUGO symbol or a brand name. Neither is worth retrying unchanged.",
121
121
  inputSchema,
122
+ describeCall: "none",
122
123
  execute: async (input) => {
123
124
  const inputs = typeof input.query === "string" ? [input.query] : input.query;
124
125
  const selected = input.sources ?? ["dgidb"];
@@ -118,6 +118,7 @@ export function createGeneDiseaseEvidenceTool(deps) {
118
118
  "could not be reached (a missing DISGENET_API_KEY lands here — tell the user and proceed with the other two); 'not_applicable' means that " +
119
119
  "corpus has no lookup for this queryType. Only 'no_data' is evidence of absence, and none is worth retrying unchanged.",
120
120
  inputSchema,
121
+ describeCall: "none",
121
122
  execute: async (input) => {
122
123
  const applicable = applicableSources(input.queryType);
123
124
  const requested = input.sources ?? applicable;
@@ -105,6 +105,7 @@ export const genePreclinicalProfileTool = defineTool({
105
105
  "SPARSE OUTPUT IS VALID NO-DATA, not an error — do not retry. A null humanEnsemblId means the symbol did not resolve; a null mouseMarkerSymbol " +
106
106
  "with no phenotype terms means the gene has not been IMPC-phenotyped, which is common.",
107
107
  inputSchema,
108
+ describeCall: "none",
108
109
  execute: async ({ geneSymbol, include, species, minRank, tissueLimit, phenotypeLimit }) => {
109
110
  const halves = include ?? ["expression", "knockout"];
110
111
  const wantExpression = halves.includes("expression");
@@ -19,8 +19,9 @@ export interface BioToolKeys {
19
19
  * The NCBI-backed literature tool, built from the shared key slice.
20
20
  *
21
21
  * `pubmed` is the consolidated literature tool (search / details / fulltext
22
- * behind one `action`). ClinVar is no longer its own tool — it is one of the
23
- * corpora behind `gene_disease_evidence`, which also needs the NCBI key.
22
+ * behind one `action`). ClinVar reaches a caller through
23
+ * `gene_disease_evidence`, as one of the corpora behind it, and it needs the
24
+ * same NCBI key.
24
25
  */
25
26
  export declare function createNcbiTools(keys: BioToolKeys): {
26
27
  pubmed: Tool;
@@ -6,8 +6,9 @@ import { createPubMedTool } from "./pubmed.js";
6
6
  * The NCBI-backed literature tool, built from the shared key slice.
7
7
  *
8
8
  * `pubmed` is the consolidated literature tool (search / details / fulltext
9
- * behind one `action`). ClinVar is no longer its own tool — it is one of the
10
- * corpora behind `gene_disease_evidence`, which also needs the NCBI key.
9
+ * behind one `action`). ClinVar reaches a caller through
10
+ * `gene_disease_evidence`, as one of the corpora behind it, and it needs the
11
+ * same NCBI key.
11
12
  */
12
13
  export function createNcbiTools(keys) {
13
14
  return {
@@ -122,6 +122,7 @@ export const lookupAnnotationTool = defineTool({
122
122
  "search_interactions({action:'enrichment'}), which does the statistics.\n" +
123
123
  "An empty terms / annotations / pathways array is valid no-data — do not retry the same input.",
124
124
  inputSchema,
125
+ describeCall: "none",
125
126
  execute: async ({ vocabulary, query, goId, geneProductId, taxonId, organism, includeGenes, limit }) => {
126
127
  const cap = limit ?? DEFAULT_LIMIT;
127
128
  if (vocabulary !== "go") {
@@ -2,9 +2,9 @@
2
2
  * opentargets — one tool over the Open Targets Platform: target-disease
3
3
  * association evidence, read from a gene or from a disease.
4
4
  *
5
- * The curated safety liabilities this tool used to serve now belong to
6
- * `target_safety`, which pairs them with the local secondary-pharmacology panel
7
- * and resolves the Ensembl id they need.
5
+ * Curated safety liabilities are not served here: `target_safety` owns them,
6
+ * because it pairs them with the local secondary-pharmacology panel and
7
+ * resolves the Ensembl id they need.
8
8
  *
9
9
  * The input is a flat object with an `action` discriminator — not a
10
10
  * `z.discriminatedUnion`, which `defineTool` rejects (model tool calling needs
@@ -2,9 +2,9 @@
2
2
  * opentargets — one tool over the Open Targets Platform: target-disease
3
3
  * association evidence, read from a gene or from a disease.
4
4
  *
5
- * The curated safety liabilities this tool used to serve now belong to
6
- * `target_safety`, which pairs them with the local secondary-pharmacology panel
7
- * and resolves the Ensembl id they need.
5
+ * Curated safety liabilities are not served here: `target_safety` owns them,
6
+ * because it pairs them with the local secondary-pharmacology panel and
7
+ * resolves the Ensembl id they need.
8
8
  *
9
9
  * The input is a flat object with an `action` discriminator — not a
10
10
  * `z.discriminatedUnion`, which `defineTool` rejects (model tool calling needs
@@ -62,6 +62,7 @@ export const openTargetsTool = defineTool({
62
62
  "disease name silently returns an EMPTY result rather than an error — resolve a symbol to its ENSG id with search_gene first.\n" +
63
63
  "NO-DATA IS FINAL — do not retry the same id. Empty associations mean no evidence, or an unresolvable id.",
64
64
  inputSchema,
65
+ describeCall: "none",
65
66
  execute: async (input) => {
66
67
  const limit = input.limit ?? DEFAULT_LIMIT;
67
68
  switch (input.action) {
@@ -76,6 +76,7 @@ export const pubchemTool = defineTool({
76
76
  "then chembl action='mechanism' / 'bioactivity' there for curated activity data. " +
77
77
  "An empty results / crossRefs / assays array is valid no-data (no match; the CID is in no external registry; the compound was never screened) — do not retry.",
78
78
  inputSchema,
79
+ describeCall: "none",
79
80
  execute: async (input) => {
80
81
  switch (input.action) {
81
82
  case "compound":
@@ -39,6 +39,7 @@ export const searchClinicalTrialsTool = defineTool({
39
39
  "landscape. Set true for the full protocol prose, and narrow `limit` when you do: a full summary runs to several thousand characters " +
40
40
  "per trial."),
41
41
  }),
42
+ describeCall: "none",
42
43
  execute: async ({ query, phase, status, limit = 10, includeSummaries = false }) => {
43
44
  const result = await searchTrials(query, { phase, status, limit });
44
45
  const trials = includeSummaries
@@ -23,6 +23,7 @@ export const searchFaersTool = defineTool({
23
23
  .describe("Max distinct adverse-reaction terms to return, most-reported first (default 15, max 100). totalReports covers the whole drug regardless."),
24
24
  serious: z.boolean().default(false).describe("When true, count only reports flagged serious (death, hospitalization, life-threatening, disabling)."),
25
25
  }),
26
+ describeCall: "none",
26
27
  execute: async ({ drugName, limit = 15, serious = false }) => {
27
28
  const result = await getFaersByDrug(drugName, { limit, serious });
28
29
  return ok({
@@ -63,6 +63,7 @@ export const searchGeoDatasetsTool = defineTool({
63
63
  .default(10)
64
64
  .describe("Max datasets to return (default 10, max 50). totalFound reports the true match count regardless."),
65
65
  }),
66
+ describeCall: "none",
66
67
  execute: async ({ query, organism, datasetType = "gse", limit = 10 }) => {
67
68
  let searchQuery = query;
68
69
  // Escape backslashes first, then quotes, so a `\` in the organism value
@@ -35,6 +35,7 @@ export const searchInteractionsTool = defineTool({
35
35
  .describe(`Max records returned — partners, network edges, or enriched terms depending on action (default ${DEFAULT_LIMIT}, max 500). ` +
36
36
  "Rows are ordered best-first (score descending for interactions, FDR ascending for enrichment), so the default keeps the strongest."),
37
37
  }),
38
+ describeCall: "none",
38
39
  execute: async ({ identifiers, species, action, minScore, limit }) => {
39
40
  if (action === "enrichment") {
40
41
  return ok(await getEnrichment(identifiers, species, { limit }));
@@ -3,11 +3,9 @@
3
3
  * sources that carry it: the curated secondary-pharmacology panel that ships
4
4
  * with the harness, and Open Targets' curated liabilities.
5
5
  *
6
- * These were `check_safety_panel` and `opentargets({action:'safety'})`, and the
7
- * panel tool's own description instructed calling them together — a pairing
8
- * every caller had to remember, expressed in prose that had already gone stale
9
- * (it named two tools that no longer exist). Both are keyed on the same target
10
- * and answer the same question, so the pairing belongs in the tool.
6
+ * Both sources are keyed on the same target and answer the same question, so
7
+ * the pairing belongs in the tool rather than in prose a caller has to
8
+ * remember: one call consults both and merges what they carry.
11
9
  *
12
10
  * Identifier resolution is the tool's job, not the caller's: Open Targets
13
11
  * accepts ONLY an Ensembl gene id, and it is taken from the panel entry when
@@ -41,7 +39,7 @@ export declare const targetSafetyTool: import("../define-tool.js").Tool<{
41
39
  identifiers: string[];
42
40
  identifierType?: "auto" | "gene_symbol" | "uniprot" | "chembl_id" | "ensembl_id" | undefined;
43
41
  sources?: ("opentargets" | "panel")[] | undefined;
44
- filterOrgan?: "hematologic" | "metabolic" | "immune" | "hepatic" | "cns" | "renal" | "respiratory" | "cardiac" | "gi" | undefined;
42
+ filterOrgan?: "hematologic" | "metabolic" | "immune" | "hepatic" | "musculoskeletal" | "cns" | "renal" | "reproductive" | "respiratory" | "cardiac" | "vascular" | "pns" | "gi" | "pancreas" | "endocrine_thyroid" | "dermatologic" | "ocular" | "oncology" | undefined;
45
43
  minSeverity?: "high" | "medium" | "low" | undefined;
46
44
  liabilityLimit?: number | undefined;
47
45
  }, {
@@ -3,11 +3,9 @@
3
3
  * sources that carry it: the curated secondary-pharmacology panel that ships
4
4
  * with the harness, and Open Targets' curated liabilities.
5
5
  *
6
- * These were `check_safety_panel` and `opentargets({action:'safety'})`, and the
7
- * panel tool's own description instructed calling them together — a pairing
8
- * every caller had to remember, expressed in prose that had already gone stale
9
- * (it named two tools that no longer exist). Both are keyed on the same target
10
- * and answer the same question, so the pairing belongs in the tool.
6
+ * Both sources are keyed on the same target and answer the same question, so
7
+ * the pairing belongs in the tool rather than in prose a caller has to
8
+ * remember: one call consults both and merges what they carry.
11
9
  *
12
10
  * Identifier resolution is the tool's job, not the caller's: Open Targets
13
11
  * accepts ONLY an Ensembl gene id, and it is taken from the panel entry when
@@ -15,7 +13,9 @@
15
13
  */
16
14
  import { ok } from "neverthrow";
17
15
  import { z } from "zod";
18
- import { CHEMBL_RE, ORGAN_SYSTEMS, SEVERITIES, SafetyPanelFileSchema, UNIPROT_RE, } from "../../data/safety-panel-schema.js";
16
+ import { ORGAN_SYSTEMS } from "../../contracts/organ-system.js";
17
+ import { SEVERITIES } from "../../contracts/severity.js";
18
+ import { CHEMBL_RE, SafetyPanelFileSchema, UNIPROT_RE } from "../../data/safety-panel-schema.js";
19
19
  import panelData from "../../data/safety-panel.json" with { type: "json" };
20
20
  import { defineTool } from "../define-tool.js";
21
21
  import { resolveSymbolToEnsemblId } from "../lib/ensembl-client.js";
@@ -100,6 +100,7 @@ export const targetSafetyTool = defineTool({
100
100
  "means no CURATED liability, not that none exists. Both are valid no-data — do not retry. Read `perSource` and each row's `opentargetsNote`: a " +
101
101
  "ChEMBL or UniProt identifier absent from the panel cannot be resolved to an Ensembl id, so its Open Targets half is skipped rather than empty.",
102
102
  inputSchema,
103
+ describeCall: "none",
103
104
  execute: async ({ identifiers, identifierType, sources, filterOrgan, minSeverity, liabilityLimit }) => {
104
105
  const selected = sources ?? [...SOURCES];
105
106
  const wantPanel = selected.includes("panel");
@@ -95,20 +95,28 @@ export interface ToolDefinition<Schema extends z.ZodType, Output> {
95
95
  readonly inputSchema: Schema;
96
96
  readonly executionMode?: ToolExecutionMode;
97
97
  /**
98
- * One line naming what THIS call is doing — the call-time counterpart of
98
+ * One line that names what THIS call does — the call-time counterpart of
99
99
  * `description`. `description` self-describes the tool at attach time, so an
100
- * agent knows what it holds; `describeCall` self-describes one invocation, so
100
+ * agent knows what it holds. `describeCall` self-describes one invocation, so
101
101
  * a surface can render four `update_working_memory` calls as four distinct
102
102
  * lines instead of four identical chips.
103
103
  *
104
- * It lives here, beside `inputSchema`, because that is the one place the
105
- * compiler checks the two against each other. A host-side formatter reading
106
- * `input.path` by string key breaks silently the day the schema moves; this
107
- * one fails to build. The tool author writes it because the tool author knows
108
- * which field matters.
104
+ * The decision is REQUIRED. Give a hook, or give the literal `"none"`.
105
+ * `"none"` declares that the input of the tool cannot distinguish its calls,
106
+ * thus a hook can only restate the name of the tool. `defineTool` consumes
107
+ * the sentinel at construction, and the sentinel never reaches a packaged
108
+ * tool. Thus a reader of a `Tool` sees a function, or sees nothing. A hook
109
+ * that returns the string `"none"` is still a hook, because the discriminator
110
+ * is `typeof`, not equality.
109
111
  *
110
- * Synchronous and pure — no I/O, no ambient state. Dispatch is what the user
111
- * waits on, and a description must never be able to fail a call: the loop
112
+ * A hook lives here, beside `inputSchema`, because that is the one place
113
+ * where the compiler checks the two against each other. A host-side formatter
114
+ * that reads `input.path` by string key breaks silently the day the schema
115
+ * moves. This one fails to build. The tool author writes it, because the tool
116
+ * author knows which field matters.
117
+ *
118
+ * A hook is synchronous and pure — no I/O, no ambient state. Dispatch is what
119
+ * the user waits on, and a description must never fail a call: the loop
112
120
  * validates the input against `inputSchema` first, guards the call, and drops
113
121
  * the detail on any failure (see the tool-call-detail capability).
114
122
  *
@@ -116,10 +124,9 @@ export interface ToolDefinition<Schema extends z.ZodType, Output> {
116
124
  * removed, secrets redacted, length capped — happens once at the emit site,
117
125
  * never here.
118
126
  *
119
- * OPTIONAL, and absence is a normal state: a tool without it is observable
120
- * exactly as it is today. The hook never reaches the model.
127
+ * The hook never reaches the model.
121
128
  */
122
- describeCall?(input: z.infer<Schema>): string;
129
+ describeCall: ((input: z.infer<Schema>) => string) | "none";
123
130
  execute(input: z.infer<Schema>, ctx: ToolContext): Promise<Result<Output, ToolError>>;
124
131
  }
125
132
  /**
@@ -44,9 +44,11 @@ export function defineTool(def) {
44
44
  inputSchema: def.inputSchema,
45
45
  jsonSchema,
46
46
  executionMode,
47
- // Spread rather than assigned: a tool that declares no hook carries no
47
+ // Spread rather than assigned: a tool that declines carries no
48
48
  // `describeCall` key at all, so "has a hook" is one property check.
49
- ...(def.describeCall === undefined ? {} : { describeCall: def.describeCall }),
49
+ // `typeof` is the discriminator, not equality with the sentinel, so a
50
+ // hook that returns the string `"none"` still packages as a hook.
51
+ ...(typeof def.describeCall === "function" ? { describeCall: def.describeCall } : {}),
50
52
  execute: def.execute,
51
53
  };
52
54
  }
@@ -61,6 +61,7 @@ export const showUserTool = defineTool({
61
61
  "One card per call, rendered in call order; an identical repeat call resolves to the same card, so compose the " +
62
62
  "finished content before calling rather than re-rendering a draft.",
63
63
  inputSchema: ShowUserInputSchema,
64
+ describeCall: ({ kind, title }) => (title === undefined ? kind : `${kind}: ${title}`),
64
65
  execute: async (input, ctx) => {
65
66
  // `dataPath` is a reference the host resolves at render time; validate its
66
67
  // shape only (existence is a render-time host concern), and treat a