@inflexa-ai/harness 0.48.3 → 0.48.5

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 (98) hide show
  1. package/dist/agents/conversation-agent.js +5 -1
  2. package/dist/agents/sandbox/index.js +3 -0
  3. package/dist/agents/sandbox/mdv-preparation-agent.d.ts +9 -0
  4. package/dist/agents/sandbox/mdv-preparation-agent.js +37 -0
  5. package/dist/agents/sandbox/network-agent.js +1 -1
  6. package/dist/agents/sandbox/shared.js +3 -1
  7. package/dist/agents/sandbox/types.d.ts +1 -1
  8. package/dist/app/chat-turn-workflow.js +1 -0
  9. package/dist/app/chat-turn.d.ts +4 -2
  10. package/dist/app/chat-turn.js +46 -46
  11. package/dist/app/work.d.ts +7 -3
  12. package/dist/app/work.js +17 -16
  13. package/dist/contracts/chat-parts.d.ts +46 -5
  14. package/dist/contracts/chat-parts.js +2 -1
  15. package/dist/contracts/index.d.ts +1 -1
  16. package/dist/contracts/part-registry.js +1 -0
  17. package/dist/contracts/schemas/chat-parts.d.ts +168 -4
  18. package/dist/contracts/schemas/chat-parts.js +21 -1
  19. package/dist/execution/run-event-stream.d.ts +2 -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.d.ts +4 -1
  27. package/dist/loop/chat-stream.js +30 -13
  28. package/dist/loop/run-loop.js +23 -35
  29. package/dist/loop/sub-agent.d.ts +2 -2
  30. package/dist/loop/sub-agent.js +1 -1
  31. package/dist/loop/test/metric-reader.d.ts +9 -0
  32. package/dist/loop/test/metric-reader.js +33 -0
  33. package/dist/loop/test/unnamed-model.d.ts +6 -0
  34. package/dist/loop/test/unnamed-model.js +24 -0
  35. package/dist/memory/conversation-display-recorder.d.ts +2 -0
  36. package/dist/memory/conversation-display-recorder.js +6 -2
  37. package/dist/memory/conversation-display-storage.d.ts +4 -2
  38. package/dist/memory/conversation-display-storage.js +19 -1
  39. package/dist/prompts/conversation.d.ts +1 -1
  40. package/dist/prompts/conversation.js +6 -4
  41. package/dist/prompts/sandbox/mdv-preparation-agent.d.ts +1 -0
  42. package/dist/prompts/sandbox/mdv-preparation-agent.js +56 -0
  43. package/dist/prompts/sandbox/network-agent.d.ts +1 -1
  44. package/dist/prompts/sandbox/network-agent.js +4 -0
  45. package/dist/providers/errors.js +2 -1
  46. package/dist/report-model/fixture-resolver.js +9 -135
  47. package/dist/report-model/production-resolver.d.ts +3 -3
  48. package/dist/report-model/production-resolver.js +12 -127
  49. package/dist/report-model/reference-read.d.ts +38 -0
  50. package/dist/report-model/reference-read.js +149 -0
  51. package/dist/report-model/reference-resolver.d.ts +7 -0
  52. package/dist/report-model/reference-resolver.js +1 -1
  53. package/dist/report-render/chart.js +48 -46
  54. package/dist/report-render/figures/common.d.ts +34 -0
  55. package/dist/report-render/figures/common.js +4 -1
  56. package/dist/report-render/figures/equal-units.js +2 -4
  57. package/dist/report-render/figures/sankey.js +1 -3
  58. package/dist/report-render/figures/upset.js +2 -5
  59. package/dist/runtime/assemble.js +2 -1
  60. package/dist/sandbox/execution-id.js +2 -14
  61. package/dist/sandbox/leases.js +46 -9
  62. package/dist/schemas/validate-plan.js +2 -14
  63. package/dist/state/active-turns.d.ts +6 -2
  64. package/dist/state/active-turns.js +11 -7
  65. package/dist/state/data-profile.d.ts +8 -0
  66. package/dist/state/data-profile.js +18 -0
  67. package/dist/state/index.d.ts +1 -1
  68. package/dist/state/index.js +1 -1
  69. package/dist/state/migrations/20261102120000_record_the_waiter_of_a_pause.d.ts +6 -0
  70. package/dist/state/migrations/20261102120000_record_the_waiter_of_a_pause.js +10 -0
  71. package/dist/state/migrations/index.js +2 -0
  72. package/dist/state/paused-work.d.ts +5 -3
  73. package/dist/state/paused-work.js +11 -7
  74. package/dist/state/runs.d.ts +3 -2
  75. package/dist/state/runs.js +4 -3
  76. package/dist/tasks/data-profile-index.js +3 -17
  77. package/dist/tasks/data-profile-resolve.js +3 -17
  78. package/dist/tasks/data-profile.js +1 -0
  79. package/dist/tools/bio/humanbase.d.ts +32 -0
  80. package/dist/tools/bio/humanbase.js +180 -0
  81. package/dist/tools/bio/index.d.ts +3 -1
  82. package/dist/tools/bio/index.js +3 -1
  83. package/dist/tools/bio/resolve-gene-ids.d.ts +13 -0
  84. package/dist/tools/bio/resolve-gene-ids.js +67 -0
  85. package/dist/tools/lib/humanbase-client.d.ts +215 -0
  86. package/dist/tools/lib/humanbase-client.js +187 -0
  87. package/dist/tools/lib/mygene-client.d.ts +150 -0
  88. package/dist/tools/lib/mygene-client.js +109 -0
  89. package/dist/tools/lib/opentargets-client.js +15 -25
  90. package/dist/tools/report-authoring/authoring-tools.d.ts +3 -3
  91. package/dist/tools/report-authoring/authoring-tools.js +7 -6
  92. package/dist/tools/research/generate-analogy-report.js +10 -2
  93. package/dist/tools/script/run-script.js +48 -51
  94. package/dist/workflows/execute-analysis.js +4 -3
  95. package/dist/workflows/pause.d.ts +3 -3
  96. package/dist/workflows/pause.js +15 -9
  97. package/dist/workflows/sandbox-step.js +1 -0
  98. package/package.json +7 -4
@@ -95,9 +95,10 @@ export function assembleCoreRuntime(deps) {
95
95
  const toolOutputStore = createToolOutputStore(conversation.pool);
96
96
  const pauseRetryDelaysMs = deps.pauseRetryDelaysMs;
97
97
  // Each child of a sub-agent pauses with the work of its caller, thus a resume of the turn or of the run ends its wait.
98
- const subAgentPauses = ({ runSession, ...work }, emit) => workPause({
98
+ const subAgentPauses = ({ runSession, ...work }, waiter, emit) => workPause({
99
99
  pool: conversation.pool,
100
100
  ...work,
101
+ waiter,
101
102
  emit,
102
103
  retryDelaysMs: pauseRetryDelaysMs,
103
104
  run: runSession === undefined ? undefined : { session: runSession, authorizer: conversation.runAuthorizer },
@@ -7,6 +7,7 @@
7
7
  * Format: {sanitizedAgentName}-{nanoid8} (e.g., qc-agent-a1b2c3d4)
8
8
  */
9
9
  import { customAlphabet } from "nanoid";
10
+ import { slugify } from "../lib/slug.js";
10
11
  /** Length of the nanoid suffix (8 characters). */
11
12
  export const EXECUTION_ID_NANOID_LENGTH = 8;
12
13
  /** Maximum length for the sanitized agent name portion. */
@@ -27,20 +28,7 @@ const nanoid = customAlphabet("abcdefghijklmnopqrstuvwxyz0123456789");
27
28
  * - Truncated to 40 characters
28
29
  */
29
30
  export function sanitizeAgentName(name) {
30
- let out = "";
31
- let inRun = false;
32
- for (const char of name.toLowerCase()) {
33
- const keeps = (char >= "a" && char <= "z") || (char >= "0" && char <= "9");
34
- if (!keeps) {
35
- inRun = true;
36
- continue;
37
- }
38
- if (inRun && out.length > 0)
39
- out += "-";
40
- inRun = false;
41
- out += char;
42
- }
43
- return out.slice(0, AGENT_NAME_MAX_LENGTH);
31
+ return slugify(name, { maxLength: AGENT_NAME_MAX_LENGTH });
44
32
  }
45
33
  /**
46
34
  * Generate a unified execution ID for an agent invocation.
@@ -109,6 +109,21 @@ export function createSandboxLeases(config) {
109
109
  recordMachineReplaced({ cause });
110
110
  };
111
111
  // ── The lease workflow ────────────────────────────────────────────
112
+ const waiterOfLease = (input) => {
113
+ const kind = input.work.kind;
114
+ switch (kind) {
115
+ case "profile":
116
+ return { kind: "profile" };
117
+ case "run":
118
+ return { kind: "step", stepId: input.session.runFrame.stepId };
119
+ case "turn":
120
+ throw new Error("A chat turn holds no sandbox lease.");
121
+ default: {
122
+ const exhaustive = kind;
123
+ throw new Error(`Unknown work kind: ${String(exhaustive)}`);
124
+ }
125
+ }
126
+ };
112
127
  const runLease = async (input) => {
113
128
  const log = logger.with({ leaseWorkflowId: input.leaseWorkflowId, ownerWorkflowId: input.ownerWorkflowId });
114
129
  const id = input.leaseWorkflowId;
@@ -116,6 +131,7 @@ export function createSandboxLeases(config) {
116
131
  pool,
117
132
  workId: input.work.id,
118
133
  workKind: input.work.kind,
134
+ waiter: waiterOfLease(input),
119
135
  analysisId: input.analysisId,
120
136
  // A host reads the streams of the work, not of the lease workflow. The wait of the consumer mirrors the pause.
121
137
  emit: () => Promise.resolve(),
@@ -317,9 +333,8 @@ export function createSandboxLeases(config) {
317
333
  for (;;) {
318
334
  const value = await dbos.getEvent(lease.leaseWorkflowId, key, { timeoutSeconds: WAIT_SLICE_SECONDS, pollingIntervalMs: EVENT_POLL_MS });
319
335
  if (value !== null) {
320
- if (mirrored !== undefined && mirror !== undefined) {
321
- await mirror({ type: "data-pause", id: lease.leaseWorkflowId, workId: lease.workId, workKind: lease.workKind, state: "running" });
322
- }
336
+ if (mirrored !== undefined && mirror !== undefined)
337
+ await mirrorEnd(lease, mirror, mirrored);
323
338
  return value;
324
339
  }
325
340
  if (await callerCanceled())
@@ -327,33 +342,55 @@ export function createSandboxLeases(config) {
327
342
  const status = await dbos.getWorkflow(lease.leaseWorkflowId);
328
343
  if (status !== undefined && TERMINAL_STATUSES.has(status.status)) {
329
344
  const last = await dbos.getEvent(lease.leaseWorkflowId, key, { timeoutSeconds: 0 });
330
- return last ?? "ended";
345
+ if (last === null)
346
+ return "ended";
347
+ if (mirrored !== undefined && mirror !== undefined)
348
+ await mirrorEnd(lease, mirror, mirrored);
349
+ return last;
331
350
  }
332
351
  if (mirror !== undefined)
333
352
  mirrored = await mirrorPause(lease, mirror, mirrored);
334
353
  }
335
354
  };
355
+ /**
356
+ * A lease workflow pauses only at its label gate with the cause `host`, thus its wait is in `paused`, and nothing
357
+ * but a resume ends it.
358
+ */
359
+ const mirrorEnd = (lease, mirror, last) => mirror({
360
+ type: "data-pause",
361
+ id: lease.leaseWorkflowId,
362
+ workId: lease.workId,
363
+ workKind: lease.workKind,
364
+ waiter: last.waiter,
365
+ state: "running",
366
+ cause: last.cause,
367
+ pausedAt: last.pausedAt,
368
+ endedAt: new Date().toISOString(),
369
+ end: "resume",
370
+ });
336
371
  const mirrorPause = async (lease, mirror, mirrored) => {
337
372
  const pauses = unwrapOrThrow(await queryPausesOfWork(pool, lease.analysisId, lease.workId));
338
373
  const pause = pauses.find((row) => row.workflowId === lease.leaseWorkflowId);
339
- const state = pause === undefined ? undefined : `${pause.state}:${pause.attempt}`;
340
- if (state === mirrored)
341
- return mirrored;
342
374
  if (pause === undefined) {
343
- await mirror({ type: "data-pause", id: lease.leaseWorkflowId, workId: lease.workId, workKind: lease.workKind, state: "running" });
375
+ if (mirrored !== undefined)
376
+ await mirrorEnd(lease, mirror, mirrored);
344
377
  return undefined;
345
378
  }
379
+ if (mirrored?.state === pause.state && mirrored.attempt === pause.attempt && mirrored.pausedAt === pause.pausedAt)
380
+ return mirrored;
346
381
  await mirror({
347
382
  type: "data-pause",
348
383
  id: lease.leaseWorkflowId,
349
384
  workId: lease.workId,
350
385
  workKind: lease.workKind,
386
+ waiter: pause.waiter,
351
387
  state: pause.state,
352
388
  cause: pause.cause,
389
+ pausedAt: pause.pausedAt,
353
390
  attempt: pause.attempt,
354
391
  retryAt: pause.retryAt,
355
392
  });
356
- return state;
393
+ return pause;
357
394
  };
358
395
  const enqueue = async (binding, lease, size) => {
359
396
  const dbos = await engine();
@@ -17,6 +17,7 @@ import { CycleError, DependencyError, topoSortIntoWaves } from "../execution/top
17
17
  import { parseQuery } from "../sandbox/package-identity.js";
18
18
  import { isLabelValue } from "../sandbox/labels.js";
19
19
  import { isSafeId, STEP_SUBDIRS, SYNTHESIS_STEP_ID } from "../workspace/paths.js";
20
+ import { slugify } from "../lib/slug.js";
20
21
  const KNOWN_AGENTS = new Set(KNOWN_AGENT_IDS);
21
22
  /**
22
23
  * Reserved step-id names. A step id equal to an artifact subdirectory name
@@ -53,20 +54,7 @@ function describeParseError(stepId, entry, error) {
53
54
  }
54
55
  /** Derive a filesystem-safe output prefix from a step ID. */
55
56
  export function deriveOutputPrefix(stepId) {
56
- let out = "";
57
- let inRun = false;
58
- for (const char of stepId.toLowerCase()) {
59
- const isSlugChar = (char >= "a" && char <= "z") || (char >= "0" && char <= "9");
60
- if (!isSlugChar) {
61
- inRun = true;
62
- continue;
63
- }
64
- if (inRun && out.length > 0)
65
- out += "-";
66
- inRun = false;
67
- out += char;
68
- }
69
- return out;
57
+ return slugify(stepId);
70
58
  }
71
59
  /**
72
60
  * Validate an analysis plan for structural correctness.
@@ -27,8 +27,12 @@ export declare function claimThreadForTurn(pool: Querier, threadId: string, turn
27
27
  export declare function closeOpenTurnRecords(pool: Querier, threadId: string): ResultAsync<void, DbError>;
28
28
  /** The turn that runs on the thread of an ask, which waits for its answer. `undefined` when no turn runs there. */
29
29
  export declare function turnOfAsk(pool: Querier, askId: string): ResultAsync<string | undefined, DbError>;
30
- /** The turn that holds each live thread of an analysis. A claim can outlive its turn, thus a caller checks that the turn runs. */
31
- export declare function queryTurnsOfAnalysis(pool: Querier, analysisId: string): ResultAsync<{
30
+ /**
31
+ * The turn that holds each live thread of an analysis, or of each analysis when `analysisId` is absent. A claim can
32
+ * outlive its turn, thus a caller checks that the turn runs.
33
+ */
34
+ export declare function queryTurns(pool: Querier, analysisId?: string): ResultAsync<{
35
+ readonly analysisId: string;
32
36
  readonly threadId: string;
33
37
  readonly turnId: string;
34
38
  }[], DbError>;
@@ -49,15 +49,19 @@ export function turnOfAsk(pool, askId) {
49
49
  return row === undefined ? undefined : (AskTurnRowSchema.parse(row).active_turn_id ?? undefined);
50
50
  });
51
51
  }
52
- const TurnOfThreadRowSchema = z.object({ thread_id: z.string(), active_turn_id: z.string() });
53
- /** The turn that holds each live thread of an analysis. A claim can outlive its turn, thus a caller checks that the turn runs. */
54
- export function queryTurnsOfAnalysis(pool, analysisId) {
55
- return tryQuery("activeTurns.ofAnalysis", async () => {
56
- const { rows } = await pool.query(`SELECT thread_id, active_turn_id FROM cortex_analysis_threads
57
- WHERE analysis_id = $1 AND active_turn_id IS NOT NULL AND deleted_at IS NULL`, [analysisId]);
52
+ const TurnOfThreadRowSchema = z.object({ analysis_id: z.string(), thread_id: z.string(), active_turn_id: z.string() });
53
+ /**
54
+ * The turn that holds each live thread of an analysis, or of each analysis when `analysisId` is absent. A claim can
55
+ * outlive its turn, thus a caller checks that the turn runs.
56
+ */
57
+ export function queryTurns(pool, analysisId) {
58
+ return tryQuery("activeTurns.list", async () => {
59
+ const { rows } = await pool.query(`SELECT analysis_id, thread_id, active_turn_id FROM cortex_analysis_threads
60
+ WHERE ($1::text IS NULL OR analysis_id = $1) AND active_turn_id IS NOT NULL AND deleted_at IS NULL
61
+ ORDER BY thread_id`, [analysisId ?? null]);
58
62
  return rows.map((row) => {
59
63
  const parsed = TurnOfThreadRowSchema.parse(row);
60
- return { threadId: parsed.thread_id, turnId: parsed.active_turn_id };
64
+ return { analysisId: parsed.analysis_id, threadId: parsed.thread_id, turnId: parsed.active_turn_id };
61
65
  });
62
66
  });
63
67
  }
@@ -240,6 +240,14 @@ export declare function completeEmptyDataProfile(pool: Querier, analysisId: stri
240
240
  * (e.g. the seed-first guard in `triggerDataProfile`).
241
241
  */
242
242
  export declare function loadDataProfileStatus(pool: Querier, analysisId: string): ResultAsync<DataProfileStatus | null, DbError>;
243
+ /**
244
+ * The data profile that runs in an analysis, or in each analysis when `analysisId` is absent: the row is `running`
245
+ * and it names its workflow. A caller checks that the workflow is active.
246
+ */
247
+ export declare function queryRunningProfiles(pool: Querier, analysisId?: string): ResultAsync<{
248
+ readonly analysisId: string;
249
+ readonly workflowId: string;
250
+ }[], DbError>;
243
251
  /**
244
252
  * Read an analysis's raw `seed_input_file_ids`, without the NULL-status collapse
245
253
  * {@link loadDataProfileStatus} applies (which hides the seed of any cleared row).
@@ -385,6 +385,24 @@ export function loadDataProfileStatus(pool, analysisId) {
385
385
  };
386
386
  });
387
387
  }
388
+ /**
389
+ * The data profile that runs in an analysis, or in each analysis when `analysisId` is absent: the row is `running`
390
+ * and it names its workflow. A caller checks that the workflow is active.
391
+ */
392
+ export function queryRunningProfiles(pool, analysisId) {
393
+ return tryQuery("dataProfile.queryRunningProfiles", async () => {
394
+ const result = await pool.query({
395
+ text: `SELECT analysis_id, data_profile_workflow_id
396
+ FROM cortex_analysis_state
397
+ WHERE ($1::text IS NULL OR analysis_id = $1)
398
+ AND data_profile_status = 'running'
399
+ AND data_profile_workflow_id IS NOT NULL
400
+ ORDER BY analysis_id`,
401
+ values: [analysisId ?? null],
402
+ });
403
+ return result.rows.map((row) => ({ analysisId: row.analysis_id, workflowId: row.data_profile_workflow_id }));
404
+ });
405
+ }
388
406
  /**
389
407
  * Read an analysis's raw `seed_input_file_ids`, without the NULL-status collapse
390
408
  * {@link loadDataProfileStatus} applies (which hides the seed of any cleared row).
@@ -18,7 +18,7 @@ export { insertStepExecution, seedStepExecutions, sweepPendingStepExecutions, up
18
18
  export type { InsertStepExecutionInput, RunStepCounts, SeedStepExecutionRow, UpdateStepExecutionInput } from "./step-executions.js";
19
19
  export { insertPlan, upsertPlan, loadPlan, loadPlanRecord, queryPlanPage } from "./plans.js";
20
20
  export type { InsertPlanInput, UpsertPlanInput, PlanRecord, PlanRowQuerier, PlanPage, PlanPageEntry } from "./plans.js";
21
- export { tryStartDataProfile, tryRetryDataProfile, tryRerunDataProfile, completeDataProfile, failDataProfile, expireStaleDataProfile, reconcileOrphanedDataProfile, clearDataProfile, completeEmptyDataProfile, loadDataProfileStatus, loadSeedInputFileIds, recordDataProfileWorkflowId, } from "./data-profile.js";
21
+ export { tryStartDataProfile, tryRetryDataProfile, tryRerunDataProfile, completeDataProfile, failDataProfile, expireStaleDataProfile, reconcileOrphanedDataProfile, clearDataProfile, completeEmptyDataProfile, loadDataProfileStatus, loadSeedInputFileIds, queryRunningProfiles, recordDataProfileWorkflowId, } from "./data-profile.js";
22
22
  export type { DataProfileStatus, DataProfileTerminalWrite, DataProfileResult, DataProfileChecked, DataProfileCompanionCompleteness, DataProfileDimension, DataProfileGroup, DataProfileGroupSlot, DataProfileFile, DataProfileMemberAnnotation, DataProfileObservation, DataProfilePartition, DataProfileProbeReport, DataProfileQualityAssessment, DataProfileQuarantine, DataProfileRecipeStep, DataProfileKind, DataProfileAxis, DataProfileInputSignature, DataProfileCoverage, } from "./data-profile.js";
23
23
  export { queryRunCountsByAnalyses, queryThreadCountsByAnalyses, queryDataProfileStatusByAnalyses, queryLatestRunsByAnalyses, queryRunProgressByRuns, queryRunsEndedSince, } from "./analyses-metrics.js";
24
24
  export type { DataProfileSummary, EndedRunSummary, LatestRunSummary, RunProgressSummary } from "./analyses-metrics.js";
@@ -13,7 +13,7 @@ export { insertRun, reserveRunById, RunDedupCollisionError, RunIdentityCollision
13
13
  setRunSynthesisOutcome, queryRun, queryActiveRun, queryNonTerminalRunsByAnalysis, queryRunsForInspection, queryRunsByAnalysis, countRunsByAnalysis, queryRunsByThread, queryRunIdsByPlan, queryNewestPlanRun, } from "./runs.js";
14
14
  export { insertStepExecution, seedStepExecutions, sweepPendingStepExecutions, updateStepExecution, queryStepsByRun, countStepsByRuns, } from "./step-executions.js";
15
15
  export { insertPlan, upsertPlan, loadPlan, loadPlanRecord, queryPlanPage } from "./plans.js";
16
- export { tryStartDataProfile, tryRetryDataProfile, tryRerunDataProfile, completeDataProfile, failDataProfile, expireStaleDataProfile, reconcileOrphanedDataProfile, clearDataProfile, completeEmptyDataProfile, loadDataProfileStatus, loadSeedInputFileIds, recordDataProfileWorkflowId, } from "./data-profile.js";
16
+ export { tryStartDataProfile, tryRetryDataProfile, tryRerunDataProfile, completeDataProfile, failDataProfile, expireStaleDataProfile, reconcileOrphanedDataProfile, clearDataProfile, completeEmptyDataProfile, loadDataProfileStatus, loadSeedInputFileIds, queryRunningProfiles, recordDataProfileWorkflowId, } from "./data-profile.js";
17
17
  export { queryRunCountsByAnalyses, queryThreadCountsByAnalyses, queryDataProfileStatusByAnalyses, queryLatestRunsByAnalyses, queryRunProgressByRuns, queryRunsEndedSince, } from "./analyses-metrics.js";
18
18
  // Not per-entity: one analysis's whole Postgres footprint, reached from its id
19
19
  // alone, with the workflow ledger behind an injected seam.
@@ -0,0 +1,6 @@
1
+ /**
2
+ * A pause row names what waits. A row that a wait wrote before this column keeps a NULL waiter, which reads as no
3
+ * waiter, and the migration deletes no row.
4
+ */
5
+ import type { Migration } from "kysely/migration";
6
+ export declare const recordTheWaiterOfAPause: Migration;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * A pause row names what waits. A row that a wait wrote before this column keeps a NULL waiter, which reads as no
3
+ * waiter, and the migration deletes no row.
4
+ */
5
+ import { sql } from "kysely";
6
+ export const recordTheWaiterOfAPause = {
7
+ async up(db) {
8
+ await sql `ALTER TABLE cortex_paused_work ADD COLUMN IF NOT EXISTS waiter JSONB`.execute(db);
9
+ },
10
+ };
@@ -24,6 +24,7 @@ import { recordTheRunSessionOfAPause } from "./20261029120100_record_the_run_ses
24
24
  import { leaseTheSandboxes } from "./20261030120000_lease_the_sandboxes.js";
25
25
  import { dropTheReportDerivations } from "./20261030120100_drop_the_report_derivations.js";
26
26
  import { keepTheReportDerivations } from "./20261030120200_keep_the_report_derivations.js";
27
+ import { recordTheWaiterOfAPause } from "./20261102120000_record_the_waiter_of_a_pause.js";
27
28
  export function cortexMigrations(logger) {
28
29
  return {
29
30
  "20260927120000_baseline": baseline(logger),
@@ -45,5 +46,6 @@ export function cortexMigrations(logger) {
45
46
  "20261030120000_lease_the_sandboxes": leaseTheSandboxes,
46
47
  "20261030120100_drop_the_report_derivations": dropTheReportDerivations,
47
48
  "20261030120200_keep_the_report_derivations": keepTheReportDerivations,
49
+ "20261102120000_record_the_waiter_of_a_pause": recordTheWaiterOfAPause,
48
50
  };
49
51
  }
@@ -5,7 +5,7 @@
5
5
  import type { ResultAsync } from "neverthrow";
6
6
  import type { QueryConfig } from "pg";
7
7
  import type { RunSession } from "../auth/types.js";
8
- import type { PauseCause, WorkKind } from "../contracts/chat-parts.js";
8
+ import type { PauseCause, PauseWaiter, WorkKind } from "../contracts/chat-parts.js";
9
9
  import { type DbError } from "../lib/db-result.js";
10
10
  import type { Querier } from "./db.js";
11
11
  /** One waiting workflow, as its row records it. */
@@ -13,6 +13,8 @@ export type PauseRecord = {
13
13
  readonly workflowId: string;
14
14
  readonly workId: string;
15
15
  readonly workKind: WorkKind;
16
+ /** What waits. Absent for a row that a wait wrote before the waiter was recorded. */
17
+ readonly waiter?: PauseWaiter;
16
18
  readonly analysisId: string;
17
19
  readonly threadId?: string;
18
20
  readonly state: "retrying" | "paused";
@@ -46,7 +48,7 @@ export declare function clearPause(pool: Querier, workflowId: string): ResultAsy
46
48
  export declare function clearPauses(pool: Querier, workflowIds: readonly string[]): ResultAsync<void, DbError>;
47
49
  /** Delete the rows of each workflow of the work, for a stop of the work. */
48
50
  export declare function clearPausesOfWork(pool: Querier, workId: string): ResultAsync<void, DbError>;
49
- /** The rows of the paused work of an analysis, the oldest pause first. */
50
- export declare function queryPausesOfAnalysis(pool: PauseRowQuerier, analysisId: string): ResultAsync<PauseRecord[], DbError>;
51
+ /** The rows of the paused work of an analysis, or of each analysis when `analysisId` is absent. The oldest pause is first. */
52
+ export declare function queryPauses(pool: PauseRowQuerier, analysisId?: string): ResultAsync<PauseRecord[], DbError>;
51
53
  /** The rows of the waiting workflows of one work in an analysis. */
52
54
  export declare function queryPausesOfWork(pool: PauseRowQuerier, analysisId: string, workId: string): ResultAsync<PauseRecord[], DbError>;
@@ -3,6 +3,7 @@
3
3
  * before it waits and deletes it when the wait ends. A stop of the work deletes the rows of the work.
4
4
  */
5
5
  import { z } from "zod";
6
+ import { PauseWaiterSchema } from "../contracts/schemas/chat-parts.js";
6
7
  import { tryMutation, tryQuery } from "../lib/db-result.js";
7
8
  const PauseCauseSchema = z.discriminatedUnion("kind", [
8
9
  z.object({ kind: z.literal("outage") }),
@@ -13,6 +14,7 @@ const PauseRowSchema = z.object({
13
14
  workflow_id: z.string(),
14
15
  work_id: z.string(),
15
16
  work_kind: z.enum(["turn", "run", "profile"]),
17
+ waiter: PauseWaiterSchema.nullable(),
16
18
  analysis_id: z.string(),
17
19
  thread_id: z.string().nullable(),
18
20
  state: z.enum(["retrying", "paused"]),
@@ -33,13 +35,14 @@ const PauseRowSchema = z.object({
33
35
  })
34
36
  .nullable(),
35
37
  });
36
- const COLUMNS = "workflow_id, work_id, work_kind, analysis_id, thread_id, state, cause, attempt, retry_at, topic, paused_at, run_session";
38
+ const COLUMNS = "workflow_id, work_id, work_kind, waiter, analysis_id, thread_id, state, cause, attempt, retry_at, topic, paused_at, run_session";
37
39
  function recordOf(row) {
38
40
  const parsed = PauseRowSchema.parse(row);
39
41
  return {
40
42
  workflowId: parsed.workflow_id,
41
43
  workId: parsed.work_id,
42
44
  workKind: parsed.work_kind,
45
+ waiter: parsed.waiter ?? undefined,
43
46
  analysisId: parsed.analysis_id,
44
47
  threadId: parsed.thread_id ?? undefined,
45
48
  state: parsed.state,
@@ -54,15 +57,16 @@ function recordOf(row) {
54
57
  /** Write the row of a waiting workflow. A workflow waits in one pause at a time, thus a new pause replaces its row. */
55
58
  export function recordPause(pool, record) {
56
59
  return tryMutation("pausedWork.record", () => pool.query(`INSERT INTO cortex_paused_work (${COLUMNS})
57
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
60
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
58
61
  ON CONFLICT (workflow_id) DO UPDATE SET
59
- work_id = EXCLUDED.work_id, work_kind = EXCLUDED.work_kind, analysis_id = EXCLUDED.analysis_id,
62
+ work_id = EXCLUDED.work_id, work_kind = EXCLUDED.work_kind, waiter = EXCLUDED.waiter, analysis_id = EXCLUDED.analysis_id,
60
63
  thread_id = EXCLUDED.thread_id, state = EXCLUDED.state, cause = EXCLUDED.cause, attempt = EXCLUDED.attempt,
61
64
  retry_at = EXCLUDED.retry_at, topic = EXCLUDED.topic, paused_at = EXCLUDED.paused_at,
62
65
  run_session = EXCLUDED.run_session`, [
63
66
  record.workflowId,
64
67
  record.workId,
65
68
  record.workKind,
69
+ record.waiter === undefined ? null : JSON.stringify(record.waiter),
66
70
  record.analysisId,
67
71
  record.threadId ?? null,
68
72
  record.state,
@@ -86,10 +90,10 @@ export function clearPauses(pool, workflowIds) {
86
90
  export function clearPausesOfWork(pool, workId) {
87
91
  return tryMutation("pausedWork.clearWork", () => pool.query("DELETE FROM cortex_paused_work WHERE work_id = $1", [workId])).map(() => undefined);
88
92
  }
89
- /** The rows of the paused work of an analysis, the oldest pause first. */
90
- export function queryPausesOfAnalysis(pool, analysisId) {
91
- return tryQuery("pausedWork.ofAnalysis", async () => {
92
- const { rows } = await pool.query(`SELECT ${COLUMNS} FROM cortex_paused_work WHERE analysis_id = $1 ORDER BY paused_at, workflow_id`, [analysisId]);
93
+ /** The rows of the paused work of an analysis, or of each analysis when `analysisId` is absent. The oldest pause is first. */
94
+ export function queryPauses(pool, analysisId) {
95
+ return tryQuery("pausedWork.list", async () => {
96
+ const { rows } = await pool.query(`SELECT ${COLUMNS} FROM cortex_paused_work WHERE ($1::text IS NULL OR analysis_id = $1) ORDER BY paused_at, workflow_id`, [analysisId ?? null]);
93
97
  return rows.map(recordOf);
94
98
  });
95
99
  }
@@ -99,9 +99,10 @@ export type RunTextFilter = {
99
99
  * in step: a new non-terminal status added to one and not the other makes a live run invisible to
100
100
  * exactly one of its readers.
101
101
  *
102
- * With `q`, only the runs that the text filter selects are members.
102
+ * With `q`, only the runs that the text filter selects are members. With no `analysisId`, the runs of each
103
+ * analysis are members, and each row names its analysis.
103
104
  */
104
- export declare function queryActiveRunsByAnalysis(pool: Querier, analysisId: string, { q }?: RunTextFilter): ResultAsync<CortexRunRow[], DbError>;
105
+ export declare function queryActiveRunsByAnalysis(pool: Querier, analysisId: string | undefined, { q }?: RunTextFilter): ResultAsync<CortexRunRow[], DbError>;
105
106
  /**
106
107
  * How long a reserved run may wait for its workflow before {@link reconcileOrphanedRuns} fails it. A launch writes
107
108
  * the workflow row seconds after the reservation, and the margin covers a slow authorization gate of a managed host.
@@ -165,18 +165,19 @@ function runTextCondition(placeholder) {
165
165
  * in step: a new non-terminal status added to one and not the other makes a live run invisible to
166
166
  * exactly one of its readers.
167
167
  *
168
- * With `q`, only the runs that the text filter selects are members.
168
+ * With `q`, only the runs that the text filter selects are members. With no `analysisId`, the runs of each
169
+ * analysis are members, and each row names its analysis.
169
170
  */
170
171
  export function queryActiveRunsByAnalysis(pool, analysisId, { q } = {}) {
171
172
  return tryQuery("runs.queryActiveRunsByAnalysis", async () => {
172
- const values = [analysisId];
173
+ const values = [analysisId ?? null];
173
174
  const text = q === undefined ? "" : `AND ${runTextCondition(`$${values.push(q)}`)}`;
174
175
  const result = await pool.query({
175
176
  text: `SELECT run_id, analysis_id, thread_id, workflow_name,
176
177
  status, started_at, completed_at, error, synthesis_status, synthesis_reason, parts,
177
178
  mandate_id, plan_id -- oss-core-managed-ok: run-mandate ledger (nullable; OSS leaves null)
178
179
  FROM cortex_runs
179
- WHERE analysis_id = $1
180
+ WHERE ($1::text IS NULL OR analysis_id = $1)
180
181
  AND status = 'running'
181
182
  ${text}
182
183
  ORDER BY started_at DESC`,
@@ -21,6 +21,7 @@
21
21
  * `workspace_search` documents, so a consumer needs no id parsing to tell them apart.
22
22
  */
23
23
  import { presentFirst } from "../lib/present.js";
24
+ import { slugify } from "../lib/slug.js";
24
25
  import { dimensionCardinalities, profileDimensions, profileFileRecords, profileGroups } from "../app/data-profile-view.js";
25
26
  /** Entity entries built for a legacy profile. Past this the entity tier is a liability, not a search aid. */
26
27
  export const MAX_ENTITY_ENTRIES = 20_000;
@@ -36,25 +37,10 @@ export const MAX_ENTITY_ENTRIES = 20_000;
36
37
  export const PROFILE_INDEX_TYPES = ["input-group", "input-dimension", "input", "input-kind"];
37
38
  /** Group names named in one legacy entity entry's text. */
38
39
  const MAX_GROUPS_IN_ENTITY_TEXT = 6;
40
+ /** The profile entry slug: capped at 60 characters, and "unnamed" when the name holds no slug character. */
39
41
  function slug(value) {
40
- let out = "";
41
- let inRun = false;
42
- for (const char of value.toLowerCase()) {
43
- if (out.length >= SLUG_MAX_LENGTH)
44
- break;
45
- const isSlugChar = (char >= "a" && char <= "z") || (char >= "0" && char <= "9");
46
- if (!isSlugChar) {
47
- inRun = true;
48
- continue;
49
- }
50
- if (inRun && out.length > 0)
51
- out += "-";
52
- inRun = false;
53
- out += char;
54
- }
55
- return out.slice(0, SLUG_MAX_LENGTH) || "unnamed";
42
+ return slugify(value, { maxLength: 60, fallback: "unnamed" });
56
43
  }
57
- const SLUG_MAX_LENGTH = 60;
58
44
  function slotSummary(slot) {
59
45
  return { id: slot.id, location: slot.location, tokenClass: slot.tokenClass, distinctValues: slot.distinctValues };
60
46
  }
@@ -18,6 +18,7 @@
18
18
  */
19
19
  import { assessCompleteness } from "../input-scan/companions.js";
20
20
  import { presentAt, presentFirst, presentGet } from "../lib/present.js";
21
+ import { slugify } from "../lib/slug.js";
21
22
  import { buildSetMenu } from "../input-scan/menu.js";
22
23
  import { MAX_SLOT_SAMPLE_VALUES } from "../input-scan/tuning.js";
23
24
  import { dimensionScope } from "../contracts/profile-vocabulary.js";
@@ -35,25 +36,10 @@ export const UNCLASSIFIED_GROUP_ID = "unclassified";
35
36
  export const MAX_UNCLASSIFIED_RECIPE_PATHS = 1000;
36
37
  /** Contested paths named in the accounting before the count stands alone. */
37
38
  export const MAX_CONTESTED_SAMPLE = 10;
39
+ /** The recipe group slug: capped at 60 characters, and "group" when the label holds no slug character. */
38
40
  function slug(value) {
39
- let out = "";
40
- let inRun = false;
41
- for (const char of value.toLowerCase()) {
42
- if (out.length >= SLUG_MAX_LENGTH)
43
- break;
44
- const isSlugChar = (char >= "a" && char <= "z") || (char >= "0" && char <= "9");
45
- if (!isSlugChar) {
46
- inRun = true;
47
- continue;
48
- }
49
- if (inRun && out.length > 0)
50
- out += "-";
51
- inRun = false;
52
- out += char;
53
- }
54
- return out.slice(0, SLUG_MAX_LENGTH) || "group";
41
+ return slugify(value, { maxLength: 60, fallback: "group" });
55
42
  }
56
- const SLUG_MAX_LENGTH = 60;
57
43
  function fileCountOf(members) {
58
44
  return members.reduce((total, member) => total + 1 + member.companions.length, 0);
59
45
  }
@@ -418,6 +418,7 @@ export async function runDataProfileBody(input, deps) {
418
418
  pool: deps.pool,
419
419
  workId: workflowId,
420
420
  workKind: "profile",
421
+ waiter: { kind: "profile" },
421
422
  analysisId,
422
423
  emit: (part) => DBOS.writeStream("events", part),
423
424
  retryDelaysMs: deps.pauseRetryDelaysMs,
@@ -0,0 +1,32 @@
1
+ /**
2
+ * humanbase — the GIANT tissue-specific functional networks of HumanBase.
3
+ *
4
+ * One database, three questions, thus one tool with an `action` (the barrel
5
+ * rule in `index.ts`). The input is a flat object with an `action` enum, not a
6
+ * `z.discriminatedUnion`, because model tool calling needs a top-level
7
+ * `"type":"object"`. The argument of each action is optional in the schema, and
8
+ * a `.refine` makes it required for its action.
9
+ *
10
+ * The GIANT version is not an input: the client derives it from the slug.
11
+ */
12
+ import { type HumanBaseContext, type HumanBaseNetwork, type HumanBaseScoredContext } from "../lib/humanbase-client.js";
13
+ export type HumanBaseOutput = ({
14
+ found: true;
15
+ } & HumanBaseNetwork) | {
16
+ found: false;
17
+ context: string;
18
+ } | {
19
+ contexts: HumanBaseScoredContext[];
20
+ totalFound: number;
21
+ } | {
22
+ contexts: HumanBaseContext[];
23
+ totalFound: number;
24
+ };
25
+ export declare const humanbaseTool: import("../define-tool.js").Tool<{
26
+ action: "contexts" | "network" | "relevant";
27
+ entrez?: number[] | undefined;
28
+ context?: string | undefined;
29
+ nodeSize?: number | undefined;
30
+ query?: string | undefined;
31
+ limit?: number | undefined;
32
+ }, HumanBaseOutput>;