@inflexa-ai/harness 0.48.3 → 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 (77) 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.js +1 -0
  5. package/dist/app/chat-turn.d.ts +4 -2
  6. package/dist/app/chat-turn.js +46 -46
  7. package/dist/app/work.d.ts +7 -3
  8. package/dist/app/work.js +17 -16
  9. package/dist/contracts/chat-parts.d.ts +46 -5
  10. package/dist/contracts/chat-parts.js +2 -1
  11. package/dist/contracts/index.d.ts +1 -1
  12. package/dist/contracts/part-registry.js +1 -0
  13. package/dist/contracts/schemas/chat-parts.d.ts +168 -4
  14. package/dist/contracts/schemas/chat-parts.js +21 -1
  15. package/dist/index.d.ts +1 -1
  16. package/dist/lib/regulatory-corpus/fetch-ich.js +1 -16
  17. package/dist/lib/slug.d.ts +18 -0
  18. package/dist/lib/slug.js +29 -0
  19. package/dist/lib/test/pdf-fixture.d.ts +5 -0
  20. package/dist/lib/test/pdf-fixture.js +14 -0
  21. package/dist/loop/run-loop.js +23 -35
  22. package/dist/loop/sub-agent.d.ts +2 -2
  23. package/dist/loop/sub-agent.js +1 -1
  24. package/dist/loop/test/metric-reader.d.ts +9 -0
  25. package/dist/loop/test/metric-reader.js +33 -0
  26. package/dist/loop/test/unnamed-model.d.ts +6 -0
  27. package/dist/loop/test/unnamed-model.js +24 -0
  28. package/dist/memory/conversation-display-recorder.d.ts +2 -0
  29. package/dist/memory/conversation-display-recorder.js +6 -2
  30. package/dist/memory/conversation-display-storage.d.ts +4 -2
  31. package/dist/memory/conversation-display-storage.js +19 -1
  32. package/dist/prompts/conversation.d.ts +1 -1
  33. package/dist/prompts/conversation.js +6 -4
  34. package/dist/prompts/sandbox/mdv-preparation-agent.d.ts +1 -0
  35. package/dist/prompts/sandbox/mdv-preparation-agent.js +56 -0
  36. package/dist/report-model/fixture-resolver.js +9 -135
  37. package/dist/report-model/production-resolver.d.ts +3 -3
  38. package/dist/report-model/production-resolver.js +12 -127
  39. package/dist/report-model/reference-read.d.ts +38 -0
  40. package/dist/report-model/reference-read.js +149 -0
  41. package/dist/report-model/reference-resolver.d.ts +7 -0
  42. package/dist/report-model/reference-resolver.js +1 -1
  43. package/dist/report-render/chart.js +48 -46
  44. package/dist/report-render/figures/common.d.ts +34 -0
  45. package/dist/report-render/figures/common.js +4 -1
  46. package/dist/report-render/figures/equal-units.js +2 -4
  47. package/dist/report-render/figures/sankey.js +1 -3
  48. package/dist/report-render/figures/upset.js +2 -5
  49. package/dist/runtime/assemble.js +2 -1
  50. package/dist/sandbox/execution-id.js +2 -14
  51. package/dist/sandbox/leases.js +46 -9
  52. package/dist/schemas/validate-plan.js +2 -14
  53. package/dist/state/active-turns.d.ts +6 -2
  54. package/dist/state/active-turns.js +11 -7
  55. package/dist/state/data-profile.d.ts +8 -0
  56. package/dist/state/data-profile.js +18 -0
  57. package/dist/state/index.d.ts +1 -1
  58. package/dist/state/index.js +1 -1
  59. package/dist/state/migrations/20261102120000_record_the_waiter_of_a_pause.d.ts +6 -0
  60. package/dist/state/migrations/20261102120000_record_the_waiter_of_a_pause.js +10 -0
  61. package/dist/state/migrations/index.js +2 -0
  62. package/dist/state/paused-work.d.ts +5 -3
  63. package/dist/state/paused-work.js +11 -7
  64. package/dist/state/runs.d.ts +3 -2
  65. package/dist/state/runs.js +4 -3
  66. package/dist/tasks/data-profile-index.js +3 -17
  67. package/dist/tasks/data-profile-resolve.js +3 -17
  68. package/dist/tasks/data-profile.js +1 -0
  69. package/dist/tools/lib/opentargets-client.js +15 -25
  70. package/dist/tools/report-authoring/authoring-tools.d.ts +3 -3
  71. package/dist/tools/report-authoring/authoring-tools.js +7 -6
  72. package/dist/tools/research/generate-analogy-report.js +10 -2
  73. package/dist/workflows/execute-analysis.js +4 -3
  74. package/dist/workflows/pause.d.ts +3 -3
  75. package/dist/workflows/pause.js +15 -9
  76. package/dist/workflows/sandbox-step.js +1 -0
  77. 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
+ }
@@ -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) => {
@@ -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) {
@@ -295,10 +293,12 @@ function failureReasonOf(err) {
295
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. */
296
294
  function closingNoteOf(outcome, recorder) {
297
295
  switch (outcome.status) {
298
- case "failed":
299
- return conversationRecordTurn(failureNote(outcome.reason));
296
+ case "failed": {
297
+ const note = conversationRecordTurn(failureNote(outcome.reason));
298
+ return { ...note, displayMessages: [...recorder.openRoundPauses(), ...note.displayMessages] };
299
+ }
300
300
  case "aborted":
301
- return interruptionNote();
301
+ return interruptionNote(recorder);
302
302
  case "done":
303
303
  return outcome.finish.reason === "content-filter" ? declineNote(recorder) : undefined;
304
304
  default: {
@@ -320,13 +320,13 @@ function declineNote(recorder) {
320
320
  }
321
321
  /**
322
322
  * The note of an aborted turn. Without it, the next turn reads the request of the aborted turn as open, and
323
- * its reply also answers that request. The transcript shows the interruption from the turn record, thus the
324
- * 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.
325
325
  */
326
- function interruptionNote() {
326
+ function interruptionNote(recorder) {
327
327
  return {
328
328
  modelMessages: [syntheticUserMessage("[The user interrupted the request above. Do not continue it unless the user asks you to.]")],
329
- displayMessages: [],
329
+ displayMessages: recorder.openRoundPauses(),
330
330
  };
331
331
  }
332
332
  function failureNote(reason) {
@@ -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 {
@@ -4,7 +4,8 @@
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)
@@ -16,6 +17,7 @@
16
17
  * Activity history: activity-entry (run stream), subagent-activity (conversation)
17
18
  * Per-step final: step-output, step-summary, step-usage
18
19
  */
20
+ import type { AnalogicalReasonerOutput } from "./analogy-report.js";
19
21
  import type { ToolCallOutcome } from "./chat-events.js";
20
22
  import type { TokenUsageRollup } from "./usage.js";
21
23
  export type PlanStep = {
@@ -72,6 +74,13 @@ export type PresentationPart = {
72
74
  title?: string;
73
75
  content: PresentationContent;
74
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
+ };
75
84
  export type PlanPart = {
76
85
  type: "data-plan";
77
86
  id: string;
@@ -518,6 +527,27 @@ export type ModelRefusalPart = {
518
527
  } & ModelRefusal;
519
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. */
520
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";
521
551
  /**
522
552
  * The state of one waiting workflow of paused work. Re-emitted under the same `id` when the wait ends, with the
523
553
  * state `running`, so reconciling readers keep the latest state of each workflow.
@@ -529,17 +559,28 @@ export type PausePart = {
529
559
  /** The id of the work: the turn, the run, or the profile workflow. A resume names it. */
530
560
  workId: string;
531
561
  workKind: WorkKind;
532
- /** `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. */
533
565
  state: "retrying" | "paused" | "running";
534
- /** Absent for the state `running`. */
535
- 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;
536
570
  /** The count of timer retries before this wait. Absent for the state `running`. */
537
571
  attempt?: number;
538
572
  /** ISO 8601. Present for the state `retrying` only. */
539
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;
540
581
  };
541
582
  /** The parts whose contracts this version of the package declares — the arms the part registry knows. */
542
- 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 | ModelRefusalPart;
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;
543
584
  /**
544
585
  * A part of a known contract, or a part whose type this build does not know. An emitter newer than this
545
586
  * build can lead the registry, and such a part already rides through the check boundary and the live view
@@ -4,7 +4,8 @@
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)
@@ -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, ModelRefusal, ModelRefusalPart, 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 },