From e2adc9623ec8135d05ba031ab826d4e3886a6a29 Mon Sep 17 00:00:00 2001 From: Kaiyi Date: Mon, 25 May 2026 20:45:01 +0800 Subject: [PATCH 01/48] feat(agent-core): re-export wire record types for in-monorepo consumers --- packages/agent-core/src/index.ts | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/packages/agent-core/src/index.ts b/packages/agent-core/src/index.ts index 5e972d9..36a44a7 100644 --- a/packages/agent-core/src/index.ts +++ b/packages/agent-core/src/index.ts @@ -43,3 +43,33 @@ export type { BearerTokenProvider, OAuthTokenProviderResolver, } from './providers/runtime-provider'; + +// ─── Wire records (for in-monorepo consumers like apps/vis) ──────────────── +export type { + AgentRecord, + AgentRecordEvents, + AgentRecordOf, + AgentRecordPersistence, +} from './agent/records'; +export { AGENT_WIRE_PROTOCOL_VERSION } from './agent/records'; +export type { AgentConfigUpdateData } from './agent/config'; +export type { CompactionBeginData, CompactionResult } from './agent/compaction'; +export type { + PermissionApprovalResultRecord, + PermissionMode, +} from './agent/permission'; +export type { UsageRecordScope } from './agent/usage'; +export type { ToolStoreUpdate } from './tools/store'; +export type { + LoopRecordedEvent, + LoopStepBeginEvent, + LoopStepEndEvent, + LoopContentPartEvent, + LoopToolCallEvent, + LoopToolResultEvent, +} from './loop'; +export type { + ExecutableToolResult, + ExecutableToolSuccessResult, + ExecutableToolErrorResult, +} from './loop/types'; From be38639f6e46ca1b4cb4cde8794d589b626dd2bd Mon Sep 17 00:00:00 2001 From: Kaiyi Date: Mon, 25 May 2026 20:50:16 +0800 Subject: [PATCH 02/48] chore(vis): purge legacy wire protocol code --- apps/vis/server/src/app.ts | 2 - apps/vis/server/src/lib/context-builder.ts | 498 ------------- apps/vis/server/src/lib/subagent-loader.ts | 213 ------ apps/vis/server/src/lib/types.ts | 725 ------------------- apps/vis/server/src/lib/wire-loader.ts | 179 ----- apps/vis/server/src/lib/wire-replay.ts | 509 ------------- apps/vis/server/src/routes/context.ts | 56 +- apps/vis/server/src/routes/session-detail.ts | 154 +--- apps/vis/server/src/routes/sessions.ts | 27 +- apps/vis/server/src/routes/subagents.ts | 225 +----- apps/vis/server/src/routes/tool-results.ts | 48 -- apps/vis/server/src/routes/wire.ts | 83 +-- apps/vis/server/test/_fixture.ts | 251 ------- apps/vis/server/test/app.test.ts | 74 -- apps/vis/server/test/context-builder.test.ts | 242 ------- apps/vis/server/test/session-lister.test.ts | 116 --- apps/vis/server/test/subagent-loader.test.ts | 47 -- apps/vis/server/test/wire-loader.test.ts | 558 -------------- 18 files changed, 15 insertions(+), 3992 deletions(-) delete mode 100644 apps/vis/server/src/lib/context-builder.ts delete mode 100644 apps/vis/server/src/lib/subagent-loader.ts delete mode 100644 apps/vis/server/src/lib/types.ts delete mode 100644 apps/vis/server/src/lib/wire-loader.ts delete mode 100644 apps/vis/server/src/lib/wire-replay.ts delete mode 100644 apps/vis/server/src/routes/tool-results.ts delete mode 100644 apps/vis/server/test/_fixture.ts delete mode 100644 apps/vis/server/test/app.test.ts delete mode 100644 apps/vis/server/test/context-builder.test.ts delete mode 100644 apps/vis/server/test/session-lister.test.ts delete mode 100644 apps/vis/server/test/subagent-loader.test.ts delete mode 100644 apps/vis/server/test/wire-loader.test.ts diff --git a/apps/vis/server/src/app.ts b/apps/vis/server/src/app.ts index 41b9abb..e4a2a3f 100644 --- a/apps/vis/server/src/app.ts +++ b/apps/vis/server/src/app.ts @@ -8,7 +8,6 @@ import { contextRoute } from './routes/context'; import { sessionDetailRoute } from './routes/session-detail'; import { sessionsRoute } from './routes/sessions'; import { subagentsRoute } from './routes/subagents'; -import { toolResultsRoute } from './routes/tool-results'; import { wireRoute } from './routes/wire'; /** Resolve the SPA bundle directory next to the compiled server.mjs, if it @@ -89,7 +88,6 @@ export async function createApp(options: CreateAppOptions = {}): Promise { api.route('/sessions', wireRoute()); api.route('/sessions', contextRoute()); api.route('/sessions', subagentsRoute()); - api.route('/sessions', toolResultsRoute()); app.route('/api', api); diff --git a/apps/vis/server/src/lib/context-builder.ts b/apps/vis/server/src/lib/context-builder.ts deleted file mode 100644 index 9cfdbe8..0000000 --- a/apps/vis/server/src/lib/context-builder.ts +++ /dev/null @@ -1,498 +0,0 @@ -import type { - AnnotatedMessage, - ContentPart, - NotificationRecord, - ProjectedStateSummary, - SessionInitializedRecord, - SessionState, - ToolCallEntry, - UserInputPart, - VisWireRecord, -} from './types'; - -const PERSISTED_OUTPUT_RE = /^/; - -/** - * Extract the on-disk path from a `` marker - * at the start of a tool-result output. Returns null when no marker is - * present. - */ -export function extractPersistedOutputPath(text: string): string | null { - const m = PERSISTED_OUTPUT_RE.exec(text.trimStart()); - return m?.[1] ?? null; -} - -/** - * Render a notification's `data` payload as the XML string that the live - * ContextMemory would inject as a user-role message. - */ -export function renderNotificationXml(data: NotificationRecord['data']): string { - const id = escAttr(data.id); - const category = escAttr(data.category); - const type = escAttr(data.type); - const sourceKind = escAttr(data.source_kind); - const sourceId = escAttr(data.source_id); - const title = data.title; - const severity = data.severity; - const body = data.body; - - const lines: string[] = [ - ``, - ]; - if (title.length > 0) lines.push(`Title: ${title}`); - if (severity.length > 0) lines.push(`Severity: ${severity}`); - if (body.length > 0) lines.push(body); - - // background_task notifications append a block - // with the last 20 lines / 3000 chars of tail_output, matching what - // the live projector would emit. - if (data.source_kind === 'background_task') { - const tail = (data as { tail_output?: string }).tail_output ?? ''; - if (tail.length > 0) { - const tailLines = tail.split('\n'); - const trimmed = (tailLines.length > 20 ? tailLines.slice(-20) : tailLines) - .join('\n') - .slice(-3000); - lines.push('', trimmed, ''); - } - } - - lines.push(''); - return lines.join('\n'); -} - -function escAttr(s: string): string { - if (s.length === 0) return 'unknown'; - return s.replaceAll('&', '&').replaceAll('"', '"'); -} - -export interface BuildAnnotatedOptions { - /** When true (default), rewind-orphaned records are kept with `out_of_context: true`. */ - preserveOutOfContext?: boolean; -} - -/** - * Walk `records` in order and emit an `AnnotatedMessage[]` that includes - * notification and system_reminder synthetic user messages. Tracks a - * numeric turn counter so `context_edit operation:'rewind' to_turn:N` - * can flag later messages as out-of-context. - */ -export function buildAnnotatedMessages( - records: readonly VisWireRecord[], - options?: BuildAnnotatedOptions, -): AnnotatedMessage[] { - const preserveOutOfContext = options?.preserveOutOfContext ?? true; - - // Track (messageIndex -> turnCounterAtEmission) so a rewind can mark - // later emissions as out_of_context. - let annotated: AnnotatedMessage[] = []; - const msgTurnIdx: number[] = []; - - let turnCounter = 0; - - // An assistant message is not a single record — it's coalesced from - // `step_begin → (content_part|tool_call)* → step_end` atoms anchored - // by `step_uuid`. We buffer the in-flight step here and emit a - // synthetic assistant message on step_end. - interface StepBuffer { - seq: number; - step_uuid: string; - text: string; - think: string; - think_encrypted?: string | undefined; - tool_calls: ToolCallEntry[]; - } - let currentStep: StepBuffer | null = null; - - const flushStep = (): void => { - if (currentStep === null) return; - const s = currentStep; - const content: ContentPart[] = []; - if (s.think.length > 0) { - const part: ContentPart = { type: 'think', think: s.think }; - if (s.think_encrypted !== undefined) part['encrypted'] = s.think_encrypted; - content.push(part); - } - if (s.text.length > 0) content.push({ type: 'text', text: s.text }); - if (content.length > 0 || s.tool_calls.length > 0) { - pushMsg({ - seq: s.seq, - message: { role: 'assistant', content, tool_calls: s.tool_calls }, - origin: { kind: 'assistant' }, - is_ephemeral: false, - out_of_context: false, - }); - } - currentStep = null; - }; - - const pushMsg = (m: AnnotatedMessage): void => { - annotated.push(m); - msgTurnIdx.push(turnCounter); - }; - - for (const r of records) { - switch (r.type) { - case 'turn_begin': { - // Close any dangling step so turn boundaries stay clean. - flushStep(); - turnCounter += 1; - break; - } - - case 'user_message': { - const text = userContentToText(r.content); - pushMsg({ - seq: r.seq, - message: { - role: 'user', - content: [{ type: 'text', text }], - tool_calls: [], - }, - origin: { kind: 'user' }, - is_ephemeral: false, - out_of_context: false, - }); - break; - } - - case 'step_begin': { - // Any prior unclosed step gets flushed; protocol guarantees order - // step_begin → … → step_end, but crashes can leave orphans. - flushStep(); - currentStep = { - seq: r.seq, - step_uuid: r.uuid, - text: '', - think: '', - tool_calls: [], - }; - break; - } - - case 'content_part': { - if (currentStep === null) break; - if (currentStep.step_uuid !== r.step_uuid) break; - if (r.part.kind === 'text') { - currentStep.text += r.part.text; - } else { - currentStep.think += r.part.think; - if (r.part.encrypted !== undefined) { - currentStep.think_encrypted = r.part.encrypted; - } - } - break; - } - - case 'tool_call': { - if (currentStep === null) break; - if (currentStep.step_uuid !== r.step_uuid) break; - currentStep.tool_calls.push({ - type: 'function', - id: r.data.tool_call_id, - name: r.data.tool_name, - arguments: r.data.args === undefined ? null : JSON.stringify(r.data.args), - }); - break; - } - - case 'step_end': { - // step_end may carry usage; we drop it here since the annotated - // message already represents what the LLM emitted. Usage lives - // on the wire record itself for the Wire tab to show. - flushStep(); - break; - } - - case 'tool_result': { - const text = typeof r.output === 'string' ? r.output : JSON.stringify(r.output); - const msg: AnnotatedMessage = { - seq: r.seq, - message: { - role: 'tool', - content: [{ type: 'text', text }], - tool_calls: [], - tool_call_id: r.tool_call_id, - }, - origin: { kind: 'tool', tool_call_id: r.tool_call_id }, - is_ephemeral: false, - out_of_context: false, - }; - if (typeof r.output === 'string') { - const persistedPath = extractPersistedOutputPath(r.output); - if (persistedPath !== null) { - msg.persisted_output_path = persistedPath; - } - } - pushMsg(msg); - break; - } - - case 'system_reminder': { - const text = `\n${r.content}\n`; - pushMsg({ - seq: r.seq, - message: { role: 'user', content: [{ type: 'text', text }], tool_calls: [] }, - origin: { kind: 'system_reminder', seq: r.seq }, - is_ephemeral: true, - out_of_context: false, - }); - break; - } - - case 'notification': { - if (!isLlmVisibleNotification(r)) break; - const text = renderNotificationXml(r.data); - pushMsg({ - seq: r.seq, - message: { role: 'user', content: [{ type: 'text', text }], tool_calls: [] }, - origin: { - kind: 'notification', - seq: r.seq, - notification_id: r.data.id, - severity: r.data.severity, - }, - is_ephemeral: true, - out_of_context: false, - }); - break; - } - - case 'compaction': { - // Wholesale replacement. Drops any open step buffer — atoms - // before compaction are superseded by the summary. - currentStep = null; - annotated = []; - msgTurnIdx.length = 0; - pushMsg({ - seq: r.seq, - message: { - role: 'assistant', - content: [{ type: 'text', text: r.summary }], - tool_calls: [], - }, - origin: { kind: 'assistant' }, - is_ephemeral: false, - out_of_context: false, - }); - break; - } - - case 'context_cleared': { - currentStep = null; - annotated = []; - msgTurnIdx.length = 0; - break; - } - - case 'context_edit': { - if (r.operation === 'rewind' && typeof r.to_turn === 'number') { - const toTurn = r.to_turn; - if (preserveOutOfContext) { - for (let i = 0; i < annotated.length; i += 1) { - const emittedAtTurn = msgTurnIdx[i] ?? 0; - if (emittedAtTurn > toTurn) { - // Replace with a marked copy (immutability not required but - // makes the "after rewind" behaviour obvious to readers). - const m = annotated[i]; - if (m !== undefined) { - annotated[i] = { ...m, out_of_context: true }; - } - } - } - } else { - // Drop messages emitted in turns past `to_turn`. - const keep: AnnotatedMessage[] = []; - const keepTurns: number[] = []; - for (let i = 0; i < annotated.length; i += 1) { - const emittedAtTurn = msgTurnIdx[i] ?? 0; - if (emittedAtTurn <= toTurn) { - const m = annotated[i]; - if (m !== undefined) { - keep.push(m); - keepTurns.push(emittedAtTurn); - } - } - } - annotated = keep; - msgTurnIdx.length = 0; - msgTurnIdx.push(...keepTurns); - } - } - break; - } - - case 'approval_request': - case 'approval_response': - case 'metadata': - case 'ownership_changed': - case 'session_initialized': - case 'skill_completed': - case 'skill_invoked': - case 'subagent_completed': - case 'subagent_failed': - case 'subagent_spawned': - case 'system_prompt_changed': - case 'team_mail': - case 'tool_denied': - case 'tools_changed': - case 'turn_end': - break; - - default: - break; - } - } - - // Flush any trailing open step (session ended mid-step). - flushStep(); - - return annotated; -} - -function userContentToText(content: string | readonly UserInputPart[]): string { - if (typeof content === 'string') return content; - return content - .map((p) => { - if (p.type === 'text') return p.text; - if (p.type === 'image_url') return ``; - return `