@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
@@ -20,6 +20,7 @@ import { createDrugRepurposingAgent, meta as drugRepurposingMeta } from "./drug-
20
20
  import { createEnrichmentAgent, meta as enrichmentMeta } from "./enrichment-agent.js";
21
21
  import { createGenomicVariantAgent, meta as genomicVariantMeta } from "./genomic-variant-agent.js";
22
22
  import { createImmuneProfilingAgent, meta as immuneProfilingMeta } from "./immune-profiling-agent.js";
23
+ import { createMdvPreparationAgent, meta as mdvPreparationMeta } from "./mdv-preparation-agent.js";
23
24
  import { createMetabolomicsAgent, meta as metabolomicsMeta } from "./metabolomics-agent.js";
24
25
  import { createMicrobiomeAgent, meta as microbiomeMeta } from "./microbiome-agent.js";
25
26
  import { createMultiOmicsIntegrationAgent, meta as multiOmicsIntegrationMeta } from "./multi-omics-integration-agent.js";
@@ -60,6 +61,7 @@ export const SANDBOX_AGENT_META = {
60
61
  [pkpdClinicalResponseMeta.id]: pkpdClinicalResponseMeta,
61
62
  [immuneProfilingMeta.id]: immuneProfilingMeta,
62
63
  [drugRepurposingMeta.id]: drugRepurposingMeta,
64
+ [mdvPreparationMeta.id]: mdvPreparationMeta,
63
65
  [scientificExecutorMeta.id]: scientificExecutorMeta,
64
66
  };
65
67
  /**
@@ -88,6 +90,7 @@ export function createSandboxAgents(deps) {
88
90
  [pkpdClinicalResponseMeta.id]: createPkpdClinicalResponseAgent(deps),
89
91
  [immuneProfilingMeta.id]: createImmuneProfilingAgent(deps),
90
92
  [drugRepurposingMeta.id]: createDrugRepurposingAgent(deps),
93
+ [mdvPreparationMeta.id]: createMdvPreparationAgent(deps),
91
94
  [scientificExecutorMeta.id]: createScientificExecutorAgent(deps),
92
95
  };
93
96
  }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * MDV Preparation Agent — turns single-cell, scATAC-seq, spatial transcriptomics, and spatial
3
+ * proteomics data into curated, validated assets that MDV (Multi-Dimensional Viewer) loads.
4
+ */
5
+ import type { AgentDefinition } from "../../loop/types.js";
6
+ import { type SandboxAgentDeps } from "./shared.js";
7
+ import type { AgentMeta } from "./types.js";
8
+ export declare const meta: AgentMeta;
9
+ export declare function createMdvPreparationAgent(deps: SandboxAgentDeps): AgentDefinition;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * MDV Preparation Agent — turns single-cell, scATAC-seq, spatial transcriptomics, and spatial
3
+ * proteomics data into curated, validated assets that MDV (Multi-Dimensional Viewer) loads.
4
+ */
5
+ import { mdvPreparationAgentPrompt } from "../../prompts/sandbox/mdv-preparation-agent.js";
6
+ import { BASE_SANDBOX_TOOLS, createSandboxAgent } from "./shared.js";
7
+ export const meta = {
8
+ id: "mdv-preparation-agent",
9
+ capabilities: [
10
+ "MDV asset preparation",
11
+ "single-cell data preparation for MDV",
12
+ "spatial data preparation for MDV",
13
+ "metadata curation for visualization",
14
+ "MDV asset validation",
15
+ ],
16
+ suitableFor: [
17
+ "scrna-seq",
18
+ "snrna-seq",
19
+ "scatac-seq",
20
+ "multiome",
21
+ "cosmx",
22
+ "xenium",
23
+ "visium",
24
+ "merfish",
25
+ "codex",
26
+ "imc",
27
+ "mibi",
28
+ "anndata",
29
+ "mudata",
30
+ "spatialdata",
31
+ ],
32
+ skills: ["mdv-preparation", "single-cell", "spatial-omics", "chromatin-regulation", "shared/omics-general"],
33
+ tools: [...BASE_SANDBOX_TOOLS],
34
+ };
35
+ export function createMdvPreparationAgent(deps) {
36
+ return createSandboxAgent(deps, meta, mdvPreparationAgentPrompt);
37
+ }
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import type { AuthContext } from "../auth/types.js";
17
17
  import type { TokenUsageRollup } from "../contracts/usage.js";
18
- import type { ChatFrame } from "../contracts/chat-events.js";
18
+ import type { ChatErrorEvent, ChatFrame, EventSource, FinishEvent } from "../contracts/chat-events.js";
19
19
  import type { AgentDefinition } from "../loop/types.js";
20
20
  import type { ThreadType } from "../memory/thread-store.js";
21
21
  import type { PromptCachePolicy } from "../providers/types.js";
@@ -110,5 +110,7 @@ export declare const DEFAULT_ASK_WAIT_SECONDS: number;
110
110
  * finds the workflow of a recovery by its registered name.
111
111
  */
112
112
  export declare function registerChatTurns(deps: ChatTurnWorkflowDeps): ChatTurns;
113
+ /** The last frame of a turn that ran: `error` for a failed turn, else `finish` with the stop reason of the turn. */
114
+ export declare function endFrameOf(ran: ChatTurnRan, rootSource: EventSource): ChatErrorEvent | FinishEvent;
113
115
  /** How a turn that ran ended, as a host reads it. */
114
116
  export declare function chatTurnEndOf(ran: ChatTurnRan): ChatTurnEnd;
@@ -258,6 +258,7 @@ async function runTurnWorkflow(deps, input, signal) {
258
258
  pool: deps.pool,
259
259
  workId: input.turnId,
260
260
  workKind: "turn",
261
+ waiter: { kind: "turn" },
261
262
  analysisId: input.analysisId,
262
263
  threadId: input.threadId,
263
264
  emit: async (part) => {
@@ -277,9 +278,7 @@ async function runTurnWorkflow(deps, input, signal) {
277
278
  };
278
279
  const ran = await runOpenTurn(deps, { analysisId: input.analysisId, threadId: input.threadId, userInput: input.userText }, { kind: "ok", threadType: input.threadType, history: view, userMessage: opening.userMessage, contextRecords: opening.contextRecords }, agent.value, { session, author: input.author, startedAtMs: input.startedAtMs, promptCache: input.promptCache }, transport);
279
280
  const { outcome } = ran;
280
- await writer.write(outcome.status === "failed"
281
- ? { type: "error", message: outcome.reason, reason: outcome.code, source: rootSource }
282
- : { type: "finish", source: rootSource, turnUsage: ran.turnUsage });
281
+ await writer.write(endFrameOf(ran, rootSource));
283
282
  await writer.settle();
284
283
  await closeDurableStream(CHAT_STREAM_KEY, outcome.status === "done" ? "stop" : outcome.status === "aborted" ? "aborted" : "error");
285
284
  // A sub-agent that a stop canceled in its pause leaves its row, thus the end of the turn clears each row of the turn.
@@ -287,6 +286,23 @@ async function runTurnWorkflow(deps, input, signal) {
287
286
  await DBOS.runStep(async () => unwrapOrThrow(await releaseThreadOfTurn(deps.pool, input.threadId, input.turnId)), { name: "chat-turn.release" });
288
287
  return chatTurnEndOf(ran);
289
288
  }
289
+ /** The last frame of a turn that ran: `error` for a failed turn, else `finish` with the stop reason of the turn. */
290
+ export function endFrameOf(ran, rootSource) {
291
+ const { outcome } = ran;
292
+ switch (outcome.status) {
293
+ case "failed":
294
+ return { type: "error", message: outcome.reason, reason: outcome.code, source: rootSource };
295
+ case "done":
296
+ return { type: "finish", source: rootSource, finishReason: outcome.finish.reason, turnUsage: ran.turnUsage };
297
+ // An abort that cut the run before its end leaves no finish of the loop.
298
+ case "aborted":
299
+ return { type: "finish", source: rootSource, finishReason: "aborted", turnUsage: ran.turnUsage };
300
+ default: {
301
+ const exhaustive = outcome;
302
+ throw new Error(`unhandled turn outcome: ${JSON.stringify(exhaustive)}`);
303
+ }
304
+ }
305
+ }
290
306
  /** How a turn that ran ended, as a host reads it. */
291
307
  export function chatTurnEndOf(ran) {
292
308
  const { outcome } = ran;
@@ -56,8 +56,10 @@ export type PreparedChatTurn = Extract<PrepareChatTurnResult, {
56
56
  */
57
57
  export declare function prepareChatTurn(deps: PrepareChatTurnDeps, params: PrepareChatTurnParams): Promise<PrepareChatTurnResult>;
58
58
  /**
59
- * Open the thread of a turn: check that the analysis owns it, make it when it is absent, and seed its title. A
60
- * thread that a different analysis owns is `not_found`.
59
+ * Open the thread of a turn: make it when it is absent, and seed its title. A thread that a different analysis
60
+ * owns is `not_found`, and so is an archived thread: a turn never writes into hidden state and never restores it.
61
+ * A failed make of the thread throws, because without its row the open cannot tell an absent thread from one that
62
+ * it must refuse.
61
63
  */
62
64
  export declare function openThread(deps: PrepareChatTurnDeps, params: PrepareChatTurnParams): Promise<{
63
65
  readonly kind: "ok";
@@ -48,8 +48,10 @@ export async function prepareChatTurn(deps, params) {
48
48
  return { kind: "ok", threadType: opened.threadType, ...assembled };
49
49
  }
50
50
  /**
51
- * Open the thread of a turn: check that the analysis owns it, make it when it is absent, and seed its title. A
52
- * thread that a different analysis owns is `not_found`.
51
+ * Open the thread of a turn: make it when it is absent, and seed its title. A thread that a different analysis
52
+ * owns is `not_found`, and so is an archived thread: a turn never writes into hidden state and never restores it.
53
+ * A failed make of the thread throws, because without its row the open cannot tell an absent thread from one that
54
+ * it must refuse.
53
55
  */
54
56
  export async function openThread(deps, params) {
55
57
  const { pool } = deps;
@@ -58,52 +60,48 @@ export async function openThread(deps, params) {
58
60
  // An unbound member gives a call that does nothing, thus the emit below needs
59
61
  // no test of its own.
60
62
  const observe = bindSessionEmit(deps.provenance, logger);
61
- // Ownership check before any read/write of the thread — a `threadId`
62
- // owned by a different analysis is indistinguishable from a missing one.
63
+ // `getThread` hides an archived row, thus a live row is the only one that this
64
+ // read can prove owned. An absent result covers a missing thread and an
65
+ // archived one, and the idempotent create below tells them apart.
63
66
  const store = createThreadStore(pool);
64
67
  const existing = unwrapOrThrow(await store.getThread(threadId));
65
68
  if (existing && existing.analysisId !== analysisId) {
66
69
  return { kind: "not_found" };
67
70
  }
68
- // The type a caller resolves the turn's agent from, and the type that the
69
- // message assembly reads for its context records. An existing row carries
70
- // its own. An absent one defaults to `conversation` — the store's own
71
- // default — so a best-effort create that fails non-fatally still leaves a
72
- // usable type; a successful create overrides it from the returned row.
73
- let threadType = existing ? existing.threadType : "conversation";
74
- // Seed the thread title from the first user message. Best-effort.
75
- try {
76
- if (!existing) {
77
- // `createThread` is idempotent (ON CONFLICT reads the row back), so
78
- // the returned row is authoritative for the type: a create that
79
- // races another writer reflects the stored type, not an assumed one.
80
- const created = unwrapOrThrow(await store.createThread({
81
- threadId,
82
- analysisId,
83
- title: deriveThreadTitle(userInput),
84
- }));
85
- threadType = created.threadType;
86
- // The row that comes back is not proof that this call wrote it. The read
87
- // above hides an archived thread, and the idempotent insert then reads
88
- // that same row back. Thus a turn on an archived thread reaches here with
89
- // a row that a different site created, and an emit would record a start
90
- // that never happened.
91
- //
92
- // This site writes a live conversation thread and nothing else. An
93
- // archived row, or a row of another kind, is therefore one that already
94
- // existed. A root session has no parent, thus the event carries none.
95
- if (created.deletedAt === null && created.threadType === "conversation") {
96
- observe({ type: "create-session", analysisId, threadId, sessionKind: created.threadType });
97
- }
71
+ if (!existing) {
72
+ // `createThread` is idempotent (ON CONFLICT reads the row back with no
73
+ // filter), so the returned row is authoritative: the inserted row, or the
74
+ // row that already existed, archived or owned by a different analysis. A
75
+ // conflict writes nothing, thus the refusal comes before each write of the
76
+ // turn.
77
+ const created = unwrapOrThrow(await store.createThread({
78
+ threadId,
79
+ analysisId,
80
+ title: deriveThreadTitle(userInput),
81
+ }));
82
+ if (created.analysisId !== analysisId || created.deletedAt !== null) {
83
+ return { kind: "not_found" };
98
84
  }
99
- else if (!existing.title || existing.title.length === 0) {
100
- unwrapOrThrow(await store.setAutoTitle(threadId, deriveThreadTitle(userInput)));
85
+ // The row that comes back is not proof that this call wrote it: a create
86
+ // that races another writer reads that writer's row back. This site writes
87
+ // a live conversation thread and nothing else, thus a row of another kind
88
+ // is one that already existed. A root session has no parent, thus the event
89
+ // carries none.
90
+ if (created.threadType === "conversation") {
91
+ observe({ type: "create-session", analysisId, threadId, sessionKind: created.threadType });
101
92
  }
93
+ return { kind: "ok", threadType: created.threadType };
102
94
  }
103
- catch (err) {
104
- logger.warn("title-seed failed (non-fatal)", logger.errorFields(err));
95
+ // Seed the thread title from the first user message. Best-effort.
96
+ if (!existing.title || existing.title.length === 0) {
97
+ try {
98
+ unwrapOrThrow(await store.setAutoTitle(threadId, deriveThreadTitle(userInput)));
99
+ }
100
+ catch (err) {
101
+ logger.warn("title-seed failed (non-fatal)", logger.errorFields(err));
102
+ }
105
103
  }
106
- return { kind: "ok", threadType };
104
+ return { kind: "ok", threadType: existing.threadType };
107
105
  }
108
106
  /** The analysis context and the run activity of a turn. A failed read gives the fallback of its kind. */
109
107
  export async function readTurnContext(pool, analysisId) {
@@ -218,7 +216,7 @@ export async function runOpenTurn(deps, turn, prepared, agent, params, transport
218
216
  await flush({
219
217
  status: outcome.status,
220
218
  reason: outcome.status === "failed" ? outcome.reason : undefined,
221
- note: outcome.status === "failed" ? conversationRecordTurn(failureNote(outcome.reason)) : outcome.status === "aborted" ? interruptionNote() : undefined,
219
+ note: closingNoteOf(outcome, recorder),
222
220
  turnUsage,
223
221
  turnDurationMs: durationMs,
224
222
  });
@@ -292,15 +290,43 @@ function failureReasonOf(err) {
292
290
  return findProviderError(err) === undefined ? "The turn stopped on an internal error." : "The model request failed.";
293
291
  }
294
292
  }
293
+ /** The note that closes a turn: a failed, an aborted, or a declined turn has one, and a turn that the model answered has none. */
294
+ function closingNoteOf(outcome, recorder) {
295
+ switch (outcome.status) {
296
+ case "failed": {
297
+ const note = conversationRecordTurn(failureNote(outcome.reason));
298
+ return { ...note, displayMessages: [...recorder.openRoundPauses(), ...note.displayMessages] };
299
+ }
300
+ case "aborted":
301
+ return interruptionNote(recorder);
302
+ case "done":
303
+ return outcome.finish.reason === "content-filter" ? declineNote(recorder) : undefined;
304
+ default: {
305
+ const exhaustive = outcome;
306
+ throw new Error(`unhandled turn outcome: ${JSON.stringify(exhaustive)}`);
307
+ }
308
+ }
309
+ }
310
+ /**
311
+ * The note of a turn that the content filter stopped. A declined request often gives no message, and a round
312
+ * with no message gives no row. The note is the row that holds the display of such a round, thus a reload
313
+ * shows the refusal. It also tells the next turn that this request has no reply.
314
+ */
315
+ function declineNote(recorder) {
316
+ return {
317
+ modelMessages: [syntheticUserMessage("[The model provider declined the request above, thus it has no reply.]")],
318
+ displayMessages: recorder.takeRound([]),
319
+ };
320
+ }
295
321
  /**
296
322
  * The note of an aborted turn. Without it, the next turn reads the request of the aborted turn as open, and
297
- * its reply also answers that request. The transcript shows the interruption from the turn record, thus the
298
- * note has no display.
323
+ * its reply also answers that request. The transcript shows the interruption from the turn record. The display
324
+ * of the note holds only the pause parts of the open round, thus a reload shows how the wait ended.
299
325
  */
300
- function interruptionNote() {
326
+ function interruptionNote(recorder) {
301
327
  return {
302
328
  modelMessages: [syntheticUserMessage("[The user interrupted the request above. Do not continue it unless the user asks you to.]")],
303
- displayMessages: [],
329
+ displayMessages: recorder.openRoundPauses(),
304
330
  };
305
331
  }
306
332
  function failureNote(reason) {
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import { randomUUID } from "node:crypto";
8
8
  import { toChatFrame } from "../contracts/chat-frame.js";
9
- import { chatTurnEndOf } from "./chat-turn-workflow.js";
9
+ import { chatTurnEndOf, endFrameOf } from "./chat-turn-workflow.js";
10
10
  import { createConversationDisplayRecorder } from "../memory/conversation-display-recorder.js";
11
11
  import { prepareChatTurn, runOpenTurn, userTextOf, } from "./chat-turn.js";
12
12
  /** Prepare a turn and resolve its agent. A refusal comes back before any row of the turn exists. */
@@ -112,11 +112,8 @@ export function inProcessChatTurns(deps) {
112
112
  promptCache: request.promptCache,
113
113
  })
114
114
  .then((ran) => {
115
- const turnEnd = chatTurnEndOf(ran);
116
- log.push(turnEnd.status === "failed"
117
- ? { type: "error", message: turnEnd.reason ?? "The turn failed.", reason: turnEnd.code, source: rootSource }
118
- : { type: "finish", source: rootSource, turnUsage: turnEnd.turnUsage });
119
- return turnEnd;
115
+ log.push(endFrameOf(ran, rootSource));
116
+ return chatTurnEndOf(ran);
120
117
  })
121
118
  .catch(() => null)
122
119
  .finally(() => log.close());
@@ -6,7 +6,7 @@
6
6
  * of a turn and each step of a run wait in their own workflows. A stop ends a work in each state. A resume and a
7
7
  * hold reach only a work that waits. Only a client resumes paused work.
8
8
  */
9
- import type { PauseCause, WorkKind } from "../contracts/chat-parts.js";
9
+ import type { PauseCause, PauseWaiter, WorkKind } from "../contracts/chat-parts.js";
10
10
  import type { RunAuthorizer } from "../execution/run-authorizer.js";
11
11
  import type { RunCanceler } from "../execution/run-canceler.js";
12
12
  import type { Logger } from "../lib/logger.js";
@@ -15,6 +15,8 @@ import type { ChatTurnSession } from "./chat-turn.js";
15
15
  /** One waiting workflow of a paused work. */
16
16
  export type PausedWorkflow = {
17
17
  readonly workflowId: string;
18
+ /** What waits. Absent for a wait that started before the harness recorded the waiter. */
19
+ readonly waiter?: PauseWaiter;
18
20
  readonly state: "retrying" | "paused";
19
21
  readonly cause: PauseCause;
20
22
  /** The count of timer retries before this wait. */
@@ -29,6 +31,7 @@ export type Work = {
29
31
  /** The turn id, the run id, or the id of the profile workflow. */
30
32
  readonly workId: string;
31
33
  readonly kind: WorkKind;
34
+ /** The analysis of the work, as its own row names it. */
32
35
  readonly analysisId: string;
33
36
  readonly threadId?: string;
34
37
  /** Each waiting workflow of the work. Empty for a work that runs. */
@@ -61,10 +64,11 @@ export type WorkHold = {
61
64
  /** The durable work of a runtime, as a host uses it. */
62
65
  export type WorkSurface = {
63
66
  /**
64
- * The active work of an analysis: each turn that holds a thread, each `running` run, and the profile that runs.
67
+ * The active work of an analysis, or of each analysis when `analysisId` is absent: each turn that holds a thread,
68
+ * each `running` run, and each profile that runs. The count of reads does not depend on the count of analyses.
65
69
  * A pause row whose workflow ended is not in it, and the list deletes that row.
66
70
  */
67
- list(analysisId: string): Promise<Work[]>;
71
+ list(analysisId?: string): Promise<Work[]>;
68
72
  /**
69
73
  * Stop the work, in each state: a turn stops, a run cancels with its children, and a profile fails. Work of a
70
74
  * different analysis than the session gives `not_found`.
package/dist/app/work.js CHANGED
@@ -11,9 +11,9 @@ import { createNoopLogger } from "../lib/console-logger.js";
11
11
  import { passGate } from "../lib/hooks.js";
12
12
  import { unwrapOrThrow } from "../lib/result.js";
13
13
  import { turnCredentials } from "../loop/turn-credentials.js";
14
- import { queryTurnsOfAnalysis } from "../state/active-turns.js";
15
- import { loadDataProfileStatus } from "../state/data-profile.js";
16
- import { clearPauses, queryPausesOfAnalysis, queryPausesOfWork } from "../state/paused-work.js";
14
+ import { queryTurns } from "../state/active-turns.js";
15
+ import { queryRunningProfiles } from "../state/data-profile.js";
16
+ import { clearPauses, queryPauses, queryPausesOfWork } from "../state/paused-work.js";
17
17
  import { queryActiveRunsByAnalysis } from "../state/runs.js";
18
18
  import { HOLD_MESSAGE, RESUME_MESSAGE } from "../workflows/pause.js";
19
19
  /** The workflow statuses that can still wait. */
@@ -23,6 +23,7 @@ const WORK_STOP_PROVENANCE = { agentId: "work-stop", callPath: ["work-stop"] };
23
23
  function workflowOf(record) {
24
24
  return {
25
25
  workflowId: record.workflowId,
26
+ waiter: record.waiter,
26
27
  state: record.state,
27
28
  cause: record.cause,
28
29
  attempt: record.attempt,
@@ -39,17 +40,16 @@ async function activeWorkflows(workflowIds) {
39
40
  export function createWork(deps) {
40
41
  const logger = (deps.logger ?? createNoopLogger()).named("harness.work");
41
42
  async function list(analysisId) {
42
- const [records, turns, runs, profile] = await Promise.all([
43
- (async () => unwrapOrThrow(await queryPausesOfAnalysis(deps.pool, analysisId)))(),
44
- (async () => unwrapOrThrow(await queryTurnsOfAnalysis(deps.pool, analysisId)))(),
43
+ const [records, turns, runs, profiles] = await Promise.all([
44
+ (async () => unwrapOrThrow(await queryPauses(deps.pool, analysisId)))(),
45
+ (async () => unwrapOrThrow(await queryTurns(deps.pool, analysisId)))(),
45
46
  (async () => unwrapOrThrow(await queryActiveRunsByAnalysis(deps.pool, analysisId)))(),
46
- (async () => unwrapOrThrow(await loadDataProfileStatus(deps.pool, analysisId)))(),
47
+ (async () => unwrapOrThrow(await queryRunningProfiles(deps.pool, analysisId)))(),
47
48
  ]);
48
- const profileWorkflowId = profile?.status === "running" ? (profile.workflowId ?? undefined) : undefined;
49
49
  const active = await activeWorkflows([
50
50
  ...records.map((record) => record.workflowId),
51
51
  ...turns.map((turn) => turn.turnId),
52
- ...(profileWorkflowId === undefined ? [] : [profileWorkflowId]),
52
+ ...profiles.map((profile) => profile.workflowId),
53
53
  ]);
54
54
  const ended = records.filter((record) => !active.has(record.workflowId)).map((record) => record.workflowId);
55
55
  if (ended.length > 0) {
@@ -64,23 +64,24 @@ export function createWork(deps) {
64
64
  waitingOf.set(record.workId, [...(waitingOf.get(record.workId) ?? []), workflowOf(record)]);
65
65
  }
66
66
  const work = new Map();
67
- const add = (workId, kind, threadId) => {
67
+ const add = (workId, kind, itemAnalysisId, threadId) => {
68
68
  if (work.has(workId))
69
69
  return;
70
- work.set(workId, { workId, kind, analysisId, threadId, waiting: waitingOf.get(workId) ?? [] });
70
+ work.set(workId, { workId, kind, analysisId: itemAnalysisId, threadId, waiting: waitingOf.get(workId) ?? [] });
71
71
  };
72
72
  // A claim can outlive its turn, thus a turn is active while its workflow is. A `running` run is active while
73
73
  // its row is, because the run canceler also converges a row whose workflow ended.
74
74
  for (const turn of turns)
75
75
  if (active.has(turn.turnId))
76
- add(turn.turnId, "turn", turn.threadId);
76
+ add(turn.turnId, "turn", turn.analysisId, turn.threadId);
77
77
  for (const run of runs)
78
- add(run.runId, "run", run.threadId ?? undefined);
79
- if (profileWorkflowId !== undefined && active.has(profileWorkflowId))
80
- add(profileWorkflowId, "profile", undefined);
78
+ add(run.runId, "run", run.analysisId, run.threadId ?? undefined);
79
+ for (const profile of profiles)
80
+ if (active.has(profile.workflowId))
81
+ add(profile.workflowId, "profile", profile.analysisId, undefined);
81
82
  for (const record of records)
82
83
  if (active.has(record.workflowId))
83
- add(record.workId, record.workKind, record.threadId);
84
+ add(record.workId, record.workKind, record.analysisId, record.threadId);
84
85
  return [...work.values()];
85
86
  }
86
87
  return {
@@ -162,6 +162,11 @@ export type ToolFinishedEvent = {
162
162
  export type FinishEvent = {
163
163
  type: "finish";
164
164
  source: EventSource;
165
+ /**
166
+ * Why the turn stopped: the finish reason of the last reply, for example `stop` or `content-filter`, or `aborted`,
167
+ * `max_iterations`, or `denied`. A word outside these stays as the provider sent it.
168
+ */
169
+ finishReason: string;
165
170
  /** What this loop's own LLM calls used. */
166
171
  usage?: TokenUsageRollup;
167
172
  /**
@@ -4,17 +4,20 @@
4
4
  *
5
5
  * Categories:
6
6
  * Presentation: data-presentation (agent-synthesized content),
7
- * data-plan (plan by id), data-file-reference (files by path)
7
+ * data-plan (plan by id), data-file-reference (files by path),
8
+ * data-analogy-report (the result of generate_analogy_report)
8
9
  * Approval: data-ask (a tool's pending user-approval prompt),
9
10
  * data-plan-approval (the record of an approved plan)
10
11
  * Compaction: data-compaction (a compaction of the conversation thread)
11
12
  * Model retry: data-model-retry (a retry of a model request inside the retry envelope of the provider)
13
+ * Model refusal: data-model-refusal (a model request that the provider declined)
12
14
  * Run lifecycle: run-started, dag-state, run-completed, run-failed, run-synthesis,
13
15
  * synthesis-progress
14
16
  * Per-step live: step-activity, step-file-tree
15
17
  * Activity history: activity-entry (run stream), subagent-activity (conversation)
16
18
  * Per-step final: step-output, step-summary, step-usage
17
19
  */
20
+ import type { AnalogicalReasonerOutput } from "./analogy-report.js";
18
21
  import type { ToolCallOutcome } from "./chat-events.js";
19
22
  import type { TokenUsageRollup } from "./usage.js";
20
23
  export type PlanStep = {
@@ -71,6 +74,13 @@ export type PresentationPart = {
71
74
  title?: string;
72
75
  content: PresentationContent;
73
76
  };
77
+ /** The result of one `generate_analogy_report` call, as the tool gave it to the model: a report or an `extraction-failed` envelope. */
78
+ export type AnalogyReportPart = {
79
+ type: "data-analogy-report";
80
+ /** The id of the tool call. */
81
+ id: string;
82
+ report: AnalogicalReasonerOutput;
83
+ };
74
84
  export type PlanPart = {
75
85
  type: "data-plan";
76
86
  id: string;
@@ -498,8 +508,46 @@ export type ModelRetryPart = {
498
508
  /** The HTTP status of the failed attempt. Absent for a failure with no status, for example a lost connection. */
499
509
  status?: number;
500
510
  };
511
+ /**
512
+ * A model request that the provider declined. `fallback`: another model answered the request, and `model` names it.
513
+ * `stopped`: no model answered, thus the reply of the request ended with the content filter.
514
+ */
515
+ export type ModelRefusal = {
516
+ /** The id of the model response. */
517
+ id: string;
518
+ outcome: "fallback";
519
+ model: string;
520
+ } | {
521
+ /** The id of the model response. */
522
+ id: string;
523
+ outcome: "stopped";
524
+ };
525
+ export type ModelRefusalPart = {
526
+ type: "data-model-refusal";
527
+ } & ModelRefusal;
501
528
  /** The kind of a durable work, and of the work that a pause belongs to. A sub-agent belongs to its turn, and a sandbox step to its run. */
502
529
  export type WorkKind = "turn" | "run" | "profile";
530
+ /**
531
+ * What waits in a pause. A sub-agent names its agent id, and a sandbox step names its step id. `synthesis` and `run`
532
+ * are the two stages of a run workflow that wait: the synthesis, and the charge gate before any step starts.
533
+ */
534
+ export type PauseWaiter = {
535
+ readonly kind: "turn";
536
+ } | {
537
+ readonly kind: "sub-agent";
538
+ readonly agentId: string;
539
+ } | {
540
+ readonly kind: "step";
541
+ readonly stepId: string;
542
+ } | {
543
+ readonly kind: "synthesis";
544
+ } | {
545
+ readonly kind: "run";
546
+ } | {
547
+ readonly kind: "profile";
548
+ };
549
+ /** How a wait ended: the deadline of `retrying`, the message of a resume, or a stop of the work. */
550
+ export type PauseEnd = "retry" | "resume" | "abort";
503
551
  /**
504
552
  * The state of one waiting workflow of paused work. Re-emitted under the same `id` when the wait ends, with the
505
553
  * state `running`, so reconciling readers keep the latest state of each workflow.
@@ -511,17 +559,28 @@ export type PausePart = {
511
559
  /** The id of the work: the turn, the run, or the profile workflow. A resume names it. */
512
560
  workId: string;
513
561
  workKind: WorkKind;
514
- /** `retrying`: the system sends the request again at `retryAt`. `paused`: only a resume ends the wait. */
562
+ /** What waits. Absent only for a wait that started before the harness recorded the waiter. */
563
+ waiter?: PauseWaiter;
564
+ /** `retrying`: the system sends the request again at `retryAt`. `paused`: only a resume ends the wait. `running`: the wait ended. */
515
565
  state: "retrying" | "paused" | "running";
516
- /** Absent for the state `running`. */
517
- cause?: PauseCause;
566
+ /** Why the work waited. The part that ends the wait keeps it. */
567
+ cause: PauseCause;
568
+ /** ISO 8601. The start of the wait. The part that ends the wait and a hold keep it. */
569
+ pausedAt: string;
518
570
  /** The count of timer retries before this wait. Absent for the state `running`. */
519
571
  attempt?: number;
520
572
  /** ISO 8601. Present for the state `retrying` only. */
521
573
  retryAt?: string;
574
+ /**
575
+ * ISO 8601. The time the wait ended. Present for the state `running` only. Absent when the replayed `pause.clear`
576
+ * step recorded no time.
577
+ */
578
+ endedAt?: string;
579
+ /** How the wait ended. Present for the state `running` only. */
580
+ end?: PauseEnd;
522
581
  };
523
582
  /** The parts whose contracts this version of the package declares — the arms the part registry knows. */
524
- export type KnownChatPart = PresentationPart | PlanPart | RunCardPart | PlanApprovalPart | FileReferencePart | AskPart | RunStartedPart | DagStatePart | StepActivityPart | ActivityEntryPart | SubAgentActivityPart | StepFileTreePart | StepOutputPart | StepSummaryPart | StepUsagePart | StepBlockedPart | RunSynthesisPart | SynthesisProgressPart | RunCompletedPart | RunFailedPart | ChildSessionStartedPart | ReportRenderedPart | CompactionPart | PausePart | ModelRetryPart;
583
+ export type KnownChatPart = PresentationPart | AnalogyReportPart | PlanPart | RunCardPart | PlanApprovalPart | FileReferencePart | AskPart | RunStartedPart | DagStatePart | StepActivityPart | ActivityEntryPart | SubAgentActivityPart | StepFileTreePart | StepOutputPart | StepSummaryPart | StepUsagePart | StepBlockedPart | RunSynthesisPart | SynthesisProgressPart | RunCompletedPart | RunFailedPart | ChildSessionStartedPart | ReportRenderedPart | CompactionPart | PausePart | ModelRetryPart | ModelRefusalPart;
525
584
  /**
526
585
  * A part of a known contract, or a part whose type this build does not know. An emitter newer than this
527
586
  * build can lead the registry, and such a part already rides through the check boundary and the live view
@@ -4,11 +4,13 @@
4
4
  *
5
5
  * Categories:
6
6
  * Presentation: data-presentation (agent-synthesized content),
7
- * data-plan (plan by id), data-file-reference (files by path)
7
+ * data-plan (plan by id), data-file-reference (files by path),
8
+ * data-analogy-report (the result of generate_analogy_report)
8
9
  * Approval: data-ask (a tool's pending user-approval prompt),
9
10
  * data-plan-approval (the record of an approved plan)
10
11
  * Compaction: data-compaction (a compaction of the conversation thread)
11
12
  * Model retry: data-model-retry (a retry of a model request inside the retry envelope of the provider)
13
+ * Model refusal: data-model-refusal (a model request that the provider declined)
12
14
  * Run lifecycle: run-started, dag-state, run-completed, run-failed, run-synthesis,
13
15
  * synthesis-progress
14
16
  * Per-step live: step-activity, step-file-tree
@@ -2,7 +2,7 @@ export { ORGAN_SYSTEMS, OrganSystemSchema, ORGAN_SYSTEM_LABELS, organSystemLabel
2
2
  export type { OrganSystem } from "./organ-system.js";
3
3
  export { SEVERITIES, SeveritySchema } from "./severity.js";
4
4
  export type { Severity } from "./severity.js";
5
- export type { PlanStep, PresentationContent, PresentationPart, PlanPart, RunCardPart, PlanApprovalPart, FileReferenceEntry, FileReferencePart, AskPart, RunStartedPart, StepStatus, DagStepState, PlanGraphStep, DagStatePart, StepPhase, StepActivityPart, ActivityEntryPart, SubAgentActivityEntry, SubAgentActivityPart, FileTreeEntry, StepFileTreePart, StepOutputFile, StepOutputPart, StepSummaryPart, StepUsagePart, StepBlockedPart, FindingReference, SynthesizedFinding, BiologicalTheme, RunSynthesisPart, SynthesisPhase, SynthesisProgressPart, RunCompletedFinding, RunCompletedPart, RunFailedPart, ChildSessionStartedPart, ReportRenderedPart, CompactionPart, PauseCause, WorkKind, PausePart, ModelRetryPart, ChatPart, KnownChatPart, } from "./chat-parts.js";
5
+ export type { PlanStep, PresentationContent, PresentationPart, AnalogyReportPart, PlanPart, RunCardPart, PlanApprovalPart, FileReferenceEntry, FileReferencePart, AskPart, RunStartedPart, StepStatus, DagStepState, PlanGraphStep, DagStatePart, StepPhase, StepActivityPart, ActivityEntryPart, SubAgentActivityEntry, SubAgentActivityPart, FileTreeEntry, StepFileTreePart, StepOutputFile, StepOutputPart, StepSummaryPart, StepUsagePart, StepBlockedPart, FindingReference, SynthesizedFinding, BiologicalTheme, RunSynthesisPart, SynthesisPhase, SynthesisProgressPart, RunCompletedFinding, RunCompletedPart, RunFailedPart, ChildSessionStartedPart, ReportRenderedPart, CompactionPart, PauseCause, PauseWaiter, PauseEnd, WorkKind, PausePart, ModelRetryPart, ModelRefusal, ModelRefusalPart, ChatPart, KnownChatPart, } from "./chat-parts.js";
6
6
  export type { EventSource, TextDeltaEvent, IterationEvent, ToolStartedEvent, ToolFinishedEvent, ToolCallLeaf, ToolCallValue, FinishEvent, ChatErrorEvent, MessageStartEvent, ChatEvent, ChatFrame, ChatPartFrame, } from "./chat-events.js";
7
7
  export type { TokenUsageRollup } from "./usage.js";
8
8
  export { DATA_PROFILE_RUN_LITERAL } from "./data-profile.js";
@@ -1,5 +1,6 @@
1
1
  export const PART_REGISTRY = {
2
2
  "data-presentation": { emitter: "conversation", consumer: "conversation", transient: false, reconciling: false },
3
+ "data-analogy-report": { emitter: "conversation", consumer: "conversation", transient: false, reconciling: false },
3
4
  "data-plan": { emitter: "conversation", consumer: "conversation", transient: false, reconciling: false },
4
5
  "data-run-card": { emitter: "conversation", consumer: "conversation", transient: false, reconciling: false },
5
6
  "data-plan-approval": { emitter: "conversation", consumer: "conversation", transient: false, reconciling: false },
@@ -24,6 +25,7 @@ export const PART_REGISTRY = {
24
25
  "data-compaction": { emitter: "conversation", consumer: "conversation", transient: true, reconciling: true },
25
26
  "data-pause": { emitter: "workflow", consumer: "conversation", transient: false, reconciling: true },
26
27
  "data-model-retry": { emitter: "conversation", consumer: "conversation", transient: true, reconciling: true },
28
+ "data-model-refusal": { emitter: "conversation", consumer: "conversation", transient: false, reconciling: false },
27
29
  };
28
30
  export function isTransient(type) {
29
31
  return PART_REGISTRY[type].transient;
@@ -259,6 +259,7 @@ export declare const FinishEventSchema: z.ZodObject<{
259
259
  callPath: z.ZodArray<z.ZodString>;
260
260
  invocationId: z.ZodOptional<z.ZodString>;
261
261
  }, z.core.$strip>;
262
+ finishReason: z.ZodString;
262
263
  usage: z.ZodOptional<z.ZodObject<{
263
264
  inputTokens: z.ZodOptional<z.ZodNumber>;
264
265
  outputTokens: z.ZodOptional<z.ZodNumber>;
@@ -484,6 +485,7 @@ export declare const ChatEventSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
484
485
  callPath: z.ZodArray<z.ZodString>;
485
486
  invocationId: z.ZodOptional<z.ZodString>;
486
487
  }, z.core.$strip>;
488
+ finishReason: z.ZodString;
487
489
  usage: z.ZodOptional<z.ZodObject<{
488
490
  inputTokens: z.ZodOptional<z.ZodNumber>;
489
491
  outputTokens: z.ZodOptional<z.ZodNumber>;