diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 7586e1d5..3eef6b24 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -17,6 +17,8 @@ "uuid": "^13.0.0" }, "devDependencies": { + "@mariozechner/pi-agent-core": "^0.50.3", + "@mariozechner/pi-ai": "^0.50.3", "@types/uuid": "^11.0.0", "typescript": "^5.9.3" } diff --git a/packages/sdk/src/actions/index.ts b/packages/sdk/src/actions/index.ts index e73947a4..4a28bc4f 100644 --- a/packages/sdk/src/actions/index.ts +++ b/packages/sdk/src/actions/index.ts @@ -33,8 +33,15 @@ export { export { StreamAction, type StreamPayload, - type StreamEvent, - type StreamMessageEvent, - type StreamToolEvent, + type AgentEvent, + type ContentBlock, + type TextContent, + type ThinkingContent, + type ToolCall, + type ImageContent, + // Backward-compatible aliases + type TextBlock, + type ThinkingBlock, + type ToolCallBlock, extractTextFromEvent, } from "./stream"; diff --git a/packages/sdk/src/actions/rpc.ts b/packages/sdk/src/actions/rpc.ts index 1f49f1d1..32c605de 100644 --- a/packages/sdk/src/actions/rpc.ts +++ b/packages/sdk/src/actions/rpc.ts @@ -1,5 +1,7 @@ /** RPC Actions - 请求/响应模式 */ +import type { Message } from "@mariozechner/pi-ai"; + export const RequestAction = "request" as const; export const ResponseAction = "response" as const; @@ -65,34 +67,11 @@ export interface GetAgentMessagesParams { limit?: number; } -/** Content block types from the agent engine */ -export interface TextContentBlock { - type: "text"; - text: string; -} - -export interface ThinkingContentBlock { - type: "thinking"; - thinking: string; -} - -export interface ToolCallBlock { - type: "tool_use"; - id: string; - name: string; - input: unknown; -} - -export interface ImageContentBlock { - type: "image"; - url: string; -} - -/** Agent message returned by getAgentMessages (mirrors pi-ai Message) */ -export type AgentMessageItem = - | { role: "user"; content: string | (TextContentBlock | ImageContentBlock)[]; timestamp: number } - | { role: "assistant"; content: (TextContentBlock | ThinkingContentBlock | ToolCallBlock)[]; timestamp: number } - | { role: "tool_result"; toolCallId: string; content: (TextContentBlock | ImageContentBlock)[]; isError: boolean; timestamp: number } +/** + * Agent message returned by getAgentMessages. + * This is pi-ai's Message type — the backend returns it as-is from SessionManager.loadMessages(). + */ +export type AgentMessageItem = Message; /** getAgentMessages - response payload */ export interface GetAgentMessagesResult { diff --git a/packages/sdk/src/actions/stream.ts b/packages/sdk/src/actions/stream.ts index 51329040..8babb7c0 100644 --- a/packages/sdk/src/actions/stream.ts +++ b/packages/sdk/src/actions/stream.ts @@ -1,49 +1,47 @@ -/** Stream Action - 流式消息传输 */ +/** Stream Action */ export const StreamAction = "stream" as const; +// --- Content block types (re-exported from pi-ai, the single source of truth) --- + +import type { + TextContent, + ThinkingContent, + ToolCall, + ImageContent, +} from "@mariozechner/pi-ai"; +import type { AgentEvent } from "@mariozechner/pi-agent-core"; + +export type { TextContent, ThinkingContent, ToolCall, ImageContent }; +export type { AgentEvent }; + +/** Backward-compatible aliases */ +export type TextBlock = TextContent; +export type ThinkingBlock = ThinkingContent; +export type ToolCallBlock = ToolCall; +export type ContentBlock = TextContent | ThinkingContent | ToolCall | ImageContent; + +// --- Stream event types --- + /** - * AgentEvent types forwarded by the Hub to frontend clients. - * These mirror the subset of AgentEvent from @mariozechner/pi-agent-core - * that the Hub forwards (filtered at the Hub layer). + * Hub forwards AgentEvent from pi-agent-core as-is. + * StreamPayload wraps it with routing metadata. */ -export interface StreamMessageEvent { - type: "message_start" | "message_update" | "message_end"; - message: { - id?: string; - role: string; - content?: Array<{ type: string; text?: string }>; - }; - assistantMessageEvent?: unknown; -} - -export interface StreamToolEvent { - type: "tool_execution_start" | "tool_execution_end"; - toolCallId: string; - toolName: string; - args?: unknown; - result?: unknown; - isError?: boolean; -} - -export type StreamEvent = StreamMessageEvent | StreamToolEvent; - -/** 流消息 payload — wraps a raw AgentEvent with stream/agent identifiers */ export interface StreamPayload { - /** 流 ID,关联同一个流的所有消息 */ streamId: string; - /** 所属 agent ID */ agentId: string; - /** Raw agent event from the engine */ - event: StreamEvent; + event: AgentEvent; } -/** Extract plain text from an AgentMessage content array */ -export function extractTextFromEvent(event: StreamMessageEvent): string { - const content = event.message?.content; +/** Extract plain text from an AgentEvent that carries a message */ +export function extractTextFromEvent(event: AgentEvent): string { + if (!("message" in event)) return ""; + const msg = event.message; + if (!msg || !("content" in msg)) return ""; + const content = msg.content; if (!Array.isArray(content)) return ""; return content - .filter((c) => c.type === "text") + .filter((c): c is TextContent => c.type === "text") .map((c) => c.text ?? "") .join(""); } diff --git a/packages/store/src/connection-store.ts b/packages/store/src/connection-store.ts index 19dde590..998a34f9 100644 --- a/packages/store/src/connection-store.ts +++ b/packages/store/src/connection-store.ts @@ -19,13 +19,14 @@ import { v7 as uuidv7 } from "uuid" import { GatewayClient, StreamAction, - extractTextFromEvent, type ConnectionState, type SendErrorResponse, type StreamPayload, - type StreamMessageEvent, + type AgentEvent, + type GetAgentMessagesResult, + type ContentBlock, } from "@multica/sdk" -import { useMessagesStore } from "./messages" +import { useMessagesStore, type Message } from "./messages" import { clearConnection, type ConnectionInfo } from "./connection" interface ConnectionStoreState { @@ -104,23 +105,40 @@ function createClient( switch (event.type) { case "message_start": { store.startStream(payload.streamId, payload.agentId) - const text = extractTextFromEvent(event as StreamMessageEvent) - if (text) store.appendStream(payload.streamId, text) + const content = extractContent(event) + if (content.length) store.appendStream(payload.streamId, content) break } case "message_update": { - const text = extractTextFromEvent(event as StreamMessageEvent) - store.appendStream(payload.streamId, text) + const content = extractContent(event) + store.appendStream(payload.streamId, content) break } case "message_end": { - const text = extractTextFromEvent(event as StreamMessageEvent) - store.endStream(payload.streamId, text) + const content = extractContent(event) + const stopReason = "message" in event + ? (event.message as { stopReason?: string })?.stopReason + : undefined + store.endStream(payload.streamId, content, stopReason) break } - case "tool_execution_start": - case "tool_execution_end": + case "tool_execution_start": { + store.startToolExecution( + payload.agentId, + event.toolCallId, + event.toolName, + event.args, + ) break + } + case "tool_execution_end": { + store.endToolExecution( + event.toolCallId, + event.result, + event.isError, + ) + break + } } return } @@ -140,20 +158,41 @@ async function fetchHistory(state: ConnectionStoreState): Promise { if (!client || !hubId || !agentId) return try { - const result = await client.request<{ - messages: Array<{ role: string; content: unknown }> - total: number - }>(hubId, "getAgentMessages", { agentId, limit: 200 }) + const result = await client.request( + hubId, "getAgentMessages", { agentId, limit: 200 }, + ) - const messages = result.messages - .filter((m) => m.role === "user" || m.role === "assistant") - .map((m) => ({ - id: uuidv7(), - role: m.role as "user" | "assistant", - content: extractText(m.content), - agentId: agentId, - })) - .filter((m) => m.content.length > 0) + // Mirror the backend message array directly + const messages: Message[] = [] + for (const m of result.messages) { + if (m.role === "user") { + messages.push({ + id: uuidv7(), + role: "user", + content: toContentBlocks(m.content), + agentId, + }) + } else if (m.role === "assistant") { + messages.push({ + id: uuidv7(), + role: "assistant", + content: toContentBlocks(m.content), + agentId, + stopReason: m.stopReason, + }) + } else if (m.role === "toolResult") { + messages.push({ + id: uuidv7(), + role: "toolResult", + content: toContentBlocks(m.content), + agentId, + toolCallId: m.toolCallId, + toolName: m.toolName, + toolStatus: m.isError ? "error" : "success", + isError: m.isError, + }) + } + } if (messages.length > 0) { useMessagesStore.getState().loadMessages(messages) @@ -163,14 +202,22 @@ async function fetchHistory(state: ConnectionStoreState): Promise { } } -/** Extract plain text from AgentMessage content (string or content block array) */ -function extractText(content: unknown): string { - if (typeof content === "string") return content - if (!Array.isArray(content)) return "" - return content - .filter((c: { type?: string }) => c.type === "text") - .map((c: { text?: string }) => c.text ?? "") - .join("") +/** Convert raw backend content (string or block array) to ContentBlock[] */ +function toContentBlocks(content: string | ContentBlock[]): ContentBlock[] { + if (typeof content === "string") { + return content ? [{ type: "text", text: content }] : [] + } + if (Array.isArray(content)) return content + return [] +} + +/** Extract content blocks from an AgentEvent that carries a message */ +function extractContent(event: AgentEvent): ContentBlock[] { + if (!("message" in event)) return [] + const msg = event.message + if (!msg || !("content" in msg)) return [] + const content = msg.content + return Array.isArray(content) ? content as ContentBlock[] : [] } export const useConnectionStore = create()( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8fd147e6..0eeabcd3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -427,6 +427,12 @@ importers: specifier: ^13.0.0 version: 13.0.0 devDependencies: + '@mariozechner/pi-agent-core': + specifier: ^0.50.3 + version: 0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.3.6))(ws@8.18.3)(zod@4.3.6) + '@mariozechner/pi-ai': + specifier: ^0.50.3 + version: 0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.3.6))(ws@8.18.3)(zod@4.3.6) '@types/uuid': specifier: ^11.0.0 version: 11.0.0 @@ -9094,6 +9100,12 @@ snapshots: optionalDependencies: zod: 3.25.76 + '@anthropic-ai/sdk@0.71.2(zod@4.3.6)': + dependencies: + json-schema-to-ts: 3.1.1 + optionalDependencies: + zod: 4.3.6 + '@aws-crypto/crc32@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 @@ -10876,6 +10888,17 @@ snapshots: - supports-color - utf-8-validate + '@google/genai@1.34.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.3.6))': + dependencies: + google-auth-library: 10.5.0 + ws: 8.18.3 + optionalDependencies: + '@modelcontextprotocol/sdk': 1.25.3(hono@4.11.7)(zod@4.3.6) + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + '@hono/node-server@1.19.9(hono@4.11.7)': dependencies: hono: 4.11.7 @@ -11218,6 +11241,19 @@ snapshots: - ws - zod + '@mariozechner/pi-agent-core@0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.3.6))(ws@8.18.3)(zod@4.3.6)': + dependencies: + '@mariozechner/pi-ai': 0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.3.6))(ws@8.18.3)(zod@4.3.6) + '@mariozechner/pi-tui': 0.50.3 + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - aws-crt + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + '@mariozechner/pi-ai@0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@3.25.76))(ws@8.18.3)(zod@3.25.76)': dependencies: '@anthropic-ai/sdk': 0.71.2(zod@3.25.76) @@ -11242,6 +11278,30 @@ snapshots: - ws - zod + '@mariozechner/pi-ai@0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.3.6))(ws@8.18.3)(zod@4.3.6)': + dependencies: + '@anthropic-ai/sdk': 0.71.2(zod@4.3.6) + '@aws-sdk/client-bedrock-runtime': 3.978.0 + '@google/genai': 1.34.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.3.6)) + '@mistralai/mistralai': 1.10.0 + '@sinclair/typebox': 0.34.48 + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + chalk: 5.6.2 + openai: 6.10.0(ws@8.18.3)(zod@4.3.6) + partial-json: 0.1.7 + proxy-agent: 6.5.0 + undici: 7.19.2 + zod-to-json-schema: 3.25.1(zod@4.3.6) + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - aws-crt + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + '@mariozechner/pi-coding-agent@0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@3.25.76))(ws@8.18.3)(zod@3.25.76)': dependencies: '@mariozechner/clipboard': 0.3.0 @@ -11305,6 +11365,29 @@ snapshots: - hono - supports-color + '@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.3.6)': + dependencies: + '@hono/node-server': 1.19.9(hono@4.11.7) + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 7.5.1(express@5.2.1) + jose: 6.1.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 4.3.6 + zod-to-json-schema: 3.25.1(zod@4.3.6) + transitivePeerDependencies: + - hono + - supports-color + optional: true + '@mozilla/readability@0.6.0': {} '@mswjs/interceptors@0.40.0': @@ -14376,7 +14459,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: @@ -14407,7 +14490,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -17124,6 +17207,11 @@ snapshots: ws: 8.18.3 zod: 3.25.76 + openai@6.10.0(ws@8.18.3)(zod@4.3.6): + optionalDependencies: + ws: 8.18.3 + zod: 4.3.6 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -19414,6 +19502,10 @@ snapshots: dependencies: zod: 3.25.76 + zod-to-json-schema@3.25.1(zod@4.3.6): + dependencies: + zod: 4.3.6 + zod-validation-error@4.0.2(zod@4.3.6): dependencies: zod: 4.3.6