@inflexa-ai/harness 0.17.0 → 0.19.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 (249) hide show
  1. package/dist/agents/report-builder.d.ts +2 -0
  2. package/dist/agents/report-builder.js +2 -0
  3. package/dist/agents/report-session-agent.d.ts +56 -0
  4. package/dist/agents/report-session-agent.js +72 -0
  5. package/dist/app/report-session-runtime.d.ts +72 -0
  6. package/dist/app/report-session-runtime.js +157 -0
  7. package/dist/app/spawn-report-session.d.ts +85 -0
  8. package/dist/app/spawn-report-session.js +83 -0
  9. package/dist/contracts/chat-events.d.ts +9 -0
  10. package/dist/contracts/index.d.ts +6 -2
  11. package/dist/contracts/index.js +3 -1
  12. package/dist/contracts/organ-system.d.ts +49 -0
  13. package/dist/contracts/organ-system.js +67 -0
  14. package/dist/contracts/report-blocks.d.ts +564 -0
  15. package/dist/contracts/report-blocks.js +117 -0
  16. package/dist/contracts/report-reference.d.ts +459 -0
  17. package/dist/contracts/report-reference.js +256 -0
  18. package/dist/contracts/schemas/chat-events.d.ts +2 -0
  19. package/dist/contracts/schemas/chat-events.js +2 -0
  20. package/dist/contracts/severity.d.ts +15 -0
  21. package/dist/contracts/severity.js +10 -0
  22. package/dist/contracts/target-dossier.d.ts +8675 -12972
  23. package/dist/contracts/target-dossier.js +534 -591
  24. package/dist/data/safety-panel-schema.d.ts +18 -5
  25. package/dist/data/safety-panel-schema.js +4 -4
  26. package/dist/execution/artifact-metadata.js +1 -0
  27. package/dist/execution/report-runner.js +1 -3
  28. package/dist/execution/run-synthesis.js +2 -0
  29. package/dist/index.d.ts +6 -0
  30. package/dist/index.js +19 -0
  31. package/dist/lib/async-utils.d.ts +16 -4
  32. package/dist/lib/async-utils.js +20 -15
  33. package/dist/lib/host-concurrency.js +1 -0
  34. package/dist/loop/__fixtures__/scripted-provider.js +23 -2
  35. package/dist/loop/run-agent.js +58 -10
  36. package/dist/loop/tool-detail.d.ts +40 -0
  37. package/dist/loop/tool-detail.js +35 -3
  38. package/dist/loop/types.d.ts +13 -0
  39. package/dist/memory/thread-history.d.ts +10 -0
  40. package/dist/memory/thread-history.js +12 -1
  41. package/dist/memory/thread-store.d.ts +4 -4
  42. package/dist/memory/thread-store.js +18 -9
  43. package/dist/prompts/report-session.d.ts +1 -0
  44. package/dist/prompts/report-session.js +89 -0
  45. package/dist/prompts/target-assessment/briefs/executive-recommendation.d.ts +1 -1
  46. package/dist/prompts/target-assessment/briefs/executive-recommendation.js +5 -5
  47. package/dist/prompts/target-assessment/briefs/liability-bullets.d.ts +1 -1
  48. package/dist/prompts/target-assessment/briefs/liability-bullets.js +1 -1
  49. package/dist/prompts/target-assessment/briefs/target-organ-liabilities.d.ts +1 -1
  50. package/dist/prompts/target-assessment/briefs/target-organ-liabilities.js +11 -3
  51. package/dist/prompts/target-assessment/investigation/adversarial-critique.d.ts +1 -0
  52. package/dist/prompts/target-assessment/investigation/adversarial-critique.js +67 -0
  53. package/dist/prompts/target-assessment/investigation/claim-reverification.d.ts +1 -0
  54. package/dist/prompts/target-assessment/investigation/claim-reverification.js +49 -0
  55. package/dist/prompts/target-assessment/investigation/index.d.ts +3 -0
  56. package/dist/prompts/target-assessment/investigation/index.js +3 -0
  57. package/dist/prompts/target-assessment/investigation/mechanism-proposal.d.ts +1 -0
  58. package/dist/prompts/target-assessment/investigation/mechanism-proposal.js +49 -0
  59. package/dist/prompts/target-assessment/tox-voice/index.d.ts +1 -1
  60. package/dist/prompts/target-assessment/tox-voice/vocabulary.d.ts +8 -5
  61. package/dist/prompts/target-assessment/tox-voice/vocabulary.js +9 -20
  62. package/dist/providers/__fixtures__/session.d.ts +5 -4
  63. package/dist/providers/__fixtures__/session.js +4 -4
  64. package/dist/providers/ai-sdk.d.ts +77 -0
  65. package/dist/providers/ai-sdk.js +149 -19
  66. package/dist/providers/errors.d.ts +14 -0
  67. package/dist/providers/errors.js +103 -18
  68. package/dist/providers/types.d.ts +16 -0
  69. package/dist/providers/types.js +10 -1
  70. package/dist/report-model/block-walk.d.ts +56 -0
  71. package/dist/report-model/block-walk.js +89 -0
  72. package/dist/report-model/draft-finish.d.ts +60 -0
  73. package/dist/report-model/draft-finish.js +56 -0
  74. package/dist/report-model/draft-operations.d.ts +125 -0
  75. package/dist/report-model/draft-operations.js +341 -0
  76. package/dist/report-model/draft-read.d.ts +59 -0
  77. package/dist/report-model/draft-read.js +109 -0
  78. package/dist/report-model/draft.d.ts +51 -0
  79. package/dist/report-model/draft.js +32 -0
  80. package/dist/report-model/fixture-resolver.d.ts +8 -0
  81. package/dist/report-model/fixture-resolver.js +252 -0
  82. package/dist/report-model/pin-snapshot.d.ts +30 -0
  83. package/dist/report-model/pin-snapshot.js +40 -0
  84. package/dist/report-model/reference-resolver.d.ts +113 -0
  85. package/dist/report-model/reference-resolver.js +60 -0
  86. package/dist/report-model/structural-validation.d.ts +22 -0
  87. package/dist/report-model/structural-validation.js +68 -0
  88. package/dist/report-model/validate.d.ts +72 -0
  89. package/dist/report-model/validate.js +95 -0
  90. package/dist/report-render/chart.d.ts +33 -0
  91. package/dist/report-render/chart.js +538 -0
  92. package/dist/report-render/page.d.ts +127 -0
  93. package/dist/report-render/page.js +307 -0
  94. package/dist/report-render/references.d.ts +32 -0
  95. package/dist/report-render/references.js +44 -0
  96. package/dist/report-render/render.d.ts +25 -0
  97. package/dist/report-render/render.js +125 -0
  98. package/dist/report-render/script-json.d.ts +13 -0
  99. package/dist/report-render/script-json.js +15 -0
  100. package/dist/report-render/types.d.ts +39 -0
  101. package/dist/report-render/types.js +10 -0
  102. package/dist/report-render/value-bridge.d.ts +62 -0
  103. package/dist/report-render/value-bridge.js +82 -0
  104. package/dist/report-render/views/chart-view.d.ts +11 -0
  105. package/dist/report-render/views/chart-view.js +23 -0
  106. package/dist/report-render/views/page-view.d.ts +20 -0
  107. package/dist/report-render/views/page-view.js +49 -0
  108. package/dist/report-render/views/prose.d.ts +28 -0
  109. package/dist/report-render/views/prose.js +75 -0
  110. package/dist/report-render/views/references-view.d.ts +20 -0
  111. package/dist/report-render/views/references-view.js +64 -0
  112. package/dist/report-render/views/values.d.ts +42 -0
  113. package/dist/report-render/views/values.js +43 -0
  114. package/dist/runtime/assemble.d.ts +15 -0
  115. package/dist/runtime/assemble.js +28 -6
  116. package/dist/runtime/boot.d.ts +3 -3
  117. package/dist/runtime/boot.js +15 -4
  118. package/dist/runtime/dbos.d.ts +4 -4
  119. package/dist/state/artifacts.d.ts +16 -0
  120. package/dist/state/artifacts.js +18 -0
  121. package/dist/state/init.js +54 -0
  122. package/dist/state/purge-analysis.js +2 -0
  123. package/dist/state/report-session-state.d.ts +112 -0
  124. package/dist/state/report-session-state.js +150 -0
  125. package/dist/state/report-versions.d.ts +129 -0
  126. package/dist/state/report-versions.js +167 -0
  127. package/dist/state/snapshot-parse.d.ts +34 -0
  128. package/dist/state/snapshot-parse.js +82 -0
  129. package/dist/tasks/data-profile.js +1 -0
  130. package/dist/tools/ad-hoc-router.d.ts +7 -2
  131. package/dist/tools/ad-hoc-router.js +2 -1
  132. package/dist/tools/bio/chembl.js +1 -0
  133. package/dist/tools/bio/comptox.js +1 -0
  134. package/dist/tools/bio/drug-gene-interactions.js +1 -0
  135. package/dist/tools/bio/gene-disease-evidence.js +1 -0
  136. package/dist/tools/bio/gene-preclinical-profile.js +1 -0
  137. package/dist/tools/bio/keys.d.ts +3 -2
  138. package/dist/tools/bio/keys.js +3 -2
  139. package/dist/tools/bio/lookup-annotation.js +1 -0
  140. package/dist/tools/bio/opentargets.d.ts +3 -3
  141. package/dist/tools/bio/opentargets.js +4 -3
  142. package/dist/tools/bio/pubchem.js +1 -0
  143. package/dist/tools/bio/search-clinical-trials.js +1 -0
  144. package/dist/tools/bio/search-faers.js +1 -0
  145. package/dist/tools/bio/search-geo-datasets.js +1 -0
  146. package/dist/tools/bio/search-interactions.js +1 -0
  147. package/dist/tools/bio/target-safety.d.ts +4 -6
  148. package/dist/tools/bio/target-safety.js +7 -6
  149. package/dist/tools/define-tool.d.ts +19 -12
  150. package/dist/tools/define-tool.js +4 -2
  151. package/dist/tools/display/show-user.js +1 -0
  152. package/dist/tools/iterate-report.js +2 -0
  153. package/dist/tools/lib/chembl-client.d.ts +2 -0
  154. package/dist/tools/lib/chembl-client.js +1 -0
  155. package/dist/tools/lib/monarch-client.d.ts +42 -0
  156. package/dist/tools/lib/monarch-client.js +100 -0
  157. package/dist/tools/report/build-report.js +1 -0
  158. package/dist/tools/report/mint-preview-url.js +1 -0
  159. package/dist/tools/report/preview-snapshot.js +1 -0
  160. package/dist/tools/report/submit-report.js +1 -0
  161. package/dist/tools/report/version-fs.js +4 -0
  162. package/dist/tools/report-authoring/authoring-tools.d.ts +241 -0
  163. package/dist/tools/report-authoring/authoring-tools.js +460 -0
  164. package/dist/tools/report-session/index.d.ts +8 -0
  165. package/dist/tools/report-session/index.js +8 -0
  166. package/dist/tools/report-session/preview-report.d.ts +90 -0
  167. package/dist/tools/report-session/preview-report.js +245 -0
  168. package/dist/tools/research/context7-docs.js +2 -0
  169. package/dist/tools/research/generate-analogy-report.js +1 -0
  170. package/dist/tools/research/generate-plan.js +7 -2
  171. package/dist/tools/research/inspect-data-profile.js +4 -0
  172. package/dist/tools/research/literature-reviewer.js +1 -0
  173. package/dist/tools/research/search-arxiv.js +1 -0
  174. package/dist/tools/research/search-github-repos.js +1 -0
  175. package/dist/tools/research/search-semantic-scholar.js +1 -0
  176. package/dist/tools/sandbox/list-available-packages.js +23 -0
  177. package/dist/tools/sandbox/list-available-refs.js +36 -0
  178. package/dist/tools/sandbox/report-blocker.js +1 -0
  179. package/dist/tools/sandbox/skills.js +2 -0
  180. package/dist/tools/workspace/file-stat.js +1 -0
  181. package/dist/tools/workspace/show-file.js +2 -0
  182. package/dist/tools/workspace/show-plan.js +1 -0
  183. package/dist/workflows/execute-analysis.d.ts +5 -1
  184. package/dist/workflows/execute-analysis.js +5 -5
  185. package/dist/workflows/execute-target-assessment.d.ts +13 -2
  186. package/dist/workflows/execute-target-assessment.js +78 -16
  187. package/dist/workflows/target-assessment/assemblers/index.d.ts +1 -1
  188. package/dist/workflows/target-assessment/assemblers/index.js +1 -1
  189. package/dist/workflows/target-assessment/assemblers/literature.d.ts +9 -27
  190. package/dist/workflows/target-assessment/assemblers/literature.js +32 -48
  191. package/dist/workflows/target-assessment/assemblers/orchestrator.d.ts +17 -9
  192. package/dist/workflows/target-assessment/assemblers/orchestrator.js +195 -85
  193. package/dist/workflows/target-assessment/assemblers/safety.d.ts +70 -48
  194. package/dist/workflows/target-assessment/assemblers/safety.js +125 -42
  195. package/dist/workflows/target-assessment/collectors/index.d.ts +5 -2
  196. package/dist/workflows/target-assessment/collectors/index.js +48 -5
  197. package/dist/workflows/target-assessment/coverage.d.ts +43 -27
  198. package/dist/workflows/target-assessment/coverage.js +33 -29
  199. package/dist/workflows/target-assessment/fanout/index.js +1 -0
  200. package/dist/workflows/target-assessment/investigation/index.d.ts +97 -0
  201. package/dist/workflows/target-assessment/investigation/index.js +419 -0
  202. package/dist/workflows/target-assessment/lib/approval-precedents.d.ts +26 -5
  203. package/dist/workflows/target-assessment/lib/approval-precedents.js +95 -29
  204. package/dist/workflows/target-assessment/lib/citation-validator.js +1 -1
  205. package/dist/workflows/target-assessment/lib/clinical-consequence-annotator.d.ts +2 -2
  206. package/dist/workflows/target-assessment/lib/clinical-evidence-attribution.d.ts +2 -3
  207. package/dist/workflows/target-assessment/lib/clinical-evidence-attribution.js +0 -4
  208. package/dist/workflows/target-assessment/lib/compute-derived.d.ts +6 -5
  209. package/dist/workflows/target-assessment/lib/compute-derived.js +14 -5
  210. package/dist/workflows/target-assessment/lib/family-complex-supplement.d.ts +5 -6
  211. package/dist/workflows/target-assessment/lib/family-complex-supplement.js +2 -3
  212. package/dist/workflows/target-assessment/lib/fda-label-safety.d.ts +72 -0
  213. package/dist/workflows/target-assessment/lib/fda-label-safety.js +166 -0
  214. package/dist/workflows/target-assessment/lib/heterodimer-filter.d.ts +0 -9
  215. package/dist/workflows/target-assessment/lib/heterodimer-filter.js +0 -10
  216. package/dist/workflows/target-assessment/lib/hpo-organ-map.d.ts +34 -0
  217. package/dist/workflows/target-assessment/lib/hpo-organ-map.js +63 -0
  218. package/dist/workflows/target-assessment/lib/impc-organ-map.d.ts +19 -0
  219. package/dist/workflows/target-assessment/lib/impc-organ-map.js +39 -0
  220. package/dist/workflows/target-assessment/lib/meddra-organ-map.d.ts +24 -14
  221. package/dist/workflows/target-assessment/lib/meddra-organ-map.js +59 -47
  222. package/dist/workflows/target-assessment/lib/recommendation-sanitizer.js +1 -1
  223. package/dist/workflows/target-assessment/lib/regulatory-actions.d.ts +2 -3
  224. package/dist/workflows/target-assessment/lib/regulatory-actions.js +33 -4
  225. package/dist/workflows/target-assessment/lib/safety-corroboration.d.ts +44 -0
  226. package/dist/workflows/target-assessment/lib/safety-corroboration.js +273 -0
  227. package/dist/workflows/target-assessment/lib/target-identity-filter.d.ts +5 -7
  228. package/dist/workflows/target-assessment/lib/target-identity-filter.js +5 -7
  229. package/dist/workflows/target-assessment/metrics.d.ts +1 -6
  230. package/dist/workflows/target-assessment/metrics.js +1 -13
  231. package/dist/workflows/target-assessment/phase4-assemble.d.ts +2147 -807
  232. package/dist/workflows/target-assessment/phase4-assemble.js +19 -19
  233. package/dist/workflows/target-assessment/phase5-persist.d.ts +20 -21
  234. package/dist/workflows/target-assessment/phase5-persist.js +56 -173
  235. package/dist/workflows/target-assessment/schemas.d.ts +46 -1
  236. package/dist/workflows/target-assessment/schemas.js +19 -0
  237. package/dist/workflows/target-assessment/steps/fanout/polypharm-for-one-modulator-step.d.ts +1 -0
  238. package/dist/workflows/target-assessment/steps/fanout/polypharm-for-one-modulator-step.js +2 -0
  239. package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +26 -62
  240. package/dist/workflows/target-assessment/steps/phase2-aggregate.js +0 -4
  241. package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +27 -154
  242. package/dist/workflows/target-assessment/steps/phase3-aggregate.js +0 -6
  243. package/dist/workflows/target-assessment/steps/synthesis/diagnostics.d.ts +0 -6
  244. package/dist/workflows/target-assessment/steps/synthesis/diagnostics.js +0 -18
  245. package/dist/workflows/target-assessment/sub-workflows/per-modulator-polypharm.d.ts +1 -0
  246. package/dist/workflows/target-assessment/synthesis/index.d.ts +52 -5
  247. package/dist/workflows/target-assessment/synthesis/index.js +4 -1
  248. package/dist/workflows/target-assessment/synthesis-probes.d.ts +3 -3
  249. package/package.json +4 -1
@@ -1,8 +1,4 @@
1
1
  import { z } from "zod";
2
- export declare const ORGAN_SYSTEMS: readonly ["cardiac", "hepatic", "renal", "cns", "hematologic", "gi", "respiratory", "metabolic", "immune"];
3
- export declare const SEVERITIES: readonly ["high", "medium", "low"];
4
- export type OrganSystem = (typeof ORGAN_SYSTEMS)[number];
5
- export type Severity = (typeof SEVERITIES)[number];
6
2
  export declare const CHEMBL_RE: RegExp;
7
3
  export declare const UNIPROT_RE: RegExp;
8
4
  export declare const SafetyTargetSchema: z.ZodObject<{
@@ -16,11 +12,20 @@ export declare const SafetyTargetSchema: z.ZodObject<{
16
12
  metabolic: "metabolic";
17
13
  immune: "immune";
18
14
  hepatic: "hepatic";
15
+ musculoskeletal: "musculoskeletal";
19
16
  cns: "cns";
20
17
  renal: "renal";
18
+ reproductive: "reproductive";
21
19
  respiratory: "respiratory";
22
20
  cardiac: "cardiac";
21
+ vascular: "vascular";
22
+ pns: "pns";
23
23
  gi: "gi";
24
+ pancreas: "pancreas";
25
+ endocrine_thyroid: "endocrine_thyroid";
26
+ dermatologic: "dermatologic";
27
+ ocular: "ocular";
28
+ oncology: "oncology";
24
29
  }>;
25
30
  clinical_consequence: z.ZodString;
26
31
  severity: z.ZodEnum<{
@@ -46,11 +51,20 @@ export declare const SafetyPanelFileSchema: z.ZodObject<{
46
51
  metabolic: "metabolic";
47
52
  immune: "immune";
48
53
  hepatic: "hepatic";
54
+ musculoskeletal: "musculoskeletal";
49
55
  cns: "cns";
50
56
  renal: "renal";
57
+ reproductive: "reproductive";
51
58
  respiratory: "respiratory";
52
59
  cardiac: "cardiac";
60
+ vascular: "vascular";
61
+ pns: "pns";
53
62
  gi: "gi";
63
+ pancreas: "pancreas";
64
+ endocrine_thyroid: "endocrine_thyroid";
65
+ dermatologic: "dermatologic";
66
+ ocular: "ocular";
67
+ oncology: "oncology";
54
68
  }>;
55
69
  clinical_consequence: z.ZodString;
56
70
  severity: z.ZodEnum<{
@@ -61,4 +75,3 @@ export declare const SafetyPanelFileSchema: z.ZodObject<{
61
75
  references: z.ZodArray<z.ZodString>;
62
76
  }, z.core.$strip>>;
63
77
  }, z.core.$strip>;
64
- export type SafetyPanelFile = z.infer<typeof SafetyPanelFileSchema>;
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
- export const ORGAN_SYSTEMS = ["cardiac", "hepatic", "renal", "cns", "hematologic", "gi", "respiratory", "metabolic", "immune"];
3
- export const SEVERITIES = ["high", "medium", "low"];
2
+ import { OrganSystemSchema } from "../contracts/organ-system.js";
3
+ import { SeveritySchema } from "../contracts/severity.js";
4
4
  export const CHEMBL_RE = /^CHEMBL\d+$/;
5
5
  // UniProt 6-char accession: letter (any A-Z, covering A-N, O, P, Q, R-Z sub-families),
6
6
  // followed by a digit, three alphanumerics, and a final digit.
@@ -21,9 +21,9 @@ export const SafetyTargetSchema = z.object({
21
21
  uniprot: UniprotSchema,
22
22
  ensembl_gene_id: EnsemblGeneSchema.optional(),
23
23
  name: z.string().min(1).max(120),
24
- organ_system: z.enum(ORGAN_SYSTEMS),
24
+ organ_system: OrganSystemSchema,
25
25
  clinical_consequence: z.string().min(1).max(200),
26
- severity: z.enum(SEVERITIES),
26
+ severity: SeveritySchema,
27
27
  references: z.array(ReferenceSchema).min(1),
28
28
  });
29
29
  export const SafetyPanelFileSchema = z.object({
@@ -95,6 +95,7 @@ function buildSubmitTool(holder, knownPaths) {
95
95
  "remaining}: accepted=true means every file now has metadata — STOP. " +
96
96
  "Otherwise drop the unknownPaths and describe the remaining files.",
97
97
  inputSchema: SubmitFileMetadataInputSchema,
98
+ describeCall: "none",
98
99
  execute: async (input) => {
99
100
  const unknownPaths = [];
100
101
  for (const entry of input.files) {
@@ -39,11 +39,9 @@ import { passthroughStep } from "../loop/run-step.js";
39
39
  import { previewVersionDir, previewDir, latestPreviewVersion } from "../workspace/paths.js";
40
40
  import { withPreviewLock } from "../tools/lib/preview-lock.js";
41
41
  import { createBuildReportTool, createSubmitReportTool, createMintPreviewUrlTool, createPreviewSnapshotTool, createVersionFsTools, } from "../tools/report/index.js";
42
- import { createReportBuilderAgent, REPORT_BUILDER_AGENT_ID } from "../agents/report-builder.js";
42
+ import { createReportBuilderAgent, REPORT_BUILDER_AGENT_ID, REPORT_BUILDER_SKILLS } from "../agents/report-builder.js";
43
43
  import { createSkillTools } from "../tools/sandbox/skills.js";
44
44
  import { createNoopLogger } from "../lib/console-logger.js";
45
- /** Skill pack the report-builder prompt directs the model to read (design-system reference). */
46
- const REPORT_BUILDER_SKILLS = ["report-html"];
47
45
  const REPORT_AGENT_MAX_STEPS = 75;
48
46
  export async function runReportIteration(deps, opts) {
49
47
  const logger = (deps.logger ?? createNoopLogger()).named("report-runner").with({ previewId: opts.previewId });
@@ -270,6 +270,7 @@ function buildValidateTool(ctx) {
270
270
  inputSchema: z.object({
271
271
  synthesis: z.unknown().describe("The candidate synthesis, in any shape. Field-by-field schema: see submit_synthesis."),
272
272
  }),
273
+ describeCall: "none",
273
274
  execute: async (input) => {
274
275
  const result = fullyValidate(input.synthesis, ctx);
275
276
  return ok(result.valid ? { valid: true, issues: [] } : { valid: false, issues: result.issues });
@@ -289,6 +290,7 @@ function buildSubmitTool(holder, ctx) {
289
290
  "already-accepted synthesis untouched; a later accepted submission " +
290
291
  "supersedes an earlier one.",
291
292
  inputSchema: z.object({ synthesis: RunSynthesisSchema }),
293
+ describeCall: "none",
292
294
  execute: async (input) => {
293
295
  const result = fullyValidate(input.synthesis, ctx);
294
296
  if (!result.valid) {
package/dist/index.d.ts CHANGED
@@ -32,6 +32,7 @@ export type { DbosWorkflowPurgerDeps } from "./execution/dbos-workflow-purger.js
32
32
  export type { WorkflowPurger } from "./execution/workflow-purger.js";
33
33
  export { defineTool, isToolError } from "./tools/define-tool.js";
34
34
  export type { Tool, ToolDefinition, ToolContext, ToolError } from "./tools/define-tool.js";
35
+ export { normalizeDetail, DETAIL_MAX_LENGTH } from "./loop/tool-detail.js";
35
36
  export { createCitationResolver } from "./citations/resolve.js";
36
37
  export type { CitationResolverConfig, CitationResolverDependencies, CitationResolverSourceConfig } from "./citations/resolve.js";
37
38
  export { CitationKindSchema, CitationInputSchema, CitationSourceSchema, CitationSourceStatusSchema, CitationResolutionResultSchema, CitationVerdictSchema, CitationCoverageSchema, ComparisonStatusSchema, CitationFieldSchema, UnsupportedWorkKindSchema, CitationIdentifiersSchema, NormalizedCitationMetadataSchema, NormalizedCitationSchema, CitationRecordSchema, IdentifierEvidenceSchema, CitationSourceOutcomeSchema, CitationFieldComparisonSchema, CitationConflictSchema, CitationCandidateClusterSchema, CitationClusterRelationSchema, CitationSourceOperationSchema, CitationSourcePlanItemSchema, } from "./citations/types.js";
@@ -77,6 +78,9 @@ export { PART_REGISTRY, isTransient, isReconciling, isSidebarPart } from "./cont
77
78
  export type { CortexChatPartType, PartDescriptor, PartEmitter, PartConsumer } from "./contracts/part-registry.js";
78
79
  export { launchDbos, shutdownDbos, sweepEphemeralWorkflows } from "./runtime/dbos.js";
79
80
  export type { DbosConfig } from "./runtime/dbos.js";
81
+ export { dbosState } from "./runtime/dbos.js";
82
+ export type { DbosState } from "./runtime/dbos.js";
83
+ export { isDraining } from "./runtime/lifecycle.js";
80
84
  export { StatusString } from "@dbos-inc/dbos-sdk";
81
85
  export type { WorkflowStatusString } from "@dbos-inc/dbos-sdk";
82
86
  export { createPool } from "./lib/storage.js";
@@ -118,6 +122,8 @@ export { registerExecuteAnalysis } from "./workflows/execute-analysis.js";
118
122
  export type { ExecuteAnalysisDeps, ExecuteAnalysisInput, ExecuteAnalysisResult, RunObservation, RunProvenanceEvent, RunStepObservation, StepRuntimeStatus, } from "./workflows/execute-analysis.js";
119
123
  export { registerSandboxStep } from "./workflows/sandbox-step.js";
120
124
  export type { SandboxStepDeps, SandboxStepInput, SandboxStepResult, SandboxAgentBuildContext } from "./workflows/sandbox-step.js";
125
+ export { DEFAULT_CLAIM_INVESTIGATION_CONFIG } from "./workflows/target-assessment/investigation/index.js";
126
+ export type { ClaimInvestigationConfig } from "./workflows/target-assessment/investigation/index.js";
121
127
  export { createRunEventStream } from "./execution/run-event-stream.js";
122
128
  export type { RunEventStream, RunEventStreamDeps, RunEventSubscribeOptions, RunEventPartHandler } from "./execution/run-event-stream.js";
123
129
  export { createSandboxAgents, SANDBOX_AGENT_META } from "./agents/sandbox/index.js";
package/dist/index.js CHANGED
@@ -33,6 +33,15 @@ export { createDbosRunLauncher } from "./execution/dbos-run-launcher.js";
33
33
  export { createDbosWorkflowPurger } from "./execution/dbos-workflow-purger.js";
34
34
  // Tool primitive.
35
35
  export { defineTool, isToolError } from "./tools/define-tool.js";
36
+ // Tool-call-detail authoring aids for an embedder that contributes a host tool.
37
+ // A `describeCall` hook that pre-empts truncation — a count in place of a
38
+ // mid-value cut that would name a file that does not exist — must gate on the
39
+ // SAME bound the harness enforces at the emit site. Thus `DETAIL_MAX_LENGTH`
40
+ // rides the curated surface, so a host reads the one number rather than copying
41
+ // it and drifting when the harness retunes the cap. `normalizeDetail` is that
42
+ // emit-site transform, exported so a host can assert a hook's output against
43
+ // exactly what the harness will render.
44
+ export { normalizeDetail, DETAIL_MAX_LENGTH } from "./loop/tool-detail.js";
36
45
  // Host-agnostic citation verification service and its public contracts.
37
46
  export { createCitationResolver } from "./citations/resolve.js";
38
47
  export { CitationKindSchema, CitationInputSchema, CitationSourceSchema, CitationSourceStatusSchema, CitationResolutionResultSchema, CitationVerdictSchema, CitationCoverageSchema, ComparisonStatusSchema, CitationFieldSchema, UnsupportedWorkKindSchema, CitationIdentifiersSchema, NormalizedCitationMetadataSchema, NormalizedCitationSchema, CitationRecordSchema, IdentifierEvidenceSchema, CitationSourceOutcomeSchema, CitationFieldComparisonSchema, CitationConflictSchema, CitationCandidateClusterSchema, CitationClusterRelationSchema, CitationSourceOperationSchema, CitationSourcePlanItemSchema, } from "./citations/types.js";
@@ -120,6 +129,12 @@ export { PART_REGISTRY, isTransient, isReconciling, isSidebarPart } from "./cont
120
129
  // older releases could leave PENDING `ephemeral:*` rows that recovery would
121
130
  // otherwise re-dispatch. No current workflow creates that prefix.
122
131
  export { launchDbos, shutdownDbos, sweepEphemeralWorkflows } from "./runtime/dbos.js";
132
+ // The readiness pair. Both flags are mutated by harness-owned code an embedder
133
+ // does not call itself — `launchDbos` flips the DBOS state, and the `shutdown`
134
+ // handle `bootHarness` returns marks draining — so the reads have to come from
135
+ // here for a host's own probe to reflect them at all.
136
+ export { dbosState } from "./runtime/dbos.js";
137
+ export { isDraining } from "./runtime/lifecycle.js";
123
138
  // DBOS workflow-status vocabulary. An embedder that reads its own
124
139
  // `dbos.workflow_status` rows must classify them against the SDK's status set,
125
140
  // but must never depend on `@dbos-inc/dbos-sdk` directly — the SDK is
@@ -179,6 +194,10 @@ export { ExecEventMessageSchema } from "./sandbox/types.js";
179
194
  // these, but an embedder running only the run engine registers them directly.
180
195
  export { registerExecuteAnalysis } from "./workflows/execute-analysis.js";
181
196
  export { registerSandboxStep } from "./workflows/sandbox-step.js";
197
+ // Target assessment — the bounds its claim-investigation phase runs under. An
198
+ // embedder wiring `executeTargetAssessment` sets these at its composition root;
199
+ // the dossier reports whichever values were in force.
200
+ export { DEFAULT_CLAIM_INVESTIGATION_CONFIG } from "./workflows/target-assessment/investigation/index.js";
182
201
  // Run-event read seam — the fine-grained durable channel beside `observeRun`.
183
202
  // The two are a deliberate pair, not a duplication: `observeRun` is the coarse,
184
203
  // in-process run/step snapshot a workflow hands its host synchronously, and it
@@ -1,10 +1,22 @@
1
1
  /**
2
- * Shared async utilities — sleep and bounded-concurrency map.
2
+ * Shared async utilities — sleep and a bounded-concurrency `Promise.all`.
3
3
  */
4
4
  /** Promise-based delay. */
5
5
  export declare function sleep(ms: number): Promise<void>;
6
6
  /**
7
- * Map over items with bounded concurrency.
8
- * At most `concurrency` items are processed simultaneously.
7
+ * Run each task with a cap on how many run at the same time, and give back the results in the order of
8
+ * the input.
9
+ *
10
+ * The signature mirrors `Promise.all`. The mapped return type is element-wise, thus a tuple of thunks
11
+ * with different return types keeps each one, and a plain array of thunks gives a plain array. The input
12
+ * is an array of thunks and not an array of promises, because a promise is already running when it is
13
+ * made, and a cap over a set of started promises caps nothing.
14
+ *
15
+ * `concurrency` must be a number at or above 1. p-queue throws a `TypeError` for any other value.
16
+ *
17
+ * The failure behavior is that of `Promise.all`. The first rejection rejects the whole call, and each
18
+ * task that the queue already started still runs to completion.
9
19
  */
10
- export declare function mapConcurrent<T, R>(items: T[], concurrency: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]>;
20
+ export declare function allWithConcurrency<T extends readonly (() => unknown)[] | []>(tasks: T, concurrency: number): Promise<{
21
+ -readonly [P in keyof T]: Awaited<T[P] extends () => infer R ? R : never>;
22
+ }>;
@@ -1,24 +1,29 @@
1
1
  /**
2
- * Shared async utilities — sleep and bounded-concurrency map.
2
+ * Shared async utilities — sleep and a bounded-concurrency `Promise.all`.
3
3
  */
4
+ import PQueue from "p-queue";
4
5
  /** Promise-based delay. */
5
6
  export function sleep(ms) {
6
7
  return new Promise((resolve) => setTimeout(resolve, ms));
7
8
  }
8
9
  /**
9
- * Map over items with bounded concurrency.
10
- * At most `concurrency` items are processed simultaneously.
10
+ * Run each task with a cap on how many run at the same time, and give back the results in the order of
11
+ * the input.
12
+ *
13
+ * The signature mirrors `Promise.all`. The mapped return type is element-wise, thus a tuple of thunks
14
+ * with different return types keeps each one, and a plain array of thunks gives a plain array. The input
15
+ * is an array of thunks and not an array of promises, because a promise is already running when it is
16
+ * made, and a cap over a set of started promises caps nothing.
17
+ *
18
+ * `concurrency` must be a number at or above 1. p-queue throws a `TypeError` for any other value.
19
+ *
20
+ * The failure behavior is that of `Promise.all`. The first rejection rejects the whole call, and each
21
+ * task that the queue already started still runs to completion.
11
22
  */
12
- export async function mapConcurrent(items, concurrency, fn) {
13
- const results = new Array(items.length);
14
- let nextIndex = 0;
15
- async function worker() {
16
- while (nextIndex < items.length) {
17
- const i = nextIndex++;
18
- results[i] = await fn(items[i], i);
19
- }
20
- }
21
- const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => worker());
22
- await Promise.all(workers);
23
- return results;
23
+ export function allWithConcurrency(tasks, concurrency) {
24
+ const queue = new PQueue({ concurrency });
25
+ // `addAll` collapses every task onto one result type, thus it cannot express the element-wise type
26
+ // above. The cast is sound because `addAll` maps the input array through `Promise.all`, which keeps
27
+ // the position of each task, and each element is the awaited return of the task at that position.
28
+ return queue.addAll(tasks);
24
29
  }
@@ -27,6 +27,7 @@ const DEFAULT_LIMITS = {
27
27
  hgnc: 6,
28
28
  uniprot: 6,
29
29
  iuphar: 4,
30
+ monarch: 3,
30
31
  ema: 2,
31
32
  pubchem: 4,
32
33
  "annotation-llm": 4,
@@ -29,6 +29,17 @@ export function thinkingBlock(thinking, signature) {
29
29
  export function toolUseBlock(id, name, input) {
30
30
  return { type: "tool-call", toolCallId: id, toolName: name, input };
31
31
  }
32
+ /**
33
+ * The text a real provider would have streamed for this reply — one delta per
34
+ * text block. Reasoning and tool-call blocks carry no text on the wire, so they
35
+ * arrive only in the terminal `done` response.
36
+ */
37
+ function textDeltas(response) {
38
+ const content = response.message.content;
39
+ if (typeof content === "string")
40
+ return content === "" ? [] : [content];
41
+ return content.filter((part) => part.type === "text").map((part) => part.text);
42
+ }
32
43
  export function scriptedProvider(script) {
33
44
  const calls = [];
34
45
  const sessions = [];
@@ -51,8 +62,18 @@ export function scriptedProvider(script) {
51
62
  sessions.push(session);
52
63
  return okAsync(reply(i, request));
53
64
  },
54
- chatStream() {
55
- throw new Error("scriptedProvider: chatStream is not used by runAgent");
65
+ chatStream(request, session) {
66
+ // The call is recorded and the reply resolved eagerly, as in `chat`, so
67
+ // an unscripted call fails at the same point either primitive is driven.
68
+ const i = calls.length;
69
+ calls.push(request);
70
+ sessions.push(session);
71
+ const response = reply(i, request);
72
+ return (async function* () {
73
+ for (const text of textDeltas(response))
74
+ yield { type: "text-delta", text };
75
+ yield { type: "done", response };
76
+ })();
56
77
  },
57
78
  };
58
79
  }
@@ -155,8 +155,12 @@ export async function runAgent(agent, initial, session, opts) {
155
155
  * in what they count. An error tool result is otherwise invisible to every sink —
156
156
  * it is not a thrown failure, so nothing reports it and the loop simply feeds it
157
157
  * back — and a run that ends badly is usually a run whose tool calls were failing.
158
+ *
159
+ * `details` and `durations` are positionally aligned with `calls`. `dispatchTools`
160
+ * measures a duration around the call itself, because this sink emits every finish
161
+ * event only after the whole round settles.
158
162
  */
159
- const settleRound = async (calls, results, details) => {
163
+ const settleRound = async (calls, results, details, durations) => {
160
164
  const errored = [];
161
165
  for (let idx = 0; idx < calls.length; idx++) {
162
166
  const tu = calls[idx];
@@ -169,7 +173,15 @@ export async function runAgent(agent, initial, session, opts) {
169
173
  toolErrorCount++;
170
174
  errored.push(tu.toolName);
171
175
  }
172
- await emit({ type: "tool-finished", source, toolUseId: tu.toolCallId, name: tu.toolName, outcome, ...detailField(details[idx]) });
176
+ await emit({
177
+ type: "tool-finished",
178
+ source,
179
+ toolUseId: tu.toolCallId,
180
+ name: tu.toolName,
181
+ outcome,
182
+ ...detailField(details[idx]),
183
+ ...durationField(durations[idx]),
184
+ });
173
185
  }
174
186
  return errored;
175
187
  };
@@ -226,8 +238,8 @@ export async function runAgent(agent, initial, session, opts) {
226
238
  for (const [idx, tu] of earlier.entries()) {
227
239
  await emit({ type: "tool-started", source, toolUseId: tu.toolCallId, name: tu.toolName, input: tu.input, ...detailField(earlierDetails[idx]) });
228
240
  }
229
- const results = await dispatchTools(earlier, toolsById, toolCtx, isFatalLoopError, runStep, formatStepName.tool);
230
- const errored = await settleRound(earlier, results, earlierDetails);
241
+ const { results, durations } = await dispatchTools(earlier, toolsById, toolCtx, isFatalLoopError, runStep, formatStepName.tool);
242
+ const errored = await settleRound(earlier, results, earlierDetails, durations);
231
243
  results.push(errorResult(trailing, TRUNCATED_TOOL_USE_ERROR));
232
244
  // The trailing call was never dispatched, but it reaches the model as an error
233
245
  // result like any other — so it counts, or `toolErrors` reports a cleaner run
@@ -256,8 +268,8 @@ export async function runAgent(agent, initial, session, opts) {
256
268
  for (const [idx, tu] of toolCalls.entries()) {
257
269
  await emit({ type: "tool-started", source, toolUseId: tu.toolCallId, name: tu.toolName, input: tu.input, ...detailField(details[idx]) });
258
270
  }
259
- const results = await dispatchTools(toolCalls, toolsById, toolCtx, isFatalLoopError, runStep, formatStepName.tool);
260
- const errored = await settleRound(toolCalls, results, details);
271
+ const { results, durations } = await dispatchTools(toolCalls, toolsById, toolCtx, isFatalLoopError, runStep, formatStepName.tool);
272
+ const errored = await settleRound(toolCalls, results, details, durations);
261
273
  if (errored.length > 0)
262
274
  log.debug("tool results returned errors", { iteration: i, tools: errored });
263
275
  messages.push({ role: "tool", content: results });
@@ -373,8 +385,22 @@ function markLastLoopAssistant(messages, initialCount) {
373
385
  }
374
386
  }
375
387
  }
388
+ /**
389
+ * Dispatch one round of tool calls, and measure the time of each call.
390
+ *
391
+ * `results` and `durations` are positionally aligned with `toolUses`.
392
+ *
393
+ * Each measurement brackets the same unit that the loop awaits for that call. For a
394
+ * step-mode call that unit is `runStep`, thus the figure includes the durable-step
395
+ * wrapper. The wrapper is part of what the call cost, and a cached replay of a step
396
+ * is genuinely fast. A bracket inside the step would report a body that did not run.
397
+ */
376
398
  async function dispatchTools(toolUses, toolsById, toolCtx, isFatalLoopError, runStep, toolStepName) {
377
399
  const results = new Array(toolUses.length);
400
+ // The array starts with holes, which read as `undefined`. The element type
401
+ // says so, thus it agrees with what `settleRound` accepts. Every index is in
402
+ // fact assigned, because the mode partition below covers each call.
403
+ const durations = new Array(toolUses.length);
378
404
  const stepTools = [];
379
405
  const workflowTools = [];
380
406
  const inlineTools = [];
@@ -387,16 +413,28 @@ async function dispatchTools(toolUses, toolsById, toolCtx, isFatalLoopError, run
387
413
  else
388
414
  stepTools.push({ tu, idx });
389
415
  }
390
- await Promise.all(stepTools.map(({ tu, idx }) => runStep(toolStepName(tu.toolName, tu.toolCallId), () => dispatchTool(tu, toolsById, toolCtx(tu), isFatalLoopError)).then((r) => {
391
- results[idx] = r;
392
- })));
416
+ await Promise.all(stepTools.map(({ tu, idx }) => {
417
+ const startedAt = performance.now();
418
+ return runStep(toolStepName(tu.toolName, tu.toolCallId), () => dispatchTool(tu, toolsById, toolCtx(tu), isFatalLoopError)).then((r) => {
419
+ durations[idx] = elapsedMs(startedAt);
420
+ results[idx] = r;
421
+ });
422
+ }));
393
423
  for (const { tu, idx } of workflowTools) {
424
+ const startedAt = performance.now();
394
425
  results[idx] = await dispatchTool(tu, toolsById, toolCtx(tu), isFatalLoopError);
426
+ durations[idx] = elapsedMs(startedAt);
395
427
  }
396
428
  for (const { tu, idx } of inlineTools) {
429
+ const startedAt = performance.now();
397
430
  results[idx] = await dispatchTool(tu, toolsById, toolCtx(tu), isFatalLoopError);
431
+ durations[idx] = elapsedMs(startedAt);
398
432
  }
399
- return results;
433
+ return { results, durations };
434
+ }
435
+ /** Whole milliseconds since `startedAt`, on the monotonic clock that took that mark. */
436
+ function elapsedMs(startedAt) {
437
+ return Math.round(performance.now() - startedAt);
400
438
  }
401
439
  async function dispatchTool(tu, toolsById, ctx, isFatalLoopError) {
402
440
  const tool = toolsById.get(tu.toolName);
@@ -500,6 +538,16 @@ function outcomeOf(result) {
500
538
  function detailField(detail) {
501
539
  return detail === undefined ? {} : { detail };
502
540
  }
541
+ /**
542
+ * The optional `durationMs` field, present only for a call that the loop measured.
543
+ *
544
+ * Spread rather than assigned, for the same reason as {@link detailField}: an
545
+ * unmeasured call emits no `durationMs` key. A zero here is a real measurement of a
546
+ * call that took under half a millisecond, and it is never a stand-in for no figure.
547
+ */
548
+ function durationField(durationMs) {
549
+ return durationMs === undefined ? {} : { durationMs };
550
+ }
503
551
  function hasDenial(results) {
504
552
  return results.some((r) => r.output.type === "execution-denied");
505
553
  }
@@ -25,6 +25,46 @@ export type { ToolCallDetail };
25
25
  * channel.
26
26
  */
27
27
  export declare const DETAIL_MAX_LENGTH = 120;
28
+ /**
29
+ * The bound a hook gives a free-form needle that it marks inside a detail.
30
+ *
31
+ * A needle is model-authored text of no fixed length, thus it is the one part of
32
+ * a detail that can run away. Left to {@link DETAIL_MAX_LENGTH}, a long needle
33
+ * takes the whole line, and the cut lands inside the mark that names it: the
34
+ * closing quote goes, and a second fact behind it goes with no trace. A hook
35
+ * bounds the needle first. Then the mark always closes, and the rest of the
36
+ * detail keeps its room.
37
+ *
38
+ * 32 code points shows the shape of a search — enough to recognize the needle a
39
+ * caller typed, and short enough that a target beside it survives whole.
40
+ */
41
+ export declare const DETAIL_NEEDLE_MAX_LENGTH = 32;
42
+ /**
43
+ * Cap `text` at `max` CODE POINTS, not UTF-16 units.
44
+ *
45
+ * A hook calls this to pre-empt the emit-site cap: that cap cuts the TAIL, so a
46
+ * detail with two parts loses the second one when the first runs long. One
47
+ * implementation serves both sites, thus a hook's own bound cuts and marks
48
+ * exactly as {@link normalizeDetail} does.
49
+ *
50
+ * `String.prototype.slice` cuts at a fixed unit index, which can land between the
51
+ * two halves of a surrogate pair and emit a lone surrogate — a terminal paints
52
+ * that as a replacement character, so the cap would corrupt the very tail it is
53
+ * supposed to merely shorten. Iterating the string yields whole code points.
54
+ *
55
+ * Code points, not display columns, is the right unit here: this cap is a safety
56
+ * valve against a runaway hook, so its job is to bound the payload. A column
57
+ * measurement would claim knowledge of the renderer's font metrics that the
58
+ * harness does not have — the host measures columns, because only the host knows
59
+ * how wide its own line is.
60
+ *
61
+ * A cut text carries `…` so that a reader can tell a shortened detail from a
62
+ * complete one. The mark costs one of the `max` code points, thus the cut goes to
63
+ * `max - 1`: a hard bound that overshoots by the width of its own mark is not a
64
+ * hard bound. `trimEnd` runs BEFORE the append, because a cut that lands on a
65
+ * space must give `word…` and not `word …`.
66
+ */
67
+ export declare function capCodePoints(text: string, max: number): string;
28
68
  /**
29
69
  * Normalize whatever a hook returned into an emittable detail, or `undefined`.
30
70
  *
@@ -22,9 +22,28 @@ import { normalizeUnicode, redactSecrets } from "../input-sanitization.js";
22
22
  * channel.
23
23
  */
24
24
  export const DETAIL_MAX_LENGTH = 120;
25
+ /**
26
+ * The bound a hook gives a free-form needle that it marks inside a detail.
27
+ *
28
+ * A needle is model-authored text of no fixed length, thus it is the one part of
29
+ * a detail that can run away. Left to {@link DETAIL_MAX_LENGTH}, a long needle
30
+ * takes the whole line, and the cut lands inside the mark that names it: the
31
+ * closing quote goes, and a second fact behind it goes with no trace. A hook
32
+ * bounds the needle first. Then the mark always closes, and the rest of the
33
+ * detail keeps its room.
34
+ *
35
+ * 32 code points shows the shape of a search — enough to recognize the needle a
36
+ * caller typed, and short enough that a target beside it survives whole.
37
+ */
38
+ export const DETAIL_NEEDLE_MAX_LENGTH = 32;
25
39
  /**
26
40
  * Cap `text` at `max` CODE POINTS, not UTF-16 units.
27
41
  *
42
+ * A hook calls this to pre-empt the emit-site cap: that cap cuts the TAIL, so a
43
+ * detail with two parts loses the second one when the first runs long. One
44
+ * implementation serves both sites, thus a hook's own bound cuts and marks
45
+ * exactly as {@link normalizeDetail} does.
46
+ *
28
47
  * `String.prototype.slice` cuts at a fixed unit index, which can land between the
29
48
  * two halves of a surrogate pair and emit a lone surrogate — a terminal paints
30
49
  * that as a replacement character, so the cap would corrupt the very tail it is
@@ -35,10 +54,19 @@ export const DETAIL_MAX_LENGTH = 120;
35
54
  * measurement would claim knowledge of the renderer's font metrics that the
36
55
  * harness does not have — the host measures columns, because only the host knows
37
56
  * how wide its own line is.
57
+ *
58
+ * A cut text carries `…` so that a reader can tell a shortened detail from a
59
+ * complete one. The mark costs one of the `max` code points, thus the cut goes to
60
+ * `max - 1`: a hard bound that overshoots by the width of its own mark is not a
61
+ * hard bound. `trimEnd` runs BEFORE the append, because a cut that lands on a
62
+ * space must give `word…` and not `word …`.
38
63
  */
39
- function capCodePoints(text, max) {
64
+ export function capCodePoints(text, max) {
40
65
  const points = Array.from(text);
41
- return points.length <= max ? text : points.slice(0, max).join("").trimEnd();
66
+ if (points.length <= max)
67
+ return text;
68
+ const cut = points.slice(0, max - 1).join("");
69
+ return `${cut.trimEnd()}…`;
42
70
  }
43
71
  /**
44
72
  * Normalize whatever a hook returned into an emittable detail, or `undefined`.
@@ -78,8 +106,12 @@ export function normalizeDetail(raw) {
78
106
  * second time from the repaired value costs more than the case is worth.
79
107
  */
80
108
  export function computeDetail(tool, rawInput, log) {
109
+ // The guard tests for a function, not for presence. A packaged tool comes from
110
+ // an open list — an embedder contributes its own through the host-tools seam —
111
+ // so a `describeCall` that is not callable is reachable. Such a tool counts as
112
+ // undescribed, because the alternative is a TypeError on every one of its calls.
81
113
  const describeCall = tool.describeCall;
82
- if (describeCall === undefined)
114
+ if (typeof describeCall !== "function")
83
115
  return undefined;
84
116
  try {
85
117
  // The parse is INSIDE the guard, not before it. `safeParse` returns an
@@ -81,6 +81,19 @@ export type EmitEvent = {
81
81
  readonly outcome: ToolOutcome;
82
82
  /** The same detail the matching `tool-started` carried. */
83
83
  readonly detail?: ToolCallDetail;
84
+ /**
85
+ * The time around this call's own dispatch, in milliseconds.
86
+ *
87
+ * A host cannot measure this itself. The loop emits every
88
+ * `tool-started` of a round before it dispatches anything, and every
89
+ * `tool-finished` after the round settles. Thus a host that brackets
90
+ * the two events measures the round, and each call of a multi-call
91
+ * round reports the same figure.
92
+ *
93
+ * The field is optional, and it is absent when no measurement was
94
+ * taken. As a result a host can fall back to its own observation.
95
+ */
96
+ readonly durationMs?: number;
84
97
  };
85
98
  /**
86
99
  * A UI presentation event a tool streams to chat — an AI-SDK `data-*`
@@ -161,6 +161,16 @@ export interface ThreadHistory {
161
161
  * empty thread reports `empty-thread`.
162
162
  */
163
163
  retractLastTurn(threadId: string): ResultAsync<RetractOutcome, DbError>;
164
+ /**
165
+ * The greatest `messages.seq` in the thread, or `null` when the thread holds
166
+ * no messages. The read takes no lock: it reports the tail at the read
167
+ * moment, and a concurrent append can move the tail one row later.
168
+ *
169
+ * `appendTurn` computes the same value under its lock to place the next row.
170
+ * This read shares the table, not that statement. A lock here buys a caller
171
+ * no guarantee, because the tail can move one append later without it.
172
+ */
173
+ latestSeq(threadId: string): ResultAsync<number | null, DbError>;
164
174
  }
165
175
  /**
166
176
  * Drop the memoized instruments so the next `loadRecent` rebinds to a
@@ -418,5 +418,16 @@ export function createThreadHistory(pool) {
418
418
  return tryMutation("thread-history.retractLastTurn.delete", () => client.query("DELETE FROM messages WHERE thread_id = $1 AND seq >= $2::bigint", [threadId, boundary])).map((res) => ({ kind: "retracted", messages: res.rowCount ?? 0 }));
419
419
  }));
420
420
  }
421
- return { appendTurn, loadRecent, loadPage, retractLastTurn };
421
+ function latestSeq(threadId) {
422
+ // `MAX(seq)` over no rows is SQL NULL, so a thread with no messages reads
423
+ // as `null`, not as a seq — the absence a caller reads before it treats
424
+ // the value as an anchor. The `::text` cast carries a seq past 2^53 across
425
+ // the driver intact, the way every other read of this bigint column does,
426
+ // and `Number` makes the crossing in one place.
427
+ return tryQuery("thread-history.latestSeq", () => pool.query("SELECT MAX(seq)::text AS max_seq FROM messages WHERE thread_id = $1", [threadId])).map(({ rows }) => {
428
+ const maxSeq = rows[0]?.max_seq ?? null;
429
+ return maxSeq === null ? null : Number(maxSeq);
430
+ });
431
+ }
432
+ return { appendTurn, loadRecent, loadPage, retractLastTurn, latestSeq };
422
433
  }
@@ -22,10 +22,10 @@
22
22
  * row and the thread reads as it did; a listing widened with `includeArchived`
23
23
  * is the only way to obtain the id it takes, so the recovery is reachable by a
24
24
  * host that holds no thread ids it obtained elsewhere.
25
- * `purgeThread` is not recoverable: it removes the `messages` rows and the
26
- * metadata rows of the whole subtree in one transaction, so a failure partway
27
- * leaves both — never a thread stripped of its transcript, nor a transcript
28
- * with nothing naming it. It is the thread-scoped member of this package's
25
+ * `purgeThread` is not recoverable: it removes the `messages` rows, the report
26
+ * session-state rows, and the metadata rows of the whole subtree in one
27
+ * transaction, so a failure partway leaves them all — never a thread stripped of
28
+ * its transcript, nor a transcript with nothing naming it. It is the thread-scoped member of this package's
29
29
  * reclamation vocabulary — `purgeAnalysis` reclaims an analysis's whole
30
30
  * persisted footprint, `purgeThread` one subtree's.
31
31
  *