@inflexa-ai/harness 0.22.1 → 0.24.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/README.md +1 -2
- package/dist/agents/conversation-agent.d.ts +0 -9
- package/dist/app/chat-turn.js +1 -0
- package/dist/app/message-assembly.d.ts +3 -0
- package/dist/app/message-assembly.js +17 -0
- package/dist/contracts/chat-parts.d.ts +38 -26
- package/dist/contracts/content-url.d.ts +7 -41
- package/dist/contracts/content-url.js +7 -48
- package/dist/contracts/index.d.ts +2 -2
- package/dist/contracts/index.js +1 -1
- package/dist/contracts/message.d.ts +1 -1
- package/dist/contracts/message.js +1 -1
- package/dist/contracts/part-registry.js +2 -2
- package/dist/contracts/schemas/chat-parts.d.ts +16 -44
- package/dist/contracts/schemas/chat-parts.js +15 -18
- package/dist/contracts/schemas/index.d.ts +1 -1
- package/dist/contracts/schemas/index.js +1 -1
- package/dist/execution/run-authorizer.d.ts +1 -1
- package/dist/execution/run-authorizer.js +1 -1
- package/dist/index.d.ts +1 -3
- package/dist/index.js +0 -2
- package/dist/lib/zod-issues.d.ts +2 -3
- package/dist/lib/zod-issues.js +2 -3
- package/dist/loop/__fixtures__/scripted-provider.js +3 -1
- package/dist/loop/run-agent.d.ts +5 -0
- package/dist/loop/run-agent.js +50 -2
- package/dist/loop/run-to-terminal.d.ts +1 -1
- package/dist/loop/run-to-terminal.js +1 -1
- package/dist/memory/card-builders.d.ts +0 -27
- package/dist/memory/card-builders.js +0 -75
- package/dist/memory/conversation-display-backfill.d.ts +2 -4
- package/dist/memory/conversation-display-backfill.js +3 -9
- package/dist/memory/conversation-display-storage.d.ts +3 -3
- package/dist/memory/conversation-display-storage.js +3 -3
- package/dist/memory/reconstruct-cards.d.ts +5 -7
- package/dist/memory/reconstruct-cards.js +6 -20
- package/dist/memory/tool-call-integrity.d.ts +46 -0
- package/dist/memory/tool-call-integrity.js +92 -0
- package/dist/runtime/boot.js +2 -12
- package/dist/tools/report-session/preview-report.d.ts +2 -1
- package/dist/tools/report-session/preview-report.js +9 -1
- package/dist/tools/start-report-session.js +17 -7
- package/dist/workspace/paths.d.ts +0 -21
- package/dist/workspace/paths.js +0 -37
- package/package.json +1 -3
- package/dist/agents/report-builder.d.ts +0 -34
- package/dist/agents/report-builder.js +0 -34
- package/dist/execution/report-render.d.ts +0 -39
- package/dist/execution/report-render.js +0 -98
- package/dist/execution/report-runner.d.ts +0 -111
- package/dist/execution/report-runner.js +0 -230
- package/dist/prompts/report-builder.d.ts +0 -1
- package/dist/prompts/report-builder.js +0 -174
- package/dist/tools/iterate-report.d.ts +0 -227
- package/dist/tools/iterate-report.js +0 -883
- package/dist/tools/lib/preview-lock.d.ts +0 -20
- package/dist/tools/lib/preview-lock.js +0 -35
- package/dist/tools/lib/report-preflight.d.ts +0 -39
- package/dist/tools/lib/report-preflight.js +0 -283
- package/dist/tools/report/build-report.d.ts +0 -13
- package/dist/tools/report/build-report.js +0 -27
- package/dist/tools/report/index.d.ts +0 -15
- package/dist/tools/report/index.js +0 -15
- package/dist/tools/report/mint-preview-url.d.ts +0 -26
- package/dist/tools/report/mint-preview-url.js +0 -47
- package/dist/tools/report/preview-publisher.d.ts +0 -53
- package/dist/tools/report/preview-publisher.js +0 -45
- package/dist/tools/report/preview-snapshot.d.ts +0 -34
- package/dist/tools/report/preview-snapshot.js +0 -98
- package/dist/tools/report/submit-report.d.ts +0 -26
- package/dist/tools/report/submit-report.js +0 -148
- package/dist/tools/report/version-fs.d.ts +0 -15
- package/dist/tools/report/version-fs.js +0 -165
package/dist/loop/run-agent.d.ts
CHANGED
|
@@ -8,6 +8,11 @@ import { type AgentRunUsage } from "./metrics.js";
|
|
|
8
8
|
import type { AgentDefinition, EmitFn, LoopMessage, RunStep } from "./types.js";
|
|
9
9
|
export interface AgentFinish {
|
|
10
10
|
readonly reason: FinishReason | "aborted" | "max_iterations" | "denied";
|
|
11
|
+
/**
|
|
12
|
+
* The endpoint's own word for the stop, when the provider captured one —
|
|
13
|
+
* `reason` normalizes it (an Anthropic `refusal` becomes `content-filter`).
|
|
14
|
+
*/
|
|
15
|
+
readonly rawFinishReason?: string;
|
|
11
16
|
readonly cappedOut: boolean;
|
|
12
17
|
readonly truncationRecoveries: number;
|
|
13
18
|
/**
|
package/dist/loop/run-agent.js
CHANGED
|
@@ -5,6 +5,7 @@ import { stripNulCharacters } from "../input-sanitization.js";
|
|
|
5
5
|
import { createNoopLogger } from "../lib/console-logger.js";
|
|
6
6
|
import { hintForZodIssue, repairToolInput } from "../lib/zod-issues.js";
|
|
7
7
|
import { markInterruptedMessage, syntheticUserMessage } from "../memory/ai-sdk-message-storage.js";
|
|
8
|
+
import { stripUnansweredToolCalls } from "../memory/tool-call-integrity.js";
|
|
8
9
|
import { classifyProviderError } from "../providers/errors.js";
|
|
9
10
|
import { DEFAULT_PROMPT_CACHE, promptCacheProviderOptions } from "../providers/prompt-cache.js";
|
|
10
11
|
import { resultStep } from "./run-step.js";
|
|
@@ -91,6 +92,28 @@ export async function runAgent(agent, initial, session, opts) {
|
|
|
91
92
|
const logFinish = (level, reason, cappedOut) => {
|
|
92
93
|
log[level]("run finished", { iterations, reason, cappedOut, truncationRecoveries, toolCalls: toolCallCount, toolErrors: toolErrorCount, usage });
|
|
93
94
|
};
|
|
95
|
+
/**
|
|
96
|
+
* Settle the transcript on the way out — called at every return. A reply can
|
|
97
|
+
* carry a complete tool call beside ANY finish reason, because the call
|
|
98
|
+
* streams before the stop reason arrives, and only the dispatching branches
|
|
99
|
+
* (`tool-calls`, a call-carrying `stop`, and `length`) run one. A call this
|
|
100
|
+
* run never dispatched is stripped
|
|
101
|
+
* rather than executed (a filtered or aborted reply must not act) or answered
|
|
102
|
+
* with an invented result (the model would read a fabrication as ground
|
|
103
|
+
* truth). Without the strip, the unanswered call violates the wire contract —
|
|
104
|
+
* every tool call carries a result — and the provider boundary then refuses
|
|
105
|
+
* the whole transcript on every later turn of the thread. Scoped past
|
|
106
|
+
* `initial`: the caller's prefix is not this run's to repair.
|
|
107
|
+
*/
|
|
108
|
+
const settleTranscript = () => {
|
|
109
|
+
const droppedCalls = stripUnansweredToolCalls(messages, initial.length);
|
|
110
|
+
if (droppedCalls.length === 0)
|
|
111
|
+
return;
|
|
112
|
+
log.warn("undispatched tool calls stripped at run exit", {
|
|
113
|
+
toolCallIds: droppedCalls.map((d) => d.toolCallId),
|
|
114
|
+
tools: droppedCalls.map((d) => d.toolName),
|
|
115
|
+
});
|
|
116
|
+
};
|
|
94
117
|
const usageRecorder = opts.usageRecorder ?? createNoopUsageRecorder();
|
|
95
118
|
/**
|
|
96
119
|
* Fold one completed call into both rollups and hand it to the recorder.
|
|
@@ -135,6 +158,7 @@ export async function runAgent(agent, initial, session, opts) {
|
|
|
135
158
|
// lands in a dispatch round — after the concurrent siblings in that same round
|
|
136
159
|
// have completed and been appended. Mirrors the clean-stop terminal path.
|
|
137
160
|
const stopOnDenial = async (i) => {
|
|
161
|
+
settleTranscript();
|
|
138
162
|
await emit({ type: "iteration", source, index: i, final: true });
|
|
139
163
|
recordAgentRun({ agentId: agent.id, iterations, cappedOut: false, usage });
|
|
140
164
|
logFinish("warn", "denied", false);
|
|
@@ -195,6 +219,7 @@ export async function runAgent(agent, initial, session, opts) {
|
|
|
195
219
|
return errored;
|
|
196
220
|
};
|
|
197
221
|
const stopOnResolved = async (i) => {
|
|
222
|
+
settleTranscript();
|
|
198
223
|
await emit({ type: "iteration", source, index: i, final: true });
|
|
199
224
|
recordAgentRun({ agentId: agent.id, iterations, cappedOut: false, usage });
|
|
200
225
|
return { messages, finish: { reason: "stop", cappedOut: false, truncationRecoveries } };
|
|
@@ -266,11 +291,32 @@ export async function runAgent(agent, initial, session, opts) {
|
|
|
266
291
|
return stopOnResolved(i);
|
|
267
292
|
continue;
|
|
268
293
|
}
|
|
269
|
-
|
|
294
|
+
// A local model behind an OpenAI-compatible server routinely labels a
|
|
295
|
+
// tool-calling reply `stop`. The calls are complete and the model waits
|
|
296
|
+
// on their results, thus the mislabeled round dispatches like a
|
|
297
|
+
// `tool-calls` one. Every other terminal keeps the strip: a filtered or
|
|
298
|
+
// aborted reply must not act.
|
|
299
|
+
const dispatchesRound = reply.finishReason === "tool-calls" || (reply.finishReason === "stop" && toolCalls.length > 0);
|
|
300
|
+
if (!dispatchesRound) {
|
|
301
|
+
settleTranscript();
|
|
302
|
+
// The settle can remove a call-only aborted partial whole; the
|
|
303
|
+
// interruption marker then rides the last assistant that survived,
|
|
304
|
+
// matching the no-output abort contract above.
|
|
305
|
+
if (reply.finishReason === "aborted")
|
|
306
|
+
markLastLoopAssistant(messages, initial.length);
|
|
270
307
|
await emit({ type: "iteration", source, index: i, final: true });
|
|
271
308
|
recordAgentRun({ agentId: agent.id, iterations, cappedOut: false, usage });
|
|
272
309
|
logFinish("info", reply.finishReason, false);
|
|
273
|
-
return {
|
|
310
|
+
return {
|
|
311
|
+
messages,
|
|
312
|
+
finish: {
|
|
313
|
+
reason: reply.finishReason,
|
|
314
|
+
...(reply.rawFinishReason === undefined ? {} : { rawFinishReason: reply.rawFinishReason }),
|
|
315
|
+
cappedOut: false,
|
|
316
|
+
truncationRecoveries,
|
|
317
|
+
...finishUsage(),
|
|
318
|
+
},
|
|
319
|
+
};
|
|
274
320
|
}
|
|
275
321
|
await emit({ type: "iteration", source, index: i, final: false });
|
|
276
322
|
log.debug("iteration", { iteration: i, tools: toolCalls.map((t) => t.toolName) });
|
|
@@ -307,6 +353,7 @@ export async function runAgent(agent, initial, session, opts) {
|
|
|
307
353
|
// exhausted its iterations, while the reason carries the abort.
|
|
308
354
|
if (assistantHasContent(wrapUp.message))
|
|
309
355
|
messages.push(wrapUp.message);
|
|
356
|
+
settleTranscript();
|
|
310
357
|
markLastLoopAssistant(messages, initial.length);
|
|
311
358
|
await emit({ type: "iteration", source, index: agent.maxIterations, final: true });
|
|
312
359
|
recordAgentRun({ agentId: agent.id, iterations, cappedOut: true, usage });
|
|
@@ -314,6 +361,7 @@ export async function runAgent(agent, initial, session, opts) {
|
|
|
314
361
|
return { messages, finish: { reason: "aborted", cappedOut: true, truncationRecoveries, ...finishUsage() } };
|
|
315
362
|
}
|
|
316
363
|
messages.push(wrapUp.message);
|
|
364
|
+
settleTranscript();
|
|
317
365
|
await emit({ type: "iteration", source, index: agent.maxIterations, final: true });
|
|
318
366
|
recordAgentRun({ agentId: agent.id, iterations, cappedOut: true, usage });
|
|
319
367
|
logFinish("warn", "max_iterations", true);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* `runToTerminal` — terminal-salvage wrapper around `runAgent`.
|
|
3
3
|
*
|
|
4
4
|
* Some agents communicate their result EXCLUSIVELY through a terminal tool
|
|
5
|
-
* (`submit_plan`, `
|
|
5
|
+
* (`submit_plan`, `submit_profile`, `submit_synthesis`, …):
|
|
6
6
|
* the loop's text reply is discarded and the outcome is read from a closure
|
|
7
7
|
* cell the terminal tool writes. Plain `runAgent` ends a capped run on a
|
|
8
8
|
* tool-LESS wrap-up turn, so an agent that burns its whole iteration budget —
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* `runToTerminal` — terminal-salvage wrapper around `runAgent`.
|
|
3
3
|
*
|
|
4
4
|
* Some agents communicate their result EXCLUSIVELY through a terminal tool
|
|
5
|
-
* (`submit_plan`, `
|
|
5
|
+
* (`submit_plan`, `submit_profile`, `submit_synthesis`, …):
|
|
6
6
|
* the loop's text reply is discarded and the outcome is read from a closure
|
|
7
7
|
* cell the terminal tool writes. Plain `runAgent` ends a capped run on a
|
|
8
8
|
* tool-LESS wrap-up turn, so an agent that burns its whole iteration budget —
|
|
@@ -39,14 +39,6 @@ export interface RunCardData {
|
|
|
39
39
|
title: string;
|
|
40
40
|
stepCount: number;
|
|
41
41
|
}
|
|
42
|
-
export interface PreviewCardData {
|
|
43
|
-
id: string;
|
|
44
|
-
previewId: string;
|
|
45
|
-
version: number;
|
|
46
|
-
title: string;
|
|
47
|
-
previewPath: string;
|
|
48
|
-
format: "html" | "pdf";
|
|
49
|
-
}
|
|
50
42
|
/** Build the `data-plan` payload by loading the stored plan. `ok(null)` when
|
|
51
43
|
* the plan is missing or no longer parses (absence is NOT an error) — the
|
|
52
44
|
* caller falls back to a chip. */
|
|
@@ -100,22 +92,3 @@ export declare function fileGroupHash(paths: string[], title: string | undefined
|
|
|
100
92
|
* path the live tool rejected would be resurrected into a card on reload.
|
|
101
93
|
*/
|
|
102
94
|
export declare function buildFileReferenceCardData(input: Record<string, unknown>): FileReferenceCardData | null;
|
|
103
|
-
/**
|
|
104
|
-
* Build the `data-report-preview` payload for an `iterate_report` tool_use. The
|
|
105
|
-
* preview's source of truth is the filesystem under
|
|
106
|
-
* `previews/{previewId}/`: directory layout yields `version`
|
|
107
|
-
* (latest `vN`) + `previewPath`, and `preview-meta.json` yields title/format.
|
|
108
|
-
* Mirrors `buildRunCardData` — the live emit knows the server-generated
|
|
109
|
-
* `previewId`, while the reconstruct-on-read path resolves it (from the
|
|
110
|
-
* iteration's `previewId` input, or else the analysis's latest preview). The
|
|
111
|
-
* card is keyed by `previewId` only, so every iteration's tool_use rebuilds
|
|
112
|
-
* the same card and the frontend groups them into one with all versions selectable.
|
|
113
|
-
* Null when nothing renders — the caller falls back to a chip.
|
|
114
|
-
*/
|
|
115
|
-
export declare function buildPreviewCardData(
|
|
116
|
-
/** Absolute host root of the analysis's workspace tree (previews live inside it). */
|
|
117
|
-
workspaceRoot: string, opts: {
|
|
118
|
-
previewId?: string;
|
|
119
|
-
title?: string;
|
|
120
|
-
format?: "html" | "pdf";
|
|
121
|
-
}): Promise<PreviewCardData | null>;
|
|
@@ -16,17 +16,12 @@
|
|
|
16
16
|
* under the part `type` directly.
|
|
17
17
|
*/
|
|
18
18
|
import { createHash } from "node:crypto";
|
|
19
|
-
import { readdir, readFile, stat } from "node:fs/promises";
|
|
20
|
-
import { join } from "node:path";
|
|
21
19
|
import { okAsync } from "neverthrow";
|
|
22
20
|
import { tryQuery } from "../lib/db-result.js";
|
|
23
|
-
import { tryFs } from "../lib/fs-result.js";
|
|
24
21
|
import { AnalysisPlanSchema } from "../schemas/workflow-state.js";
|
|
25
22
|
import { loadPlan } from "../state/index.js";
|
|
26
23
|
import { normalizeEchartSpec } from "../tools/display/normalize-echart-spec.js";
|
|
27
24
|
import { validatePath } from "../tools/lib/path-validation.js";
|
|
28
|
-
import { latestPreviewVersion, previewDir, PREVIEWS_ROOT } from "../workspace/paths.js";
|
|
29
|
-
const PREVIEW_META_FILE = "preview-meta.json";
|
|
30
25
|
function isRecord(value) {
|
|
31
26
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
32
27
|
}
|
|
@@ -176,73 +171,3 @@ export function buildFileReferenceCardData(input) {
|
|
|
176
171
|
const id = `pres-${fileGroupHash(files.map((f) => f.path), title)}`;
|
|
177
172
|
return { id, ...(title !== undefined ? { title } : {}), files };
|
|
178
173
|
}
|
|
179
|
-
/** The stored preview metadata, or `null` when the file is absent — a malformed
|
|
180
|
-
* file is a genuine read failure (`err`), folded to `null` at the call site. */
|
|
181
|
-
function readPreviewMeta(previewRootAbs) {
|
|
182
|
-
const path = join(previewRootAbs, PREVIEW_META_FILE);
|
|
183
|
-
return tryFs("cardBuilders.readPreviewMeta", async () => JSON.parse(await readFile(path, "utf8")), { path, onAbsent: () => null });
|
|
184
|
-
}
|
|
185
|
-
/** Directory names (not files) directly under `dir`, or `[]` when absent. */
|
|
186
|
-
function subdirs(dir) {
|
|
187
|
-
return tryFs("cardBuilders.subdirs", async () => (await readdir(dir, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name), {
|
|
188
|
-
path: dir,
|
|
189
|
-
onAbsent: () => [],
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
/** The most-recently-modified previewId under an analysis, or null when none. */
|
|
193
|
-
function latestPreviewId(previewsRootAbs) {
|
|
194
|
-
return subdirs(previewsRootAbs).map(async (dirs) => {
|
|
195
|
-
if (dirs.length <= 1)
|
|
196
|
-
return dirs[0] ?? null;
|
|
197
|
-
let best = null;
|
|
198
|
-
for (const name of dirs) {
|
|
199
|
-
const path = join(previewsRootAbs, name);
|
|
200
|
-
const mtimeMs = await tryFs("cardBuilders.latestPreviewId.stat", async () => (await stat(path)).mtimeMs, { path }).unwrapOr(null); // skip an unreadable/absent entry
|
|
201
|
-
if (mtimeMs !== null && (!best || mtimeMs > best.mtimeMs)) {
|
|
202
|
-
best = { name, mtimeMs };
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
return best?.name ?? dirs[0];
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
/**
|
|
209
|
-
* Build the `data-report-preview` payload for an `iterate_report` tool_use. The
|
|
210
|
-
* preview's source of truth is the filesystem under
|
|
211
|
-
* `previews/{previewId}/`: directory layout yields `version`
|
|
212
|
-
* (latest `vN`) + `previewPath`, and `preview-meta.json` yields title/format.
|
|
213
|
-
* Mirrors `buildRunCardData` — the live emit knows the server-generated
|
|
214
|
-
* `previewId`, while the reconstruct-on-read path resolves it (from the
|
|
215
|
-
* iteration's `previewId` input, or else the analysis's latest preview). The
|
|
216
|
-
* card is keyed by `previewId` only, so every iteration's tool_use rebuilds
|
|
217
|
-
* the same card and the frontend groups them into one with all versions selectable.
|
|
218
|
-
* Null when nothing renders — the caller falls back to a chip.
|
|
219
|
-
*/
|
|
220
|
-
export async function buildPreviewCardData(
|
|
221
|
-
/** Absolute host root of the analysis's workspace tree (previews live inside it). */
|
|
222
|
-
workspaceRoot, opts) {
|
|
223
|
-
const previewsRootAbs = join(workspaceRoot, PREVIEWS_ROOT);
|
|
224
|
-
const resolveId = opts.previewId ? okAsync(opts.previewId) : latestPreviewId(previewsRootAbs);
|
|
225
|
-
return resolveId
|
|
226
|
-
.andThen((previewId) => {
|
|
227
|
-
if (!previewId)
|
|
228
|
-
return okAsync(null);
|
|
229
|
-
const previewRootAbs = join(workspaceRoot, previewDir(previewId));
|
|
230
|
-
return subdirs(previewRootAbs).andThen((versionDirs) => {
|
|
231
|
-
const version = latestPreviewVersion(versionDirs);
|
|
232
|
-
if (version === 0)
|
|
233
|
-
return okAsync(null);
|
|
234
|
-
return readPreviewMeta(previewRootAbs).map((meta) => {
|
|
235
|
-
const id = `prev-${createHash("sha256").update(previewId).digest("hex").slice(0, 16)}`;
|
|
236
|
-
return {
|
|
237
|
-
id,
|
|
238
|
-
previewId,
|
|
239
|
-
version,
|
|
240
|
-
title: opts.title ?? meta?.title ?? "Report",
|
|
241
|
-
previewPath: `v${version}/index.html`,
|
|
242
|
-
format: opts.format ?? (meta?.format === "pdf" ? "pdf" : "html"),
|
|
243
|
-
};
|
|
244
|
-
});
|
|
245
|
-
});
|
|
246
|
-
})
|
|
247
|
-
.unwrapOr(null);
|
|
248
|
-
}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import type { Pool } from "pg";
|
|
2
2
|
import type { Tool } from "../tools/define-tool.js";
|
|
3
|
-
import type { ResolveWorkspaceRoot } from "../workspace/paths.js";
|
|
4
3
|
export interface ConversationDisplayBackfillDeps {
|
|
5
4
|
readonly pool: Pool;
|
|
6
|
-
readonly resolveWorkspaceRoot: ResolveWorkspaceRoot;
|
|
7
5
|
/**
|
|
8
6
|
* The conversation agent's composed roster, used to recover each legacy call's
|
|
9
7
|
* one-line detail from its persisted input. Tools contributed through the
|
|
@@ -17,8 +15,8 @@ export interface ConversationDisplayBackfillDeps {
|
|
|
17
15
|
* Freeze the migration renderer's output into display envelopes, once per legacy
|
|
18
16
|
* turn, so the runtime read path never has to reconstruct anything.
|
|
19
17
|
*
|
|
20
|
-
* Missing mutable resources degrade to a generic, cardless display — a
|
|
21
|
-
* that
|
|
18
|
+
* Missing mutable resources degrade to a generic, cardless display — a plan
|
|
19
|
+
* that is absent is normal historical absence, not a fault. Malformed stored rows
|
|
22
20
|
* and database faults remain startup-fatal: a turn that cannot be migrated must
|
|
23
21
|
* not be silently skipped and then read as if it had no display.
|
|
24
22
|
*/
|
|
@@ -10,8 +10,8 @@ const GENUINE_USER_SQL = `m.message_envelope->'message'->>'role' = 'user'
|
|
|
10
10
|
* Freeze the migration renderer's output into display envelopes, once per legacy
|
|
11
11
|
* turn, so the runtime read path never has to reconstruct anything.
|
|
12
12
|
*
|
|
13
|
-
* Missing mutable resources degrade to a generic, cardless display — a
|
|
14
|
-
* that
|
|
13
|
+
* Missing mutable resources degrade to a generic, cardless display — a plan
|
|
14
|
+
* that is absent is normal historical absence, not a fault. Malformed stored rows
|
|
15
15
|
* and database faults remain startup-fatal: a turn that cannot be migrated must
|
|
16
16
|
* not be silently skipped and then read as if it had no display.
|
|
17
17
|
*/
|
|
@@ -64,13 +64,7 @@ export async function backfillConversationDisplayEnvelopes(deps) {
|
|
|
64
64
|
});
|
|
65
65
|
let resolveCard;
|
|
66
66
|
if (head.analysis_id !== null) {
|
|
67
|
-
|
|
68
|
-
resolveCard = createCardResolver(deps.pool, head.analysis_id, deps.resolveWorkspaceRoot(head.analysis_id));
|
|
69
|
-
}
|
|
70
|
-
catch {
|
|
71
|
-
// A removed/moved workspace is normal historical absence.
|
|
72
|
-
resolveCard = undefined;
|
|
73
|
-
}
|
|
67
|
+
resolveCard = createCardResolver(deps.pool, head.analysis_id);
|
|
74
68
|
}
|
|
75
69
|
const cortex = await contentToCortexMessages(stored, { resolveCard, resolveDetail });
|
|
76
70
|
const uiMessages = cortexMessagesToConversationUI(cortex);
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* reconstruction.
|
|
16
16
|
*/
|
|
17
17
|
import { type DataUIPart, type UIMessage } from "ai";
|
|
18
|
-
import type { AskPart, FileReferencePart, PlanPart, PresentationPart,
|
|
18
|
+
import type { AskPart, ChildSessionStartedPart, FileReferencePart, PlanPart, PresentationPart, ReportRenderedPart, RunCardPart } from "../contracts/chat-parts.js";
|
|
19
19
|
import type { ToolCallOutcome } from "../contracts/chat-events.js";
|
|
20
20
|
import type { CortexMessage, CortexPart, ToolCallPart } from "../contracts/message.js";
|
|
21
21
|
export declare const SUPPORTED_DISPLAY_SCHEMA_VERSION = 1;
|
|
@@ -38,8 +38,8 @@ export type ConversationUIData = {
|
|
|
38
38
|
"run-card": Payload<RunCardPart>;
|
|
39
39
|
"file-reference": Payload<FileReferencePart>;
|
|
40
40
|
ask: Payload<AskPart>;
|
|
41
|
-
"
|
|
42
|
-
"report-
|
|
41
|
+
"child-session-started": Payload<ChildSessionStartedPart>;
|
|
42
|
+
"report-rendered": Payload<ReportRenderedPart>;
|
|
43
43
|
};
|
|
44
44
|
export interface ConversationDisplayMetadata {
|
|
45
45
|
interrupted?: boolean;
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { validateUIMessages } from "ai";
|
|
18
18
|
import { z } from "zod";
|
|
19
|
-
import { AskPartSchema,
|
|
19
|
+
import { AskPartSchema, ChildSessionStartedPartSchema, FileReferencePartSchema, PlanPartSchema, PresentationPartSchema, ReportRenderedPartSchema, RunCardPartSchema, } from "../contracts/schemas/chat-parts.js";
|
|
20
20
|
import { ToolCallOutcomeSchema } from "../contracts/schemas/chat-events.js";
|
|
21
21
|
export const SUPPORTED_DISPLAY_SCHEMA_VERSION = 1;
|
|
22
22
|
const DisplayEnvelopeHeaderSchema = z.object({
|
|
@@ -54,8 +54,8 @@ const dataSchemas = {
|
|
|
54
54
|
"run-card": RunCardPartSchema.omit({ type: true }).strict(),
|
|
55
55
|
"file-reference": FileReferencePartSchema.omit({ type: true }).strict(),
|
|
56
56
|
ask: AskPartSchema.omit({ type: true }).strict(),
|
|
57
|
-
"
|
|
58
|
-
"report-
|
|
57
|
+
"child-session-started": ChildSessionStartedPartSchema.omit({ type: true }).strict(),
|
|
58
|
+
"report-rendered": ReportRenderedPartSchema.omit({ type: true }).strict(),
|
|
59
59
|
};
|
|
60
60
|
export function envelopeDisplayMessages(messages) {
|
|
61
61
|
if (messages.length === 0)
|
|
@@ -4,15 +4,13 @@
|
|
|
4
4
|
* through the pre-v1 direct-upgrade window, then remove its runtime fallback
|
|
5
5
|
* in a dedicated compatibility change.
|
|
6
6
|
*
|
|
7
|
-
* `show_plan` / `show_user` / `show_file` / `execute_analysis`
|
|
8
|
-
* `data-plan` / `data-presentation` / `data-file-reference` / `data-run-card`
|
|
9
|
-
* `data-report-preview` cards
|
|
7
|
+
* `show_plan` / `show_user` / `show_file` / `execute_analysis` emit
|
|
8
|
+
* `data-plan` / `data-presentation` / `data-file-reference` / `data-run-card` cards
|
|
10
9
|
* live over the chat SSE stream. On reload, `content-to-cortex` calls this
|
|
11
10
|
* resolver for each AI SDK tool-call part: a recognised display tool yields its card (rebuilt via the
|
|
12
11
|
* shared `card-builders`); anything else yields `null` and falls back to a
|
|
13
|
-
* generic `tool-call` chip. Keyed by the analysis `Pool` + id (plan / run)
|
|
14
|
-
* the
|
|
15
|
-
* be re-loaded.
|
|
12
|
+
* generic `tool-call` chip. Keyed by the analysis `Pool` + id (plan / run) so
|
|
13
|
+
* the card can be re-loaded.
|
|
16
14
|
*/
|
|
17
15
|
import type { CortexPart } from "@inflexa-ai/harness/contracts/message.js";
|
|
18
16
|
import type { Pool } from "pg";
|
|
@@ -26,4 +24,4 @@ export interface StoredToolCallForCard {
|
|
|
26
24
|
/** @deprecated Used only by the legacy display backfill/fallback. */
|
|
27
25
|
export type ToolCardResolver = (block: StoredToolCallForCard) => Promise<CortexPart | null>;
|
|
28
26
|
/** @deprecated Use persisted conversation display envelopes for runtime reads. */
|
|
29
|
-
export declare function createCardResolver(pool: Pool, analysisId: string
|
|
27
|
+
export declare function createCardResolver(pool: Pool, analysisId: string): ToolCardResolver;
|
|
@@ -4,38 +4,24 @@
|
|
|
4
4
|
* through the pre-v1 direct-upgrade window, then remove its runtime fallback
|
|
5
5
|
* in a dedicated compatibility change.
|
|
6
6
|
*
|
|
7
|
-
* `show_plan` / `show_user` / `show_file` / `execute_analysis`
|
|
8
|
-
* `data-plan` / `data-presentation` / `data-file-reference` / `data-run-card`
|
|
9
|
-
* `data-report-preview` cards
|
|
7
|
+
* `show_plan` / `show_user` / `show_file` / `execute_analysis` emit
|
|
8
|
+
* `data-plan` / `data-presentation` / `data-file-reference` / `data-run-card` cards
|
|
10
9
|
* live over the chat SSE stream. On reload, `content-to-cortex` calls this
|
|
11
10
|
* resolver for each AI SDK tool-call part: a recognised display tool yields its card (rebuilt via the
|
|
12
11
|
* shared `card-builders`); anything else yields `null` and falls back to a
|
|
13
|
-
* generic `tool-call` chip. Keyed by the analysis `Pool` + id (plan / run)
|
|
14
|
-
* the
|
|
15
|
-
* be re-loaded.
|
|
12
|
+
* generic `tool-call` chip. Keyed by the analysis `Pool` + id (plan / run) so
|
|
13
|
+
* the card can be re-loaded.
|
|
16
14
|
*/
|
|
17
15
|
import { unwrapOrThrow } from "../lib/result.js";
|
|
18
16
|
import { adHocPlanId, adHocRunId } from "../tools/analysis-invocation.js";
|
|
19
17
|
import { validatePath } from "../tools/lib/path-validation.js";
|
|
20
|
-
import { buildFileReferenceCardData, buildPlanCardData, buildPresentationCardData,
|
|
18
|
+
import { buildFileReferenceCardData, buildPlanCardData, buildPresentationCardData, buildRunCardData } from "./card-builders.js";
|
|
21
19
|
/** @deprecated Use persisted conversation display envelopes for runtime reads. */
|
|
22
|
-
export function createCardResolver(pool, analysisId
|
|
20
|
+
export function createCardResolver(pool, analysisId) {
|
|
23
21
|
return async (block) => {
|
|
24
22
|
if (block.type !== "tool_use")
|
|
25
23
|
return null;
|
|
26
24
|
const input = (block.input ?? {});
|
|
27
|
-
// The report tool is `submit_report`; legacy transcripts carry the
|
|
28
|
-
// former ids `iterate_report` / `iterateReport` — accept all three. The
|
|
29
|
-
// brief-carrying field is still `report`, so the card reads identically.
|
|
30
|
-
if (block.name === "submit_report" || block.name === "iterate_report" || block.name === "iterateReport") {
|
|
31
|
-
const report = (input.report ?? null);
|
|
32
|
-
const card = await buildPreviewCardData(workspaceRoot, {
|
|
33
|
-
previewId: typeof input.previewId === "string" ? input.previewId : undefined,
|
|
34
|
-
title: report && typeof report.title === "string" ? report.title : undefined,
|
|
35
|
-
format: input.format === "pdf" || input.format === "html" ? input.format : undefined,
|
|
36
|
-
});
|
|
37
|
-
return card ? { type: "data-report-preview", ...card } : null;
|
|
38
|
-
}
|
|
39
25
|
if (block.name === "show_user") {
|
|
40
26
|
// The live tool rejects a malformed/traversal echart `dataPath` and emits nothing; the
|
|
41
27
|
// persisted tool_use still carries it, so the reload path MUST re-validate before building
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transcript integrity for tool calls.
|
|
3
|
+
*
|
|
4
|
+
* The wire contract of every provider is that each tool call in an assistant
|
|
5
|
+
* message has a matching tool result. A transcript that violates it is refused
|
|
6
|
+
* WHOLE — the AI SDK throws at prompt conversion, and the Anthropic API answers
|
|
7
|
+
* 400 — so a single unanswered call makes a thread unable to take any further
|
|
8
|
+
* turn. An unanswered call is a reachable state, not a hypothetical: a reply
|
|
9
|
+
* can carry a complete tool call beside ANY finish reason, because the call
|
|
10
|
+
* streams before the stop reason arrives, and only a `tool-calls` (and, for its
|
|
11
|
+
* leading calls, a `length`) finish dispatches it.
|
|
12
|
+
*
|
|
13
|
+
* The repair is a STRIP, never an invented result. A call that was not
|
|
14
|
+
* dispatched did not happen, and a fabricated result would record an execution
|
|
15
|
+
* that did not occur — the model would read it as ground truth on the next
|
|
16
|
+
* turn. The prose of the message survives the strip; a message left with
|
|
17
|
+
* nothing the wire can render disappears entirely.
|
|
18
|
+
*/
|
|
19
|
+
import type { ModelMessage } from "ai";
|
|
20
|
+
/** One tool call removed by {@link stripUnansweredToolCalls}, identified for the caller's diagnostic record. */
|
|
21
|
+
export interface DroppedToolCall {
|
|
22
|
+
readonly toolCallId: string;
|
|
23
|
+
readonly toolName: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Remove, in place, every tool-call part at or past `fromIndex` that has no
|
|
27
|
+
* matching tool result anywhere in `messages`. Returns the removed calls in
|
|
28
|
+
* transcript order — empty on the healthy path — so the caller can log what
|
|
29
|
+
* a reader of the stored thread would otherwise never learn.
|
|
30
|
+
*
|
|
31
|
+
* Results are matched by id across the whole array, not by adjacency: the loop
|
|
32
|
+
* appends a round's results directly after its assistant message, but a
|
|
33
|
+
* deferred-image user message can ride between rounds, and the stored shape is
|
|
34
|
+
* not this function's to assume.
|
|
35
|
+
*
|
|
36
|
+
* A message that keeps a text or file part survives the strip — refusal prose
|
|
37
|
+
* is the one account the reader has of a filtered reply. A message left with
|
|
38
|
+
* no such part (a call-only message, or reasoning beside the removed call) is
|
|
39
|
+
* removed whole: reasoning without the output it preceded is not replayable,
|
|
40
|
+
* and an empty assistant message is itself a wire violation.
|
|
41
|
+
*
|
|
42
|
+
* `fromIndex` bounds the REPAIR, not the result scan. The loop passes its
|
|
43
|
+
* `initial.length` so it never rewrites the caller's prefix; the assembly
|
|
44
|
+
* repair passes nothing and covers the whole stored window.
|
|
45
|
+
*/
|
|
46
|
+
export declare function stripUnansweredToolCalls(messages: ModelMessage[], fromIndex?: number): DroppedToolCall[];
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transcript integrity for tool calls.
|
|
3
|
+
*
|
|
4
|
+
* The wire contract of every provider is that each tool call in an assistant
|
|
5
|
+
* message has a matching tool result. A transcript that violates it is refused
|
|
6
|
+
* WHOLE — the AI SDK throws at prompt conversion, and the Anthropic API answers
|
|
7
|
+
* 400 — so a single unanswered call makes a thread unable to take any further
|
|
8
|
+
* turn. An unanswered call is a reachable state, not a hypothetical: a reply
|
|
9
|
+
* can carry a complete tool call beside ANY finish reason, because the call
|
|
10
|
+
* streams before the stop reason arrives, and only a `tool-calls` (and, for its
|
|
11
|
+
* leading calls, a `length`) finish dispatches it.
|
|
12
|
+
*
|
|
13
|
+
* The repair is a STRIP, never an invented result. A call that was not
|
|
14
|
+
* dispatched did not happen, and a fabricated result would record an execution
|
|
15
|
+
* that did not occur — the model would read it as ground truth on the next
|
|
16
|
+
* turn. The prose of the message survives the strip; a message left with
|
|
17
|
+
* nothing the wire can render disappears entirely.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Whether this tool call needs a client-supplied result. A provider-executed
|
|
21
|
+
* call is answered by the provider inside the same assistant message, so the
|
|
22
|
+
* wire contract makes no demand on the client for it — the AI SDK's own
|
|
23
|
+
* missing-result validation skips these, and so does the strip.
|
|
24
|
+
*/
|
|
25
|
+
function needsClientResult(part) {
|
|
26
|
+
return part.providerExecuted !== true;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Remove, in place, every tool-call part at or past `fromIndex` that has no
|
|
30
|
+
* matching tool result anywhere in `messages`. Returns the removed calls in
|
|
31
|
+
* transcript order — empty on the healthy path — so the caller can log what
|
|
32
|
+
* a reader of the stored thread would otherwise never learn.
|
|
33
|
+
*
|
|
34
|
+
* Results are matched by id across the whole array, not by adjacency: the loop
|
|
35
|
+
* appends a round's results directly after its assistant message, but a
|
|
36
|
+
* deferred-image user message can ride between rounds, and the stored shape is
|
|
37
|
+
* not this function's to assume.
|
|
38
|
+
*
|
|
39
|
+
* A message that keeps a text or file part survives the strip — refusal prose
|
|
40
|
+
* is the one account the reader has of a filtered reply. A message left with
|
|
41
|
+
* no such part (a call-only message, or reasoning beside the removed call) is
|
|
42
|
+
* removed whole: reasoning without the output it preceded is not replayable,
|
|
43
|
+
* and an empty assistant message is itself a wire violation.
|
|
44
|
+
*
|
|
45
|
+
* `fromIndex` bounds the REPAIR, not the result scan. The loop passes its
|
|
46
|
+
* `initial.length` so it never rewrites the caller's prefix; the assembly
|
|
47
|
+
* repair passes nothing and covers the whole stored window.
|
|
48
|
+
*/
|
|
49
|
+
export function stripUnansweredToolCalls(messages, fromIndex = 0) {
|
|
50
|
+
const resultIds = new Set();
|
|
51
|
+
for (const message of messages) {
|
|
52
|
+
if (message.role === "tool") {
|
|
53
|
+
for (const part of message.content) {
|
|
54
|
+
if (part.type === "tool-result")
|
|
55
|
+
resultIds.add(part.toolCallId);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else if (message.role === "assistant" && typeof message.content !== "string") {
|
|
59
|
+
for (const part of message.content) {
|
|
60
|
+
if (part.type === "tool-result")
|
|
61
|
+
resultIds.add(part.toolCallId);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const dropped = [];
|
|
66
|
+
// Backward, so a whole-message removal cannot shift an index this loop has
|
|
67
|
+
// yet to visit.
|
|
68
|
+
for (let idx = messages.length - 1; idx >= fromIndex; idx--) {
|
|
69
|
+
const message = messages[idx];
|
|
70
|
+
if (message.role !== "assistant" || typeof message.content === "string")
|
|
71
|
+
continue;
|
|
72
|
+
const removed = [];
|
|
73
|
+
for (const part of message.content) {
|
|
74
|
+
if (part.type === "tool-call" && needsClientResult(part) && !resultIds.has(part.toolCallId))
|
|
75
|
+
removed.push(part);
|
|
76
|
+
}
|
|
77
|
+
if (removed.length === 0)
|
|
78
|
+
continue;
|
|
79
|
+
// Prepended as a batch: the walk visits messages tail-first, but the
|
|
80
|
+
// caller's record reads in transcript order.
|
|
81
|
+
dropped.unshift(...removed.map((part) => ({ toolCallId: part.toolCallId, toolName: part.toolName })));
|
|
82
|
+
const removedParts = new Set(removed);
|
|
83
|
+
const kept = message.content.filter((part) => !removedParts.has(part));
|
|
84
|
+
if (kept.some((part) => part.type === "text" || part.type === "file" || part.type === "tool-call" || part.type === "tool-result")) {
|
|
85
|
+
messages[idx] = { ...message, content: kept };
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
messages.splice(idx, 1);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return dropped;
|
|
92
|
+
}
|
package/dist/runtime/boot.js
CHANGED
|
@@ -38,7 +38,6 @@
|
|
|
38
38
|
*/
|
|
39
39
|
import { unwrapOrThrow } from "../lib/result.js";
|
|
40
40
|
import { SANDBOX_AGENT_META } from "../agents/sandbox/index.js";
|
|
41
|
-
import { REPORT_BUILDER_AGENT_ID, REPORT_BUILDER_SKILLS } from "../agents/report-builder.js";
|
|
42
41
|
import { validateAgentSkills } from "../agents/sandbox/validate-skills.js";
|
|
43
42
|
import { initCortexState } from "../state/init.js";
|
|
44
43
|
import { backfillConversationDisplayEnvelopes } from "../memory/conversation-display-backfill.js";
|
|
@@ -49,16 +48,8 @@ import { markDraining } from "./lifecycle.js";
|
|
|
49
48
|
import { runShutdownSequence } from "./shutdown.js";
|
|
50
49
|
const noop = () => { };
|
|
51
50
|
const noopAsync = () => Promise.resolve();
|
|
52
|
-
/**
|
|
53
|
-
|
|
54
|
-
* report-builder is not in the sandbox catalog — it is not plannable — but it
|
|
55
|
-
* declares a pack all the same, and a missing one there surfaces as a failed
|
|
56
|
-
* render partway through a report rather than at boot.
|
|
57
|
-
*/
|
|
58
|
-
const SKILL_DECLARING_AGENTS = {
|
|
59
|
-
...SANDBOX_AGENT_META,
|
|
60
|
-
[REPORT_BUILDER_AGENT_ID]: { id: REPORT_BUILDER_AGENT_ID, skills: REPORT_BUILDER_SKILLS },
|
|
61
|
-
};
|
|
51
|
+
/** Every agent whose declared packs must resolve before the first run. */
|
|
52
|
+
const SKILL_DECLARING_AGENTS = SANDBOX_AGENT_META;
|
|
62
53
|
export async function bootHarness(deps) {
|
|
63
54
|
const { core, pool, skillsDir, logger } = deps;
|
|
64
55
|
(deps.initTelemetry ?? noop)();
|
|
@@ -73,7 +64,6 @@ export async function bootHarness(deps) {
|
|
|
73
64
|
const conversationAgent = unwrapOrThrow(runtime.agents.forThread("conversation"));
|
|
74
65
|
await backfillConversationDisplayEnvelopes({
|
|
75
66
|
pool,
|
|
76
|
-
resolveWorkspaceRoot: core.conversation.resolveWorkspaceRoot,
|
|
77
67
|
tools: conversationAgent.tools,
|
|
78
68
|
});
|
|
79
69
|
await deps.beforeLaunch?.();
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
* render problem, a figure that escapes the workspace root, a write failure, and a stamp failure. The tool
|
|
29
29
|
* never throws for one of them. When the page
|
|
30
30
|
* lands, the tool stamps the hash of the rendered draft on the session state, thus the eyes and the record
|
|
31
|
-
* know which draft the page shows.
|
|
31
|
+
* know which draft the page shows. It then emits one durable `data-report-rendered` part, thus the
|
|
32
|
+
* transcript places the render and a live client learns that a fresh page exists. The filesystem speaks the
|
|
32
33
|
* throw protocol, and the workspace-root seam signals an unresolvable resource the same way. Thus the
|
|
33
34
|
* write runs through the `tryFs` glue, which turns a genuine fault into the ok-channel outcome and lets a
|
|
34
35
|
* control-flow exception propagate.
|
|
@@ -28,12 +28,14 @@
|
|
|
28
28
|
* render problem, a figure that escapes the workspace root, a write failure, and a stamp failure. The tool
|
|
29
29
|
* never throws for one of them. When the page
|
|
30
30
|
* lands, the tool stamps the hash of the rendered draft on the session state, thus the eyes and the record
|
|
31
|
-
* know which draft the page shows.
|
|
31
|
+
* know which draft the page shows. It then emits one durable `data-report-rendered` part, thus the
|
|
32
|
+
* transcript places the render and a live client learns that a fresh page exists. The filesystem speaks the
|
|
32
33
|
* throw protocol, and the workspace-root seam signals an unresolvable resource the same way. Thus the
|
|
33
34
|
* write runs through the `tryFs` glue, which turns a genuine fault into the ok-channel outcome and lets a
|
|
34
35
|
* control-flow exception propagate.
|
|
35
36
|
*/
|
|
36
37
|
import { err, ok } from "neverthrow";
|
|
38
|
+
import { randomUUID } from "node:crypto";
|
|
37
39
|
import { copyFile, mkdir, readdir, rm, writeFile } from "node:fs/promises";
|
|
38
40
|
import { extname, join } from "node:path";
|
|
39
41
|
import { relative as relativePosix } from "node:path/posix";
|
|
@@ -475,6 +477,12 @@ export function createPreviewReportTool(deps) {
|
|
|
475
477
|
logger.warn("the rendered hash did not stamp", { threadId, analysisId, detail });
|
|
476
478
|
return ok({ outcome: "stamp-failed", pagePath: written.value, detail });
|
|
477
479
|
}
|
|
480
|
+
// The part rides the rendered arm only. Each degraded arm shows no
|
|
481
|
+
// fresh page, thus it emits nothing.
|
|
482
|
+
await ctx.emit({
|
|
483
|
+
type: "data-report-rendered",
|
|
484
|
+
data: { id: randomUUID(), renderedAt: new Date().toISOString(), title: document.title },
|
|
485
|
+
});
|
|
478
486
|
const access = await mintAccess(deps.makeSessionPages, analysisId, threadId, ctx.session.auth, logger);
|
|
479
487
|
return ok({ outcome: "rendered", pagePath: written.value, ...(access !== undefined ? { access } : {}) });
|
|
480
488
|
},
|