@inflexa-ai/harness 0.20.0 → 0.21.1

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 (133) 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 +35 -9
  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/loop/run-agent.js +58 -23
  27. package/dist/loop/tool-detail.d.ts +19 -2
  28. package/dist/loop/tool-detail.js +35 -2
  29. package/dist/memory/content-to-cortex.d.ts +3 -2
  30. package/dist/memory/content-to-cortex.js +21 -12
  31. package/dist/memory/conversation-display-replay.js +14 -7
  32. package/dist/memory/thread-history.d.ts +60 -3
  33. package/dist/memory/thread-history.js +55 -16
  34. package/dist/memory/thread-store.d.ts +11 -1
  35. package/dist/memory/thread-store.js +12 -2
  36. package/dist/prompts/conversation.d.ts +1 -1
  37. package/dist/prompts/conversation.js +23 -6
  38. package/dist/prompts/report-session.d.ts +1 -1
  39. package/dist/prompts/report-session.js +190 -9
  40. package/dist/report-model/authoring-grammar.d.ts +463 -0
  41. package/dist/report-model/authoring-grammar.js +77 -0
  42. package/dist/report-model/block-walk.d.ts +57 -14
  43. package/dist/report-model/block-walk.js +116 -11
  44. package/dist/report-model/draft-finish.d.ts +22 -4
  45. package/dist/report-model/draft-finish.js +39 -7
  46. package/dist/report-model/draft-operations.d.ts +3 -0
  47. package/dist/report-model/draft-operations.js +19 -5
  48. package/dist/report-model/draft-read.d.ts +8 -5
  49. package/dist/report-model/draft-read.js +21 -5
  50. package/dist/report-model/fixture-resolver.js +15 -4
  51. package/dist/report-model/pin-snapshot.d.ts +24 -6
  52. package/dist/report-model/pin-snapshot.js +204 -6
  53. package/dist/report-model/production-resolver.js +15 -4
  54. package/dist/report-model/reference-resolver.d.ts +50 -1
  55. package/dist/report-model/reference-resolver.js +94 -1
  56. package/dist/report-model/reference-stamp.d.ts +45 -0
  57. package/dist/report-model/reference-stamp.js +173 -0
  58. package/dist/report-model/structural-validation.d.ts +4 -1
  59. package/dist/report-model/structural-validation.js +48 -3
  60. package/dist/report-model/validate.d.ts +1 -1
  61. package/dist/report-model/validate.js +1 -1
  62. package/dist/report-render/asset-lookup.d.ts +19 -0
  63. package/dist/report-render/asset-lookup.js +37 -0
  64. package/dist/report-render/assets.d.ts +45 -1
  65. package/dist/report-render/assets.js +65 -6
  66. package/dist/report-render/chart-presets.d.ts +102 -0
  67. package/dist/report-render/chart-presets.js +205 -0
  68. package/dist/report-render/chart.d.ts +112 -10
  69. package/dist/report-render/chart.js +1164 -41
  70. package/dist/report-render/design.d.ts +99 -0
  71. package/dist/report-render/design.js +289 -107
  72. package/dist/report-render/number-format.d.ts +117 -0
  73. package/dist/report-render/number-format.js +479 -0
  74. package/dist/report-render/page.d.ts +121 -5
  75. package/dist/report-render/page.js +689 -7
  76. package/dist/report-render/references.d.ts +70 -16
  77. package/dist/report-render/references.js +54 -22
  78. package/dist/report-render/render.d.ts +23 -5
  79. package/dist/report-render/render.js +81 -22
  80. package/dist/report-render/table-data.d.ts +92 -0
  81. package/dist/report-render/table-data.js +129 -0
  82. package/dist/report-render/types.d.ts +15 -0
  83. package/dist/report-render/value-bridge.js +6 -2
  84. package/dist/report-render/views/chart-view.d.ts +12 -5
  85. package/dist/report-render/views/chart-view.js +14 -8
  86. package/dist/report-render/views/page-view.d.ts +36 -15
  87. package/dist/report-render/views/page-view.js +45 -23
  88. package/dist/report-render/views/prose.d.ts +12 -2
  89. package/dist/report-render/views/prose.js +29 -7
  90. package/dist/report-render/views/references-view.d.ts +22 -8
  91. package/dist/report-render/views/references-view.js +101 -13
  92. package/dist/report-render/views/values.d.ts +128 -13
  93. package/dist/report-render/views/values.js +199 -18
  94. package/dist/runtime/assemble.d.ts +39 -5
  95. package/dist/runtime/assemble.js +47 -6
  96. package/dist/sandbox/client.d.ts +10 -1
  97. package/dist/sandbox/create-sandbox.d.ts +9 -4
  98. package/dist/sandbox/create-sandbox.js +22 -16
  99. package/dist/sandbox/docker-client.js +10 -7
  100. package/dist/sandbox/mount-plan.d.ts +45 -7
  101. package/dist/sandbox/mount-plan.js +43 -5
  102. package/dist/sandbox/types.d.ts +6 -0
  103. package/dist/state/init.js +38 -9
  104. package/dist/state/report-session-state.d.ts +42 -1
  105. package/dist/state/report-session-state.js +70 -2
  106. package/dist/state/report-versions.d.ts +22 -17
  107. package/dist/state/report-versions.js +48 -35
  108. package/dist/state/snapshot-parse.js +16 -1
  109. package/dist/tools/define-tool.d.ts +32 -0
  110. package/dist/tools/define-tool.js +4 -0
  111. package/dist/tools/detail-resolver.d.ts +7 -2
  112. package/dist/tools/detail-resolver.js +7 -2
  113. package/dist/tools/iterate-report.d.ts +1 -1
  114. package/dist/tools/report/preview-snapshot.js +1 -0
  115. package/dist/tools/report-authoring/authoring-tools.d.ts +15 -6
  116. package/dist/tools/report-authoring/authoring-tools.js +99 -18
  117. package/dist/tools/report-session/derive-table.d.ts +162 -0
  118. package/dist/tools/report-session/derive-table.js +459 -0
  119. package/dist/tools/report-session/examine-page.d.ts +48 -18
  120. package/dist/tools/report-session/examine-page.js +204 -24
  121. package/dist/tools/report-session/index.d.ts +2 -0
  122. package/dist/tools/report-session/index.js +2 -0
  123. package/dist/tools/report-session/list-artifacts.d.ts +97 -0
  124. package/dist/tools/report-session/list-artifacts.js +197 -0
  125. package/dist/tools/report-session/preview-report.d.ts +6 -0
  126. package/dist/tools/report-session/preview-report.js +180 -28
  127. package/dist/tools/report-session/record-version.d.ts +22 -10
  128. package/dist/tools/report-session/record-version.js +99 -14
  129. package/dist/tools/start-report-session.d.ts +115 -0
  130. package/dist/tools/start-report-session.js +179 -0
  131. package/dist/workspace/paths.d.ts +50 -0
  132. package/dist/workspace/paths.js +63 -0
  133. package/package.json +2 -1
@@ -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
  }
@@ -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.
@@ -17,27 +17,33 @@
17
17
  * nested union. A conflict between the flat fields refuses before the core runs.
18
18
  */
19
19
  import { err, ok } from "neverthrow";
20
+ import { basename } from "node:path";
20
21
  import { z } from "zod";
22
+ import { capCodePoints, DETAIL_NEEDLE_MAX_LENGTH } from "../../loop/tool-detail.js";
21
23
  import { isSafeId } from "../../workspace/paths.js";
22
24
  import { addBlock, changeBlock, locate, moveBlock, removeBlock, setTitle, } from "../../report-model/draft-operations.js";
23
25
  import { buildOutline, childOutline, readBlock } from "../../report-model/draft-read.js";
24
26
  import { finishDraft } from "../../report-model/draft-finish.js";
25
- import { DraftBlockSchema } from "../../report-model/draft.js";
27
+ import { AuthoringBlockSchema } from "../../report-model/authoring-grammar.js";
26
28
  import { defineTool } from "../define-tool.js";
27
29
  /**
28
30
  * The block payload.
29
31
  *
30
- * The published JSON Schema carries the whole draft grammar, because a tool is self-describing at attach
31
- * time and the schema is the only place where the model learns the shape of the eight block kinds. A bare
32
- * `z.unknown()` emits the empty schema, which tells the model nothing and leaves it to discover each
32
+ * The published JSON Schema carries the whole authoring grammar, because a tool is self-describing at
33
+ * attach time and the schema is the only place where the model learns the shape of the eight block kinds.
34
+ * A bare `z.unknown()` emits the empty schema, which tells the model nothing and leaves it to discover each
33
35
  * required field through one refusal at a time.
34
36
  *
37
+ * The grammar is the authoring one, thus no artifact reference of the published schema carries a hash. The
38
+ * pinned snapshot owns that value, and the land path stamps it from the path.
39
+ *
35
40
  * The `z.unknown()` member keeps the runtime parse permissive. Thus a malformed payload still reaches the
36
- * core, which parses it with the same grammar and refuses `malformed-block` as typed data in the ok
41
+ * core, which parses it with the draft grammar and refuses `malformed-block` as typed data in the ok
37
42
  * channel. A strict input schema would turn that designed refusal into a hard input-validation error from
38
- * the loop.
43
+ * the loop. A payload that carries a hash rides the same arm, and the stamp drops the value before the
44
+ * parse.
39
45
  */
40
- const blockPayload = z.union([DraftBlockSchema, z.unknown()]);
46
+ const blockPayload = z.union([AuthoringBlockSchema, z.unknown()]);
41
47
  /**
42
48
  * A flat field that the model can leave out.
43
49
  *
@@ -119,7 +125,14 @@ export async function openReportThread(gateway, scope) {
119
125
  detail: `the scope names the analysis ${analysisId}, but the thread ${threadId} belongs to the analysis ${loaded.analysisId}`,
120
126
  });
121
127
  }
122
- return ok({ threadId, analysisId, state: loaded.state, token: loaded.token, seenDocumentHash: loaded.seenDocumentHash });
128
+ return ok({
129
+ threadId,
130
+ analysisId,
131
+ state: loaded.state,
132
+ token: loaded.token,
133
+ seenDocumentHash: loaded.seenDocumentHash,
134
+ derivations: loaded.derivations,
135
+ });
123
136
  }
124
137
  /** Read an absentable field. An explicit `null` and an omitted key both mean that the model gave no value. */
125
138
  function given(value) {
@@ -208,15 +221,75 @@ function readKind(block) {
208
221
  function readId(block) {
209
222
  return readStringField(block, "id");
210
223
  }
224
+ /** Read one field of an untyped payload, or `undefined` when the payload is not an object. */
225
+ function fieldOf(value, field) {
226
+ return value !== null && typeof value === "object" ? value[field] : undefined;
227
+ }
211
228
  function readStringField(block, field) {
212
- if (block !== null && typeof block === "object") {
213
- const value = block[field];
214
- if (typeof value === "string") {
215
- return value;
229
+ const value = fieldOf(block, field);
230
+ return typeof value === "string" ? value : undefined;
231
+ }
232
+ /** The pinned path of an untyped reference payload, or `undefined` when it names none. */
233
+ function referencePath(reference) {
234
+ const path = readStringField(reference, "path");
235
+ return path === undefined || path.length === 0 ? undefined : path;
236
+ }
237
+ /**
238
+ * The artifact path that an untyped block payload binds, or `undefined` when the kind binds no path.
239
+ *
240
+ * Where a reference sits differs per kind, thus the walk reads the one field of each kind and never
241
+ * searches the payload. A claim can hold a citation first, which names no path, thus the walk takes the
242
+ * first binding that names one. A metric holds a scalar reference, and a derivation holds its own two
243
+ * inputs, thus the first input names the derived value. A text block, a citation block, and a section
244
+ * bind no artifact at all.
245
+ *
246
+ * The payload is model-authored and unvalidated here, thus each read is a guarded read and a wrong shape
247
+ * gives `undefined`.
248
+ */
249
+ function boundArtifactPath(kind, block) {
250
+ if (kind === "table" || kind === "chart" || kind === "figure") {
251
+ return referencePath(fieldOf(block, "binding"));
252
+ }
253
+ if (kind === "claim") {
254
+ const bindings = fieldOf(block, "bindings");
255
+ if (!Array.isArray(bindings)) {
256
+ return undefined;
216
257
  }
258
+ for (const binding of bindings) {
259
+ const path = referencePath(binding);
260
+ if (path !== undefined) {
261
+ return path;
262
+ }
263
+ }
264
+ return undefined;
265
+ }
266
+ if (kind === "metric") {
267
+ const value = fieldOf(block, "value");
268
+ const direct = referencePath(value);
269
+ if (direct !== undefined) {
270
+ return direct;
271
+ }
272
+ const inputs = fieldOf(value, "inputs");
273
+ return Array.isArray(inputs) ? referencePath(inputs[0]) : undefined;
217
274
  }
218
275
  return undefined;
219
276
  }
277
+ /**
278
+ * The subject that names one added block: the title of a section in quotes, or the file name of the
279
+ * artifact that the block binds. A block that names neither gives no subject, and the kind stands alone.
280
+ *
281
+ * The title is model-authored text of no fixed length, thus the needle bound cuts it before the emit-site
282
+ * cap can cut the quote that closes it. The file name is the base name alone, because the directory
283
+ * segments of an analysis path repeat across every block and the file name is what tells two apart.
284
+ */
285
+ function addedSubject(kind, block) {
286
+ if (kind === "section") {
287
+ const title = readStringField(block, "title");
288
+ return title === undefined || title.length === 0 ? undefined : `"${capCodePoints(title, DETAIL_NEEDLE_MAX_LENGTH)}"`;
289
+ }
290
+ const path = boundArtifactPath(kind, block);
291
+ return path === undefined ? undefined : basename(path);
292
+ }
220
293
  /**
221
294
  * The id of the section that holds a block, or `undefined` when the block sits at the root.
222
295
  *
@@ -299,12 +372,18 @@ export function createReportAuthoringTools(gateway) {
299
372
  description: "Add one block to the draft. The `block` schema gives the eight kinds and the fields of each one. " +
300
373
  "You choose the `id` of the block, and it must be unique in the draft. " +
301
374
  "Name the destination with `parentId` and one of `place`, `before`, or `after`. " +
302
- "The root admits a section only, and an atom needs a section as its parent.",
375
+ "The root admits a section only, and an atom needs a section as its parent. " +
376
+ "A reference names the path of a pinned artifact, and the session stamps the hash from the pinned evidence.",
303
377
  inputSchema: addBlockInput,
304
378
  executionMode: AUTHORING_EXECUTION_MODE,
305
379
  describeCall: (input) => {
306
- const kind = readKind(decodeBlockPayload(input.block));
307
- return kind !== undefined ? `add ${kind}` : "add a block";
380
+ const block = decodeBlockPayload(input.block);
381
+ const kind = readKind(block);
382
+ if (kind === undefined) {
383
+ return "add a block";
384
+ }
385
+ const subject = addedSubject(kind, block);
386
+ return subject === undefined ? `add ${kind}` : `add ${kind} ${subject}`;
308
387
  },
309
388
  execute: async (input, ctx) => {
310
389
  const opened = await openThread(ctx);
@@ -324,7 +403,8 @@ export function createReportAuthoringTools(gateway) {
324
403
  const change_block = defineTool({
325
404
  id: "change_block",
326
405
  description: "Change one block by its id. Give a `title` to retitle a section, or a `block` to replace an atom, and never both. " +
327
- "The change keeps the id of the target, thus the `id` of the payload does not matter. A kind change is permitted.",
406
+ "The change keeps the id of the target, thus the `id` of the payload does not matter. A kind change is permitted. " +
407
+ "A reference names the path of a pinned artifact, and the session stamps the hash from the pinned evidence.",
328
408
  inputSchema: changeBlockInput,
329
409
  executionMode: AUTHORING_EXECUTION_MODE,
330
410
  describeCall: (input) => `change ${input.targetId}`,
@@ -416,7 +496,8 @@ export function createReportAuthoringTools(gateway) {
416
496
  });
417
497
  const read_block = defineTool({
418
498
  id: "read_block",
419
- description: "Read one block by its id. An atom gives the full block with its bindings. A section gives its title and the id of each child, " +
499
+ description: "Read one block by its id. An atom gives the full block with its bindings. A binding gives its path and no hash, " +
500
+ "because the session owns the hash. A section gives its title and the id of each child, " +
420
501
  "because the outline already names every block under it. Use it when the outline label is not enough.",
421
502
  inputSchema: readBlockInput,
422
503
  executionMode: AUTHORING_EXECUTION_MODE,
@@ -444,7 +525,7 @@ export function createReportAuthoringTools(gateway) {
444
525
  if (opened.isErr()) {
445
526
  return ok({ refused: opened.error });
446
527
  }
447
- return ok(finishDraft(opened.value.state.document, opened.value.state.snapshot));
528
+ return ok(finishDraft(opened.value.state.document, opened.value.state.snapshot, opened.value.derivations));
448
529
  },
449
530
  });
450
531
  return {