@inflexa-ai/harness 0.41.1 → 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.
- package/dist/agents/conversation-agent.js +3 -0
- package/dist/agents/report-session-agent.d.ts +2 -2
- package/dist/agents/report-session-agent.js +4 -0
- package/dist/app/chat-turn.d.ts +0 -4
- package/dist/app/chat-turn.js +39 -25
- package/dist/app/message-assembly.d.ts +2 -2
- package/dist/app/message-assembly.js +1 -1
- package/dist/contracts/chat-events.d.ts +12 -5
- package/dist/contracts/chat-events.js +3 -3
- package/dist/contracts/chat-frame.d.ts +40 -0
- package/dist/contracts/chat-frame.js +166 -0
- package/dist/contracts/chat-parts.d.ts +15 -3
- package/dist/contracts/chat-parts.js +2 -2
- package/dist/contracts/index.d.ts +6 -4
- package/dist/contracts/index.js +1 -0
- package/dist/contracts/message.d.ts +7 -5
- package/dist/contracts/message.js +1 -1
- package/dist/contracts/part-registry.d.ts +6 -6
- package/dist/contracts/report-blocks.d.ts +10 -10
- package/dist/contracts/report-reference.d.ts +4 -4
- package/dist/contracts/schemas/chat-events.d.ts +2 -2
- package/dist/contracts/schemas/chat-events.js +2 -2
- package/dist/contracts/schemas/chat-parts.d.ts +34 -2
- package/dist/contracts/schemas/chat-parts.js +9 -3
- package/dist/contracts/schemas/index.d.ts +2 -2
- package/dist/contracts/schemas/index.js +2 -2
- package/dist/execution/run-event-parts.d.ts +4 -4
- package/dist/execution/run-event-parts.js +0 -0
- package/dist/execution/run-event-stream.d.ts +2 -2
- package/dist/index.d.ts +9 -6
- package/dist/index.js +3 -2
- package/dist/lib/page-capture.d.ts +27 -9
- package/dist/lib/page-capture.js +81 -33
- package/dist/loop/compaction.d.ts +8 -3
- package/dist/loop/run-agent.d.ts +11 -0
- package/dist/loop/run-agent.js +62 -35
- package/dist/loop/types.d.ts +3 -0
- package/dist/memory/ai-sdk-message-storage.d.ts +18 -3
- package/dist/memory/ai-sdk-message-storage.js +23 -1
- package/dist/memory/conversation-display-recorder.d.ts +7 -1
- package/dist/memory/conversation-display-recorder.js +59 -119
- package/dist/memory/conversation-display-replay.d.ts +3 -3
- package/dist/memory/conversation-display-replay.js +7 -21
- package/dist/memory/conversation-display-storage.d.ts +10 -10
- package/dist/memory/conversation-display-storage.js +9 -8
- package/dist/memory/conversation-view.d.ts +5 -4
- package/dist/memory/conversation-view.js +14 -18
- package/dist/memory/count-tokens.js +1 -0
- package/dist/memory/message-backfill.d.ts +8 -2
- package/dist/memory/thread-store.d.ts +9 -2
- package/dist/memory/thread-store.js +8 -2
- package/dist/prompts/compaction.d.ts +2 -2
- package/dist/prompts/compaction.js +2 -2
- package/dist/prompts/conversation.d.ts +1 -1
- package/dist/prompts/conversation.js +12 -0
- package/dist/prompts/report-session.d.ts +1 -1
- package/dist/prompts/report-session.js +40 -28
- package/dist/report-model/authoring-grammar.d.ts +4 -4
- package/dist/report-render/views/block-mark.d.ts +8 -0
- package/dist/report-render/views/block-mark.js +10 -0
- package/dist/report-render/views/chart-view.js +2 -1
- package/dist/report-render/views/prose.js +10 -6
- package/dist/report-render/views/values.js +5 -4
- package/dist/runtime/boot.d.ts +2 -2
- package/dist/runtime/boot.js +3 -3
- package/dist/state/init.d.ts +7 -6
- package/dist/state/init.js +51 -672
- package/dist/state/migrations/20260927120000_baseline.d.ts +11 -0
- package/dist/state/migrations/20260927120000_baseline.js +675 -0
- package/dist/state/migrations/20260927180000_thread_title_set_by_user.d.ts +6 -0
- package/dist/state/migrations/20260927180000_thread_title_set_by_user.js +10 -0
- package/dist/state/migrations/index.d.ts +10 -0
- package/dist/state/migrations/index.js +15 -0
- package/dist/tools/display/show-user.js +6 -4
- package/dist/tools/report-authoring/authoring-tools.d.ts +16 -5
- package/dist/tools/report-authoring/authoring-tools.js +52 -24
- package/dist/tools/report-session/examine-page.d.ts +29 -14
- package/dist/tools/report-session/examine-page.js +40 -16
- package/dist/tools/report-session/list-artifacts.d.ts +23 -10
- package/dist/tools/report-session/list-artifacts.js +51 -31
- package/dist/tools/report-session/page-faults.d.ts +29 -0
- package/dist/tools/report-session/page-faults.js +48 -0
- package/dist/tools/report-session/preview-report.d.ts +9 -2
- package/dist/tools/report-session/preview-report.js +13 -3
- package/dist/tools/report-session/record-version.d.ts +5 -1
- package/dist/tools/report-session/record-version.js +4 -3
- package/dist/tools/workspace/show-file.js +4 -3
- package/package.json +2 -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
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
/**
|
|
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.
|
|
128
|
-
|
|
129
|
-
"
|
|
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
|
|
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: "
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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. `
|
|
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
|
|
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
|
});
|
|
@@ -25,7 +25,7 @@ const ShowFileInputSchema = z.object({
|
|
|
25
25
|
}))
|
|
26
26
|
.min(1)
|
|
27
27
|
.max(MAX_FILES)
|
|
28
|
-
.describe(`The
|
|
28
|
+
.describe(`The files to show together, 1 to ${MAX_FILES} per call; multiple render as ONE gallery card. Pass all the figures that one prose section discusses in a single call. Figures for different sections go in separate calls.`),
|
|
29
29
|
});
|
|
30
30
|
export const showFileTool = defineTool({
|
|
31
31
|
id: "show_file",
|
|
@@ -36,8 +36,9 @@ export const showFileTool = defineTool({
|
|
|
36
36
|
"Paths are analysis-rooted (no leading slash, no `..`); discover them with `workspace_search` or `list_files`. " +
|
|
37
37
|
"The tool checks the shape of each path only, not that the file exists, so a wrong path fails only when the user views the card; " +
|
|
38
38
|
"`shown: false` with `invalid_path` means a path has a leading slash or a `..` segment. " +
|
|
39
|
-
"Up to 10 files per call
|
|
40
|
-
"Cards render in call order, so to
|
|
39
|
+
"Up to 10 files per call, rendered as one gallery — pass all the figures that one prose section discusses in ONE call. " +
|
|
40
|
+
"Cards render in call order, so to put figures next to their prose, emit `show_user(markdown)` sections and `show_file` " +
|
|
41
|
+
"calls in one batch, in reading order: a section, then the `show_file` for its figures, then the next section " +
|
|
41
42
|
"(markdown image syntax pointing at a workspace file does NOT render — this tool is the only way to show one).",
|
|
42
43
|
inputSchema: ShowFileInputSchema,
|
|
43
44
|
// `.min(1)` on `files` makes index 0 total after the parse.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inflexa-ai/harness",
|
|
3
|
-
"version": "0.
|
|
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",
|
|
@@ -77,6 +77,7 @@
|
|
|
77
77
|
"hono": "^4.12.34",
|
|
78
78
|
"hyparquet": "^1.28.1",
|
|
79
79
|
"js-tiktoken": "^1.0.21",
|
|
80
|
+
"kysely": "^0.29.6",
|
|
80
81
|
"mime": "^4.1.0",
|
|
81
82
|
"nanoid": "^5.1.16",
|
|
82
83
|
"neverthrow": "^8.2.0",
|