refactor(sdk): unify message types with pi-ai source of truth
Replace hand-written message/content types in @multica/sdk with `import type` from @mariozechner/pi-ai and @mariozechner/pi-agent-core. This ensures compile-time correctness and eliminates type drift between backend and frontend (e.g. "tool_use" vs "toolCall", "tool_result" vs "toolResult"). - stream.ts: re-export TextContent, ThinkingContent, ToolCall, ImageContent from pi-ai; use AgentEvent from pi-agent-core - rpc.ts: AgentMessageItem = pi-ai Message (no more manual mirroring) - connection-store.ts: use SDK types instead of inline hand-written ones - ContentBlock now includes ImageContent to match backend reality Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
caee37c631
commit
ef4e57ffdd
6 changed files with 224 additions and 99 deletions
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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("");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
|||
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<GetAgentMessagesResult>(
|
||||
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<void> {
|
|||
}
|
||||
}
|
||||
|
||||
/** 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<ConnectionStore>()(
|
||||
|
|
|
|||
96
pnpm-lock.yaml
generated
96
pnpm-lock.yaml
generated
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue