@inflexa-ai/harness 0.22.0 → 0.23.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/report-session-agent.d.ts +10 -7
- package/dist/agents/report-session-agent.js +2 -2
- 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 +17 -1
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/part-registry.js +1 -0
- package/dist/contracts/schemas/chat-parts.d.ts +9 -0
- package/dist/contracts/schemas/chat-parts.js +7 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4 -2
- package/dist/loop/run-agent.js +40 -1
- package/dist/memory/conversation-display-storage.d.ts +2 -1
- package/dist/memory/conversation-display-storage.js +2 -1
- package/dist/memory/tool-call-integrity.d.ts +46 -0
- package/dist/memory/tool-call-integrity.js +92 -0
- package/dist/runtime/assemble.d.ts +10 -7
- package/dist/runtime/assemble.js +1 -1
- package/dist/tools/report-session/examine-page.d.ts +4 -0
- package/dist/tools/report-session/examine-page.js +4 -1
- package/dist/tools/report-session/index.d.ts +1 -1
- package/dist/tools/report-session/preview-report.d.ts +13 -10
- package/dist/tools/report-session/preview-report.js +16 -13
- package/dist/tools/report-session/session-page-publisher.d.ts +21 -8
- package/dist/tools/report-session/session-page-publisher.js +7 -4
- package/dist/tools/start-report-session.js +17 -7
- package/package.json +1 -1
|
@@ -38,7 +38,7 @@ import type { ThreadStore } from "../memory/thread-store.js";
|
|
|
38
38
|
import type { ReferenceResolver } from "../report-model/reference-resolver.js";
|
|
39
39
|
import type { ReportVersionStore } from "../state/report-versions.js";
|
|
40
40
|
import type { ReportSessionStateGateway } from "../tools/report-authoring/authoring-tools.js";
|
|
41
|
-
import { type
|
|
41
|
+
import { type MakeSessionPagePublisher, type ResolvePageAsset, type ResolvePageUrl } from "../tools/report-session/index.js";
|
|
42
42
|
/** Canonical agent id -- distinct from the id of the old templating agent. */
|
|
43
43
|
export declare const REPORT_SESSION_AGENT_ID: "report-session";
|
|
44
44
|
/**
|
|
@@ -99,15 +99,18 @@ export interface ReportSessionAgentDeps {
|
|
|
99
99
|
*/
|
|
100
100
|
readonly resolvePageAsset?: ResolvePageAsset;
|
|
101
101
|
/**
|
|
102
|
-
* Session-page publisher -- the hosted view of the rendered page.
|
|
103
|
-
*
|
|
104
|
-
*
|
|
102
|
+
* Session-page publisher factory -- the hosted view of the rendered page. The factory
|
|
103
|
+
* binds one analysis and the auth of the call, like `makeResolver`, thus the mint runs
|
|
104
|
+
* under the credential of the caller. Bound, the preview tool builds the publisher over
|
|
105
|
+
* the scope of the call, mints one grant after the page lands, and attaches the URL
|
|
106
|
+
* beside the path. Omitted, the result carries the page path alone.
|
|
105
107
|
*/
|
|
106
|
-
readonly
|
|
108
|
+
readonly makeSessionPages?: MakeSessionPagePublisher;
|
|
107
109
|
/**
|
|
108
110
|
* Page-URL formation of the eyes -- the URL that one look opens. A composition whose
|
|
109
|
-
* browser cannot reach the workspace tree binds a resolver that names a served URL
|
|
110
|
-
*
|
|
111
|
+
* browser cannot reach the workspace tree binds a resolver that names a served URL, and
|
|
112
|
+
* the args carry the auth of the tool call beside the page identity. Omitted, the eyes
|
|
113
|
+
* tool navigates through a `file://` URL of the page path.
|
|
111
114
|
*/
|
|
112
115
|
readonly resolvePageUrl?: ResolvePageUrl;
|
|
113
116
|
/** Operational logging seam; omitted falls back to no-op. */
|
|
@@ -39,7 +39,7 @@ const REPORT_SESSION_MAX_ITERATIONS = 75;
|
|
|
39
39
|
/** Build the report `AgentDefinition` with every tool bound to its deps. */
|
|
40
40
|
export function createReportSessionAgent(deps) {
|
|
41
41
|
const { model, pool, embedding, workspaceFs, gateway, resolveWorkspaceRoot, store, threads, chrome, eyes, makeResolver, resolvePageAsset, logger } = deps;
|
|
42
|
-
const { derivations, sandboxClient, runAuthorizer,
|
|
42
|
+
const { derivations, sandboxClient, runAuthorizer, makeSessionPages, resolvePageUrl } = deps;
|
|
43
43
|
const authoring = createReportAuthoringTools(gateway);
|
|
44
44
|
const tools = [
|
|
45
45
|
// The read surface toward the analysis. It roams the tree read-only.
|
|
@@ -83,7 +83,7 @@ export function createReportSessionAgent(deps) {
|
|
|
83
83
|
resolveWorkspaceRoot,
|
|
84
84
|
...(makeResolver ? { makeResolver } : {}),
|
|
85
85
|
...(resolvePageAsset ? { resolvePageAsset } : {}),
|
|
86
|
-
...(
|
|
86
|
+
...(makeSessionPages ? { makeSessionPages } : {}),
|
|
87
87
|
...(logger ? { logger } : {}),
|
|
88
88
|
}),
|
|
89
89
|
// The eyes tool. It opens the rendered page in headless Chrome.
|
package/dist/app/chat-turn.js
CHANGED
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
* thread store.
|
|
36
36
|
*/
|
|
37
37
|
import type { ModelMessage } from "ai";
|
|
38
|
+
import type { Logger } from "../lib/logger.js";
|
|
38
39
|
import type { LoopMessage } from "../loop/types.js";
|
|
39
40
|
import type { ThreadHistory } from "../memory/thread-history.js";
|
|
40
41
|
import type { ThreadType } from "../memory/thread-store.js";
|
|
@@ -64,6 +65,8 @@ export interface AssembleMessagesArgs {
|
|
|
64
65
|
readonly workingMemory: WorkingMemoryStore;
|
|
65
66
|
/** History-window token budget. Defaults to {@link DEFAULT_HISTORY_TOKEN_BUDGET}. */
|
|
66
67
|
readonly tokenBudget?: number;
|
|
68
|
+
/** Diagnostic seam for the history repair record; omitted falls back to no-op. */
|
|
69
|
+
readonly logger?: Logger;
|
|
67
70
|
}
|
|
68
71
|
export interface AssembledMessages {
|
|
69
72
|
/** The full message array for `runAgent`. */
|
|
@@ -34,7 +34,9 @@
|
|
|
34
34
|
* tail injections (ephemeral, re-derived each turn) are never written to the
|
|
35
35
|
* thread store.
|
|
36
36
|
*/
|
|
37
|
+
import { createNoopLogger } from "../lib/console-logger.js";
|
|
37
38
|
import { unwrapOrThrow } from "../lib/result.js";
|
|
39
|
+
import { stripUnansweredToolCalls } from "../memory/tool-call-integrity.js";
|
|
38
40
|
import { normalizeUnicode, redactSecrets } from "../input-sanitization.js";
|
|
39
41
|
/**
|
|
40
42
|
* Token budget for the `loadRecent` history window. Sized to leave headroom
|
|
@@ -49,6 +51,21 @@ export const DEFAULT_HISTORY_TOKEN_BUDGET = 120_000;
|
|
|
49
51
|
export async function assembleMessages(args) {
|
|
50
52
|
const budget = args.tokenBudget ?? DEFAULT_HISTORY_TOKEN_BUDGET;
|
|
51
53
|
const history = unwrapOrThrow(await args.history.loadRecent(args.threadId, budget, { keepFirstTurn: args.threadType === "report" }));
|
|
54
|
+
// Read-side safety net for the wire contract: every tool call carries a
|
|
55
|
+
// result. The loop upholds it at every exit, but the store outlives any one
|
|
56
|
+
// writer — a row an older build wrote, or a hand-edited database, can carry
|
|
57
|
+
// an unanswered call, and the provider boundary then refuses the WHOLE
|
|
58
|
+
// transcript on every turn: the thread is wedged forever. Stripping the
|
|
59
|
+
// orphan here degrades one message instead. The warning is the only account
|
|
60
|
+
// a reader gets that the stored thread and the assembled one differ.
|
|
61
|
+
const repaired = stripUnansweredToolCalls(history);
|
|
62
|
+
if (repaired.length > 0) {
|
|
63
|
+
(args.logger ?? createNoopLogger()).named("assembly").warn("unanswered tool calls stripped from thread history", {
|
|
64
|
+
threadId: args.threadId,
|
|
65
|
+
toolCallIds: repaired.map((d) => d.toolCallId),
|
|
66
|
+
tools: repaired.map((d) => d.toolName),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
52
69
|
// Sanitization — applied once, here, to the new user input only.
|
|
53
70
|
const userMessage = {
|
|
54
71
|
role: "user",
|
|
@@ -359,4 +359,20 @@ export interface DataPreviewFailedPart {
|
|
|
359
359
|
/** Optional taxonomy of failure kinds. */
|
|
360
360
|
errorKind?: "render" | "submit" | "build" | "timeout" | "internal";
|
|
361
361
|
}
|
|
362
|
-
|
|
362
|
+
/**
|
|
363
|
+
* The record that a turn started a report session. It rides the chat stream
|
|
364
|
+
* live, and it persists into the turn at the position of the spawn.
|
|
365
|
+
*
|
|
366
|
+
* The part is a placement record and a freshness signal only. The thread
|
|
367
|
+
* store is the authority for the session: its existence, its title, and its
|
|
368
|
+
* archived state. When the thread of the part is archived or absent, a
|
|
369
|
+
* consumer renders nothing for it.
|
|
370
|
+
*/
|
|
371
|
+
export interface ReportSessionStartedPart {
|
|
372
|
+
type: "data-report-session-started";
|
|
373
|
+
/** The id of the report thread that the spawn made. */
|
|
374
|
+
threadId: string;
|
|
375
|
+
/** The id of the conversation thread that spawned it. */
|
|
376
|
+
parentThreadId: string;
|
|
377
|
+
}
|
|
378
|
+
export type CortexChatPart = PresentationPart | PlanPart | RunCardPart | FileReferencePart | AskPart | RunStartedPart | DagStatePart | StepActivityPart | StepFileTreePart | StepOutputPart | StepSummaryPart | StepUsagePart | StepBlockedPart | RunSynthesisPart | SynthesisProgressPart | RunCompletedPart | RunFailedPart | PreviewPart | DataPreviewFailedPart | ReportSessionStartedPart;
|
|
@@ -2,7 +2,7 @@ export { ORGAN_SYSTEMS, OrganSystemSchema, ORGAN_SYSTEM_LABELS, organSystemLabel
|
|
|
2
2
|
export type { OrganSystem } from "./organ-system.js";
|
|
3
3
|
export { SEVERITIES, SeveritySchema } from "./severity.js";
|
|
4
4
|
export type { Severity } from "./severity.js";
|
|
5
|
-
export type { PlanStep, PresentationContent, PresentationPart, PlanPart, RunCardPart, FileReferenceEntry, FileReferencePart, AskPart, RunStartedPart, StepStatus, DagStepState, DagStatePart, StepPhase, StepActivityPart, FileTreeEntry, StepFileTreePart, StepOutputFile, StepOutputPart, StepSummaryPart, StepUsagePart, StepBlockedPart, SynthesizedFinding, BiologicalTheme, RunSynthesisPart, SynthesisPhase, SynthesisProgressPart, RunCompletedFinding, RunCompletedPart, RunFailedPart, PreviewPart, DataPreviewFailedPart, CortexChatPart, } from "./chat-parts.js";
|
|
5
|
+
export type { PlanStep, PresentationContent, PresentationPart, PlanPart, RunCardPart, FileReferenceEntry, FileReferencePart, AskPart, RunStartedPart, StepStatus, DagStepState, DagStatePart, StepPhase, StepActivityPart, FileTreeEntry, StepFileTreePart, StepOutputFile, StepOutputPart, StepSummaryPart, StepUsagePart, StepBlockedPart, SynthesizedFinding, BiologicalTheme, RunSynthesisPart, SynthesisPhase, SynthesisProgressPart, RunCompletedFinding, RunCompletedPart, RunFailedPart, PreviewPart, DataPreviewFailedPart, ReportSessionStartedPart, CortexChatPart, } from "./chat-parts.js";
|
|
6
6
|
export type { EventSource, TextDeltaEvent, ToolStartedEvent, ToolFinishedEvent, FinishEvent, ChatErrorEvent, CortexChatEvent } from "./chat-events.js";
|
|
7
7
|
export type { TokenUsageRollup } from "./usage.js";
|
|
8
8
|
export { DATA_PROFILE_RUN_LITERAL } from "./data-profile.js";
|
|
@@ -18,6 +18,7 @@ export const PART_REGISTRY = {
|
|
|
18
18
|
"data-run-failed": { emitter: "workflow", consumer: "sidebar", transient: false, reconciling: false },
|
|
19
19
|
"data-report-preview": { emitter: "conversation", consumer: "conversation", transient: false, reconciling: false },
|
|
20
20
|
"data-report-preview-failed": { emitter: "conversation", consumer: "conversation", transient: false, reconciling: false },
|
|
21
|
+
"data-report-session-started": { emitter: "conversation", consumer: "conversation", transient: false, reconciling: false },
|
|
21
22
|
};
|
|
22
23
|
export function isTransient(type) {
|
|
23
24
|
return PART_REGISTRY[type].transient;
|
|
@@ -467,6 +467,11 @@ export declare const DataPreviewFailedPartSchema: z.ZodObject<{
|
|
|
467
467
|
internal: "internal";
|
|
468
468
|
}>>;
|
|
469
469
|
}, z.core.$strip>;
|
|
470
|
+
export declare const ReportSessionStartedPartSchema: z.ZodObject<{
|
|
471
|
+
type: z.ZodLiteral<"data-report-session-started">;
|
|
472
|
+
threadId: z.ZodString;
|
|
473
|
+
parentThreadId: z.ZodString;
|
|
474
|
+
}, z.core.$strip>;
|
|
470
475
|
export declare const CortexChatPartSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
471
476
|
type: z.ZodLiteral<"data-presentation">;
|
|
472
477
|
id: z.ZodString;
|
|
@@ -763,4 +768,8 @@ export declare const CortexChatPartSchema: z.ZodDiscriminatedUnion<[z.ZodObject<
|
|
|
763
768
|
build: "build";
|
|
764
769
|
internal: "internal";
|
|
765
770
|
}>>;
|
|
771
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
772
|
+
type: z.ZodLiteral<"data-report-session-started">;
|
|
773
|
+
threadId: z.ZodString;
|
|
774
|
+
parentThreadId: z.ZodString;
|
|
766
775
|
}, z.core.$strip>], "type">;
|
|
@@ -294,6 +294,12 @@ export const DataPreviewFailedPartSchema = z.object({
|
|
|
294
294
|
reason: z.string(),
|
|
295
295
|
errorKind: z.enum(["render", "submit", "build", "timeout", "internal"]).optional(),
|
|
296
296
|
});
|
|
297
|
+
// ── Report Session Started ─────────────────────────────────────────
|
|
298
|
+
export const ReportSessionStartedPartSchema = z.object({
|
|
299
|
+
type: z.literal("data-report-session-started"),
|
|
300
|
+
threadId: z.string(),
|
|
301
|
+
parentThreadId: z.string(),
|
|
302
|
+
});
|
|
297
303
|
// ── Union ───────────────────────────────────────────────────────────
|
|
298
304
|
export const CortexChatPartSchema = z.discriminatedUnion("type", [
|
|
299
305
|
PresentationPartSchema,
|
|
@@ -315,4 +321,5 @@ export const CortexChatPartSchema = z.discriminatedUnion("type", [
|
|
|
315
321
|
RunFailedPartSchema,
|
|
316
322
|
PreviewPartSchema,
|
|
317
323
|
DataPreviewFailedPartSchema,
|
|
324
|
+
ReportSessionStartedPartSchema,
|
|
318
325
|
]);
|
package/dist/index.d.ts
CHANGED
|
@@ -36,7 +36,7 @@ export type { ProvenanceCollector } from "./provenance/collector.js";
|
|
|
36
36
|
export { UnavailablePreviewPublisher } from "./tools/report/preview-publisher.js";
|
|
37
37
|
export type { PreviewPublisher, PreviewMintResult } from "./tools/report/preview-publisher.js";
|
|
38
38
|
export { UnavailableSessionPagePublisher } from "./tools/report-session/session-page-publisher.js";
|
|
39
|
-
export type { SessionPagePublisher, SessionPageMintResult } from "./tools/report-session/session-page-publisher.js";
|
|
39
|
+
export type { MakeSessionPagePublisher, SessionPagePublisher, SessionPageMintResult } from "./tools/report-session/session-page-publisher.js";
|
|
40
40
|
export type { ResolvePageUrl, SessionPageAccess } from "./tools/report-session/index.js";
|
|
41
41
|
export { createDbosRunLauncher } from "./execution/dbos-run-launcher.js";
|
|
42
42
|
export type { RunLauncher, LaunchOptions } from "./execution/run-launcher.js";
|
package/dist/index.js
CHANGED
|
@@ -44,8 +44,10 @@ export { createNoopArtifactRegistry } from "./execution/noop-artifact-registry.j
|
|
|
44
44
|
export { UnavailablePreviewPublisher } from "./tools/report/preview-publisher.js";
|
|
45
45
|
// Seam: the hosted view of a report-session page. A managed host serves the URL space
|
|
46
46
|
// `report-sessions/{analysisId}/{threadId}` and mints access through its own publisher,
|
|
47
|
-
// thus the seam belongs on the front door.
|
|
48
|
-
//
|
|
47
|
+
// thus the seam belongs on the front door. Both seams bind per call over the auth of the
|
|
48
|
+
// tool call, thus a host mints under the credential of the caller and holds no ambient
|
|
49
|
+
// state. The URL seam of the eyes rides beside the publisher factory: a host whose
|
|
50
|
+
// browser cannot reach the workspace tree binds a served URL for the look.
|
|
49
51
|
export { UnavailableSessionPagePublisher } from "./tools/report-session/session-page-publisher.js";
|
|
50
52
|
// Run launching.
|
|
51
53
|
export { createDbosRunLauncher } from "./execution/dbos-run-launcher.js";
|
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,7 +291,19 @@ 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);
|
|
@@ -307,6 +344,7 @@ export async function runAgent(agent, initial, session, opts) {
|
|
|
307
344
|
// exhausted its iterations, while the reason carries the abort.
|
|
308
345
|
if (assistantHasContent(wrapUp.message))
|
|
309
346
|
messages.push(wrapUp.message);
|
|
347
|
+
settleTranscript();
|
|
310
348
|
markLastLoopAssistant(messages, initial.length);
|
|
311
349
|
await emit({ type: "iteration", source, index: agent.maxIterations, final: true });
|
|
312
350
|
recordAgentRun({ agentId: agent.id, iterations, cappedOut: true, usage });
|
|
@@ -314,6 +352,7 @@ export async function runAgent(agent, initial, session, opts) {
|
|
|
314
352
|
return { messages, finish: { reason: "aborted", cappedOut: true, truncationRecoveries, ...finishUsage() } };
|
|
315
353
|
}
|
|
316
354
|
messages.push(wrapUp.message);
|
|
355
|
+
settleTranscript();
|
|
317
356
|
await emit({ type: "iteration", source, index: agent.maxIterations, final: true });
|
|
318
357
|
recordAgentRun({ agentId: agent.id, iterations, cappedOut: true, usage });
|
|
319
358
|
logFinish("warn", "max_iterations", true);
|
|
@@ -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, PreviewPart, DataPreviewFailedPart, RunCardPart } from "../contracts/chat-parts.js";
|
|
18
|
+
import type { AskPart, FileReferencePart, PlanPart, PresentationPart, PreviewPart, DataPreviewFailedPart, ReportSessionStartedPart, 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;
|
|
@@ -40,6 +40,7 @@ export type ConversationUIData = {
|
|
|
40
40
|
ask: Payload<AskPart>;
|
|
41
41
|
"report-preview": Payload<PreviewPart>;
|
|
42
42
|
"report-preview-failed": Payload<DataPreviewFailedPart>;
|
|
43
|
+
"report-session-started": Payload<ReportSessionStartedPart>;
|
|
43
44
|
};
|
|
44
45
|
export interface ConversationDisplayMetadata {
|
|
45
46
|
interrupted?: boolean;
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { validateUIMessages } from "ai";
|
|
18
18
|
import { z } from "zod";
|
|
19
|
-
import { AskPartSchema, DataPreviewFailedPartSchema, FileReferencePartSchema, PlanPartSchema, PresentationPartSchema, PreviewPartSchema, RunCardPartSchema, } from "../contracts/schemas/chat-parts.js";
|
|
19
|
+
import { AskPartSchema, DataPreviewFailedPartSchema, FileReferencePartSchema, PlanPartSchema, PresentationPartSchema, PreviewPartSchema, ReportSessionStartedPartSchema, 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({
|
|
@@ -56,6 +56,7 @@ const dataSchemas = {
|
|
|
56
56
|
ask: AskPartSchema.omit({ type: true }).strict(),
|
|
57
57
|
"report-preview": PreviewPartSchema.omit({ type: true }).strict(),
|
|
58
58
|
"report-preview-failed": DataPreviewFailedPartSchema.omit({ type: true }).strict(),
|
|
59
|
+
"report-session-started": ReportSessionStartedPartSchema.omit({ type: true }).strict(),
|
|
59
60
|
};
|
|
60
61
|
export function envelopeDisplayMessages(messages) {
|
|
61
62
|
if (messages.length === 0)
|
|
@@ -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
|
+
}
|
|
@@ -32,7 +32,7 @@ import { type CitationResolverConfig } from "../citations/resolve.js";
|
|
|
32
32
|
import type { CitationResolver } from "../citations/types.js";
|
|
33
33
|
import type { AuthContext } from "../auth/types.js";
|
|
34
34
|
import type { ReferenceResolver } from "../report-model/reference-resolver.js";
|
|
35
|
-
import type { ResolvePageAsset, ResolvePageUrl
|
|
35
|
+
import type { MakeSessionPagePublisher, ResolvePageAsset, ResolvePageUrl } from "../tools/report-session/index.js";
|
|
36
36
|
/** Registered child sandbox-step callable the parent's child dispatch closes over. */
|
|
37
37
|
export type SandboxStepCallable = (input: SandboxStepInput) => Promise<SandboxStepResult>;
|
|
38
38
|
/**
|
|
@@ -134,15 +134,18 @@ export interface CoreRuntimeDeps {
|
|
|
134
134
|
readonly resolveReportPageAsset?: ResolvePageAsset;
|
|
135
135
|
/**
|
|
136
136
|
* The hosted view of a report-session page. A managed host that serves the session URL
|
|
137
|
-
* space binds its publisher here, and the preview tool of the report agent then
|
|
138
|
-
* the minted URL beside the page path.
|
|
139
|
-
*
|
|
137
|
+
* space binds its publisher factory here, and the preview tool of the report agent then
|
|
138
|
+
* attaches the minted URL beside the page path. The factory binds one analysis and the
|
|
139
|
+
* auth of the tool call, like `makeReportReferenceResolver`, thus a host that mints under
|
|
140
|
+
* the credential of the caller binds it with no ambient state. Absent, the tool attaches
|
|
141
|
+
* nothing and the page path stays the whole result, which is the whole local path.
|
|
140
142
|
*/
|
|
141
|
-
readonly
|
|
143
|
+
readonly makeSessionPagePublisher?: MakeSessionPagePublisher;
|
|
142
144
|
/**
|
|
143
145
|
* The page-URL formation of the report-session eyes. A managed host whose browser cannot
|
|
144
|
-
* reach the workspace tree binds a resolver that names a served URL for the look
|
|
145
|
-
* the
|
|
146
|
+
* reach the workspace tree binds a resolver that names a served URL for the look, and the
|
|
147
|
+
* args carry the auth of the tool call beside the page identity. Absent, the eyes tool
|
|
148
|
+
* navigates through a `file://` URL of the page path.
|
|
146
149
|
*/
|
|
147
150
|
readonly resolveReportPageUrl?: ResolvePageUrl;
|
|
148
151
|
/**
|
package/dist/runtime/assemble.js
CHANGED
|
@@ -164,7 +164,7 @@ export function assembleCoreRuntime(deps) {
|
|
|
164
164
|
makeResolver: makeReportResolver,
|
|
165
165
|
...(eyes ? { eyes } : {}),
|
|
166
166
|
...(deps.resolveReportPageAsset ? { resolvePageAsset: deps.resolveReportPageAsset } : {}),
|
|
167
|
-
...(deps.
|
|
167
|
+
...(deps.makeSessionPagePublisher ? { makeSessionPages: deps.makeSessionPagePublisher } : {}),
|
|
168
168
|
...(deps.resolveReportPageUrl ? { resolvePageUrl: deps.resolveReportPageUrl } : {}),
|
|
169
169
|
...(conversation.logger ? { logger: conversation.logger } : {}),
|
|
170
170
|
});
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
* and no outcome would ever arrive.
|
|
46
46
|
*/
|
|
47
47
|
import { z } from "zod";
|
|
48
|
+
import type { AuthContext } from "../../auth/types.js";
|
|
48
49
|
import { type ChromeConfig } from "../../lib/chrome.js";
|
|
49
50
|
import { type AcquireEyes } from "../../lib/eyes.js";
|
|
50
51
|
import { type Logger } from "../../lib/logger.js";
|
|
@@ -56,11 +57,14 @@ export type { CapturePage, FailedRequest, PageCapture };
|
|
|
56
57
|
/**
|
|
57
58
|
* The URL formation of one look. It maps the page of the thread onto the URL that the browser opens.
|
|
58
59
|
* A composition whose browser cannot reach the workspace tree binds a resolver that names a served URL.
|
|
60
|
+
* The args carry the auth of the tool call beside the page identity, thus a realization mints the URL
|
|
61
|
+
* under the credential of the caller and holds no ambient state.
|
|
59
62
|
*/
|
|
60
63
|
export type ResolvePageUrl = (args: {
|
|
61
64
|
pagePath: string;
|
|
62
65
|
analysisId: string;
|
|
63
66
|
threadId: string;
|
|
67
|
+
auth: AuthContext;
|
|
64
68
|
}) => Promise<string>;
|
|
65
69
|
/** The empty input. The tool examines the current page of the thread, thus it needs no field. */
|
|
66
70
|
declare const examinePageInput: z.ZodObject<{}, z.core.$strip>;
|
|
@@ -276,7 +276,10 @@ export function createExaminePageTool(deps) {
|
|
|
276
276
|
// becomes a typed outcome and the look never starts.
|
|
277
277
|
let url;
|
|
278
278
|
try {
|
|
279
|
-
url =
|
|
279
|
+
url =
|
|
280
|
+
deps.resolvePageUrl !== undefined
|
|
281
|
+
? await deps.resolvePageUrl({ pagePath, analysisId, threadId, auth: ctx.session.auth })
|
|
282
|
+
: pathToFileURL(pagePath).href;
|
|
280
283
|
}
|
|
281
284
|
catch (cause) {
|
|
282
285
|
logger.warn("the page URL did not resolve", { threadId, analysisId, ...defaultErrorFields(cause) });
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export { createPreviewReportTool, type PreviewReportInput, type PreviewReportResult, type PreviewReportToolDeps, type ResolvePageAsset, type SessionPageAccess, } from "./preview-report.js";
|
|
9
9
|
export { createExaminePageTool, type CapturePage, type ExaminePageInput, type ExaminePageResult, type ExaminePageToolDeps, type FailedRequest, type PageCapture, type ResolvePageUrl, } from "./examine-page.js";
|
|
10
|
-
export { describeSessionPageMintFailure, UnavailableSessionPagePublisher, type SessionPageMintFailure, type SessionPageMintResult, type SessionPagePublisher, } from "./session-page-publisher.js";
|
|
10
|
+
export { describeSessionPageMintFailure, UnavailableSessionPagePublisher, type MakeSessionPagePublisher, type SessionPageMintFailure, type SessionPageMintResult, type SessionPagePublisher, } from "./session-page-publisher.js";
|
|
11
11
|
export { createRecordVersionTool, type RecordVersionInput, type RecordVersionResult, type RecordVersionToolDeps } from "./record-version.js";
|
|
12
12
|
export { createDeriveTableTool, type DeriveTableInput, type DeriveTableResult, type DeriveTableToolDeps } from "./derive-table.js";
|
|
13
13
|
export { createListPinnedArtifactsTool, type ListPinnedArtifactsInput, type ListPinnedArtifactsResult, type ListPinnedArtifactsToolDeps, type PinnedArtifact, type PinnedCitation, } from "./list-artifacts.js";
|
|
@@ -16,11 +16,12 @@
|
|
|
16
16
|
* namespace belongs to this path alone, thus the tool never writes under the old `previews/` or `reports/`
|
|
17
17
|
* trees. The result carries the absolute page path, thus a local host shows the page with no seam.
|
|
18
18
|
*
|
|
19
|
-
* A hosted view rides the same result. When the composition binds the session-page
|
|
20
|
-
* mints one grant after the page lands, and the
|
|
21
|
-
* `report-sessions/{analysisId}/{threadId}`
|
|
22
|
-
* rides the arm as data, and the page path
|
|
23
|
-
*
|
|
19
|
+
* A hosted view rides the same result. When the composition binds the session-page factory, the tool
|
|
20
|
+
* builds the publisher over the scope of the call, mints one grant after the page lands, and the
|
|
21
|
+
* `rendered` arm carries the URL of the URL space `report-sessions/{analysisId}/{threadId}`
|
|
22
|
+
* (`contracts/content-url.ts`) beside the path. A refused mint rides the arm as data, and the page path
|
|
23
|
+
* stays good — a broken grant surface never costs the render. An unbound factory changes nothing: the
|
|
24
|
+
* arm carries no access field, and a local host opens the file.
|
|
24
25
|
*
|
|
25
26
|
* Each degraded condition is a typed outcome in the ok channel: a session refusal, a gap list, a resolver
|
|
26
27
|
* absence, an unresolvable root at resolver construction, an unresolved reference, a bridge mismatch, a
|
|
@@ -43,7 +44,7 @@ import { type BridgeMismatch } from "../../report-render/value-bridge.js";
|
|
|
43
44
|
import type { RenderProblem } from "../../report-render/types.js";
|
|
44
45
|
import { type Tool } from "../define-tool.js";
|
|
45
46
|
import { type ReportSessionStateGateway, type SessionRefusal } from "../report-authoring/authoring-tools.js";
|
|
46
|
-
import { type
|
|
47
|
+
import { type MakeSessionPagePublisher } from "./session-page-publisher.js";
|
|
47
48
|
/** The empty input. The tool renders the current draft of the thread, thus it needs no field. */
|
|
48
49
|
declare const previewReportInput: z.ZodObject<{}, z.core.$strip>;
|
|
49
50
|
export type PreviewReportInput = z.infer<typeof previewReportInput>;
|
|
@@ -118,9 +119,11 @@ export type ResolvePageAsset = (specifier: string) => string;
|
|
|
118
119
|
* harness. An embedder that ships the asset bytes packed, for example a compiled single-file binary with no
|
|
119
120
|
* `node_modules` tree, materializes them to disk and binds its own lookup here.
|
|
120
121
|
*
|
|
121
|
-
* `
|
|
122
|
-
*
|
|
123
|
-
*
|
|
122
|
+
* `makeSessionPages` is optional, because a local host opens the page path itself. It binds one analysis
|
|
123
|
+
* and the auth of the call, like `makeResolver`, thus a realization mints under the credential of the
|
|
124
|
+
* caller. Bound, the tool builds the publisher over the scope of the call, mints one grant after the page
|
|
125
|
+
* lands, and attaches the URL beside the path. A refused mint rides the `rendered` arm as data, and it
|
|
126
|
+
* never fails the render.
|
|
124
127
|
*/
|
|
125
128
|
export interface PreviewReportToolDeps {
|
|
126
129
|
readonly gateway: ReportSessionStateGateway;
|
|
@@ -130,7 +133,7 @@ export interface PreviewReportToolDeps {
|
|
|
130
133
|
}) => ReferenceResolver;
|
|
131
134
|
readonly resolveWorkspaceRoot: ResolveWorkspaceRoot;
|
|
132
135
|
readonly resolvePageAsset?: ResolvePageAsset;
|
|
133
|
-
readonly
|
|
136
|
+
readonly makeSessionPages?: MakeSessionPagePublisher;
|
|
134
137
|
readonly logger?: Logger;
|
|
135
138
|
}
|
|
136
139
|
/**
|
|
@@ -16,11 +16,12 @@
|
|
|
16
16
|
* namespace belongs to this path alone, thus the tool never writes under the old `previews/` or `reports/`
|
|
17
17
|
* trees. The result carries the absolute page path, thus a local host shows the page with no seam.
|
|
18
18
|
*
|
|
19
|
-
* A hosted view rides the same result. When the composition binds the session-page
|
|
20
|
-
* mints one grant after the page lands, and the
|
|
21
|
-
* `report-sessions/{analysisId}/{threadId}`
|
|
22
|
-
* rides the arm as data, and the page path
|
|
23
|
-
*
|
|
19
|
+
* A hosted view rides the same result. When the composition binds the session-page factory, the tool
|
|
20
|
+
* builds the publisher over the scope of the call, mints one grant after the page lands, and the
|
|
21
|
+
* `rendered` arm carries the URL of the URL space `report-sessions/{analysisId}/{threadId}`
|
|
22
|
+
* (`contracts/content-url.ts`) beside the path. A refused mint rides the arm as data, and the page path
|
|
23
|
+
* stays good — a broken grant surface never costs the render. An unbound factory changes nothing: the
|
|
24
|
+
* arm carries no access field, and a local host opens the file.
|
|
24
25
|
*
|
|
25
26
|
* Each degraded condition is a typed outcome in the ok channel: a session refusal, a gap list, a resolver
|
|
26
27
|
* absence, an unresolvable root at resolver construction, an unresolved reference, a bridge mismatch, a
|
|
@@ -342,18 +343,20 @@ async function sweepAssets(assetsDir, staged, logger) {
|
|
|
342
343
|
/**
|
|
343
344
|
* Mint the hosted view of the page that landed.
|
|
344
345
|
*
|
|
345
|
-
* An absent
|
|
346
|
-
* local contract.
|
|
347
|
-
*
|
|
348
|
-
*
|
|
346
|
+
* An absent factory gives no grant, and the arm carries no access field — the page path stays the whole
|
|
347
|
+
* local contract. The tool builds the publisher over the scope of the call, thus the mint runs under the
|
|
348
|
+
* auth of the caller. A refusal, a thrown construction, and a thrown realization each become the
|
|
349
|
+
* not-granted arm, thus a broken grant surface never costs the render. The URL spells through
|
|
350
|
+
* `buildReportSessionUrl`, thus the formula lives in the contract and the seam gives the content-server
|
|
351
|
+
* base alone.
|
|
349
352
|
*/
|
|
350
|
-
async function mintAccess(
|
|
351
|
-
if (
|
|
353
|
+
async function mintAccess(makeSessionPages, analysisId, threadId, auth, logger) {
|
|
354
|
+
if (makeSessionPages === undefined) {
|
|
352
355
|
return undefined;
|
|
353
356
|
}
|
|
354
357
|
let minted;
|
|
355
358
|
try {
|
|
356
|
-
minted = await
|
|
359
|
+
minted = await makeSessionPages({ analysisId, auth }).mintSessionPageAccess(threadId);
|
|
357
360
|
}
|
|
358
361
|
catch (cause) {
|
|
359
362
|
logger.warn("the session-page mint threw", { threadId, analysisId, ...defaultErrorFields(cause) });
|
|
@@ -472,7 +475,7 @@ export function createPreviewReportTool(deps) {
|
|
|
472
475
|
logger.warn("the rendered hash did not stamp", { threadId, analysisId, detail });
|
|
473
476
|
return ok({ outcome: "stamp-failed", pagePath: written.value, detail });
|
|
474
477
|
}
|
|
475
|
-
const access = await mintAccess(deps.
|
|
478
|
+
const access = await mintAccess(deps.makeSessionPages, analysisId, threadId, ctx.session.auth, logger);
|
|
476
479
|
return ok({ outcome: "rendered", pagePath: written.value, ...(access !== undefined ? { access } : {}) });
|
|
477
480
|
},
|
|
478
481
|
});
|
|
@@ -7,10 +7,14 @@
|
|
|
7
7
|
* `report-sessions/{analysisId}/{threadId}` (`contracts/content-url.ts`). That mint hides behind this
|
|
8
8
|
* one method.
|
|
9
9
|
*
|
|
10
|
-
* The managed realization owns the grant mint and its client.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* The managed realization owns the grant mint and its client. A mint runs under the credential of the
|
|
11
|
+
* tool call, thus the composition binds a factory and the preview tool builds the publisher over the
|
|
12
|
+
* scope of the call — exactly as it builds the reference resolver. A boot-time singleton could not
|
|
13
|
+
* carry that credential, and a host that bans ambient state could not bind one at all. The local
|
|
14
|
+
* default (`UnavailableSessionPagePublisher`) returns not-ok, thus the preview tool attaches no URL
|
|
15
|
+
* and the render still lands — the page path stays the whole local contract.
|
|
13
16
|
*/
|
|
17
|
+
import type { AuthContext } from "../../auth/types.js";
|
|
14
18
|
export type SessionPageMintResult = {
|
|
15
19
|
ok: true;
|
|
16
20
|
data: {
|
|
@@ -40,16 +44,25 @@ export type SessionPageMintFailure = Extract<SessionPageMintResult, {
|
|
|
40
44
|
export declare function describeSessionPageMintFailure(failure: SessionPageMintFailure): string;
|
|
41
45
|
export interface SessionPagePublisher {
|
|
42
46
|
/**
|
|
43
|
-
* Mint the access grant of one session page.
|
|
44
|
-
*
|
|
45
|
-
*
|
|
47
|
+
* Mint the access grant of one session page. The publisher binds the analysis, thus the mint takes
|
|
48
|
+
* the thread id alone. `baseUrl` is the base URL of the content server, with no res path — the
|
|
49
|
+
* caller spells the whole URL through `buildReportSessionUrl`, thus the formula lives in the
|
|
50
|
+
* contract and never in a realization.
|
|
46
51
|
*/
|
|
47
|
-
mintSessionPageAccess(
|
|
52
|
+
mintSessionPageAccess(threadId: string): Promise<SessionPageMintResult>;
|
|
48
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* The per-call construction of the publisher. The factory binds one analysis and the auth of the tool
|
|
56
|
+
* call, thus a realization mints under the credential of the caller and holds no ambient state.
|
|
57
|
+
*/
|
|
58
|
+
export type MakeSessionPagePublisher = (scope: {
|
|
59
|
+
analysisId: string;
|
|
60
|
+
auth: AuthContext;
|
|
61
|
+
}) => SessionPagePublisher;
|
|
49
62
|
/**
|
|
50
63
|
* Local default — no hosted view of a session page. Returns not-ok, thus the preview tool carries the
|
|
51
64
|
* refusal as data and the page path stays the whole result.
|
|
52
65
|
*/
|
|
53
66
|
export declare class UnavailableSessionPagePublisher implements SessionPagePublisher {
|
|
54
|
-
mintSessionPageAccess(
|
|
67
|
+
mintSessionPageAccess(_threadId: string): Promise<SessionPageMintResult>;
|
|
55
68
|
}
|
|
@@ -7,9 +7,12 @@
|
|
|
7
7
|
* `report-sessions/{analysisId}/{threadId}` (`contracts/content-url.ts`). That mint hides behind this
|
|
8
8
|
* one method.
|
|
9
9
|
*
|
|
10
|
-
* The managed realization owns the grant mint and its client.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* The managed realization owns the grant mint and its client. A mint runs under the credential of the
|
|
11
|
+
* tool call, thus the composition binds a factory and the preview tool builds the publisher over the
|
|
12
|
+
* scope of the call — exactly as it builds the reference resolver. A boot-time singleton could not
|
|
13
|
+
* carry that credential, and a host that bans ambient state could not bind one at all. The local
|
|
14
|
+
* default (`UnavailableSessionPagePublisher`) returns not-ok, thus the preview tool attaches no URL
|
|
15
|
+
* and the render still lands — the page path stays the whole local contract.
|
|
13
16
|
*/
|
|
14
17
|
/**
|
|
15
18
|
* Renders a failed mint as the line an agent reads back from the preview tool.
|
|
@@ -33,7 +36,7 @@ export function describeSessionPageMintFailure(failure) {
|
|
|
33
36
|
* refusal as data and the page path stays the whole result.
|
|
34
37
|
*/
|
|
35
38
|
export class UnavailableSessionPagePublisher {
|
|
36
|
-
async mintSessionPageAccess(
|
|
39
|
+
async mintSessionPageAccess(_threadId) {
|
|
37
40
|
return {
|
|
38
41
|
ok: false,
|
|
39
42
|
error: { message: "the hosted view of a session page is unavailable in this environment" },
|
|
@@ -167,13 +167,23 @@ export function createStartReportSessionTool(deps) {
|
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
169
|
const spawned = await spawn.spawnReportSession(parentThreadId, brief);
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
170
|
+
if (spawned.isOk()) {
|
|
171
|
+
const child = spawned.value;
|
|
172
|
+
// The part rides the started arm only. The existing-session arm
|
|
173
|
+
// names a session that an earlier turn announced, thus a second
|
|
174
|
+
// part would put a second entry into the transcript.
|
|
175
|
+
await ctx.emit({
|
|
176
|
+
type: "data-report-session-started",
|
|
177
|
+
data: { threadId: child.threadId, parentThreadId },
|
|
178
|
+
});
|
|
179
|
+
return ok({ outcome: "started", threadId: child.threadId, title: child.title });
|
|
180
|
+
}
|
|
181
|
+
const fault = spawned.error;
|
|
182
|
+
const outcome = toOutcome(fault);
|
|
183
|
+
if (outcome.outcome === "failed") {
|
|
184
|
+
logger.warn("the report session did not start", { parentThreadId, reason: fault.type, ...logger.errorFields(causeOf(fault)) });
|
|
185
|
+
}
|
|
186
|
+
return ok(outcome);
|
|
177
187
|
},
|
|
178
188
|
});
|
|
179
189
|
}
|
package/package.json
CHANGED