@inflexa-ai/harness 0.19.2 → 0.21.0

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 (139) hide show
  1. package/dist/agents/conversation-agent.d.ts +26 -14
  2. package/dist/agents/conversation-agent.js +12 -23
  3. package/dist/agents/report-session-agent.d.ts +31 -5
  4. package/dist/agents/report-session-agent.js +31 -6
  5. package/dist/app/chat-turn.js +3 -1
  6. package/dist/app/message-assembly.d.ts +15 -1
  7. package/dist/app/message-assembly.js +22 -8
  8. package/dist/app/report-session-runtime.d.ts +21 -4
  9. package/dist/app/report-session-runtime.js +53 -6
  10. package/dist/app/spawn-report-session.d.ts +174 -17
  11. package/dist/app/spawn-report-session.js +233 -21
  12. package/dist/contracts/chat-events.d.ts +7 -2
  13. package/dist/contracts/message.d.ts +10 -0
  14. package/dist/contracts/report-blocks.d.ts +743 -19
  15. package/dist/contracts/report-blocks.js +186 -11
  16. package/dist/contracts/report-reference.d.ts +88 -0
  17. package/dist/contracts/report-reference.js +48 -0
  18. package/dist/index.d.ts +9 -0
  19. package/dist/index.js +20 -0
  20. package/dist/lib/chrome.d.ts +26 -6
  21. package/dist/lib/chrome.js +99 -32
  22. package/dist/lib/eyes.d.ts +57 -0
  23. package/dist/lib/eyes.js +45 -0
  24. package/dist/lib/page-capture.d.ts +14 -0
  25. package/dist/lib/page-capture.js +56 -2
  26. package/dist/literature/sources/pubmed.js +2 -1
  27. package/dist/loop/run-agent.js +58 -23
  28. package/dist/loop/tool-detail.d.ts +19 -2
  29. package/dist/loop/tool-detail.js +35 -2
  30. package/dist/memory/content-to-cortex.d.ts +3 -2
  31. package/dist/memory/content-to-cortex.js +21 -12
  32. package/dist/memory/conversation-display-replay.js +14 -7
  33. package/dist/memory/thread-history.d.ts +60 -3
  34. package/dist/memory/thread-history.js +55 -16
  35. package/dist/memory/thread-store.d.ts +11 -1
  36. package/dist/memory/thread-store.js +12 -2
  37. package/dist/prompts/conversation.d.ts +1 -1
  38. package/dist/prompts/conversation.js +23 -6
  39. package/dist/prompts/report-session.d.ts +1 -1
  40. package/dist/prompts/report-session.js +183 -9
  41. package/dist/report-model/authoring-grammar.d.ts +463 -0
  42. package/dist/report-model/authoring-grammar.js +77 -0
  43. package/dist/report-model/block-walk.d.ts +57 -14
  44. package/dist/report-model/block-walk.js +116 -11
  45. package/dist/report-model/draft-finish.d.ts +22 -4
  46. package/dist/report-model/draft-finish.js +39 -7
  47. package/dist/report-model/draft-operations.d.ts +3 -0
  48. package/dist/report-model/draft-operations.js +19 -5
  49. package/dist/report-model/draft-read.d.ts +8 -5
  50. package/dist/report-model/draft-read.js +21 -5
  51. package/dist/report-model/fixture-resolver.js +15 -4
  52. package/dist/report-model/pin-snapshot.d.ts +24 -6
  53. package/dist/report-model/pin-snapshot.js +204 -6
  54. package/dist/report-model/production-resolver.js +15 -4
  55. package/dist/report-model/reference-resolver.d.ts +50 -1
  56. package/dist/report-model/reference-resolver.js +94 -1
  57. package/dist/report-model/reference-stamp.d.ts +45 -0
  58. package/dist/report-model/reference-stamp.js +173 -0
  59. package/dist/report-model/structural-validation.d.ts +4 -1
  60. package/dist/report-model/structural-validation.js +48 -3
  61. package/dist/report-model/validate.d.ts +1 -1
  62. package/dist/report-model/validate.js +1 -1
  63. package/dist/report-render/asset-lookup.d.ts +19 -0
  64. package/dist/report-render/asset-lookup.js +37 -0
  65. package/dist/report-render/assets.d.ts +45 -1
  66. package/dist/report-render/assets.js +65 -6
  67. package/dist/report-render/chart-presets.d.ts +102 -0
  68. package/dist/report-render/chart-presets.js +205 -0
  69. package/dist/report-render/chart.d.ts +112 -10
  70. package/dist/report-render/chart.js +1164 -41
  71. package/dist/report-render/design.d.ts +99 -0
  72. package/dist/report-render/design.js +289 -107
  73. package/dist/report-render/number-format.d.ts +117 -0
  74. package/dist/report-render/number-format.js +479 -0
  75. package/dist/report-render/page.d.ts +121 -5
  76. package/dist/report-render/page.js +689 -7
  77. package/dist/report-render/references.d.ts +70 -16
  78. package/dist/report-render/references.js +54 -22
  79. package/dist/report-render/render.d.ts +23 -5
  80. package/dist/report-render/render.js +81 -22
  81. package/dist/report-render/table-data.d.ts +92 -0
  82. package/dist/report-render/table-data.js +129 -0
  83. package/dist/report-render/types.d.ts +15 -0
  84. package/dist/report-render/value-bridge.js +6 -2
  85. package/dist/report-render/views/chart-view.d.ts +12 -5
  86. package/dist/report-render/views/chart-view.js +14 -8
  87. package/dist/report-render/views/page-view.d.ts +36 -15
  88. package/dist/report-render/views/page-view.js +45 -23
  89. package/dist/report-render/views/prose.d.ts +12 -2
  90. package/dist/report-render/views/prose.js +29 -7
  91. package/dist/report-render/views/references-view.d.ts +22 -8
  92. package/dist/report-render/views/references-view.js +101 -13
  93. package/dist/report-render/views/values.d.ts +128 -13
  94. package/dist/report-render/views/values.js +199 -18
  95. package/dist/runtime/assemble.d.ts +39 -5
  96. package/dist/runtime/assemble.js +47 -6
  97. package/dist/sandbox/client.d.ts +10 -1
  98. package/dist/sandbox/create-sandbox.d.ts +9 -4
  99. package/dist/sandbox/create-sandbox.js +22 -16
  100. package/dist/sandbox/docker-client.js +10 -7
  101. package/dist/sandbox/k8s-client.js +11 -18
  102. package/dist/sandbox/mount-plan.d.ts +45 -7
  103. package/dist/sandbox/mount-plan.js +43 -5
  104. package/dist/sandbox/types.d.ts +10 -6
  105. package/dist/state/init.js +38 -9
  106. package/dist/state/report-session-state.d.ts +42 -1
  107. package/dist/state/report-session-state.js +70 -2
  108. package/dist/state/report-versions.d.ts +22 -17
  109. package/dist/state/report-versions.js +48 -35
  110. package/dist/state/snapshot-parse.js +16 -1
  111. package/dist/tasks/data-profile.d.ts +7 -4
  112. package/dist/tasks/data-profile.js +9 -9
  113. package/dist/tools/define-tool.d.ts +32 -0
  114. package/dist/tools/define-tool.js +4 -0
  115. package/dist/tools/detail-resolver.d.ts +7 -2
  116. package/dist/tools/detail-resolver.js +7 -2
  117. package/dist/tools/iterate-report.d.ts +1 -1
  118. package/dist/tools/report/preview-snapshot.js +1 -0
  119. package/dist/tools/report-authoring/authoring-tools.d.ts +15 -6
  120. package/dist/tools/report-authoring/authoring-tools.js +99 -18
  121. package/dist/tools/report-session/derive-table.d.ts +162 -0
  122. package/dist/tools/report-session/derive-table.js +459 -0
  123. package/dist/tools/report-session/examine-page.d.ts +48 -18
  124. package/dist/tools/report-session/examine-page.js +204 -24
  125. package/dist/tools/report-session/index.d.ts +2 -0
  126. package/dist/tools/report-session/index.js +2 -0
  127. package/dist/tools/report-session/list-artifacts.d.ts +97 -0
  128. package/dist/tools/report-session/list-artifacts.js +197 -0
  129. package/dist/tools/report-session/preview-report.d.ts +6 -0
  130. package/dist/tools/report-session/preview-report.js +180 -28
  131. package/dist/tools/report-session/record-version.d.ts +22 -10
  132. package/dist/tools/report-session/record-version.js +99 -14
  133. package/dist/tools/start-report-session.d.ts +115 -0
  134. package/dist/tools/start-report-session.js +179 -0
  135. package/dist/workflows/sandbox-step.d.ts +7 -4
  136. package/dist/workflows/sandbox-step.js +10 -12
  137. package/dist/workspace/paths.d.ts +50 -0
  138. package/dist/workspace/paths.js +63 -0
  139. package/package.json +2 -1
@@ -21,6 +21,12 @@
21
21
  * document can be incomplete. A column that fails a parse reads as a typed error, and
22
22
  * it does not crash. An absent row reads as a normal absence.
23
23
  *
24
+ * The row also holds the derivation list of the session. One record chains a derived
25
+ * table to the evidence that made it: the output path, the output hash, the source
26
+ * paths with their hashes, the script hash, and the script text. A record is
27
+ * immutable, and the append refuses a repeated output path. A row that holds no list
28
+ * reads as an empty list.
29
+ *
24
30
  * A persist is a compare-and-swap. The load reads the prior document, and the persist
25
31
  * lands only when the row still holds it. Thus two concurrent turns cannot both land,
26
32
  * and the loser reads the state again.
@@ -28,12 +34,22 @@
28
34
  * A purge of the analysis removes the row through the analysis-id cascade.
29
35
  */
30
36
  import { err, ok } from "neverthrow";
37
+ import { z } from "zod";
31
38
  import { tryMutation, tryQuery } from "../lib/db-result.js";
32
39
  import { DraftDocumentSchema } from "../report-model/draft.js";
33
40
  import { parseSnapshot, reduceIssues } from "./snapshot-parse.js";
41
+ const DerivationSourceSchema = z.object({ path: z.string(), hash: z.string() });
42
+ const DerivationRecordSchema = z.object({
43
+ outputPath: z.string(),
44
+ outputHash: z.string(),
45
+ sources: z.array(DerivationSourceSchema),
46
+ scriptHash: z.string(),
47
+ script: z.string(),
48
+ });
49
+ const DerivationListSchema = z.array(DerivationRecordSchema);
34
50
  const _assertReadError = true;
35
51
  /** The full row projection every read shares. */
36
- const STATE_COLUMNS = "thread_id, analysis_id, document, snapshot, rendered_document_hash, seen_document_hash, created_at";
52
+ const STATE_COLUMNS = "thread_id, analysis_id, document, snapshot, derivations, rendered_document_hash, seen_document_hash, created_at";
37
53
  /**
38
54
  * The insert. The row carries the snapshot at creation, and the document lands later
39
55
  * through `persistDocument`. ON CONFLICT DO NOTHING keeps the first row, thus a
@@ -72,11 +88,28 @@ function rowToState(row) {
72
88
  }
73
89
  snapshot = parsed.value;
74
90
  }
91
+ // A row that predates the list carries SQL NULL, and a session that derived nothing carries none
92
+ // either. Both read as an empty list, because a session with no derivation has none.
93
+ let derivations = [];
94
+ if (row.derivations !== null && row.derivations !== undefined) {
95
+ const parsed = DerivationListSchema.safeParse(row.derivations);
96
+ if (!parsed.success) {
97
+ return err({
98
+ type: "corrupt_session_state",
99
+ op: "report-session-state.read",
100
+ threadId: row.thread_id,
101
+ part: "derivations",
102
+ issues: reduceIssues(parsed.error),
103
+ });
104
+ }
105
+ derivations = parsed.data;
106
+ }
75
107
  return ok({
76
108
  threadId: row.thread_id,
77
109
  analysisId: row.analysis_id,
78
110
  document,
79
111
  snapshot,
112
+ derivations,
80
113
  renderedDocumentHash: row.rendered_document_hash,
81
114
  seenDocumentHash: row.seen_document_hash,
82
115
  createdAt: row.created_at,
@@ -173,5 +206,40 @@ export function createReportSessionStateStore({ pool }) {
173
206
  return row.copied === null ? "no-rendered" : "stamped";
174
207
  });
175
208
  }
176
- return { readState, writeSnapshot, persistDocument, stampRendered, stampSeen };
209
+ function appendDerivation(threadId, record) {
210
+ return tryMutation("report-session-state.appendDerivation", async () => {
211
+ // One statement tells a duplicate from an absence, the same way the persist does. `existed`
212
+ // counts the row, and `appended` counts the update.
213
+ //
214
+ // The name rule sits in the qualification of the UPDATE, over the row that the statement locks.
215
+ // A read of the CTE would test the list as it stood at the start of the statement: two
216
+ // concurrent appends of one name would each pass that test, and both would land. Postgres
217
+ // re-evaluates the qualification of an UPDATE against the row that it waited for, thus the
218
+ // loser of the race sees the list of the winner and it appends nothing.
219
+ const { rows } = await pool.query({
220
+ text: `WITH target AS (
221
+ SELECT thread_id
222
+ FROM cortex_report_session_state
223
+ WHERE thread_id = $1
224
+ ), appended AS (
225
+ UPDATE cortex_report_session_state AS s
226
+ SET derivations = COALESCE(s.derivations, '[]'::jsonb) || $2::jsonb
227
+ WHERE s.thread_id = $1
228
+ AND NOT EXISTS (
229
+ SELECT 1 FROM jsonb_array_elements(COALESCE(s.derivations, '[]'::jsonb)) AS held WHERE held->>'outputPath' = $3
230
+ )
231
+ RETURNING 1
232
+ )
233
+ SELECT (SELECT COUNT(*) FROM target)::int AS existed, (SELECT COUNT(*) FROM appended)::int AS appended`,
234
+ // The record rides as a one-element array, thus the `||` operator appends one element and
235
+ // never merges the fields of an object into the list.
236
+ values: [threadId, JSON.stringify([record]), record.outputPath],
237
+ });
238
+ const row = rows[0];
239
+ if (row === undefined || row.existed === 0)
240
+ return "absent";
241
+ return row.appended > 0 ? "appended" : "duplicate";
242
+ });
243
+ }
244
+ return { readState, writeSnapshot, persistDocument, stampRendered, stampSeen, appendDerivation };
177
245
  }
@@ -1,14 +1,17 @@
1
1
  /**
2
- * The report version store -- the append-only record of report versions.
2
+ * The report version store -- the one recorded version of each report thread.
3
3
  *
4
- * A version is one immutable row. It holds the block document, the pinned
5
- * snapshot, and the anchor. The store records a version and reads a version. It
6
- * never updates a recorded row, thus a correction is a new version.
4
+ * A version is one row. It holds the block document, the pinned snapshot, and the
5
+ * anchor. The store records a version and reads a version. A record on a thread
6
+ * that holds a version replaces the document, the snapshot, and the anchor of that
7
+ * row, whole. The version id stays, thus a consumer that names the version keeps
8
+ * its name.
7
9
  *
8
10
  * A thread holds at most one version, and a named unique constraint on the thread
9
- * id enforces it. The record reads nothing before the insert. A second record for
10
- * one thread trips the constraint, and the store maps that trip to the typed
11
- * refusal thread_already_holds_version.
11
+ * id enforces it. The record reads nothing before the write, and the constraint
12
+ * routes it: a fresh thread takes the insert arm, and a thread that holds a version
13
+ * takes the replace arm. The store writes the full triple each time, thus a partial
14
+ * update is not representable.
12
15
  *
13
16
  * The store keeps the snapshot and the anchor as given, and it never pins a
14
17
  * snapshot. A read parses the stored document and the stored snapshot with the
@@ -42,9 +45,14 @@ export interface RecordVersionInput {
42
45
  /** The earlier version that this one reuses. The parent must belong to the same analysis. */
43
46
  readonly parentVersionId?: string;
44
47
  }
45
- /** The stable reference of a freshly recorded version. */
48
+ /** The stable reference of a recorded version, and how the record landed. */
46
49
  export interface RecordedVersionRef {
47
50
  readonly versionId: string;
51
+ /**
52
+ * `created` minted the row. `replaced` wrote over the one version of the
53
+ * thread, under the same version id.
54
+ */
55
+ readonly outcome: "created" | "replaced";
48
56
  }
49
57
  /**
50
58
  * A record the store refuses as data, before or in place of a row. Distinct from
@@ -54,9 +62,9 @@ export interface RecordedVersionRef {
54
62
  * refuses a malformed document or a malformed snapshot here as typed data, and
55
63
  * no row lands.
56
64
  *
57
- * A second record for one thread is `thread_already_holds_version`. The unique
58
- * constraint on the thread id trips, and the record maps that one constraint by
59
- * its name. Every other constraint trip stays a `DbError`.
65
+ * A second record for one thread is no refusal. The unique constraint on the
66
+ * thread id routes it into the replace arm, and the row carries the new triple.
67
+ * Every constraint trip stays a `DbError`.
60
68
  *
61
69
  * A `parentVersionId` naming no row is not one of these -- the self foreign key
62
70
  * refuses an unknown parent id, and `tryMutation` classifies that refusal as the
@@ -76,10 +84,6 @@ export type RecordVersionError = {
76
84
  readonly analysisId: string;
77
85
  readonly parentVersionId: string;
78
86
  readonly parentAnalysisId: string;
79
- } | {
80
- readonly type: "thread_already_holds_version";
81
- readonly op: string;
82
- readonly threadId: string;
83
87
  } | DbError;
84
88
  /**
85
89
  * A stored row that a read cannot parse with the current schemas. It names the
@@ -109,8 +113,9 @@ export interface ReportVersionStore {
109
113
  /**
110
114
  * Record a version. The record parses the document, refuses a malformed value
111
115
  * as data with no row, and refuses a parent from a different analysis. It reads
112
- * nothing before the insert. A second record for one thread refuses with
113
- * thread_already_holds_version. The snapshot and the anchor store as given.
116
+ * nothing before the write. A record on a thread that holds a version replaces
117
+ * that row whole, under the same version id. The snapshot and the anchor store
118
+ * as given, and the reference names a creation or a replacement.
114
119
  */
115
120
  record(input: RecordVersionInput): ResultAsync<RecordedVersionRef, RecordVersionError>;
116
121
  /** One version by its id, or `null` when no row holds it. */
@@ -1,14 +1,17 @@
1
1
  /**
2
- * The report version store -- the append-only record of report versions.
2
+ * The report version store -- the one recorded version of each report thread.
3
3
  *
4
- * A version is one immutable row. It holds the block document, the pinned
5
- * snapshot, and the anchor. The store records a version and reads a version. It
6
- * never updates a recorded row, thus a correction is a new version.
4
+ * A version is one row. It holds the block document, the pinned snapshot, and the
5
+ * anchor. The store records a version and reads a version. A record on a thread
6
+ * that holds a version replaces the document, the snapshot, and the anchor of that
7
+ * row, whole. The version id stays, thus a consumer that names the version keeps
8
+ * its name.
7
9
  *
8
10
  * A thread holds at most one version, and a named unique constraint on the thread
9
- * id enforces it. The record reads nothing before the insert. A second record for
10
- * one thread trips the constraint, and the store maps that trip to the typed
11
- * refusal thread_already_holds_version.
11
+ * id enforces it. The record reads nothing before the write, and the constraint
12
+ * routes it: a fresh thread takes the insert arm, and a thread that holds a version
13
+ * takes the replace arm. The store writes the full triple each time, thus a partial
14
+ * update is not representable.
12
15
  *
13
16
  * The store keeps the snapshot and the anchor as given, and it never pins a
14
17
  * snapshot. A read parses the stored document and the stored snapshot with the
@@ -25,9 +28,8 @@ import { ReportDocumentSchema } from "../contracts/report-blocks.js";
25
28
  import { tryMutation, tryQuery } from "../lib/db-result.js";
26
29
  import { parseSnapshot, reduceIssues } from "./snapshot-parse.js";
27
30
  /**
28
- * The name of the unique constraint on thread_id. Postgres reports it on a
29
- * duplicate-thread refusal, and the record matches it to map the trip to the
30
- * thread_already_holds_version refusal.
31
+ * The name of the unique constraint on thread_id. The write names it as its
32
+ * conflict target, thus the replace arm binds to the one row of the thread.
31
33
  */
32
34
  const THREAD_UNIQUE_CONSTRAINT = "cortex_report_versions_one_per_thread";
33
35
  const _assertRecordError = true;
@@ -39,13 +41,30 @@ const _assertReadError = true;
39
41
  */
40
42
  const VERSION_COLUMNS = "version_id, analysis_id, thread_id, parent_thread_id, parent_seq::text AS parent_seq, parent_version_id, document, snapshot, created_at";
41
43
  /**
42
- * The insert. The row carries no ordinal, thus the statement writes the given
43
- * columns directly. `created_at` takes the column default.
44
+ * The write. The row carries no ordinal, thus the statement writes the given
45
+ * columns directly. A fresh thread takes the insert arm, and `created_at` takes
46
+ * the column default. A thread that holds a version trips the named constraint,
47
+ * and the conflict target turns the trip into the replace of that one row.
48
+ *
49
+ * The replace arm writes each given column, thus the stored row equals the input.
50
+ * It writes neither `version_id` nor `created_at`: the version keeps its name, and
51
+ * the row keeps the time of its first record.
52
+ *
53
+ * The returned id names the row that stands. It equals the minted id on the insert
54
+ * arm alone, thus the caller reads which arm ran.
44
55
  */
45
- const INSERT_SQL = `INSERT INTO cortex_report_versions
56
+ const RECORD_SQL = `INSERT INTO cortex_report_versions
46
57
  (version_id, analysis_id, thread_id, parent_thread_id, parent_seq,
47
58
  parent_version_id, document, snapshot)
48
- VALUES ($1, $2, $3, $4, $5::bigint, $6, $7::jsonb, $8::jsonb)`;
59
+ VALUES ($1, $2, $3, $4, $5::bigint, $6, $7::jsonb, $8::jsonb)
60
+ ON CONFLICT ON CONSTRAINT ${THREAD_UNIQUE_CONSTRAINT} DO UPDATE SET
61
+ analysis_id = EXCLUDED.analysis_id,
62
+ parent_thread_id = EXCLUDED.parent_thread_id,
63
+ parent_seq = EXCLUDED.parent_seq,
64
+ parent_version_id = EXCLUDED.parent_version_id,
65
+ document = EXCLUDED.document,
66
+ snapshot = EXCLUDED.snapshot
67
+ RETURNING version_id`;
49
68
  function rowToVersion(row) {
50
69
  const document = ReportDocumentSchema.safeParse(row.document);
51
70
  if (!document.success) {
@@ -67,16 +86,6 @@ function rowToVersion(row) {
67
86
  createdAt: row.created_at,
68
87
  });
69
88
  }
70
- /**
71
- * Map an insert failure to a record refusal. A trip of the thread constraint is
72
- * thread_already_holds_version. Every other DbError stays unchanged.
73
- */
74
- function mapInsertError(error, threadId) {
75
- if (error.type === "constraint_violation" && error.constraint === THREAD_UNIQUE_CONSTRAINT) {
76
- return { type: "thread_already_holds_version", op: "report-versions.record", threadId };
77
- }
78
- return error;
79
- }
80
89
  /**
81
90
  * Create a `ReportVersionStore` bound to a Postgres pool. The
82
91
  * `cortex_report_versions` table is provisioned by the state-init DDL.
@@ -85,7 +94,7 @@ export function createReportVersionStore({ pool }) {
85
94
  /**
86
95
  * The analysis scope of a parent version, or `null` when no such row exists.
87
96
  * Absence is not a verdict here: an unknown parent id falls through to the
88
- * insert, where the self foreign key refuses it as a `DbError`.
97
+ * write, where the self foreign key refuses it as a `DbError`.
89
98
  */
90
99
  function parentAnalysis(parentVersionId) {
91
100
  return tryQuery("report-versions.record.parentScope", () => pool.query({
@@ -105,13 +114,13 @@ export function createReportVersionStore({ pool }) {
105
114
  if (parsedSnapshot.isErr()) {
106
115
  return errAsync({ type: "malformed_snapshot", op: "report-versions.record", issues: parsedSnapshot.error });
107
116
  }
108
- const insert = () => {
109
- const versionId = randomUUID();
110
- return tryMutation("report-versions.record.insert", async () => {
111
- await pool.query({
112
- text: INSERT_SQL,
117
+ const write = () => {
118
+ const minted = randomUUID();
119
+ return tryMutation("report-versions.record.write", async () => {
120
+ const { rows } = await pool.query({
121
+ text: RECORD_SQL,
113
122
  values: [
114
- versionId,
123
+ minted,
115
124
  input.analysisId,
116
125
  input.threadId,
117
126
  input.parentThreadId,
@@ -121,11 +130,15 @@ export function createReportVersionStore({ pool }) {
121
130
  JSON.stringify(input.snapshot),
122
131
  ],
123
132
  });
124
- return { versionId };
125
- }).mapErr((error) => mapInsertError(error, input.threadId));
133
+ // Each arm of the write returns its row. The replace arm keeps the
134
+ // version id of the row that stood, thus an id that differs from the
135
+ // minted one names a replacement.
136
+ const landed = rows[0]?.version_id ?? minted;
137
+ return { versionId: landed, outcome: landed === minted ? "created" : "replaced" };
138
+ });
126
139
  };
127
140
  if (input.parentVersionId === undefined) {
128
- return insert();
141
+ return write();
129
142
  }
130
143
  const parentVersionId = input.parentVersionId;
131
144
  return parentAnalysis(parentVersionId).andThen((parentAnalysisId) => {
@@ -138,7 +151,7 @@ export function createReportVersionStore({ pool }) {
138
151
  parentAnalysisId,
139
152
  });
140
153
  }
141
- return insert();
154
+ return write();
142
155
  });
143
156
  }
144
157
  function getVersion(versionId) {
@@ -23,6 +23,11 @@ const ArtifactSnapshotSchema = z.object({
23
23
  });
24
24
  /** The citation list of a stored snapshot. A snapshot with no list reads as an absent field. */
25
25
  const CitationsSchema = z.array(z.string()).optional();
26
+ /**
27
+ * The citation records of a stored snapshot, keyed by the citation key. A snapshot with no map reads as
28
+ * an absent field, thus a pin that predates the map loads the same as it did before.
29
+ */
30
+ const CitationRecordsSchema = z.record(z.string(), z.object({ citation: z.string(), description: z.string().optional() })).optional();
26
31
  /** Reduce a zod error to the dotted path and the message of each issue. */
27
32
  export function reduceIssues(error) {
28
33
  return error.issues.map((issue) => ({
@@ -63,6 +68,10 @@ export function parseSnapshot(stored) {
63
68
  if (!citations.success) {
64
69
  return err(prefixIssues("citations", reduceIssues(citations.error)));
65
70
  }
71
+ const citationRecords = CitationRecordsSchema.safeParse(stored.citationRecords);
72
+ if (!citationRecords.success) {
73
+ return err(prefixIssues("citationRecords", reduceIssues(citationRecords.error)));
74
+ }
66
75
  // The map takes a null prototype, the same as the pin, thus a path such as
67
76
  // `__proto__` stays an ordinary entry and never reaches a prototype slot.
68
77
  const artifacts = Object.create(null);
@@ -78,5 +87,11 @@ export function parseSnapshot(stored) {
78
87
  if (issues.length > 0) {
79
88
  return err(issues);
80
89
  }
81
- return citations.data === undefined ? ok({ artifacts }) : ok({ artifacts, citations: citations.data });
90
+ // An absent part stays absent in the parsed value. Thus a reload gives the same shape that the pin
91
+ // wrote, and an equality test over a stored snapshot holds across the round trip.
92
+ return ok({
93
+ artifacts,
94
+ ...(citations.data === undefined ? {} : { citations: citations.data }),
95
+ ...(citationRecords.data === undefined ? {} : { citationRecords: citationRecords.data }),
96
+ });
82
97
  }
@@ -23,7 +23,6 @@ import type { RunAuthorization, RunAuthorizer } from "../execution/run-authorize
23
23
  import type { StagedInput } from "../execution/staged-input.js";
24
24
  import type { BioToolKeys } from "../tools/bio/keys.js";
25
25
  import type { Logger } from "../lib/logger.js";
26
- import type { ResolveBilling } from "../billing/resolver.js";
27
26
  import type { UsageRecorder } from "../billing/usage-recorder.js";
28
27
  import type { ChatProvider, EmbeddingProvider } from "../providers/types.js";
29
28
  import type { SandboxClient } from "../sandbox/client.js";
@@ -57,9 +56,13 @@ export interface DataProfileDeps extends EnvironmentStorePaths {
57
56
  readonly skillsDir: string;
58
57
  /** LLM usage-accounting seam for the profiler agent loop; omitted falls back to the no-op recorder. */
59
58
  readonly usageRecorder?: UsageRecorder;
60
- /** Resolves the billing context stamped as the sandbox pod's OpenCost
61
- * labels. Absent in upstream-less (dev/OSS) wiring — pods spawn unlabeled. */
62
- readonly resolveBilling?: ResolveBilling;
59
+ /**
60
+ * Host-supplied labels for the profiler's sandbox pod, resolved under the
61
+ * profiling session. The map is opaque to the harness: it stamps each entry
62
+ * and interprets none of it. Absent in a wiring that attributes nothing —
63
+ * the pod then carries the harness's own labels only.
64
+ */
65
+ readonly resolvePodLabels?: (session: RunSession) => Promise<Record<string, string>>;
63
66
  }
64
67
  /**
65
68
  * Workflow input. JSON-serialisable — DBOS persists it as the workflow's
@@ -42,8 +42,6 @@ import { ensureSearchIndex, searchIndexName } from "../workspace/search-config.j
42
42
  import { DATA_PROFILE_RUN_LITERAL } from "../contracts/data-profile.js";
43
43
  const DATA_PROFILE_STEP_LITERAL = "profile";
44
44
  const DATA_PROFILE_AGENT_ID = "data-profiler";
45
- /** Resolved-header key carrying the billing-context id (see `ResolveBilling`). */
46
- const BILLING_CONTEXT_HEADER = "X-Inflexa-Billing-Context";
47
45
  /** Sandbox-server exec budget for the profile run. */
48
46
  const DEFAULT_DEADLINE_MS = 300_000;
49
47
  /** Build the canonical artifact path for a staged input file. */
@@ -228,16 +226,18 @@ export async function runDataProfileBody(input, deps) {
228
226
  // from the agent loop would leave the longest wait of all unreported — the same defect the
229
227
  // run panel's activity readout was built to remove.
230
228
  await activity.sandboxInit();
231
- let billingContextId;
232
- if (deps.resolveBilling) {
229
+ let podLabels;
230
+ if (deps.resolvePodLabels) {
233
231
  try {
234
- billingContextId = (await deps.resolveBilling(childSession))[BILLING_CONTEXT_HEADER];
232
+ podLabels = await deps.resolvePodLabels(childSession);
235
233
  }
236
234
  catch (err) {
237
- logger.error("[billing] data-profile billing resolution failed", logger.errorFields(err));
235
+ logger.warn("pod-label resolution failed", logger.errorFields(err));
238
236
  }
239
- if (!billingContextId)
240
- logger.error("[billing] sandbox spawned without billing labels");
237
+ // A wired resolver that yields nothing is the one loud case: the host
238
+ // asked for attribution and the pod spawns without it.
239
+ if (!podLabels || Object.keys(podLabels).length === 0)
240
+ logger.warn("sandbox spawned with no pod labels");
241
241
  }
242
242
  const sandbox = await deps.sandboxClient.createSandbox({
243
243
  runId: DATA_PROFILE_RUN_LITERAL,
@@ -245,7 +245,7 @@ export async function runDataProfileBody(input, deps) {
245
245
  analysisId,
246
246
  childWorkflowId: workflowId,
247
247
  resources: estimateDataProfileResources(stagedInputs),
248
- billing: billingContextId ? { billingContextId, userId: childSession.identity.user } : undefined,
248
+ podLabels,
249
249
  }, mintSandboxIdentity(DATA_PROFILE_RUN_LITERAL));
250
250
  try {
251
251
  // Checkpointed clock, not `Date.now()`: `awaitExec` gates on this absolute
@@ -116,6 +116,8 @@ export interface Tool<Input = unknown, Output = unknown> {
116
116
  readonly executionMode: ToolExecutionMode;
117
117
  /** See {@link ToolDefinition.describeCall}. Absent when the tool declares none. */
118
118
  describeCall?(input: Input): string;
119
+ /** See {@link ToolDefinition.describeResult}. Absent when the tool declares none. */
120
+ describeResult?(input: Input, result: Output): string;
119
121
  execute(input: Input, ctx: ToolContext): Promise<Result<Output, ToolError>>;
120
122
  }
121
123
  export interface ToolDefinition<Schema extends z.ZodType, Output> {
@@ -156,6 +158,36 @@ export interface ToolDefinition<Schema extends z.ZodType, Output> {
156
158
  * The hook never reaches the model.
157
159
  */
158
160
  describeCall: ((input: z.infer<Schema>) => string) | "none";
161
+ /**
162
+ * One line that names what THIS call PRODUCED — the outcome-time counterpart
163
+ * of `describeCall`. A page path, a recorded id, and a listed count are facts
164
+ * of the result, thus no hook over the input can name one. The finished event
165
+ * carries this line, and it carries the call detail when the hook gives
166
+ * nothing.
167
+ *
168
+ * The decision is OPTIONAL, unlike `describeCall`. A call always has an input
169
+ * to describe or an input that cannot distinguish it, thus that decision is
170
+ * forced. A result is different: a tool whose ok value adds no fact that a
171
+ * reader wants leaves the call detail to stand for the whole call. Thus the
172
+ * absent key is the whole of "this tool describes no result", and the `"none"`
173
+ * sentinel has no counterpart here.
174
+ *
175
+ * The hook runs on an ok outcome only. An error outcome already names itself,
176
+ * and a hook over a failed call reads a shape that does not exist.
177
+ *
178
+ * A hook is synchronous and pure — no I/O, no ambient state — and a
179
+ * description must never fail a call: the loop guards the call and drops the
180
+ * detail on any failure (see the tool-call-detail capability). The result
181
+ * arrives as the tool produced it, thus the hook reads a value of its own
182
+ * declared `Output` type and no parse stands between the two.
183
+ *
184
+ * Return whatever reads best. Normalization — one line, control characters
185
+ * removed, secrets redacted, length capped — happens once at the emit site,
186
+ * never here.
187
+ *
188
+ * The hook never reaches the model.
189
+ */
190
+ readonly describeResult?: (input: z.infer<Schema>, result: Output) => string;
159
191
  execute(input: z.infer<Schema>, ctx: ToolContext): Promise<Result<Output, ToolError>>;
160
192
  }
161
193
  /**
@@ -75,6 +75,10 @@ export function defineTool(def) {
75
75
  // `typeof` is the discriminator, not equality with the sentinel, so a
76
76
  // hook that returns the string `"none"` still packages as a hook.
77
77
  ...(typeof def.describeCall === "function" ? { describeCall: def.describeCall } : {}),
78
+ // The same property-presence pattern as `describeCall`: a tool that
79
+ // declares no result hook carries no key, thus "has a hook" is one
80
+ // property check on either side.
81
+ ...(typeof def.describeResult === "function" ? { describeResult: def.describeResult } : {}),
78
82
  execute: def.execute,
79
83
  };
80
84
  }
@@ -15,8 +15,13 @@
15
15
  * registry construction.
16
16
  *
17
17
  * It resolves through the same `computeDetail` the loop uses, so a reloaded
18
- * thread cannot show a different detail from the one the live turn showed.
19
- * Nothing is persisted: storage stays the pure model transcript.
18
+ * thread shows the same CALL detail the live turn showed.
19
+ *
20
+ * The bound is the input. A reload reconstructs from the persisted call alone, and a
21
+ * tool that describes its own RESULT names a fact that the input cannot carry — a page
22
+ * path, a recorded version. That line reaches the live surface on the finished event and
23
+ * it does not reappear here, thus a reloaded call falls back to its call detail. Nothing
24
+ * is persisted: storage stays the pure model transcript.
20
25
  */
21
26
  import { type ToolCallDetail } from "../loop/tool-detail.js";
22
27
  import type { Logger } from "../lib/logger.js";
@@ -15,8 +15,13 @@
15
15
  * registry construction.
16
16
  *
17
17
  * It resolves through the same `computeDetail` the loop uses, so a reloaded
18
- * thread cannot show a different detail from the one the live turn showed.
19
- * Nothing is persisted: storage stays the pure model transcript.
18
+ * thread shows the same CALL detail the live turn showed.
19
+ *
20
+ * The bound is the input. A reload reconstructs from the persisted call alone, and a
21
+ * tool that describes its own RESULT names a fact that the input cannot carry — a page
22
+ * path, a recorded version. That line reaches the live surface on the finished event and
23
+ * it does not reappear here, thus a reloaded call falls back to its call detail. Nothing
24
+ * is persisted: storage stays the pure model transcript.
20
25
  */
21
26
  import { computeDetail } from "../loop/tool-detail.js";
22
27
  import { createNoopLogger } from "../lib/console-logger.js";
@@ -120,8 +120,8 @@ export declare const ReportBriefSchema: z.ZodObject<{
120
120
  source: z.ZodString;
121
121
  }, z.core.$strip>>;
122
122
  chartType: z.ZodEnum<{
123
- line: "line";
124
123
  bar: "bar";
124
+ line: "line";
125
125
  scatter: "scatter";
126
126
  histogram: "histogram";
127
127
  box: "box";
@@ -80,6 +80,7 @@ export function createPreviewSnapshotTool(state) {
80
80
  return ok({
81
81
  ok: true,
82
82
  screenshotBase64: captured.screenshotBase64,
83
+ coverage: captured.coverage,
83
84
  consoleErrors: captured.consoleErrors,
84
85
  failedRequests: captured.failedRequests,
85
86
  });
@@ -22,12 +22,13 @@ import type { Scope } from "../../auth/types.js";
22
22
  import { type DraftRefusal } from "../../report-model/draft-operations.js";
23
23
  import { type OutlineEntry, type ReadableBlock } from "../../report-model/draft-read.js";
24
24
  import { type FinishResult } from "../../report-model/draft-finish.js";
25
- import { type DraftDocument } from "../../report-model/draft.js";
25
+ import type { DraftDocument } from "../../report-model/draft.js";
26
26
  import type { ReportSnapshot } from "../../report-model/reference-resolver.js";
27
+ import type { DerivationRecord } from "../../state/report-session-state.js";
27
28
  import { type Tool } from "../define-tool.js";
28
29
  /** The flat destination fields. An anchor is `before` or `after`, and a place is `start` or `end`. */
29
30
  declare const addBlockInput: z.ZodObject<{
30
- block: z.ZodUnion<readonly [z.ZodType<import("../../report-model/draft.js").DraftBlock, unknown, z.core.$ZodTypeInternals<import("../../report-model/draft.js").DraftBlock, unknown>>, z.ZodUnknown]>;
31
+ block: z.ZodUnion<readonly [z.ZodType<import("../../report-model/authoring-grammar.js").AuthoringBlock, unknown, z.core.$ZodTypeInternals<import("../../report-model/authoring-grammar.js").AuthoringBlock, unknown>>, z.ZodUnknown]>;
31
32
  parentId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
32
33
  place: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
33
34
  end: "end";
@@ -44,7 +45,7 @@ declare const addBlockInput: z.ZodObject<{
44
45
  declare const changeBlockInput: z.ZodObject<{
45
46
  targetId: z.ZodString;
46
47
  title: z.ZodOptional<z.ZodNullable<z.ZodString>>;
47
- block: z.ZodOptional<z.ZodUnion<readonly [z.ZodType<import("../../report-model/draft.js").DraftBlock, unknown, z.core.$ZodTypeInternals<import("../../report-model/draft.js").DraftBlock, unknown>>, z.ZodUnknown]>>;
48
+ block: z.ZodOptional<z.ZodUnion<readonly [z.ZodType<import("../../report-model/authoring-grammar.js").AuthoringBlock, unknown, z.core.$ZodTypeInternals<import("../../report-model/authoring-grammar.js").AuthoringBlock, unknown>>, z.ZodUnknown]>>;
48
49
  }, z.core.$strip>;
49
50
  declare const removeBlockInput: z.ZodObject<{
50
51
  targetId: z.ZodString;
@@ -158,12 +159,17 @@ export interface ReportSessionState {
158
159
  export type SessionStateToken = DraftDocument | null;
159
160
  /**
160
161
  * The outcome of a gateway load. `found` carries the state, the analysis that owns the thread, the
161
- * concurrency token, and the seen-document hash of the look-before-record rule. `absent` and `wrong-type`
162
- * are permanent conditions, and `failed` is a transient fault that names its cause.
162
+ * concurrency token, the seen-document hash of the look-before-record rule, and the derivations of the
163
+ * session. `absent` and `wrong-type` are permanent conditions, and `failed` is a transient fault that names
164
+ * its cause.
163
165
  *
164
166
  * `seenDocumentHash` is the hash that the last eyes capture saw, or `null` before the first look. The record
165
167
  * tool compares it against the hash of the current draft, thus a never-seen page and a stale look each
166
168
  * refuse.
169
+ *
170
+ * `derivations` is the ledger of the session, in the order that the records landed. The snapshot already
171
+ * carries each derived path as a member, thus a tool that binds reads the snapshot. The records themselves
172
+ * carry the chain, and the finish, the appendix, and the prune each read them.
167
173
  */
168
174
  export type SessionStateLoad = {
169
175
  outcome: "found";
@@ -171,6 +177,7 @@ export type SessionStateLoad = {
171
177
  analysisId: string;
172
178
  token: SessionStateToken;
173
179
  seenDocumentHash: string | null;
180
+ derivations: readonly DerivationRecord[];
174
181
  } | {
175
182
  outcome: "absent";
176
183
  } | {
@@ -243,7 +250,7 @@ export interface ReportSessionStateGateway {
243
250
  }
244
251
  /**
245
252
  * The thread of a call, its analysis, the loaded state, the concurrency token that the persist compares
246
- * against, and the seen-document hash of the look-before-record rule.
253
+ * against, the seen-document hash of the look-before-record rule, and the derivations of the session.
247
254
  */
248
255
  export interface OpenedThread {
249
256
  readonly threadId: string;
@@ -252,6 +259,8 @@ export interface OpenedThread {
252
259
  readonly token: SessionStateToken;
253
260
  /** The hash that the last eyes capture saw, or `null` before the first look. */
254
261
  readonly seenDocumentHash: string | null;
262
+ /** The derivations of the session, in the order that the records landed. */
263
+ readonly derivations: readonly DerivationRecord[];
255
264
  }
256
265
  /**
257
266
  * Resolve the report thread of a call, and load its state through the gateway.