@inflexa-ai/harness 0.48.2 → 0.48.4

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 (85) hide show
  1. package/dist/agents/sandbox/index.js +3 -0
  2. package/dist/agents/sandbox/mdv-preparation-agent.d.ts +9 -0
  3. package/dist/agents/sandbox/mdv-preparation-agent.js +37 -0
  4. package/dist/app/chat-turn-workflow.d.ts +3 -1
  5. package/dist/app/chat-turn-workflow.js +19 -3
  6. package/dist/app/chat-turn.d.ts +4 -2
  7. package/dist/app/chat-turn.js +70 -44
  8. package/dist/app/in-process-turn.js +3 -6
  9. package/dist/app/work.d.ts +7 -3
  10. package/dist/app/work.js +17 -16
  11. package/dist/contracts/chat-events.d.ts +5 -0
  12. package/dist/contracts/chat-parts.d.ts +64 -5
  13. package/dist/contracts/chat-parts.js +3 -1
  14. package/dist/contracts/index.d.ts +1 -1
  15. package/dist/contracts/part-registry.js +2 -0
  16. package/dist/contracts/schemas/chat-events.d.ts +2 -0
  17. package/dist/contracts/schemas/chat-events.js +1 -0
  18. package/dist/contracts/schemas/chat-parts.d.ts +197 -5
  19. package/dist/contracts/schemas/chat-parts.js +32 -2
  20. package/dist/index.d.ts +1 -1
  21. package/dist/lib/regulatory-corpus/fetch-ich.js +1 -16
  22. package/dist/lib/slug.d.ts +18 -0
  23. package/dist/lib/slug.js +29 -0
  24. package/dist/lib/test/pdf-fixture.d.ts +5 -0
  25. package/dist/lib/test/pdf-fixture.js +14 -0
  26. package/dist/loop/chat-stream.js +1 -1
  27. package/dist/loop/run-loop.js +39 -35
  28. package/dist/loop/sub-agent.d.ts +2 -2
  29. package/dist/loop/sub-agent.js +1 -1
  30. package/dist/loop/test/metric-reader.d.ts +9 -0
  31. package/dist/loop/test/metric-reader.js +33 -0
  32. package/dist/loop/test/unnamed-model.d.ts +6 -0
  33. package/dist/loop/test/unnamed-model.js +24 -0
  34. package/dist/memory/conversation-display-recorder.d.ts +2 -0
  35. package/dist/memory/conversation-display-recorder.js +6 -2
  36. package/dist/memory/conversation-display-storage.d.ts +5 -2
  37. package/dist/memory/conversation-display-storage.js +28 -1
  38. package/dist/prompts/conversation.d.ts +1 -1
  39. package/dist/prompts/conversation.js +6 -4
  40. package/dist/prompts/sandbox/mdv-preparation-agent.d.ts +1 -0
  41. package/dist/prompts/sandbox/mdv-preparation-agent.js +56 -0
  42. package/dist/providers/ai-sdk.d.ts +17 -14
  43. package/dist/providers/ai-sdk.js +14 -16
  44. package/dist/report-model/fixture-resolver.js +9 -135
  45. package/dist/report-model/production-resolver.d.ts +3 -3
  46. package/dist/report-model/production-resolver.js +12 -127
  47. package/dist/report-model/reference-read.d.ts +38 -0
  48. package/dist/report-model/reference-read.js +149 -0
  49. package/dist/report-model/reference-resolver.d.ts +7 -0
  50. package/dist/report-model/reference-resolver.js +1 -1
  51. package/dist/report-render/chart.js +48 -46
  52. package/dist/report-render/figures/common.d.ts +34 -0
  53. package/dist/report-render/figures/common.js +4 -1
  54. package/dist/report-render/figures/equal-units.js +2 -4
  55. package/dist/report-render/figures/sankey.js +1 -3
  56. package/dist/report-render/figures/upset.js +2 -5
  57. package/dist/runtime/assemble.js +2 -1
  58. package/dist/sandbox/execution-id.js +2 -14
  59. package/dist/sandbox/leases.js +46 -9
  60. package/dist/schemas/validate-plan.js +2 -14
  61. package/dist/state/active-turns.d.ts +6 -2
  62. package/dist/state/active-turns.js +11 -7
  63. package/dist/state/data-profile.d.ts +8 -0
  64. package/dist/state/data-profile.js +18 -0
  65. package/dist/state/index.d.ts +1 -1
  66. package/dist/state/index.js +1 -1
  67. package/dist/state/migrations/20261102120000_record_the_waiter_of_a_pause.d.ts +6 -0
  68. package/dist/state/migrations/20261102120000_record_the_waiter_of_a_pause.js +10 -0
  69. package/dist/state/migrations/index.js +2 -0
  70. package/dist/state/paused-work.d.ts +5 -3
  71. package/dist/state/paused-work.js +11 -7
  72. package/dist/state/runs.d.ts +3 -2
  73. package/dist/state/runs.js +4 -3
  74. package/dist/tasks/data-profile-index.js +3 -17
  75. package/dist/tasks/data-profile-resolve.js +3 -17
  76. package/dist/tasks/data-profile.js +1 -0
  77. package/dist/tools/lib/opentargets-client.js +15 -25
  78. package/dist/tools/report-authoring/authoring-tools.d.ts +3 -3
  79. package/dist/tools/report-authoring/authoring-tools.js +7 -6
  80. package/dist/tools/research/generate-analogy-report.js +10 -2
  81. package/dist/workflows/execute-analysis.js +4 -3
  82. package/dist/workflows/pause.d.ts +3 -3
  83. package/dist/workflows/pause.js +15 -9
  84. package/dist/workflows/sandbox-step.js +1 -0
  85. package/package.json +7 -4
@@ -70,12 +70,16 @@ const DESTINATION_NOTES = {
70
70
  after: "The id of a sibling block. The block lands directly after it.",
71
71
  };
72
72
  /** The flat destination fields. An anchor is `before` or `after`, and a place is `start` or `end`. */
73
- const addBlockInput = z.object({
74
- block: blockPayload,
73
+ const destinationFields = {
75
74
  parentId: absentable(z.string()).describe(DESTINATION_NOTES.parentId),
76
75
  place: absentable(z.enum(["start", "end"])).describe(DESTINATION_NOTES.place),
77
76
  before: absentable(z.string()).describe(DESTINATION_NOTES.before),
78
77
  after: absentable(z.string()).describe(DESTINATION_NOTES.after),
78
+ };
79
+ /** The flat destination fields of the add, plus the payload of the new block. */
80
+ const addBlockInput = z.object({
81
+ block: blockPayload,
82
+ ...destinationFields,
79
83
  });
80
84
  /**
81
85
  * The change payload is a title for a section target, or a block for an atom target, and exactly one of
@@ -92,10 +96,7 @@ const removeBlockInput = z.object({
92
96
  });
93
97
  const moveBlockInput = z.object({
94
98
  targetId: z.string().describe("The id of the block to move, as the outline gives it."),
95
- parentId: absentable(z.string()).describe(DESTINATION_NOTES.parentId),
96
- place: absentable(z.enum(["start", "end"])).describe(DESTINATION_NOTES.place),
97
- before: absentable(z.string()).describe(DESTINATION_NOTES.before),
98
- after: absentable(z.string()).describe(DESTINATION_NOTES.after),
99
+ ...destinationFields,
99
100
  });
100
101
  const readOutlineInput = z.object({});
101
102
  /** The read names its target with the same field name as each mutation, thus one id key serves them all. */
@@ -260,7 +260,9 @@ export function createGenerateAnalogyReportTool(deps, registerSubAgent) {
260
260
  "drives a multi-step research sub-agent (up to 40 model turns) over live " +
261
261
  "literature search, so it is slow; a fact that one lookup answers does " +
262
262
  "not need it.\n" +
263
- "RETURNS an AnalogyReport envelope the UI renders as an inline card. An " +
263
+ "RETURNS an AnalogyReport envelope. The analogy block shows the full " +
264
+ "report to the user, thus your reply names the analogies that matter by " +
265
+ "their `analogyTitle` and does not copy the report. An " +
264
266
  "analogy the search could not fill carries a `coverage` tag " +
265
267
  "(`queried_no_data` | `search_failed` | `not_loaded`) with an empty " +
266
268
  "`solutions` array — a partial result, not an error. A top-level `error` " +
@@ -273,7 +275,13 @@ export function createGenerateAnalogyReportTool(deps, registerSubAgent) {
273
275
  view: "none",
274
276
  describeCall: "none",
275
277
  startsWorkflow: true,
276
- execute: async (input, ctx) => ok(await research(input, ctx)),
278
+ execute: async (input, ctx) => {
279
+ const report = await research(input, ctx);
280
+ // The execute runs in the body of the turn, where a stream write is checkpointed, thus a replay gives no second
281
+ // part. The root session keeps the part in the stored display, which drops each frame of a sub-agent.
282
+ await ctx.emit({ type: "data-analogy-report", source: ctx.session.provenance, data: { id: ctx.invocationId, report } });
283
+ return ok(report);
284
+ },
277
285
  });
278
286
  }
279
287
  /** The envelope of a run that recorded no outcome, also after its salvage. */
@@ -590,17 +590,18 @@ export async function runExecuteAnalysisBody(input, deps) {
590
590
  // the run authorization is already minted and rides in `input.runSession`.
591
591
  const runId = input.runSession.runFrame.runId;
592
592
  const workflowId = runId;
593
- const pause = workPause({
593
+ const pauseAs = (waiter) => workPause({
594
594
  pool: deps.pool,
595
595
  workId: runId,
596
596
  workKind: "run",
597
+ waiter,
597
598
  analysisId: input.analysisId,
598
599
  emit: emitStreamPart,
599
600
  retryDelaysMs: deps.pauseRetryDelaysMs,
600
601
  run: { session: input.runSession, authorizer: deps.runAuthorizer },
601
602
  logger,
602
603
  });
603
- const init = await validateAndInit(input, runId, deps, pause);
604
+ const init = await validateAndInit(input, runId, deps, pauseAs({ kind: "run" }));
604
605
  if (init.kind === "joined-existing") {
605
606
  return {
606
607
  runId: init.runId,
@@ -728,7 +729,7 @@ export async function runExecuteAnalysisBody(input, deps) {
728
729
  completedSteps: [...final.completed],
729
730
  session: input.runSession,
730
731
  deps,
731
- pause,
732
+ pause: pauseAs({ kind: "synthesis" }),
732
733
  });
733
734
  synthesisFindings = synthOut.findings;
734
735
  synthesisOutcome = { status: synthOut.synthesisStatus, reason: synthOut.synthesisReason };
@@ -11,7 +11,7 @@
11
11
  * with the message of a resume, or with a stop. The server never sends that message by itself.
12
12
  */
13
13
  import type { RunSession } from "../auth/types.js";
14
- import type { PauseCause, WorkKind, PausePart } from "../contracts/chat-parts.js";
14
+ import type { PauseCause, PauseEnd, PauseWaiter, WorkKind, PausePart } from "../contracts/chat-parts.js";
15
15
  import type { RunAuthorizer } from "../execution/run-authorizer.js";
16
16
  import type { Logger } from "../lib/logger.js";
17
17
  import type { Querier } from "../state/db.js";
@@ -29,14 +29,14 @@ export declare const HOLD_MESSAGE: {
29
29
  export declare function pauseTopic(pauseId: string): string;
30
30
  /** The cause of a pause in a failure, or `undefined` for a failure that no person or time can repair. */
31
31
  export declare function pauseCauseOf(failure: unknown): PauseCause | undefined;
32
- /** How a wait ended: the deadline of `retrying`, the message of a resume, or a stop of the work. */
33
- export type PauseEnd = "retry" | "resume" | "abort";
34
32
  /** The work that a pause belongs to, and where the part of the pause goes. */
35
33
  export type PauseScope = {
36
34
  readonly pool: Querier;
37
35
  /** The turn id, the run id, or the id of the profile workflow. A sub-agent names its turn, and a step its run. */
38
36
  readonly workId: string;
39
37
  readonly workKind: WorkKind;
38
+ /** What waits in this workflow. */
39
+ readonly waiter: PauseWaiter;
40
40
  readonly analysisId: string;
41
41
  readonly threadId?: string;
42
42
  /** Writes the part of the pause to the stream of the work. */
@@ -96,10 +96,11 @@ export function workPause(scope) {
96
96
  const delayMs = cause.kind === "outage" ? delays[attempt] : undefined;
97
97
  const state = delayMs === undefined ? "paused" : "retrying";
98
98
  const pauseId = await DBOS.randomUUID();
99
- const pausedAt = await DBOS.now();
100
- const retryAt = delayMs === undefined ? undefined : new Date(pausedAt + delayMs).toISOString();
99
+ const pausedAtMs = await DBOS.now();
100
+ const pausedAt = new Date(pausedAtMs).toISOString();
101
+ const retryAt = delayMs === undefined ? undefined : new Date(pausedAtMs + delayMs).toISOString();
101
102
  const topic = pauseTopic(pauseId);
102
- const work = { workId: scope.workId, workKind: scope.workKind };
103
+ const work = { workId: scope.workId, workKind: scope.workKind, waiter: scope.waiter };
103
104
  await DBOS.runStep(async () => unwrapOrThrow(await recordPause(scope.pool, {
104
105
  workflowId,
105
106
  ...work,
@@ -110,7 +111,7 @@ export function workPause(scope) {
110
111
  attempt,
111
112
  retryAt,
112
113
  topic,
113
- pausedAt: new Date(pausedAt).toISOString(),
114
+ pausedAt,
114
115
  runSession: scope.run?.session,
115
116
  })), { name: "pause.record" });
116
117
  // Only a person ends a wait in `paused`, thus the credential stops until the renewal of the resume.
@@ -122,7 +123,7 @@ export function workPause(scope) {
122
123
  };
123
124
  if (state === "paused")
124
125
  await suspendCredential("pause.suspend");
125
- await scope.emit({ type: "data-pause", id: workflowId, ...work, state, cause, attempt, retryAt });
126
+ await scope.emit({ type: "data-pause", id: workflowId, ...work, state, cause, pausedAt, attempt, retryAt });
126
127
  log.warn("work paused", { workflowId, ...work, state, cause: cause.kind, attempt });
127
128
  let end;
128
129
  const untrack = trackWorkPaused({ workKind: scope.workKind });
@@ -139,11 +140,11 @@ export function workPause(scope) {
139
140
  cause,
140
141
  attempt,
141
142
  topic,
142
- pausedAt: new Date(pausedAt).toISOString(),
143
+ pausedAt,
143
144
  runSession: scope.run?.session,
144
145
  })), { name: "pause.hold" });
145
146
  await suspendCredential("pause.hold.suspend");
146
- await scope.emit({ type: "data-pause", id: workflowId, ...work, state: "paused", cause, attempt });
147
+ await scope.emit({ type: "data-pause", id: workflowId, ...work, state: "paused", cause, pausedAt, attempt });
147
148
  log.info("work held", { workflowId, ...work, attempt });
148
149
  const held = await waitForMessage(topic, undefined, signal);
149
150
  end = held === "hold" ? "resume" : held;
@@ -155,8 +156,13 @@ export function workPause(scope) {
155
156
  finally {
156
157
  untrack();
157
158
  }
158
- await DBOS.runStep(async () => unwrapOrThrow(await clearPause(scope.pool, workflowId)), { name: "pause.clear" });
159
- await scope.emit({ type: "data-pause", id: workflowId, ...work, state: "running" });
159
+ // The time is read inside the step, thus a recovery replays the recorded time and the end keeps one time.
160
+ // The replayed step output is `undefined` when the recorded step has no time.
161
+ const endedAt = await DBOS.runStep(async () => {
162
+ unwrapOrThrow(await clearPause(scope.pool, workflowId));
163
+ return new Date().toISOString();
164
+ }, { name: "pause.clear" });
165
+ await scope.emit({ type: "data-pause", id: workflowId, ...work, state: "running", cause, pausedAt, endedAt, end });
160
166
  log.info("pause ended", { workflowId, ...work, end });
161
167
  return end;
162
168
  };
@@ -157,6 +157,7 @@ export async function runSandboxStepBody(input, deps) {
157
157
  pool: deps.pool,
158
158
  workId: input.runId,
159
159
  workKind: "run",
160
+ waiter: { kind: "step", stepId: input.stepId },
160
161
  analysisId: input.analysisId,
161
162
  emit: safeEmit,
162
163
  retryDelaysMs: deps.pauseRetryDelaysMs,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inflexa-ai/harness",
3
- "version": "0.48.2",
3
+ "version": "0.48.4",
4
4
  "description": "Host-agnostic bioinformatics agent harness — an agent loop on the AI SDK, DBOS-durable workflows, sandboxed compute.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -37,8 +37,10 @@
37
37
  "build": "tsc -p tsconfig.build.json && node scripts/copy-assets.mjs",
38
38
  "design:fixture": "bun scripts/render-fixture.ts",
39
39
  "design:gallery": "bun scripts/render-gallery.ts",
40
- "format": "prettier --write src/",
41
- "format:file": "prettier --write",
40
+ "dupes": "jscpd src scripts --exit-code --fail-on-empty",
41
+ "format": "oxfmt src/",
42
+ "format:check": "oxfmt --check src/",
43
+ "format:file": "oxfmt",
42
44
  "gallery:data": "bash scripts/gallery-data.sh",
43
45
  "lint": "bun --bun oxlint",
44
46
  "lint:python": "ruff check src/input-scan/decoder && ruff format --check src/input-scan/decoder",
@@ -119,9 +121,10 @@
119
121
  "@typescript/native": "npm:typescript@7.0.2",
120
122
  "csstree-validator": "^4.0.1",
121
123
  "html-validate": "^11.6.2",
124
+ "jscpd": "5.4.2",
125
+ "oxfmt": "0.71.0",
122
126
  "oxlint": "1.85.0",
123
127
  "oxlint-tsgolint": "7.0.2003",
124
- "prettier": "^3.9.5",
125
128
  "testcontainers": "^10.28.0",
126
129
  "typescript": "^6.0.3"
127
130
  },