@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
@@ -0,0 +1,149 @@
1
+ /**
2
+ * The reads of a report reference that the two resolver realizations share.
3
+ *
4
+ * The fixture resolver reads the rows of a snapshot, and the production resolver reads the rows of the
5
+ * pinned artifact on disk. What both do with those rows — the locator walk, the table read, and the
6
+ * derivation arithmetic — lives here, so one semantics exists for both realizations.
7
+ */
8
+ import { err, ok } from "neverthrow";
9
+ import { asFiniteNumber, cellMatchesFilterValue, checkCitationAssertion, checkValueAssertion } from "./assert-rules.js";
10
+ import { applyRowBound, columnsHeldByNoRow, defineOwn } from "./reference-resolver.js";
11
+ /** Build an `Err` that carries the unresolved reference. The `detail` key is present only when there is a detail to carry. */
12
+ export function fail(reference, reason, detail) {
13
+ const failure = detail !== undefined ? { reference, reason, detail } : { reference, reason };
14
+ return err(failure);
15
+ }
16
+ /** A short account of a derivation input, for a detail that names which input broke the arithmetic. */
17
+ function describeReference(reference) {
18
+ return `artifact value at ${reference.path} column ${reference.locator.column}`;
19
+ }
20
+ /**
21
+ * Walk a locator over the rows, and give back the one row that it selects, or the failure.
22
+ *
23
+ * A row index selects the row at that position. A row filter selects the one row that matches; no match
24
+ * selects nothing, and two or more matches are ambiguous.
25
+ */
26
+ export function selectRow(rows, locator) {
27
+ if (locator.row !== undefined) {
28
+ const row = rows[locator.row];
29
+ if (row === undefined) {
30
+ return err({ reason: "locator-out-of-range", detail: `row ${locator.row} is outside the ${rows.length} rows` });
31
+ }
32
+ return ok(row);
33
+ }
34
+ if (locator.rowFilter !== undefined) {
35
+ const filter = locator.rowFilter;
36
+ const matches = rows.filter((row) => cellMatchesFilterValue(row[filter.column], filter.value));
37
+ const match = matches[0];
38
+ if (match === undefined) {
39
+ return err({ reason: "locator-out-of-range", detail: `no row where ${filter.column} equals ${String(filter.value)}` });
40
+ }
41
+ if (matches.length > 1) {
42
+ return err({ reason: "ambiguous-match", detail: `${matches.length} rows where ${filter.column} equals ${String(filter.value)}` });
43
+ }
44
+ return ok(match);
45
+ }
46
+ // The locator schema forbids a locator with neither selector. The resolver cannot depend on a
47
+ // prior parse, thus it treats a row that no selector addresses as out of range.
48
+ return err({ reason: "locator-out-of-range", detail: "the locator selects no row" });
49
+ }
50
+ /**
51
+ * Read the table that a table reference addresses out of rows that the caller read from the artifact:
52
+ * cut the rows on the row bound, refuse a column that no row holds, and project the requested columns.
53
+ */
54
+ export function readArtifactTable(reference, allRows) {
55
+ // The bound ranks the rows as the artifact holds them, thus a bound over a column that the subset
56
+ // below leaves out still ranks them. An unknown bound column addresses nothing, exactly as an
57
+ // unknown subset column does.
58
+ const bound = reference.rowBound;
59
+ if (bound !== undefined && columnsHeldByNoRow(allRows, [bound.column]).length > 0) {
60
+ return fail(reference, "locator-out-of-range", `the row bound names column ${bound.column}, which the table at ${reference.path} does not hold`);
61
+ }
62
+ const rows = bound === undefined ? allRows : applyRowBound(allRows, bound);
63
+ // The page states the shown count against the count of the artifact. This read is the one place that
64
+ // holds both, thus a bounded table carries the pre-bound total out with its rows.
65
+ const total = bound === undefined ? {} : { total: allRows.length };
66
+ const columns = reference.columns;
67
+ if (columns === undefined) {
68
+ return ok({ type: "table", rows, ...total });
69
+ }
70
+ // A name that no row holds addresses nothing. Without this check a projection onto an invented column
71
+ // gives an empty cell for each row and still resolves, which is the fabrication that grounding exists
72
+ // to reject.
73
+ const absent = columnsHeldByNoRow(rows, columns);
74
+ if (absent.length > 0) {
75
+ return fail(reference, "locator-out-of-range", `the table at ${reference.path} holds no column ${absent.join(", ")}`);
76
+ }
77
+ // Project each row onto the requested columns. A table tolerates a ragged row, thus a column that a
78
+ // given row lacks is left out of that row and is not a failure.
79
+ const projectedRows = rows.map((row) => {
80
+ const projected = {};
81
+ for (const column of columns) {
82
+ const cell = row[column];
83
+ if (cell !== undefined) {
84
+ defineOwn(projected, column, cell);
85
+ }
86
+ }
87
+ return projected;
88
+ });
89
+ return ok({ type: "table", rows: projectedRows, columns, ...total });
90
+ }
91
+ /**
92
+ * Resolve a derivation: read each input with `resolveInput`, then run the arithmetic of the operation.
93
+ *
94
+ * The read of an input stays with the realization: the fixture resolver reads its snapshot, and the
95
+ * production resolver reads the pinned artifact on disk.
96
+ */
97
+ export async function resolveDerivation(reference, snapshot, resolveInput) {
98
+ const numbers = [];
99
+ for (const input of reference.inputs) {
100
+ const inputResult = await resolveInput(input, snapshot);
101
+ if (inputResult.isErr()) {
102
+ // Keep the inner reason so a reviewer sees the real cause, for example a missing artifact under
103
+ // the derivation. The derivation itself did not fail on its own terms.
104
+ return fail(reference, inputResult.error.reason, inputResult.error.detail);
105
+ }
106
+ const resolved = inputResult.value;
107
+ const numeric = resolved.type === "scalar" ? asFiniteNumber(resolved.value) : undefined;
108
+ if (numeric === undefined) {
109
+ // The arithmetic needs a finite number. A cell that holds text does not address a usable
110
+ // scalar, thus the closest reason is that the coordinate is out of range.
111
+ return fail(reference, "locator-out-of-range", `input ${describeReference(input)} did not resolve to a finite number`);
112
+ }
113
+ numbers.push(numeric);
114
+ }
115
+ const a = numbers[0];
116
+ const b = numbers[1];
117
+ if (a === undefined || b === undefined) {
118
+ return fail(reference, "locator-out-of-range", `operation ${reference.op} needs two inputs, and the derivation holds ${numbers.length}`);
119
+ }
120
+ let result;
121
+ switch (reference.op) {
122
+ case "ratio":
123
+ result = a / b;
124
+ break;
125
+ case "delta":
126
+ result = a - b;
127
+ break;
128
+ case "pctChange":
129
+ // A fraction, not a percent. A change of one half gives 0.5, thus an author asserts 0.5 and a
130
+ // `unit` of `%` only tells a renderer how to show it.
131
+ result = (a - b) / b;
132
+ break;
133
+ }
134
+ if (!Number.isFinite(result)) {
135
+ return fail(reference, "locator-out-of-range", `operation ${reference.op} does not yield a finite value, because the divisor is zero`);
136
+ }
137
+ const valueFailure = checkValueAssertion(reference, reference.assert?.value, reference.assert?.tolerance, result);
138
+ return valueFailure !== undefined ? err(valueFailure) : ok({ type: "scalar", value: result });
139
+ }
140
+ /** Resolve a citation: confirm that the id is in the pinned evidence, and echo the key back. */
141
+ export function resolveCitation(reference, snapshot) {
142
+ const key = `${reference.idKind}:${reference.id}`;
143
+ if (snapshot.citations === undefined || !snapshot.citations.includes(key)) {
144
+ return fail(reference, "artifact-missing", `the citation ${key} is not in the pinned evidence`);
145
+ }
146
+ const value = { type: "citation", id: key };
147
+ const citationFailure = checkCitationAssertion(reference, reference.assert?.value, key);
148
+ return citationFailure !== undefined ? err(citationFailure) : ok(value);
149
+ }
@@ -34,6 +34,13 @@ export type ArtifactSnapshot = {
34
34
  fileType?: string | null;
35
35
  rows?: Array<Record<string, string | number | null | undefined>>;
36
36
  };
37
+ /**
38
+ * Put one own data property on a row.
39
+ *
40
+ * A plain assignment at the key `__proto__` reaches the prototype setter, thus the row loses a column
41
+ * that the payload carries. The definition makes an own data property for each key.
42
+ */
43
+ export declare function defineOwn(target: Record<string, string | number>, key: string, value: string | number): void;
37
44
  /**
38
45
  * Give back the present cells of a snapshot entry's rows, or an empty array for an entry with no rows.
39
46
  *
@@ -16,7 +16,7 @@ const CellSchema = z.union([z.string(), z.number()]).nullish();
16
16
  * A plain assignment at the key `__proto__` reaches the prototype setter, thus the row loses a column
17
17
  * that the payload carries. The definition makes an own data property for each key.
18
18
  */
19
- function defineOwn(target, key, value) {
19
+ export function defineOwn(target, key, value) {
20
20
  Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true });
21
21
  }
22
22
  /**
@@ -888,18 +888,10 @@ function deriveTransformedRows(rows, derived) {
888
888
  * parts of a category.
889
889
  */
890
890
  function deriveBar(block, rows, columns, mode) {
891
- const xResult = requireColumn(block, rows, columns, "x");
892
- if (xResult.isErr())
893
- return err(xResult.error);
894
- const yResult = requireColumn(block, rows, columns, "y");
895
- if (yResult.isErr())
896
- return err(yResult.error);
897
- const x = xResult.value;
898
- const y = yResult.value;
899
- const ordered = orderQuickCategories(block.id, rows, x, block.orders.x);
900
- if (ordered.isErr())
901
- return err(ordered.error);
902
- const categories = ordered.value;
891
+ const axes = axesOf(block, rows, columns);
892
+ if (axes.isErr())
893
+ return err(axes.error);
894
+ const { x, y, categories } = axes.value;
903
895
  const horizontal = block.orientation === "horizontal";
904
896
  let series;
905
897
  if (mode === "bar") {
@@ -1188,23 +1180,14 @@ function deriveHistogram(block, rows, columns) {
1188
1180
  * point inside the slot of its group.
1189
1181
  */
1190
1182
  function deriveBox(block, rows, columns) {
1191
- const xResult = requireColumn(block, rows, columns, "x");
1192
- if (xResult.isErr())
1193
- return err(xResult.error);
1194
- const yResult = requireColumn(block, rows, columns, "y");
1195
- if (yResult.isErr())
1196
- return err(yResult.error);
1197
- const x = xResult.value;
1198
- const y = yResult.value;
1199
- const ordered = orderQuickCategories(block.id, rows, x, block.orders.x);
1200
- if (ordered.isErr())
1201
- return err(ordered.error);
1202
- const categories = ordered.value;
1203
- const groupColumn = block.encoding.group;
1204
- const groups = groupColumn === undefined ? [{ name: undefined }] : firstAppearance(rows.map((row) => row[groupColumn])).map((name) => ({ name }));
1205
- const bound = slotFault(block.id, categories.length, groups.length);
1206
- if (bound !== undefined)
1207
- return err(bound);
1183
+ const axes = axesOf(block, rows, columns);
1184
+ if (axes.isErr())
1185
+ return err(axes.error);
1186
+ const { x, y, categories } = axes.value;
1187
+ const slots = slotGroupsOf(block, rows, categories);
1188
+ if (slots.isErr())
1189
+ return err(slots.error);
1190
+ const { groupColumn, groups } = slots.value;
1208
1191
  const focused = focusSet(block.id, block.focus, rows.length, groups.flatMap((group) => (group.name === undefined ? [] : [group.name])));
1209
1192
  if (focused.isErr())
1210
1193
  return err(focused.error);
@@ -1446,23 +1429,14 @@ function derivePie(block, rows, columns) {
1446
1429
  * violin draws its points on the hidden slot axis, each point inside the slot of its group.
1447
1430
  */
1448
1431
  function deriveViolin(block, rows, columns) {
1449
- const xResult = requireColumn(block, rows, columns, "x");
1450
- if (xResult.isErr())
1451
- return err(xResult.error);
1452
- const yResult = requireColumn(block, rows, columns, "y");
1453
- if (yResult.isErr())
1454
- return err(yResult.error);
1455
- const x = xResult.value;
1456
- const y = yResult.value;
1457
- const ordered = orderQuickCategories(block.id, rows, x, block.orders.x);
1458
- if (ordered.isErr())
1459
- return err(ordered.error);
1460
- const categories = ordered.value;
1461
- const groupColumn = block.encoding.group;
1462
- const groups = groupColumn === undefined ? [{ name: undefined }] : firstAppearance(rows.map((row) => row[groupColumn])).map((name) => ({ name }));
1463
- const bound = slotFault(block.id, categories.length, groups.length);
1464
- if (bound !== undefined)
1465
- return err(bound);
1432
+ const axes = axesOf(block, rows, columns);
1433
+ if (axes.isErr())
1434
+ return err(axes.error);
1435
+ const { x, y, categories } = axes.value;
1436
+ const slots = slotGroupsOf(block, rows, categories);
1437
+ if (slots.isErr())
1438
+ return err(slots.error);
1439
+ const { groupColumn, groups } = slots.value;
1466
1440
  const named = groups.flatMap((group) => (group.name === undefined ? [] : [group.name]));
1467
1441
  const focused = focusSet(block.id, block.focus, rows.length, named);
1468
1442
  if (focused.isErr())
@@ -1651,6 +1625,34 @@ function slotFault(blockId, categories, groups) {
1651
1625
  return undefined;
1652
1626
  return problem(blockId, `The chart holds ${categories} categories and ${groups} groups, thus ${slots} slots. A chart holds ${CHART_SLOT_LIMIT} slots at most, thus a table of fewer categories or fewer groups serves the reader.`);
1653
1627
  }
1628
+ /**
1629
+ * The x and y columns and the category order that the category derivations share: each one demands both
1630
+ * channels and reads its categories through the quick-path order.
1631
+ */
1632
+ function axesOf(block, rows, columns) {
1633
+ const xResult = requireColumn(block, rows, columns, "x");
1634
+ if (xResult.isErr())
1635
+ return err(xResult.error);
1636
+ const yResult = requireColumn(block, rows, columns, "y");
1637
+ if (yResult.isErr())
1638
+ return err(yResult.error);
1639
+ const ordered = orderQuickCategories(block.id, rows, xResult.value, block.orders.x);
1640
+ if (ordered.isErr())
1641
+ return err(ordered.error);
1642
+ return ok({ x: xResult.value, y: yResult.value, categories: ordered.value });
1643
+ }
1644
+ /**
1645
+ * The slot layout that the box and the violin share: a chart with no group channel holds one unnamed group,
1646
+ * and a layout past the slot limit refuses before any series is built.
1647
+ */
1648
+ function slotGroupsOf(block, rows, categories) {
1649
+ const groupColumn = block.encoding.group;
1650
+ const groups = groupColumn === undefined ? [{ name: undefined }] : firstAppearance(rows.map((row) => row[groupColumn])).map((name) => ({ name }));
1651
+ const bound = slotFault(block.id, categories.length, groups.length);
1652
+ if (bound !== undefined)
1653
+ return err(bound);
1654
+ return ok({ groupColumn, groups });
1655
+ }
1654
1656
  /** The radius of a radar. The legend and the indicator names sit outside it. */
1655
1657
  const RADAR_RADIUS = "62%";
1656
1658
  /** The tier of one row count. The two counts are constants of the design source. */
@@ -22,6 +22,40 @@ import { type ChannelOrder, type ChartEncoding, type ChartTransform } from "../.
22
22
  import { type ArtifactTableReference, type ColumnMeaning } from "../../contracts/report-reference.js";
23
23
  import type { Cell, ChartRow, EchartOption } from "../chart.js";
24
24
  import type { RenderProblem } from "../types.js";
25
+ /** The size of one chart box and its text: the page, or one export. */
26
+ export type ChartBox = {
27
+ readonly widthPx: number;
28
+ readonly heightPx: number;
29
+ readonly textPx: number;
30
+ };
31
+ /** The page box, and the export sizes that take a rule of their own: the two journal columns and the slide. */
32
+ export declare const PAGE_BOX: ChartBox;
33
+ export declare const EXPORT_SIZES: readonly [{
34
+ readonly theme: "inflexa-print";
35
+ readonly textPx: 9.33;
36
+ readonly widthPx: 336;
37
+ readonly heightPx: 253;
38
+ readonly widthMm: 89;
39
+ readonly heightMm: 67;
40
+ readonly pixelRatio: number;
41
+ readonly label: "89 mm";
42
+ }, {
43
+ readonly theme: "inflexa-print";
44
+ readonly textPx: 9.33;
45
+ readonly widthPx: 692;
46
+ readonly heightPx: 348;
47
+ readonly widthMm: 183;
48
+ readonly heightMm: 92;
49
+ readonly pixelRatio: number;
50
+ readonly label: "183 mm";
51
+ }, {
52
+ readonly theme: "inflexa-slide";
53
+ readonly textPx: 24;
54
+ readonly widthPx: 1920;
55
+ readonly heightPx: 1080;
56
+ readonly pixelRatio: 1;
57
+ readonly label: "16:9";
58
+ }];
25
59
  /** The display labels that a bound table declares, keyed by the raw column name. */
26
60
  export type ColumnLabels = ArtifactTableReference["columnLabels"];
27
61
  /** The column meanings that a bound table declares, keyed by the raw column name. */
@@ -23,9 +23,12 @@ import { channelColumn, channelOrder, channelTransform } from "../../contracts/r
23
23
  import { declaredForColumn } from "../../contracts/report-reference.js";
24
24
  import { isUnknownArray } from "../../lib/unknown-array.js";
25
25
  import { parseUnknownRecord } from "../../lib/unknown-containers.js";
26
- import { CHART_BODY_MAX_PX, CHART_FONT_STACK, CHART_INK, CHART_PAGE_TEXT_PX, CHART_PALETTE, CHART_WIDE_PALETTE, DIVERGING_RAMP, GUIDE_LINE_COLOR, GUIDE_LINE_WIDTH_PX, MUTED_CHART_COLOR, SEQUENTIAL_RAMP, SIZE_CHANNEL_RANGE_PX, } from "../design.js";
26
+ import { CHART_BODY_MAX_PX, CHART_BODY_PX, CHART_EXPORT_SIZES, CHART_FONT_STACK, CHART_INK, CHART_PAGE_TEXT_PX, CHART_PAGE_WIDTH_PX, CHART_PALETTE, CHART_WIDE_PALETTE, DIVERGING_RAMP, GUIDE_LINE_COLOR, GUIDE_LINE_WIDTH_PX, MUTED_CHART_COLOR, SEQUENTIAL_RAMP, SIZE_CHANNEL_RANGE_PX, } from "../design.js";
27
27
  import { formatNumberCell, selectNumberKind, typographicExponent } from "../number-format.js";
28
28
  import { STEM_RENDERER } from "../chart-renderers.js";
29
+ /** The page box, and the export sizes that take a rule of their own: the two journal columns and the slide. */
30
+ export const PAGE_BOX = { widthPx: CHART_PAGE_WIDTH_PX, heightPx: CHART_BODY_PX, textPx: CHART_PAGE_TEXT_PX };
31
+ export const EXPORT_SIZES = [CHART_EXPORT_SIZES.single, CHART_EXPORT_SIZES.double, CHART_EXPORT_SIZES.slide];
29
32
  // ── The cells ───────────────────────────────────────────────────────────────
30
33
  /** A typed `invalid-chart-input` problem that names the block and the cause. */
31
34
  export function chartProblem(blockId, detail) {
@@ -22,8 +22,8 @@
22
22
  * largest container, thus no other container meets it.
23
23
  */
24
24
  import { TOOLBOX_BAND_PX } from "../chart-toolbox.js";
25
- import { CHART_EXPORT_SIZES, CHART_PAGE_TEXT_PX, FACET_COLUMNS } from "../design.js";
26
- import { legendLineCount, legendLinePx } from "./common.js";
25
+ import { CHART_PAGE_TEXT_PX, FACET_COLUMNS } from "../design.js";
26
+ import { EXPORT_SIZES, legendLineCount, legendLinePx } from "./common.js";
27
27
  /** The smallest square side, in pixels. A container smaller than its rule still draws this square. */
28
28
  const SQUARE_MIN_PX = 120;
29
29
  /** The largest square side, in pixels. The slide export is the largest container. */
@@ -83,8 +83,6 @@ export function squareLayout(fields) {
83
83
  media,
84
84
  };
85
85
  }
86
- /** The export sizes that take a rule of their own: the two journal columns and the slide. */
87
- const EXPORT_SIZES = [CHART_EXPORT_SIZES.single, CHART_EXPORT_SIZES.double, CHART_EXPORT_SIZES.slide];
88
86
  /** The scale of the margins of a page square: one up to the page square, and larger with a larger square. */
89
87
  function marginScale(side) {
90
88
  return Math.min(MARGIN_SCALE_MAX, Math.max(1, side / MARGIN_SCALE_FROM_PX));
@@ -25,7 +25,7 @@
25
25
  import { err, ok } from "neverthrow";
26
26
  import { declaredForColumn } from "../../contracts/report-reference.js";
27
27
  import { CHART_BODY_PX, CHART_EXPORT_SIZES, CHART_INK, CHART_PAGE_TEXT_PX, CHART_PAGE_WIDTH_PX, MUTED_CHART_COLOR } from "../design.js";
28
- import { AREA_LEGEND_ICON, categoricalPalette, chartProblem, demandedColumn, firstAppearance, legendLineCount, legendLinePx, plainColumn, toNumber, } from "./common.js";
28
+ import { AREA_LEGEND_ICON, categoricalPalette, chartProblem, demandedColumn, EXPORT_SIZES, firstAppearance, legendLineCount, legendLinePx, PAGE_BOX, plainColumn, toNumber, } from "./common.js";
29
29
  import { largest } from "./dense.js";
30
30
  /** The opacity of a flow. A flow reads as the path of its source, and a label reads over it. */
31
31
  export const SANKEY_LINK_OPACITY = 0.35;
@@ -182,8 +182,6 @@ function readFlows(rows, source, target, value, group) {
182
182
  }
183
183
  return ok(flows);
184
184
  }
185
- const PAGE_BOX = { widthPx: CHART_PAGE_WIDTH_PX, heightPx: CHART_BODY_PX, textPx: CHART_PAGE_TEXT_PX };
186
- const EXPORT_SIZES = [CHART_EXPORT_SIZES.single, CHART_EXPORT_SIZES.double, CHART_EXPORT_SIZES.slide];
187
185
  /**
188
186
  * The text and the node size of one box: the label text size, the room of a label between two stages, the node
189
187
  * width, and the node gap. A figure of one stage gives its label the half of the width.
@@ -25,8 +25,8 @@
25
25
  * each export size states the dot size and the turn of the count text there.
26
26
  */
27
27
  import { err, ok } from "neverthrow";
28
- import { CHART_BODY_PX, CHART_EXPORT_SIZES, CHART_INK, CHART_PAGE_TEXT_PX, CHART_PAGE_WIDTH_PX } from "../design.js";
29
- import { axisNameFields, chartProblem, demandedColumn, valueAxis } from "./common.js";
28
+ import { CHART_EXPORT_SIZES, CHART_INK } from "../design.js";
29
+ import { axisNameFields, chartProblem, demandedColumn, EXPORT_SIZES, PAGE_BOX, valueAxis } from "./common.js";
30
30
  /**
31
31
  * The most sets that one figure draws. Each set takes one row of the matrix, and the rows of twelve sets hold
32
32
  * one line of the column-export text each.
@@ -125,9 +125,6 @@ function compareMembers(a, b) {
125
125
  }
126
126
  return a.length - b.length;
127
127
  }
128
- /** The page box and the box of each export size. */
129
- const PAGE_BOX = { widthPx: CHART_PAGE_WIDTH_PX, heightPx: CHART_BODY_PX, textPx: CHART_PAGE_TEXT_PX };
130
- const EXPORT_SIZES = [CHART_EXPORT_SIZES.single, CHART_EXPORT_SIZES.double, CHART_EXPORT_SIZES.slide];
131
128
  /**
132
129
  * The places of the grids for some sets, and set names of some length.
133
130
  *
@@ -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).