@inflexa-ai/harness 0.41.2 → 0.42.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/agents/conversation-agent.js +3 -0
  2. package/dist/agents/report-session-agent.d.ts +2 -2
  3. package/dist/agents/report-session-agent.js +4 -0
  4. package/dist/app/chat-turn.d.ts +0 -4
  5. package/dist/app/chat-turn.js +39 -25
  6. package/dist/app/message-assembly.d.ts +2 -2
  7. package/dist/app/message-assembly.js +1 -1
  8. package/dist/contracts/chat-events.d.ts +12 -5
  9. package/dist/contracts/chat-events.js +3 -3
  10. package/dist/contracts/chat-frame.d.ts +41 -0
  11. package/dist/contracts/chat-frame.js +173 -0
  12. package/dist/contracts/chat-parts.d.ts +15 -3
  13. package/dist/contracts/chat-parts.js +2 -2
  14. package/dist/contracts/index.d.ts +6 -4
  15. package/dist/contracts/index.js +1 -0
  16. package/dist/contracts/message.d.ts +7 -5
  17. package/dist/contracts/message.js +1 -1
  18. package/dist/contracts/part-registry.d.ts +6 -6
  19. package/dist/contracts/schemas/chat-events.d.ts +2 -2
  20. package/dist/contracts/schemas/chat-events.js +2 -2
  21. package/dist/contracts/schemas/chat-parts.d.ts +34 -2
  22. package/dist/contracts/schemas/chat-parts.js +9 -3
  23. package/dist/contracts/schemas/index.d.ts +2 -2
  24. package/dist/contracts/schemas/index.js +2 -2
  25. package/dist/execution/run-event-parts.d.ts +19 -11
  26. package/dist/execution/run-event-parts.js +0 -0
  27. package/dist/execution/run-event-stream.d.ts +2 -2
  28. package/dist/index.d.ts +10 -6
  29. package/dist/index.js +4 -2
  30. package/dist/lib/page-capture.d.ts +27 -9
  31. package/dist/lib/page-capture.js +81 -33
  32. package/dist/loop/compaction.d.ts +8 -3
  33. package/dist/loop/run-agent.d.ts +11 -0
  34. package/dist/loop/run-agent.js +62 -35
  35. package/dist/loop/types.d.ts +3 -0
  36. package/dist/memory/ai-sdk-message-storage.d.ts +18 -3
  37. package/dist/memory/ai-sdk-message-storage.js +23 -1
  38. package/dist/memory/conversation-display-recorder.d.ts +7 -1
  39. package/dist/memory/conversation-display-recorder.js +59 -119
  40. package/dist/memory/conversation-display-replay.d.ts +3 -3
  41. package/dist/memory/conversation-display-replay.js +7 -21
  42. package/dist/memory/conversation-display-storage.d.ts +10 -10
  43. package/dist/memory/conversation-display-storage.js +9 -8
  44. package/dist/memory/conversation-view.d.ts +5 -4
  45. package/dist/memory/conversation-view.js +14 -18
  46. package/dist/memory/count-tokens.js +1 -0
  47. package/dist/memory/thread-store.d.ts +9 -2
  48. package/dist/memory/thread-store.js +8 -2
  49. package/dist/prompts/compaction.d.ts +2 -2
  50. package/dist/prompts/compaction.js +2 -2
  51. package/dist/prompts/report-session.d.ts +1 -1
  52. package/dist/prompts/report-session.js +40 -28
  53. package/dist/report-render/views/block-mark.d.ts +8 -0
  54. package/dist/report-render/views/block-mark.js +10 -0
  55. package/dist/report-render/views/chart-view.js +2 -1
  56. package/dist/report-render/views/prose.js +10 -6
  57. package/dist/report-render/views/values.js +5 -4
  58. package/dist/state/migrations/20260927180000_thread_title_set_by_user.d.ts +6 -0
  59. package/dist/state/migrations/20260927180000_thread_title_set_by_user.js +10 -0
  60. package/dist/state/migrations/index.js +2 -0
  61. package/dist/tools/report-authoring/authoring-tools.d.ts +16 -5
  62. package/dist/tools/report-authoring/authoring-tools.js +52 -24
  63. package/dist/tools/report-session/examine-page.d.ts +29 -14
  64. package/dist/tools/report-session/examine-page.js +40 -16
  65. package/dist/tools/report-session/list-artifacts.d.ts +23 -10
  66. package/dist/tools/report-session/list-artifacts.js +51 -31
  67. package/dist/tools/report-session/page-faults.d.ts +29 -0
  68. package/dist/tools/report-session/page-faults.js +48 -0
  69. package/dist/tools/report-session/preview-report.d.ts +9 -2
  70. package/dist/tools/report-session/preview-report.js +13 -3
  71. package/dist/tools/report-session/record-version.d.ts +5 -1
  72. package/dist/tools/report-session/record-version.js +4 -3
  73. package/package.json +1 -1
@@ -44,6 +44,8 @@ import { createResolveCitationTool } from "../tools/research/resolve-citation.js
44
44
  export const CONVERSATION_AGENT_ID = "conversation-agent";
45
45
  /** Runaway guard — heavy tool-driving turns need generous headroom. */
46
46
  const CONVERSATION_MAX_ITERATIONS = 200;
47
+ /** Compaction budgets, in the input tokens of a request. */
48
+ const CONVERSATION_COMPACTION = { turnStartBudget: 150_000, budget: 200_000 };
47
49
  /** Build the conversation `AgentDefinition` with every tool fully dep-bound. */
48
50
  export function createConversationAgent(deps) {
49
51
  const { provider, pool, embedding, workspaceFs, model, utilityProvider, utilityModel, executeAnalysisWorkflow, runAuthorizer, runLauncher, bioKeys, chrome, resourcePolicy, hostTools, refStorePath, farmLockFile, imagePackagesFile, readPoolInventory, usageRecorder, toolOutputStore, citationResolver, } = deps;
@@ -195,5 +197,6 @@ export function createConversationAgent(deps) {
195
197
  model,
196
198
  tools,
197
199
  maxIterations: CONVERSATION_MAX_ITERATIONS,
200
+ compaction: CONVERSATION_COMPACTION,
198
201
  };
199
202
  }
@@ -77,8 +77,8 @@ export interface ReportSessionAgentDeps {
77
77
  * Omitted, the tool derives nothing, the same as an absent sandbox client.
78
78
  */
79
79
  readonly runAuthorizer?: RunAuthorizer;
80
- /** Thread reader -- the record tool reads the anchor of the report thread. */
81
- readonly threads: Pick<ThreadStore, "getThread">;
80
+ /** Thread store -- the record tool reads the anchor of the report thread, and the preview tool names it. */
81
+ readonly threads: Pick<ThreadStore, "getThread" | "setAutoTitle">;
82
82
  /** Headless-Chrome config -- the eyes tool opens the rendered page. */
83
83
  readonly chrome: ChromeConfig;
84
84
  /**
@@ -37,6 +37,8 @@ export const REPORT_SESSION_AGENT_ID = "report-session";
37
37
  * gets the headroom of the report runner (REPORT_AGENT_MAX_STEPS).
38
38
  */
39
39
  const REPORT_SESSION_MAX_ITERATIONS = 200;
40
+ /** A session is one long turn that holds page captures, thus it compacts only during the turn, at a higher budget. */
41
+ const REPORT_SESSION_COMPACTION = { budget: 250_000 };
40
42
  /** Build the report `AgentDefinition` with every tool bound to its deps. */
41
43
  export function createReportSessionAgent(deps) {
42
44
  const { model, pool, embedding, workspaceFs, gateway, resolveWorkspaceRoot, store, threads, chrome, eyes, makeResolver, resolvePageAsset, logger } = deps;
@@ -87,6 +89,7 @@ export function createReportSessionAgent(deps) {
87
89
  createPreviewReportTool({
88
90
  gateway,
89
91
  resolveWorkspaceRoot,
92
+ threads,
90
93
  ...(makeResolver ? { makeResolver } : {}),
91
94
  ...(resolvePageAsset ? { resolvePageAsset } : {}),
92
95
  ...(makeSessionPages ? { makeSessionPages } : {}),
@@ -122,5 +125,6 @@ export function createReportSessionAgent(deps) {
122
125
  model,
123
126
  tools,
124
127
  maxIterations: REPORT_SESSION_MAX_ITERATIONS,
128
+ compaction: REPORT_SESSION_COMPACTION,
125
129
  };
126
130
  }
@@ -49,8 +49,6 @@ export type PrepareChatTurnResult = ({
49
49
  * inside {@link assembleMessages} (applied once to the new user input only).
50
50
  */
51
51
  export declare function prepareChatTurn(deps: PrepareChatTurnDeps, params: PrepareChatTurnParams): Promise<PrepareChatTurnResult>;
52
- /** The root loop of a chat turn compacts its view past this estimate, in tokens. */
53
- export declare const DEFAULT_CONVERSATION_BUDGET = 150000;
54
52
  export interface RunChatTurnDeps extends PrepareChatTurnDeps {
55
53
  /** The agent of a turn comes from the thread type, which only the preparation knows. */
56
54
  readonly agents: ThreadAgentResolver;
@@ -82,8 +80,6 @@ export interface RunOpenChatTurnParams {
82
80
  readonly startedAtMs?: number;
83
81
  /** The cache policy of the root loop. Absent gives {@link CONVERSATION_PROMPT_CACHE}. */
84
82
  readonly promptCache?: PromptCachePolicy;
85
- /** The budget of the view of the root loop. Absent gives {@link DEFAULT_CONVERSATION_BUDGET}. */
86
- readonly conversationBudget?: number;
87
83
  }
88
84
  /** The values of one whole turn. */
89
85
  export type RunChatTurnParams = OpenChatTurnParams & RunOpenChatTurnParams;
@@ -73,24 +73,23 @@ export async function prepareChatTurn(deps, params) {
73
73
  }
74
74
  }
75
75
  else if (!existing.title || existing.title.length === 0) {
76
- unwrapOrThrow(await store.updateTitle(threadId, deriveThreadTitle(userInput)));
76
+ unwrapOrThrow(await store.setAutoTitle(threadId, deriveThreadTitle(userInput)));
77
77
  }
78
78
  }
79
79
  catch (err) {
80
80
  logger.warn("title-seed failed (non-fatal)", logger.errorFields(err));
81
81
  }
82
- const history = createThreadHistory(pool);
83
- const { messages, userMessage, contextRecords } = await assembleMessages({
82
+ const assembled = await assembleMessages({
84
83
  threadId,
85
84
  threadType,
86
85
  analysisId,
87
86
  userInput,
88
87
  ...(await readTurnContext(pool, analysisId)),
89
- history,
88
+ history: createThreadHistory(pool),
90
89
  workingMemory: createWorkingMemory(pool),
91
90
  ...(deps.logger ? { logger: deps.logger } : {}),
92
91
  });
93
- return { kind: "ok", threadType, messages, userMessage, contextRecords };
92
+ return { kind: "ok", threadType, ...assembled };
94
93
  }
95
94
  /** The analysis context and the run activity of a turn. A failed read gives the fallback of its kind. */
96
95
  async function readTurnContext(pool, analysisId) {
@@ -98,8 +97,6 @@ async function readTurnContext(pool, analysisId) {
98
97
  const runActivityContext = await queryNonTerminalRunsByAnalysis(pool, analysisId, RUN_ACTIVITY_DETAIL_LIMIT).match((activity) => renderRunActivity(activity), () => renderRunActivityUnavailable());
99
98
  return { analysisContext: analysisState?.context ?? null, runActivityContext };
100
99
  }
101
- /** The root loop of a chat turn compacts its view past this estimate, in tokens. */
102
- export const DEFAULT_CONVERSATION_BUDGET = 150_000;
103
100
  /**
104
101
  * Open one chat turn: prepare it and resolve its agent. A refusal comes back before any row of the
105
102
  * turn exists, thus a host can answer it before it starts a stream.
@@ -142,22 +139,18 @@ async function runOpenTurn(deps, turn, prepared, agent, params) {
142
139
  const history = createThreadHistory(deps.pool, deps.logger);
143
140
  const recorder = createConversationDisplayRecorder({
144
141
  userText: userTextOf(prepared.userMessage),
145
- topLevelCallPath: session.provenance.callPath,
142
+ agentId: agent.id,
146
143
  sink: params.emit,
147
144
  });
148
145
  // The groups that did not land yet, in the order of the turn. A failed write keeps them for the next write.
149
- let pending = [
150
- {
151
- modelMessages: [prepared.userMessage, ...prepared.contextRecords],
152
- displayMessages: recorder.takeOpening(),
153
- ...(params.author === undefined ? {} : { author: params.author }),
154
- },
155
- ];
146
+ let pending = [];
156
147
  let startSeq;
157
148
  let storeError;
149
+ const opened = () => startSeq !== undefined || pending.length > 0;
158
150
  // A store fault never stops the turn: the error rides the result.
159
151
  const flush = async (close) => {
160
- if (pending.length === 0 && close === undefined)
152
+ // A turn that ends before its user message was sent has no rows to close.
153
+ if (!opened() || (pending.length === 0 && close === undefined))
161
154
  return;
162
155
  const closing = close === undefined ? {} : { close };
163
156
  const write = startSeq === undefined ? { opening: pending[0], rounds: pending.slice(1), ...closing } : { startSeq, rounds: pending, ...closing };
@@ -170,12 +163,32 @@ async function runOpenTurn(deps, turn, prepared, agent, params) {
170
163
  logger.warn("chat turn write failed", { threadId: turn.threadId, op: error.op, ...logger.errorFields(error.cause) });
171
164
  });
172
165
  };
173
- await flush();
166
+ const storeRound = async (round) => {
167
+ if (!opened() && round.messages[0] !== prepared.userMessage) {
168
+ // A turn-start compaction comes before the user message, thus its rows land before the turn opens. A later
169
+ // write cannot keep that order, thus a failed write is not tried again, and the next turn compacts again.
170
+ const appended = await history.appendTurn(turn.threadId, { modelMessages: round.messages, displayMessages: recorder.takeRound(round.messages) });
171
+ if (appended.isErr()) {
172
+ logger.warn("chat turn compaction write failed", {
173
+ threadId: turn.threadId,
174
+ op: appended.error.op,
175
+ ...logger.errorFields(appended.error.cause),
176
+ });
177
+ }
178
+ return;
179
+ }
180
+ const author = params.author === undefined ? {} : { author: params.author };
181
+ pending.push(opened()
182
+ ? { modelMessages: round.messages, displayMessages: recorder.takeRound(round.messages) }
183
+ : { modelMessages: round.messages, displayMessages: recorder.takeOpening(), ...author });
184
+ await flush();
185
+ };
174
186
  const { ask } = params;
187
+ const compaction = compactionPolicy(deps, turn.analysisId, params, prepared.threadType, agent, recorder.emit);
175
188
  let outcome;
176
189
  let fallbackText;
177
190
  try {
178
- const run = await runAgent(agent, prepared.messages, session, {
191
+ const run = await runAgent(agent, prepared.history, session, {
179
192
  provider: params.chat(recorder.emit),
180
193
  signal: params.signal,
181
194
  emit: recorder.emit,
@@ -183,17 +196,16 @@ async function runOpenTurn(deps, turn, prepared, agent, params) {
183
196
  usageRecorder: params.usageRecorder,
184
197
  toolOutputStore: createToolOutputStore(deps.pool),
185
198
  promptCache: params.promptCache ?? CONVERSATION_PROMPT_CACHE,
186
- compaction: compactionPolicy(deps, turn.analysisId, params, prepared.threadType, agent, recorder.emit),
199
+ turnInput: [prepared.userMessage, ...prepared.contextRecords],
200
+ ...(compaction === undefined ? {} : { compaction }),
187
201
  ...(deps.logger === undefined ? {} : { logger: deps.logger }),
188
202
  ...(ask === undefined ? {} : { ask: (request) => ask(request, recorder.emit) }),
189
- onRound: async (round) => {
190
- pending.push({ modelMessages: round.messages, displayMessages: recorder.takeRound(round.messages) });
191
- await flush();
192
- },
203
+ onRound: storeRound,
193
204
  });
194
205
  outcome = run.finish.reason === "aborted" ? { status: "aborted", finish: run.finish } : { status: "done", finish: run.finish };
195
206
  // An abort can end the run inside an exchange, and the text of an exchange is a summary, and no reply.
196
- fallbackText = finalText(run.messages.filter((message) => compactionExchangeOf(message) === undefined));
207
+ // The history ends on the reply of the last turn, thus only the messages of this run can give the text.
208
+ fallbackText = finalText(run.messages.slice(prepared.history.length).filter((message) => compactionExchangeOf(message) === undefined));
197
209
  }
198
210
  catch (cause) {
199
211
  // A provider failure that races an abort stays a failure, thus both facts must hold.
@@ -226,11 +238,13 @@ function userTextOf(message) {
226
238
  }
227
239
  const WORKING_MEMORY_TOOL_ID = "update_working_memory";
228
240
  function compactionPolicy(deps, analysisId, params, threadType, agent, emit) {
241
+ if (agent.compaction === undefined)
242
+ return undefined;
229
243
  // A report thread reads a frozen copy of the working memory, thus its agent declares no memory tool.
230
244
  const remembers = agent.tools.some((tool) => tool.id === WORKING_MEMORY_TOOL_ID);
231
245
  const workingMemory = createWorkingMemory(deps.pool);
232
246
  return {
233
- budget: params.conversationBudget ?? DEFAULT_CONVERSATION_BUDGET,
247
+ ...agent.compaction,
234
248
  // A streaming provider sends each text delta with no source, thus a delta of the summary would show as a reply.
235
249
  provider: params.chat((event) => (event.type === "text-delta" ? undefined : emit(event))),
236
250
  request: remembers ? MEMORY_COMPACTION_REQUEST : SUMMARY_COMPACTION_REQUEST,
@@ -33,8 +33,8 @@ export interface AssembleMessagesArgs {
33
33
  readonly logger?: Logger;
34
34
  }
35
35
  export interface AssembledMessages {
36
- /** The full message array for `runAgent`. */
37
- readonly messages: LoopMessage[];
36
+ /** The view of the thread before the turn. */
37
+ readonly history: LoopMessage[];
38
38
  /** The sanitized user input as its own message — the genuine turn start. */
39
39
  readonly userMessage: ModelMessage;
40
40
  /** The context records after the user message. The turn stores them with the user message. */
@@ -39,7 +39,7 @@ export async function assembleMessages(args) {
39
39
  content: normalizeUnicode(redactSecrets(args.userInput)),
40
40
  };
41
41
  const contextRecords = await contextRecordsFor(args, history);
42
- return { messages: [...history, userMessage, ...contextRecords], userMessage, contextRecords };
42
+ return { history, userMessage, contextRecords };
43
43
  }
44
44
  /** The context records of one turn, in the order analysis context, run activity, working memory. */
45
45
  export async function contextRecordsFor(args, history) {
@@ -1,15 +1,16 @@
1
1
  /**
2
- * Cortex-native chat-stream events — the harness wire vocabulary.
2
+ * Chat-stream events — the harness wire vocabulary.
3
3
  *
4
4
  * Distinct from the 15 `data-*` chat parts (`chat-parts.ts`): those are UI
5
5
  * presentation payloads a tool emits; these are the agent-loop stream events
6
- * the harness chat route frames as Cortex-native SSE. Together they are the
7
- * sole Cortex↔frontend wire contract — there is no AI SDK UI Message Stream
6
+ * the host chat route frames as SSE. Together they are the sole
7
+ * host↔frontend wire contract — there is no AI SDK UI Message Stream
8
8
  * Protocol and no translation layer.
9
9
  *
10
10
  * Every event carries `source` — the agent call chain at the point of
11
11
  * emission, derived from the harness `Session.callPath`.
12
12
  */
13
+ import type { ChatPart } from "./chat-parts.js";
13
14
  import type { TokenUsageRollup } from "./usage.js";
14
15
  /**
15
16
  * Provenance stamped on every chat-stream event: the emitting agent and the
@@ -131,5 +132,11 @@ export interface ChatErrorEvent {
131
132
  reason?: string;
132
133
  source: EventSource;
133
134
  }
134
- /** The discriminated union of all Cortex-native chat-stream events. */
135
- export type CortexChatEvent = TextDeltaEvent | ToolStartedEvent | ToolFinishedEvent | FinishEvent | ChatErrorEvent;
135
+ /** The discriminated union of all chat-stream events. */
136
+ export type ChatEvent = TextDeltaEvent | ToolStartedEvent | ToolFinishedEvent | FinishEvent | ChatErrorEvent;
137
+ /** A `data-*` part as a frame, with the source of its emitter when the emitter stamped one. */
138
+ export type ChatPartFrame = ChatPart & {
139
+ source?: EventSource;
140
+ };
141
+ /** One frame of a chat stream or a run stream: a loop event or a `data-*` part. */
142
+ export type ChatFrame = ChatEvent | ChatPartFrame;
@@ -1,10 +1,10 @@
1
1
  /**
2
- * Cortex-native chat-stream events — the harness wire vocabulary.
2
+ * Chat-stream events — the harness wire vocabulary.
3
3
  *
4
4
  * Distinct from the 15 `data-*` chat parts (`chat-parts.ts`): those are UI
5
5
  * presentation payloads a tool emits; these are the agent-loop stream events
6
- * the harness chat route frames as Cortex-native SSE. Together they are the
7
- * sole Cortex↔frontend wire contract — there is no AI SDK UI Message Stream
6
+ * the host chat route frames as SSE. Together they are the sole
7
+ * host↔frontend wire contract — there is no AI SDK UI Message Stream
8
8
  * Protocol and no translation layer.
9
9
  *
10
10
  * Every event carries `source` — the agent call chain at the point of
@@ -0,0 +1,41 @@
1
+ import type { ChatDataPart, EmitEvent } from "../loop/types.js";
2
+ import type { ChatStreamEvent } from "../providers/types.js";
3
+ import type { ChatErrorEvent, ChatFrame, ChatPartFrame } from "./chat-events.js";
4
+ import type { ChatMessage, MessagePart } from "./message.js";
5
+ export type TurnTerminal = null | "finish" | {
6
+ error: ChatErrorEvent;
7
+ };
8
+ export interface ApplyFrameResult {
9
+ messages: ChatMessage[];
10
+ terminal: TurnTerminal;
11
+ }
12
+ export type ChatPartCheck = {
13
+ readonly ok: true;
14
+ readonly frame: ChatPartFrame;
15
+ } | {
16
+ readonly ok: false;
17
+ readonly error: string;
18
+ };
19
+ /**
20
+ * The frame of one emitted event, or `null` for `iteration` and `done`. A frame of a sub-agent keeps its source, thus a
21
+ * consumer can filter it. A `text-delta` has no source, thus its frame gets `fallbackSource`, the source of the root agent.
22
+ * A part frame is not checked here: {@link checkChatPart} checks it where it arrives.
23
+ */
24
+ export declare function toChatFrame(event: EmitEvent | ChatStreamEvent | ChatDataPart, fallbackSource: EmitEvent["source"]): ChatFrame | null;
25
+ /**
26
+ * Check a part frame against the schema of its type, one time, where it arrives. The schema drops a field that it
27
+ * does not know. A type that the registry does not know passes unchanged, because a newer emitter can send it. The
28
+ * source is checked for each type, because {@link isRootFrame} reads its call path.
29
+ */
30
+ export declare function checkChatPart(frame: ChatPartFrame): ChatPartCheck;
31
+ /** A frame of the root agent of a chat turn, whose call path is `[agent.id]`. A longer path is a sub-agent. */
32
+ export declare function isRootFrame(frame: ChatFrame): boolean;
33
+ /**
34
+ * The key that a part reconciles on: a tool call by its call id, and a part of a `reconciling` type by its type and
35
+ * id. A part with no key always appends. The type is part of the key, because an id is unique only in its own type.
36
+ */
37
+ export declare function reconcileKey(part: MessagePart): string | undefined;
38
+ /** Add a part, or replace the part with the same {@link reconcileKey} in the position of that part. */
39
+ export declare function upsertPart(parts: readonly MessagePart[], part: MessagePart): MessagePart[];
40
+ /** Apply one frame to the messages of a live turn, with no change to the input. A frame of a sub-agent changes nothing. */
41
+ export declare function applyChatFrame(messages: ChatMessage[], frame: ChatFrame, assistantId: string): ApplyFrameResult;
@@ -0,0 +1,173 @@
1
+ // The one translation path of a chat stream. Browser-safe: each import from outside `contracts/` is a type import.
2
+ import { isReconciling, PART_REGISTRY } from "./part-registry.js";
3
+ import { EventSourceSchema } from "./schemas/chat-events.js";
4
+ import { ChatPartSchema } from "./schemas/chat-parts.js";
5
+ function toSource(source) {
6
+ return { agentId: source.agentId, callPath: [...source.callPath] };
7
+ }
8
+ /**
9
+ * The frame of one emitted event, or `null` for `iteration` and `done`. A frame of a sub-agent keeps its source, thus a
10
+ * consumer can filter it. A `text-delta` has no source, thus its frame gets `fallbackSource`, the source of the root agent.
11
+ * A part frame is not checked here: {@link checkChatPart} checks it where it arrives.
12
+ */
13
+ export function toChatFrame(event, fallbackSource) {
14
+ switch (event.type) {
15
+ case "iteration":
16
+ case "done":
17
+ return null;
18
+ case "tool-started":
19
+ return {
20
+ type: "tool-started",
21
+ toolUseId: event.toolUseId,
22
+ name: event.name,
23
+ ...(event.detail === undefined ? {} : { detail: event.detail }),
24
+ source: toSource(event.source),
25
+ };
26
+ case "tool-finished":
27
+ return {
28
+ type: "tool-finished",
29
+ toolUseId: event.toolUseId,
30
+ name: event.name,
31
+ outcome: event.outcome,
32
+ ...(event.detail === undefined ? {} : { detail: event.detail }),
33
+ ...(event.durationMs === undefined ? {} : { durationMs: event.durationMs }),
34
+ source: toSource(event.source),
35
+ };
36
+ case "text-delta":
37
+ return { type: "text-delta", text: event.text, source: toSource(fallbackSource) };
38
+ default: {
39
+ // A `ChatDataPart`, whose payload the emitter types as `unknown`. The wire part is flat: `{ type, ...data }`.
40
+ // The cast names the frame that the type claims. `checkChatPart` is the check.
41
+ const data = (event.data ?? {});
42
+ return { ...data, type: event.type, ...(event.source === undefined ? {} : { source: toSource(event.source) }) };
43
+ }
44
+ }
45
+ }
46
+ /**
47
+ * Check a part frame against the schema of its type, one time, where it arrives. The schema drops a field that it
48
+ * does not know. A type that the registry does not know passes unchanged, because a newer emitter can send it. The
49
+ * source is checked for each type, because {@link isRootFrame} reads its call path.
50
+ */
51
+ export function checkChatPart(frame) {
52
+ const { source, ...part } = frame;
53
+ if (source !== undefined) {
54
+ const checkedSource = EventSourceSchema.safeParse(source);
55
+ if (!checkedSource.success)
56
+ return { ok: false, error: checkedSource.error.message };
57
+ }
58
+ if (!Object.hasOwn(PART_REGISTRY, frame.type))
59
+ return { ok: true, frame };
60
+ const parsed = ChatPartSchema.safeParse(part);
61
+ if (!parsed.success)
62
+ return { ok: false, error: parsed.error.message };
63
+ return { ok: true, frame: source === undefined ? parsed.data : { ...parsed.data, source } };
64
+ }
65
+ /** A frame of the root agent of a chat turn, whose call path is `[agent.id]`. A longer path is a sub-agent. */
66
+ export function isRootFrame(frame) {
67
+ return frame.source === undefined || frame.source.callPath.length <= 1;
68
+ }
69
+ /**
70
+ * The key that a part reconciles on: a tool call by its call id, and a part of a `reconciling` type by its type and
71
+ * id. A part with no key always appends. The type is part of the key, because an id is unique only in its own type.
72
+ */
73
+ export function reconcileKey(part) {
74
+ if (part.type === "tool-call")
75
+ return `tool-call:${part.toolCallId}`;
76
+ if (part.type === "text" || !Object.hasOwn(PART_REGISTRY, part.type) || !isReconciling(part.type))
77
+ return undefined;
78
+ return "id" in part && typeof part.id === "string" ? `${part.type}:${part.id}` : undefined;
79
+ }
80
+ /** Add a part, or replace the part with the same {@link reconcileKey} in the position of that part. */
81
+ export function upsertPart(parts, part) {
82
+ const key = reconcileKey(part);
83
+ const index = key === undefined ? -1 : parts.findIndex((candidate) => reconcileKey(candidate) === key);
84
+ if (index === -1)
85
+ return [...parts, part];
86
+ const next = parts.slice();
87
+ next[index] = part;
88
+ return next;
89
+ }
90
+ /** Apply one frame to the messages of a live turn, with no change to the input. A frame of a sub-agent changes nothing. */
91
+ export function applyChatFrame(messages, frame, assistantId) {
92
+ if (!isRootFrame(frame))
93
+ return { messages, terminal: null };
94
+ if (frame.type === "finish") {
95
+ return {
96
+ messages: frame.turnUsage === undefined ? messages : stampUsage(messages, assistantId, frame.turnUsage),
97
+ terminal: "finish",
98
+ };
99
+ }
100
+ if (frame.type === "error") {
101
+ return { messages, terminal: { error: frame } };
102
+ }
103
+ const partFrame = frame;
104
+ const next = updateAssistant(messages, assistantId, (parts) => applyPartFrame(parts, partFrame));
105
+ return { messages: next, terminal: null };
106
+ }
107
+ function applyPartFrame(parts, frame) {
108
+ switch (frame.type) {
109
+ case "text-delta":
110
+ return mergeTextDelta(parts, frame.text);
111
+ case "tool-started":
112
+ return [
113
+ ...parts,
114
+ {
115
+ type: "tool-call",
116
+ toolCallId: frame.toolUseId,
117
+ toolName: frame.name,
118
+ ...(frame.detail === undefined ? {} : { detail: frame.detail }),
119
+ },
120
+ ];
121
+ case "tool-finished": {
122
+ const finished = {
123
+ type: "tool-call",
124
+ toolCallId: frame.toolUseId,
125
+ toolName: frame.name,
126
+ outcome: frame.outcome,
127
+ ...(frame.detail === undefined ? {} : { detail: frame.detail }),
128
+ ...(frame.durationMs === undefined ? {} : { durationMs: frame.durationMs }),
129
+ };
130
+ let started = false;
131
+ const next = parts.map((p) => {
132
+ if (p.type !== "tool-call" || p.toolCallId !== frame.toolUseId)
133
+ return p;
134
+ started = true;
135
+ return { ...p, ...finished };
136
+ });
137
+ // The stored transcript keeps a call whose start never arrived, thus a live view keeps it too.
138
+ return started ? next : [...parts, finished];
139
+ }
140
+ default: {
141
+ // The source routes the frame, and it is not a field of the part. A rest type does not distribute over the
142
+ // union of the part frames, thus the cast names the part that remains.
143
+ const { source: _source, ...part } = frame;
144
+ return upsertPart(parts, part);
145
+ }
146
+ }
147
+ }
148
+ function mergeTextDelta(parts, delta) {
149
+ const last = parts[parts.length - 1];
150
+ if (last && last.type === "text") {
151
+ const merged = { type: "text", text: last.text + delta };
152
+ return [...parts.slice(0, -1), merged];
153
+ }
154
+ return [...parts, { type: "text", text: delta }];
155
+ }
156
+ function stampUsage(messages, assistantId, usage) {
157
+ const idx = messages.findIndex((m) => m.id === assistantId);
158
+ const message = messages[idx];
159
+ if (message === undefined)
160
+ return messages;
161
+ const next = messages.slice();
162
+ next[idx] = { ...message, usage };
163
+ return next;
164
+ }
165
+ function updateAssistant(messages, assistantId, updater) {
166
+ const idx = messages.findIndex((m) => m.id === assistantId);
167
+ const existing = messages[idx];
168
+ if (existing === undefined)
169
+ return [...messages, { id: assistantId, role: "assistant", parts: updater([]) }];
170
+ const next = messages.slice();
171
+ next[idx] = { ...existing, parts: updater(existing.parts) };
172
+ return next;
173
+ }
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Cortex chat data parts — custom parts emitted via writer.write() / writer.custom()
3
- * that flow through the chat stream and are rendered by the frontend.
2
+ * Chat data parts — the `data-*` parts that a tool or a workflow emits, and that
3
+ * a surface renders from the chat stream or the run stream.
4
4
  *
5
5
  * Categories:
6
6
  * Presentation: data-presentation (agent-synthesized content),
@@ -22,8 +22,16 @@ export interface PlanStep {
22
22
  resources?: {
23
23
  cpu: number;
24
24
  memoryGb: number;
25
+ gpu?: {
26
+ count: number;
27
+ };
25
28
  };
26
29
  maxSteps: number;
30
+ track?: string;
31
+ step_type?: string;
32
+ acceptance_criteria?: string[];
33
+ constraints?: string[];
34
+ caveats?: string[];
27
35
  }
28
36
  export type PresentationContent =
29
37
  /** `dataPath` (optional): an analysis-rooted CSV artifact path the host loads and injects as the
@@ -388,8 +396,12 @@ export interface CompactionPart {
388
396
  type: "data-compaction";
389
397
  id: string;
390
398
  status: "running" | "done" | "failed";
399
+ /** The input tokens that the provider reported for the last request before the compaction. */
391
400
  tokensBefore: number;
401
+ /** Only a part that an older harness stored carries it: an estimate of the view after the compaction. */
392
402
  tokensAfter?: number;
393
403
  durationMs?: number;
404
+ /** Before the user message of a turn, or before a later request. */
405
+ trigger?: "turn-start" | "mid-turn";
394
406
  }
395
- export type CortexChatPart = PresentationPart | PlanPart | RunCardPart | FileReferencePart | AskPart | RunStartedPart | DagStatePart | StepActivityPart | StepFileTreePart | StepOutputPart | StepSummaryPart | StepUsagePart | StepBlockedPart | RunSynthesisPart | SynthesisProgressPart | RunCompletedPart | RunFailedPart | ChildSessionStartedPart | ReportRenderedPart | CompactionPart;
407
+ export type ChatPart = PresentationPart | PlanPart | RunCardPart | FileReferencePart | AskPart | RunStartedPart | DagStatePart | StepActivityPart | StepFileTreePart | StepOutputPart | StepSummaryPart | StepUsagePart | StepBlockedPart | RunSynthesisPart | SynthesisProgressPart | RunCompletedPart | RunFailedPart | ChildSessionStartedPart | ReportRenderedPart | CompactionPart;
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Cortex chat data parts — custom parts emitted via writer.write() / writer.custom()
3
- * that flow through the chat stream and are rendered by the frontend.
2
+ * Chat data parts — the `data-*` parts that a tool or a workflow emits, and that
3
+ * a surface renders from the chat stream or the run stream.
4
4
  *
5
5
  * Categories:
6
6
  * Presentation: data-presentation (agent-synthesized content),
@@ -2,16 +2,18 @@ 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, ChildSessionStartedPart, ReportRenderedPart, CompactionPart, CortexChatPart, } from "./chat-parts.js";
6
- export type { EventSource, TextDeltaEvent, ToolStartedEvent, ToolFinishedEvent, FinishEvent, ChatErrorEvent, CortexChatEvent } from "./chat-events.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, ChildSessionStartedPart, ReportRenderedPart, CompactionPart, ChatPart, } from "./chat-parts.js";
6
+ export type { EventSource, TextDeltaEvent, ToolStartedEvent, ToolFinishedEvent, FinishEvent, ChatErrorEvent, ChatEvent, ChatFrame, ChatPartFrame, } from "./chat-events.js";
7
7
  export type { TokenUsageRollup } from "./usage.js";
8
8
  export { DATA_PROFILE_RUN_LITERAL } from "./data-profile.js";
9
9
  export type { DataProfileAxis, DataProfileChecked, DataProfileCompanionCompleteness, DataProfileCoverage, DataProfileDimension, DataProfileFile, DataProfileGroup, DataProfileGroupSlot, DataProfileInputSignature, DataProfileKind, DataProfileLifecycleStatus, DataProfileMemberAnnotation, DataProfileObservation, DataProfileOrganism, DataProfilePartition, DataProfileProbeReport, DataProfileQualityAssessment, DataProfileQuarantine, DataProfileRecipeStep, DataProfileResult, DataProfileSubjectSource, } from "./data-profile.js";
10
10
  export { DIMENSION_CATEGORIES, DIMENSION_CATEGORY_IDS, DIMENSION_PROBE_IDS, DIMENSION_PROBES, GROUP_CATEGORIES, GROUP_CATEGORY_IDS, GROUP_ROLE_IDS, GROUP_ROLES, PROBE_OUTCOME_IDS, dimensionCategoryEntry, dimensionScope, groupCategoryEntry, } from "./profile-vocabulary.js";
11
11
  export type { DimensionCategory, DimensionCategoryEntry, DimensionProbe, DimensionProbeEntry, DimensionScope, DimensionTreatment, GroupCategory, GroupCategoryEntry, GroupRole, GroupRoleEntry, ProbeOutcome, } from "./profile-vocabulary.js";
12
- export type { TextPart, ToolCallPart, CortexPart, CortexMessage } from "./message.js";
12
+ export type { TextPart, ToolCallPart, MessagePart, ChatMessage } from "./message.js";
13
+ export { toChatFrame, applyChatFrame, checkChatPart } from "./chat-frame.js";
14
+ export type { TurnTerminal, ApplyFrameResult, ChatPartCheck } from "./chat-frame.js";
13
15
  export { PART_REGISTRY, isTransient, isReconciling, isSidebarPart } from "./part-registry.js";
14
- export type { CortexChatPartType, PartDescriptor, PartEmitter, PartConsumer } from "./part-registry.js";
16
+ export type { ChatPartType, PartDescriptor, PartEmitter, PartConsumer } from "./part-registry.js";
15
17
  export { AnalogyCoverageSchema, AnalogyReportSchema, AnalogyReportErrorSchema, AnalogicalReasonerOutputSchema } from "./analogy-report.js";
16
18
  export type { AnalogyCoverage, AnalogyReport, AnalogyReportError, AnalogicalReasonerOutput } from "./analogy-report.js";
17
19
  export { buildReportSessionUrl, reportSessionResourceId } from "./content-url.js";
@@ -2,6 +2,7 @@ export { ORGAN_SYSTEMS, OrganSystemSchema, ORGAN_SYSTEM_LABELS, organSystemLabel
2
2
  export { SEVERITIES, SeveritySchema } from "./severity.js";
3
3
  export { DATA_PROFILE_RUN_LITERAL } from "./data-profile.js";
4
4
  export { DIMENSION_CATEGORIES, DIMENSION_CATEGORY_IDS, DIMENSION_PROBE_IDS, DIMENSION_PROBES, GROUP_CATEGORIES, GROUP_CATEGORY_IDS, GROUP_ROLE_IDS, GROUP_ROLES, PROBE_OUTCOME_IDS, dimensionCategoryEntry, dimensionScope, groupCategoryEntry, } from "./profile-vocabulary.js";
5
+ export { toChatFrame, applyChatFrame, checkChatPart } from "./chat-frame.js";
5
6
  export { PART_REGISTRY, isTransient, isReconciling, isSidebarPart } from "./part-registry.js";
6
7
  export { AnalogyCoverageSchema, AnalogyReportSchema, AnalogyReportErrorSchema, AnalogicalReasonerOutputSchema } from "./analogy-report.js";
7
8
  export { buildReportSessionUrl, reportSessionResourceId } from "./content-url.js";
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Cortex-owned chat message types — the wire shape, the in-memory shape,
2
+ * Chat message types — the wire shape, the in-memory shape,
3
3
  * and the public API for everything the chat path exchanges.
4
4
  *
5
5
  * No AI SDK types appear here or anywhere else in the package. The
@@ -7,7 +7,7 @@
7
7
  * narrows the union member without any `as` cast.
8
8
  */
9
9
  import type { ToolCallDetail, ToolCallOutcome } from "./chat-events.js";
10
- import type { CortexChatPart } from "./chat-parts.js";
10
+ import type { ChatPart } from "./chat-parts.js";
11
11
  import type { TokenUsageRollup } from "./usage.js";
12
12
  /** A plain assistant/user text run. */
13
13
  export interface TextPart {
@@ -38,14 +38,16 @@ export interface ToolCallPart {
38
38
  outcome?: ToolCallOutcome;
39
39
  /** See {@link ToolCallDetail}. Absent when no detail was produced. */
40
40
  detail?: ToolCallDetail;
41
+ /** The time in milliseconds around the dispatch of the call, from `tool-finished`. Absent when no measurement was taken. */
42
+ durationMs?: number;
41
43
  }
42
44
  /** The discriminated union of every part the chat path can carry. */
43
- export type CortexPart = TextPart | ToolCallPart | CortexChatPart;
45
+ export type MessagePart = TextPart | ToolCallPart | ChatPart;
44
46
  /** A single chat message — user, assistant, or system. */
45
- export interface CortexMessage {
47
+ export interface ChatMessage {
46
48
  id: string;
47
49
  role: "user" | "assistant" | "system";
48
- parts: CortexPart[];
50
+ parts: MessagePart[];
49
51
  /**
50
52
  * Set when this message's production was cut off by a client abort, so the UI
51
53
  * can badge it. Absent means not interrupted — the field is optional so every
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Cortex-owned chat message types — the wire shape, the in-memory shape,
2
+ * Chat message types — the wire shape, the in-memory shape,
3
3
  * and the public API for everything the chat path exchanges.
4
4
  *
5
5
  * No AI SDK types appear here or anywhere else in the package. The