@inflexa-ai/harness 0.41.2 → 0.42.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 (73) hide show
  1. package/dist/agents/conversation-agent.js +3 -0
  2. package/dist/agents/report-session-agent.d.ts +2 -2
  3. package/dist/agents/report-session-agent.js +4 -0
  4. package/dist/app/chat-turn.d.ts +0 -4
  5. package/dist/app/chat-turn.js +39 -25
  6. package/dist/app/message-assembly.d.ts +2 -2
  7. package/dist/app/message-assembly.js +1 -1
  8. package/dist/contracts/chat-events.d.ts +12 -5
  9. package/dist/contracts/chat-events.js +3 -3
  10. package/dist/contracts/chat-frame.d.ts +40 -0
  11. package/dist/contracts/chat-frame.js +166 -0
  12. package/dist/contracts/chat-parts.d.ts +15 -3
  13. package/dist/contracts/chat-parts.js +2 -2
  14. package/dist/contracts/index.d.ts +6 -4
  15. package/dist/contracts/index.js +1 -0
  16. package/dist/contracts/message.d.ts +7 -5
  17. package/dist/contracts/message.js +1 -1
  18. package/dist/contracts/part-registry.d.ts +6 -6
  19. package/dist/contracts/schemas/chat-events.d.ts +2 -2
  20. package/dist/contracts/schemas/chat-events.js +2 -2
  21. package/dist/contracts/schemas/chat-parts.d.ts +34 -2
  22. package/dist/contracts/schemas/chat-parts.js +9 -3
  23. package/dist/contracts/schemas/index.d.ts +2 -2
  24. package/dist/contracts/schemas/index.js +2 -2
  25. package/dist/execution/run-event-parts.d.ts +4 -4
  26. package/dist/execution/run-event-parts.js +0 -0
  27. package/dist/execution/run-event-stream.d.ts +2 -2
  28. package/dist/index.d.ts +9 -6
  29. package/dist/index.js +3 -2
  30. package/dist/lib/page-capture.d.ts +27 -9
  31. package/dist/lib/page-capture.js +81 -33
  32. package/dist/loop/compaction.d.ts +8 -3
  33. package/dist/loop/run-agent.d.ts +11 -0
  34. package/dist/loop/run-agent.js +62 -35
  35. package/dist/loop/types.d.ts +3 -0
  36. package/dist/memory/ai-sdk-message-storage.d.ts +18 -3
  37. package/dist/memory/ai-sdk-message-storage.js +23 -1
  38. package/dist/memory/conversation-display-recorder.d.ts +7 -1
  39. package/dist/memory/conversation-display-recorder.js +59 -119
  40. package/dist/memory/conversation-display-replay.d.ts +3 -3
  41. package/dist/memory/conversation-display-replay.js +7 -21
  42. package/dist/memory/conversation-display-storage.d.ts +10 -10
  43. package/dist/memory/conversation-display-storage.js +9 -8
  44. package/dist/memory/conversation-view.d.ts +5 -4
  45. package/dist/memory/conversation-view.js +14 -18
  46. package/dist/memory/count-tokens.js +1 -0
  47. package/dist/memory/thread-store.d.ts +9 -2
  48. package/dist/memory/thread-store.js +8 -2
  49. package/dist/prompts/compaction.d.ts +2 -2
  50. package/dist/prompts/compaction.js +2 -2
  51. package/dist/prompts/report-session.d.ts +1 -1
  52. package/dist/prompts/report-session.js +40 -28
  53. package/dist/report-render/views/block-mark.d.ts +8 -0
  54. package/dist/report-render/views/block-mark.js +10 -0
  55. package/dist/report-render/views/chart-view.js +2 -1
  56. package/dist/report-render/views/prose.js +10 -6
  57. package/dist/report-render/views/values.js +5 -4
  58. package/dist/state/migrations/20260927180000_thread_title_set_by_user.d.ts +6 -0
  59. package/dist/state/migrations/20260927180000_thread_title_set_by_user.js +10 -0
  60. package/dist/state/migrations/index.js +2 -0
  61. package/dist/tools/report-authoring/authoring-tools.d.ts +16 -5
  62. package/dist/tools/report-authoring/authoring-tools.js +52 -24
  63. package/dist/tools/report-session/examine-page.d.ts +29 -14
  64. package/dist/tools/report-session/examine-page.js +40 -16
  65. package/dist/tools/report-session/list-artifacts.d.ts +23 -10
  66. package/dist/tools/report-session/list-artifacts.js +51 -31
  67. package/dist/tools/report-session/page-faults.d.ts +29 -0
  68. package/dist/tools/report-session/page-faults.js +48 -0
  69. package/dist/tools/report-session/preview-report.d.ts +9 -2
  70. package/dist/tools/report-session/preview-report.js +13 -3
  71. package/dist/tools/report-session/record-version.d.ts +5 -1
  72. package/dist/tools/report-session/record-version.js +4 -3
  73. package/package.json +1 -1
@@ -1,10 +1,14 @@
1
1
  /**
2
2
  * The pinned-artifact listing tool of a report session.
3
3
  *
4
- * The tool reads the frozen snapshot of the thread, and it gives the pinned set: the path, the content
5
- * hash, the file type, and the header columns of a tabular artifact, plus the pinned citations with the
6
- * short citation of each. Thus one call orients the agent, and a reference binds to a path or to a
7
- * citation key of that set.
4
+ * The tool reads the frozen snapshot of the thread, and it gives the pinned set: the path, the file type,
5
+ * and the count of the header columns of a tabular artifact, plus the pinned citations with the short
6
+ * citation of each. Thus one call orients the agent, and a reference binds to a path or to a citation key of
7
+ * that set. A call with a path gives the header columns of that one artifact.
8
+ *
9
+ * A wide table can have hundreds of columns, and the listing stays in the context of each later request.
10
+ * Thus the listing counts the columns, and the agent reads the names of one artifact when it binds to it.
11
+ * A reference names the path alone and the session stamps the hash, thus the tool gives no hash.
8
12
  *
9
13
  * The order is the code-unit order of the path. Two calls over one snapshot give one listing, thus the
10
14
  * agent reads a stable set. A snapshot can pin many thousands of staged inputs, thus the listing stops at
@@ -28,8 +32,14 @@ import { defaultErrorFields } from "../../lib/logger.js";
28
32
  import { citationRecordOf, fileTypeHoldsNoCell } from "../../report-model/reference-resolver.js";
29
33
  import { defineTool } from "../define-tool.js";
30
34
  import { openReportThread } from "../report-authoring/authoring-tools.js";
31
- /** The empty input. The tool lists the pinned set of the thread, thus it needs no field. */
32
- const listPinnedArtifactsInput = z.object({});
35
+ /** No path lists the pinned set. A path gives the header columns of that one pinned artifact. */
36
+ const listPinnedArtifactsInput = z.object({
37
+ path: z
38
+ .string()
39
+ .min(1)
40
+ .optional()
41
+ .describe("The path of one pinned artifact, as the listing gives it. The result then gives the header columns of that artifact."),
42
+ });
33
43
  /**
34
44
  * The cap of the header read. A header line of an analysis output sits far under it, and a file with no
35
45
  * newline at all costs this many bytes and no more.
@@ -124,24 +134,29 @@ export function createListPinnedArtifactsTool(deps) {
124
134
  const logger = (deps.logger ?? createNoopLogger()).named("list-pinned-artifacts");
125
135
  return defineTool({
126
136
  id: "list_pinned_artifacts",
127
- description: "List the pinned evidence of this session. Each entry gives the path of an artifact, its content hash, its file type, " +
128
- "and the header columns of a .csv or a .tsv file. Another extension, a header line that holds a double quote, and a " +
129
- "file whose bytes do not read each give no columns. " +
137
+ description: "List the pinned evidence of this session. With no path, each entry gives the path of an artifact, its file type, " +
138
+ 'and "columnCount", the count of the header columns of a .csv or a .tsv file. ' +
139
+ 'Give "path" to get that one artifact with "columns", the names of its header columns. A path that the pin does not hold gives not-pinned. ' +
140
+ "Another extension, a header line that holds a double quote, and a file whose bytes do not read each give no columns. " +
130
141
  `The listing gives a maximum of ${LISTING_CAP} entries: "total" gives the size of the pinned set, and "truncated" says that the ` +
131
142
  "set holds more artifacts than this listing names. " +
132
143
  '"citations" gives the pinned literature of this session. Each entry carries "key", the citation id in the "idKind:id" form, and ' +
133
144
  '"citation", the short citation of the paper. A key that the pin recorded no citation for carries no "citation". A citation block ' +
134
145
  "binds to a key of this list, thus take a key from here and never from a refusal. " +
135
146
  "The pin freezes at the start of the session, thus a reference binds to an artifact of this pinned set. " +
136
- "Read it to orient before you bind a block, and to choose the column that a locator names. " +
137
- "A reference names the path alone: the session stamps the hash from this evidence.",
147
+ "Read the listing to orient before you bind a block. Read the columns of one artifact to choose the column that a locator names. " +
148
+ "A reference names the path alone: the session stamps the hash from this evidence, thus the tool gives no hash.",
138
149
  inputSchema: listPinnedArtifactsInput,
139
150
  executionMode: "inline",
140
- describeCall: "none",
151
+ describeCall: (input) => input.path ?? "the pinned set",
141
152
  // The size of the listing is what a watcher reads, and the cap makes that size differ from the
142
153
  // pinned set. Thus a truncated listing names the total beside the count, and a whole listing names
143
154
  // the count alone, because a total that equals the count says the same thing two times.
144
155
  describeResult: (_input, result) => {
156
+ if (result.outcome === "artifact") {
157
+ const count = result.artifact.columnCount;
158
+ return count === undefined ? "no columns" : `${count} ${count === 1 ? "column" : "columns"}`;
159
+ }
145
160
  if (result.outcome !== "listed") {
146
161
  return result.outcome;
147
162
  }
@@ -149,18 +164,14 @@ export function createListPinnedArtifactsTool(deps) {
149
164
  const listed = `${count} ${count === 1 ? "artifact" : "artifacts"}`;
150
165
  return result.truncated ? `${listed} of ${result.total}` : listed;
151
166
  },
152
- execute: async (_input, ctx) => {
167
+ execute: async (input, ctx) => {
153
168
  const opened = await openReportThread(deps.gateway, ctx.session.scope);
154
169
  if (opened.isErr()) {
155
170
  return ok({ outcome: "refused", refusal: opened.error });
156
171
  }
157
172
  const { threadId, analysisId, state } = opened.value;
158
- const paths = Object.keys(state.snapshot.artifacts).sort();
159
- // The cap cuts the tail of the same order, thus a truncated listing is a prefix of the whole
160
- // listing and a second call over one snapshot names the same entries.
161
- const listed = paths.slice(0, LISTING_CAP);
162
173
  // The seam signals an unresolvable resource by a throw. The listing still serves the path and
163
- // the hash of each entry, thus the fault costs the columns alone and never the whole call.
174
+ // the file type of each entry, thus the fault costs the columns alone and never the whole call.
164
175
  let root;
165
176
  try {
166
177
  root = deps.resolveWorkspaceRoot(analysisId);
@@ -168,21 +179,30 @@ export function createListPinnedArtifactsTool(deps) {
168
179
  catch (cause) {
169
180
  logger.warn("the workspace root did not resolve", { threadId, analysisId, ...defaultErrorFields(cause) });
170
181
  }
182
+ const describe = async (path) => {
183
+ const fileType = state.snapshot.artifacts[path].fileType;
184
+ const artifact = { path, ...(typeof fileType === "string" ? { fileType } : {}) };
185
+ if (root === undefined || fileTypeHoldsNoCell(fileType)) {
186
+ return { artifact };
187
+ }
188
+ const resolved = resolveWorkspacePath({ workspaceRoot: root, analysisId, path });
189
+ const columns = resolved.kind === "ok" ? await readHeaderColumns(path, resolved.absolute) : undefined;
190
+ return columns === undefined ? { artifact } : { artifact: { ...artifact, columnCount: columns.length }, columns };
191
+ };
192
+ if (input.path !== undefined) {
193
+ if (!Object.hasOwn(state.snapshot.artifacts, input.path)) {
194
+ return ok({ outcome: "not-pinned", path: input.path });
195
+ }
196
+ const { artifact, columns } = await describe(input.path);
197
+ return ok({ outcome: "artifact", artifact: columns === undefined ? artifact : { ...artifact, columns } });
198
+ }
199
+ const paths = Object.keys(state.snapshot.artifacts).sort();
200
+ // The cap cuts the tail of the same order, thus a truncated listing is a prefix of the whole
201
+ // listing and a second call over one snapshot names the same entries.
202
+ const listed = paths.slice(0, LISTING_CAP);
171
203
  const artifacts = [];
172
204
  for (const path of listed) {
173
- const entry = state.snapshot.artifacts[path];
174
- const fileType = entry.fileType;
175
- const artifact = { path, hash: entry.hash, ...(typeof fileType === "string" ? { fileType } : {}) };
176
- if (root !== undefined && !fileTypeHoldsNoCell(fileType)) {
177
- const resolved = resolveWorkspacePath({ workspaceRoot: root, analysisId, path });
178
- if (resolved.kind === "ok") {
179
- const columns = await readHeaderColumns(path, resolved.absolute);
180
- if (columns !== undefined) {
181
- artifact.columns = columns;
182
- }
183
- }
184
- }
185
- artifacts.push(artifact);
205
+ artifacts.push((await describe(path)).artifact);
186
206
  }
187
207
  // The pin stores the citation keys sorted, thus the listing keeps that order. A snapshot that
188
208
  // pinned no citation gives an empty list, and an empty list is a complete answer. A key with no
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The digest of the faults of one look. A blocked inline font logs its whole base64 body, often many times,
3
+ * thus the raw lists can cost more context than the pictures of the look.
4
+ */
5
+ import type { FailedRequest } from "../../lib/page-capture.js";
6
+ /** One distinct console error of a look, and how many times the page logged it. */
7
+ export interface ConsoleErrorEntry {
8
+ text: string;
9
+ count: number;
10
+ }
11
+ /** One distinct failed request of a look, and how many times the page made it. */
12
+ export interface FailedRequestEntry {
13
+ url: string;
14
+ reason: string;
15
+ count: number;
16
+ }
17
+ /** The digest of one look. `omittedFaults` counts the distinct entries that the cap left out, thus a cut is never silent. */
18
+ export interface PageFaults {
19
+ consoleErrors: ConsoleErrorEntry[];
20
+ failedRequests: FailedRequestEntry[];
21
+ omittedFaults?: {
22
+ consoleErrors: number;
23
+ failedRequests: number;
24
+ };
25
+ }
26
+ /** Shorten one fault text: the inline data becomes placeholders, and the text stops at the cap. */
27
+ export declare function shortenFaultText(text: string): string;
28
+ /** Digest the raw faults of one capture into the short lists that the agent reads. */
29
+ export declare function digestPageFaults(consoleErrors: readonly string[], failedRequests: readonly FailedRequest[]): PageFaults;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The digest of the faults of one look. A blocked inline font logs its whole base64 body, often many times,
3
+ * thus the raw lists can cost more context than the pictures of the look.
4
+ */
5
+ import { capCodePoints } from "../../loop/tool-detail.js";
6
+ /** The most code points of one fault text, after the placeholders replace the inline data. */
7
+ const MAX_FAULT_TEXT = 300;
8
+ /** The most distinct entries of one list. */
9
+ const MAX_FAULT_ENTRIES = 20;
10
+ /** A data URI. A fault text quotes a URI in quotes or parentheses, thus those characters end the data. */
11
+ const DATA_URI = /data:([a-z0-9.+-]+\/[a-z0-9.+-]+)?((?:;[^,;\s'"()<>]*)*),([^\s'"()<>]*)/gi;
12
+ /** A base64 run of 100 characters or more. A content hash in hex is 64 characters, thus a hash stays. */
13
+ const BASE64_RUN = /[A-Za-z0-9+/]{100,}={0,2}/g;
14
+ /** Shorten one fault text: the inline data becomes placeholders, and the text stops at the cap. */
15
+ export function shortenFaultText(text) {
16
+ const inlined = text.replace(DATA_URI, (_uri, mediaType, _parameters, data) => {
17
+ return `[inline ${mediaType ?? "text/plain"} data, ${data.length} chars]`;
18
+ });
19
+ const unrun = inlined.replace(BASE64_RUN, (run) => `[base64 data, ${run.length} chars]`);
20
+ return capCodePoints(unrun, MAX_FAULT_TEXT);
21
+ }
22
+ /** Collapse each repeat of one list into one entry with a count. A `Map` keeps the first occurrence order. */
23
+ function collapse(items, keyOf) {
24
+ const byKey = new Map();
25
+ for (const item of items) {
26
+ const key = keyOf(item);
27
+ const held = byKey.get(key);
28
+ if (held === undefined) {
29
+ byKey.set(key, { item, count: 1 });
30
+ }
31
+ else {
32
+ held.count += 1;
33
+ }
34
+ }
35
+ return [...byKey.values()];
36
+ }
37
+ /** Digest the raw faults of one capture into the short lists that the agent reads. */
38
+ export function digestPageFaults(consoleErrors, failedRequests) {
39
+ const errors = collapse(consoleErrors.map(shortenFaultText), (text) => text).map(({ item, count }) => ({ text: item, count }));
40
+ const requests = collapse(failedRequests.map((request) => ({ url: shortenFaultText(request.url), reason: shortenFaultText(request.reason) })), (request) => `${request.url}\n${request.reason}`).map(({ item, count }) => ({ ...item, count }));
41
+ const omittedErrors = Math.max(0, errors.length - MAX_FAULT_ENTRIES);
42
+ const omittedRequests = Math.max(0, requests.length - MAX_FAULT_ENTRIES);
43
+ return {
44
+ consoleErrors: errors.slice(0, MAX_FAULT_ENTRIES),
45
+ failedRequests: requests.slice(0, MAX_FAULT_ENTRIES),
46
+ ...(omittedErrors + omittedRequests > 0 ? { omittedFaults: { consoleErrors: omittedErrors, failedRequests: omittedRequests } } : {}),
47
+ };
48
+ }
@@ -4,7 +4,8 @@
4
4
  * The tool renders the current draft of a thread. It runs the finish first, because a mid-composition
5
5
  * draft is not a `ReportDocument`. A gap list returns as data, and no render runs. On a pass the tool
6
6
  * resolves each reference through the injected resolver, bridges the resolved values into the render model,
7
- * and renders the page.
7
+ * and renders the page. The gap list and the rendered page each carry the advisory warnings of the finish,
8
+ * thus the agent reads them with no separate finish.
8
9
  *
9
10
  * The renderer writes no file. It gives the page and the data asset of each table, and this tool stages
10
11
  * them beside the figures. The tool asks the optional provenance source for the frozen document of the
@@ -40,6 +41,8 @@ import { z } from "zod";
40
41
  import { type ResolveWorkspaceRoot } from "../../workspace/paths.js";
41
42
  import type { AuthContext } from "../../auth/types.js";
42
43
  import { type Logger } from "../../lib/logger.js";
44
+ import type { ThreadStore } from "../../memory/thread-store.js";
45
+ import { type ReportWarning } from "../../report-model/block-walk.js";
43
46
  import { type FinishGap } from "../../report-model/draft-finish.js";
44
47
  import type { ReferenceResolver } from "../../report-model/reference-resolver.js";
45
48
  import { type ResolutionFailure } from "../../report-model/validate.js";
@@ -68,7 +71,7 @@ export type SessionPageAccess = {
68
71
  /**
69
72
  * The typed outcome of the preview tool. Each arm is ok-channel data, thus the tool never throws for a
70
73
  * degraded condition. `rendered` carries the absolute page path, and the access grant of the hosted view
71
- * when the composition binds the publisher.
74
+ * when the composition binds the publisher. `gaps` and `rendered` carry the advisory warnings of the finish.
72
75
  */
73
76
  export type PreviewReportResult = {
74
77
  outcome: "refused";
@@ -76,6 +79,7 @@ export type PreviewReportResult = {
76
79
  } | {
77
80
  outcome: "gaps";
78
81
  gaps: FinishGap[];
82
+ warnings: ReportWarning[];
79
83
  } | {
80
84
  outcome: "resolver-unavailable";
81
85
  } | {
@@ -105,6 +109,7 @@ export type PreviewReportResult = {
105
109
  outcome: "rendered";
106
110
  pagePath: string;
107
111
  access?: SessionPageAccess;
112
+ warnings: ReportWarning[];
108
113
  };
109
114
  /**
110
115
  * The lookup of the source file of one staged asset. It maps the module specifier of a manifest entry
@@ -144,6 +149,8 @@ export interface PreviewReportToolDeps {
144
149
  * then carries no provenance asset.
145
150
  */
146
151
  readonly provenance?: ProvenanceSeam;
152
+ /** Names the report thread after the rendered document. Omitted, the thread keeps its title. */
153
+ readonly threads?: Pick<ThreadStore, "setAutoTitle">;
147
154
  readonly logger?: Logger;
148
155
  }
149
156
  /**
@@ -4,7 +4,8 @@
4
4
  * The tool renders the current draft of a thread. It runs the finish first, because a mid-composition
5
5
  * draft is not a `ReportDocument`. A gap list returns as data, and no render runs. On a pass the tool
6
6
  * resolves each reference through the injected resolver, bridges the resolved values into the render model,
7
- * and renders the page.
7
+ * and renders the page. The gap list and the rendered page each carry the advisory warnings of the finish,
8
+ * thus the agent reads them with no separate finish.
8
9
  *
9
10
  * The renderer writes no file. It gives the page and the data asset of each table, and this tool stages
10
11
  * them beside the figures. The tool asks the optional provenance source for the frozen document of the
@@ -396,6 +397,7 @@ export function createPreviewReportTool(deps) {
396
397
  description: "Render the current draft to an HTML page in the session directory, and give back the page path. " +
397
398
  "The page loads its figures, table data, and libraries from an assets directory that the tool stages beside it. " +
398
399
  "The tool finishes the draft first: an incomplete draft gives back the gap list, and no page renders. " +
400
+ "The gap list and the rendered page each carry the advisory warnings of the finish, for example a number typed into the prose. " +
399
401
  "On a pass it resolves each reference, stages each bound image beside the page, and writes the page. " +
400
402
  "An unresolved reference gives back the block and the binding to repair. " +
401
403
  "The outcome resolver-unavailable means that this host gives no reference resolver, and a repeat gives the same outcome. " +
@@ -419,7 +421,7 @@ export function createPreviewReportTool(deps) {
419
421
  const { document: draft, snapshot } = state;
420
422
  const finished = finishDraft(draft, snapshot, derivations);
421
423
  if (!finished.valid) {
422
- return ok({ outcome: "gaps", gaps: finished.gaps });
424
+ return ok({ outcome: "gaps", gaps: finished.gaps, warnings: finished.warnings });
423
425
  }
424
426
  const document = finished.document;
425
427
  if (deps.makeResolver === undefined) {
@@ -498,6 +500,14 @@ export function createPreviewReportTool(deps) {
498
500
  logger.warn("the rendered hash did not stamp", { threadId, analysisId, detail });
499
501
  return ok({ outcome: "stamp-failed", pagePath: written.value, detail });
500
502
  }
503
+ // Before the part, thus a client that reads the thread list on the part reads the new title.
504
+ const title = document.title.trim();
505
+ if (deps.threads && title.length > 0) {
506
+ const named = await deps.threads.setAutoTitle(threadId, title);
507
+ if (named.isErr()) {
508
+ logger.warn("the report title did not reach the thread", { threadId, analysisId, ...defaultErrorFields(named.error.cause) });
509
+ }
510
+ }
501
511
  // The part rides the rendered arm only. Each degraded arm shows no
502
512
  // fresh page, thus it emits nothing.
503
513
  await ctx.emit({
@@ -508,7 +518,7 @@ export function createPreviewReportTool(deps) {
508
518
  // the draft that the page shows, thus a consumer ties the page to the document that made it.
509
519
  observe({ type: "preview", analysisId, threadId, pagePath: written.value, documentHash: renderedHash });
510
520
  const access = await mintAccess(deps.makeSessionPages, analysisId, threadId, ctx.session.auth, logger);
511
- return ok({ outcome: "rendered", pagePath: written.value, ...(access !== undefined ? { access } : {}) });
521
+ return ok({ outcome: "rendered", pagePath: written.value, ...(access !== undefined ? { access } : {}), warnings: finished.warnings });
512
522
  },
513
523
  });
514
524
  }
@@ -32,6 +32,7 @@ import { z } from "zod";
32
32
  import type { AuthContext } from "../../auth/types.js";
33
33
  import type { Logger } from "../../lib/logger.js";
34
34
  import type { ThreadStore } from "../../memory/thread-store.js";
35
+ import { type ReportWarning } from "../../report-model/block-walk.js";
35
36
  import { type FinishGap } from "../../report-model/draft-finish.js";
36
37
  import type { ReferenceResolver } from "../../report-model/reference-resolver.js";
37
38
  import { type ResolutionFailure, type SchemaIssue } from "../../report-model/validate.js";
@@ -47,7 +48,8 @@ export type RecordVersionInput = z.infer<typeof recordVersionInput>;
47
48
  * The typed outcome of the record tool. Each arm is ok-channel data, thus the tool never throws for a
48
49
  * degraded condition.
49
50
  *
50
- * `gaps` names each completeness gap of the draft. `never-seen` means that no eyes ran on the current draft.
51
+ * `gaps` names each completeness gap of the draft. `gaps` and `recorded` carry the advisory warnings of the
52
+ * finish. `never-seen` means that no eyes ran on the current draft.
51
53
  * `stale-look` means that the eyes ran, and the agent then changed the draft. `root-unresolvable` means that
52
54
  * the resolver construction cannot resolve the workspace root. `invalid` names each gate failure, and a
53
55
  * resolution failure carries the block that holds it. `recorded` carries the version id, and `replaced` is
@@ -59,6 +61,7 @@ export type RecordVersionResult = {
59
61
  } | {
60
62
  outcome: "gaps";
61
63
  gaps: FinishGap[];
64
+ warnings: ReportWarning[];
62
65
  } | {
63
66
  outcome: "never-seen";
64
67
  } | {
@@ -80,6 +83,7 @@ export type RecordVersionResult = {
80
83
  outcome: "recorded";
81
84
  versionId: string;
82
85
  replaced: boolean;
86
+ warnings: ReportWarning[];
83
87
  };
84
88
  /**
85
89
  * The construction deps of the record tool.
@@ -128,7 +128,8 @@ export function createRecordVersionTool(deps) {
128
128
  id: "record_report_version",
129
129
  description: "Record the current draft as one report version. The tool runs the whole gate first: it finishes the draft, " +
130
130
  "resolves each reference, matches each chart encoding, and matches each assert. An incomplete draft gives back the gap list, " +
131
- "and a failed reference gives back the block that broke. The tool records a version only after examine_page looked at the page of the current draft: " +
131
+ "and a failed reference gives back the block that broke. The gap list and the recorded version each carry the advisory warnings of the finish. " +
132
+ "The tool records a version only after examine_page looked at the page of the current draft: " +
132
133
  "with no look it gives back never-seen, and after an edit since the look it gives back stale-look. " +
133
134
  "A thread holds one version, thus a later record replaces it whole. Amend the draft, look at the page again, and record again. " +
134
135
  "After the version lands, the tool removes the file of each derived table that no block of the recorded report binds.",
@@ -152,7 +153,7 @@ export function createRecordVersionTool(deps) {
152
153
  const { document: draft, snapshot } = state;
153
154
  const finished = finishDraft(draft, snapshot, derivations);
154
155
  if (!finished.valid) {
155
- return ok({ outcome: "gaps", gaps: finished.gaps });
156
+ return ok({ outcome: "gaps", gaps: finished.gaps, warnings: finished.warnings });
156
157
  }
157
158
  // The look-before-record rule runs before the resolver and the validation, on the state that the
158
159
  // load already read. The preview stamps the rendered hash, and the eyes copy it onto the seen
@@ -220,7 +221,7 @@ export function createRecordVersionTool(deps) {
220
221
  logger,
221
222
  });
222
223
  }
223
- return recorded.match((ref) => ok({ outcome: "recorded", versionId: ref.versionId, replaced: ref.outcome === "replaced" }), (error) => {
224
+ return recorded.match((ref) => ok({ outcome: "recorded", versionId: ref.versionId, replaced: ref.outcome === "replaced", warnings: finished.warnings }), (error) => {
224
225
  logger.warn("the version did not record", { threadId, analysisId, reason: error.type });
225
226
  return ok({ outcome: "record-failed", detail: describeRecordFailure(error) });
226
227
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inflexa-ai/harness",
3
- "version": "0.41.2",
3
+ "version": "0.42.0",
4
4
  "description": "Host-agnostic bioinformatics agent harness — hand-rolled agent loop, DBOS-durable workflows, sandboxed compute.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",