/** * 防腐层端口契约(ACL ports)。 * * 本文件是 linkque-agent-runtime 防腐层的「类型契约层」,定义四个端口: * - {@link IAgentLoop}:**向下端口**,把一次请求跑成 {@link AgentStreamEvent} 流。 * 默认实现 {@link PiAgentLoop} 基于 pi-agent-core;换 opencode / google-adk 时 * 只需新写一个 IAgentLoop 实现,不动组合根与上行端口。 * - {@link ISessionManager} / {@link IMcpServerProvider} / {@link ISkillLoader}:**向上端口**, * 由各运行环境(Appbase、CLI agentdev、未来其它 host)提供自己的实现;本包提供 * 零副作用的默认适配器,确保「不注入任何端口」时行为等同改造前。 * * 设计要点: * - **环境无关**:本文件只依赖 {@link IAgentAuth}/{@link AgentStreamEvent}(agent 语义类型, * 不带 pi 痕迹)与 {@link ISkillResource}/{@link IMcpResource}(agentConfig 的配置级资源抽象, * 纯类型无运行时)。绝不 import `@earendil-works/pi-*` 或 `@modelcontextprotocol/sdk`。 * - **env 无关工具模型**:{@link IAgentToolDescriptor} 用「name + inputSchema + execute 闭包」描述, * 不绑 pi 的 `AgentTool`,各下行适配器各自转译成原生工具形态。 * - **命名约定**:端口接口由外部环境实现,属公共扩展点,遵循 `IAgentSession`/ * `IAuthorizationSigner` 先例——**导出且不加 `I` 前缀**(I 前缀仅限 agentConfig 那类 * 未导出的内部配置协议层)。 */ import type { IAgentAuth, AgentStreamEvent, IAgentStreamA2UICard } from './types.js'; import type { IMcpResource, ISkillResource } from '../vendor/linkque-agent-protocol/index.js'; /** 思考强度枚举值(与 pi-agent-core ThinkingLevel 对齐的 env 无关字面量)。 */ export type ThinkingLevel = 'off' | 'low' | 'medium' | 'high'; /** * 内核 runAgentLoop 的请求消息(env 无关,core 不依赖 conversation-contract)。 * impl/agui 下行适配器负责把 `ConversationRunRequest.messages` 映射成它。 */ export interface IAgentRunMessage { /** 发言角色(env 无关)。 */ role: AgentTurnRole; /** 消息正文(字符串或结构化内容,原样透传);缺省视为空。 */ content?: unknown; /** 可选发言者名(多 user 场景)。 */ name?: string; } /** * A2UI 卡片触发的新一轮 Agent 输入。 * * 该类型是 Conversation Contract 的环境无关镜像,使 AgentLoop 能显式识别卡片 * Event,同时保持 core 不依赖外部协议包。`displayText` 是用户在会话中看到的自然 * 语言,`event` 则携带 Skill 可稳定消费的事件名与精确参数。 */ export interface IAgentA2UIEventInput { type: 'a2ui.event'; invocationId: string; displayText?: string; event: { name: string; payload: Record; }; source: { itemId: string; surfaceId: string; catalogId?: string; componentId: string; componentName: string; instanceIndex?: number; cardRef?: string; cardRevision?: string; cardInstanceId?: string; }; } /** * 内核 `AgentRuntime.runAgentLoop` 的请求入参(env 无关,尽量简洁)。 * 不直接复用 conversation-contract 的 `ConversationRunRequest`,使 core 零外部协议依赖; * impl/agui 在 SSE 入口负责协议→本类型的映射。 */ export interface IAgentRunRequest { /** 会话线程 id */ threadId: string; /** 本次 run id(诊断)。 */ runId: string; /** 本次新消息(不含历史回灌;历史由 sessionManager.load 在内核内并入)。 */ messages: IAgentRunMessage[]; } /** * 一次 agent 执行的环境无关输入。由组合根从 conversation 请求 + 上行端口拼装而来, * 交给 {@link IAgentLoop.run} 消费。 */ export interface IAgentLoopRequest { /** * 完整多轮消息(内核组装:sessionManager 历史回灌 + 本轮新消息)。 * 下行适配器按原生形态消费——pi 适配器内部拍扁成字符串(opencode / google-adk 等可直接吃 messages)。 */ messages: IAgentTurn[]; /** 模型 provider + apiKey(进程内注入,不落盘)。 */ auth: IAgentAuth; /** * 系统提示词(可选;来自 protocol.agent.systemPrompt.content,经 AgentRuntime.create * 固化进实例,runAgentLoop 透传至此)。下行适配器(PiAgentLoop)把它并入 prompt 或 * 原生 system 通道。 */ systemPrompt?: string; /** 主模型 id(可选,缺省由 provider 选首个可用模型)。 */ model?: string; /** 思考强度(可选)。 */ thinkingLevel?: ThinkingLevel; /** 工作目录(可选,工具执行上下文)。 */ cwd?: string; /** 取消信号(可选,用于终止模型请求和 Agent Session)。 */ signal?: AbortSignal; /** * skillLoader 解析出的技能正文。默认下行适配器(PiAgentLoop)以 `` 段落 * 并入 prompt;换其它 agent loop 时可改注入 systemPrompt 或原生 skill 机制。 */ skillContents?: string[]; /** * mcpServerProvider 汇总的可调用工具(env 无关描述,带 execute 闭包)。 * PiAgentLoop 会把它们转为真实 AgentTool;MCP 与 CARD 工具均在组合根完成命名隔离。 */ tools?: IAgentToolDescriptor[]; /** 会话标识,供 sessionManager 关联历史,缺省表示无会话。 */ sessionId?: string; /** * 智能体标识(计费归因 key)。由组合根从可信运行上下文(runtimeContext.agentId) * 透传至此,下行适配器据此把它注入到模型网关请求头(Linkque 网关经 `x-agent-id` * 读取,用于模型用量上报)。绝不从模型/客户端字段名推断。 */ agentId?: string; /** * 智能体运行 ID(网关 `x-run-id`,用量上报归因维度)。源自 {@link IAgentRunRequest.runId} * (runConversation 已把 ConversationRunRequest.runId 写入)。下行适配器据此注入网关 * 请求头,落库 `run_id` 列供链路归因;缺失则不注入,网关侧 `run_id` 为 null。 */ runId?: string; /** * 会话标识(网关 `x-chat-id`,会话归因)。源自可信 runtimeContext.threadId * (runConversation 已把 request.threadId 写入)。下行适配器据此注入网关请求头。 */ chatId?: string; /** * 调用用户标识(网关 `x-user-id`,SPEC 待定/可选)。源自可信 runtimeContext.userId * (由 CLI/gateway 调用方按环境注入)。缺失则不注入,网关计 unknown。 */ userId?: string; /** * 诊断日志下沉点(透传至下行适配器的 streamChat)。 * 缺省不写。调用方注入写文件/打印实现,便于排查内核事件流。 */ debugLog?: (line: string) => void; } /** * env 无关工具描述。不绑 pi 的 `AgentTool`,由各下行适配器各自转译成原生工具形态。 * `execute` 闭包通常由 {@link IMcpServer} 提供(转发到远端/本地 MCP server)。 */ export interface IAgentToolDescriptor { /** MCP 原始工具名或 Presentation Tool 的稳定工具名。 */ name: string; /** 暴露给模型的稳定命名;MCP 必须包含 resourceId 命名空间。 */ modelName?: string; /** MCP Resource ID;Presentation Tool 为 CARD resourceId。 */ resourceId?: string; /** 人类可读标签。 */ label?: string; /** 工具描述(供模型理解用途)。 */ description?: string; /** MCP 风格的 args JSON schema。 */ inputSchema?: unknown; /** MCP 明确声明的结构化输出 Schema。 */ outputSchema?: unknown; /** MCP Tool annotations;Resolver 仅接受 readOnlyHint=true。 */ annotations?: Record & { readOnlyHint?: boolean; }; /** 执行句柄:下行适配器在模型发起工具调用时回调。 */ execute: (args: unknown, signal?: AbortSignal) => Promise; } /** Presentation Tool 返回给 Pi 适配器的内部结果;完整卡片不会进入模型文本。 */ export interface IAgentPresentationToolResult { type: 'linkque.presentation-tool-result'; modelText: string; card: IAgentStreamA2UICard; } /** 消息角色(env 无关,与 conversation 协议字面量对齐,不 import contract)。 */ export type AgentTurnRole = 'user' | 'assistant' | 'system' | 'developer' | 'tool'; /** 单轮消息(role + 原始内容 + 可选 name),供 IAgentLoop 消费 / sessionManager 维护历史。 */ export interface IAgentTurn { role: AgentTurnRole; content: unknown; /** 可选发言者名(多 user 场景),下行适配器可纳入角色标注。 */ name?: string; } /** * 向下端口:把一次请求跑成 {@link AgentStreamEvent} 流。 * * 这是防腐层向下隔离 agent loop 实现的核心 seam:默认实现基于 pi-agent-core, * 换 opencode / google-adk 时新写一个本接口实现即可,组合根与上行端口无需改动。 */ export interface IAgentLoop { run(req: IAgentLoopRequest): AsyncIterable; } /** 会话记录(供 sessionManager 管理历史)。 */ export interface ISessionRecord { sessionId: string; turns: IAgentTurn[]; } /** * 向上端口:会话历史管理。 * * 默认实现 {@link InMemorySessionManager} 无状态(load 恒空、append no-op), * 等价于改造前「无盘、不跨轮」行为。环境(如 Appbase 落库)可注入持久化实现, * 在 load 时回灌历史、append 时落库,从而支持真实多轮。 */ export interface ISessionManager { /** 取某会话的累积历史;无会话或无历史时返回空数组。 */ load(sessionId: string): Promise; /** 追加一轮到会话历史;无状态实现为 no-op。 */ append(sessionId: string, turn: IAgentTurn): Promise; /** 原子追加多轮;持久化实现应保证全有或全无,缺省时 runtime 退化为逐条 append。 */ appendMany?(sessionId: string, turns: IAgentTurn[]): Promise; } /** * 单个 MCP server 视图:agent loop 据此把它的工具挂为己用。 * * 抽象「能列出工具、能调用工具」的最小面;具体实现可包装远端网关 Client * (见 {@link RemoteGatewayMcpServerProvider})或本地 in-process 工具集。 */ export interface IMcpServer { /** 精确 MCP Resource ID;用于隔离同名 server/tool。 */ resourceId: string; /** server 标识(诊断用)。 */ serverName: string; /** 列出该 server 提供的工具(env 无关描述)。 */ listTools(): Promise; /** 调用指定工具,返回结果(原样透传,结构因工具而异)。 */ callTool(name: string, args: unknown, signal?: AbortSignal): Promise; /** 可选:释放底层连接。 */ close?(): Promise; } /** * 单个 MCP 工具的可调用句柄(供卡片 resolver 等需要按名直调 MCP 工具的内部消费者)。 * * proxy 化后,MCP 工具不再 eager 展开成 `IAgentToolDescriptor[]` 暴露给 LLM;改由 * {@link IMcpToolRegistry} 按需解析。本句柄携带 resolver 所需的 schema/只读语义与 * execute 闭包(转发到对应 {@link IMcpServer.callTool})。 */ export interface IMcpToolHandle { /** MCP 风格的 args JSON schema(可能为空)。 */ inputSchema?: unknown; /** MCP 明确声明的结构化输出 Schema(可能为空)。 */ outputSchema?: unknown; /** 是否只读(取自 annotations.readOnlyHint)。 */ readOnly: boolean; /** 执行句柄:转发到对应 MCP server 的 callTool。 */ execute: (args: unknown, signal?: AbortSignal) => Promise; } /** * MCP 工具注册表:按 `(resourceId, toolName)` 解析出可调用句柄。 * * 默认实现由 {@link McpProxyTool} 提供(`resolve` 内部触发懒连接 + 读 run 内缓存)。 * 卡片 resolver 经此访问 MCP 工具,而不是吃预先展开的 `IAgentToolDescriptor[]`—— * 这样 MCP 工具不必在 run 开始就全量连入,支持懒发现。 */ export interface IMcpToolRegistry { /** * 解析单个 MCP 工具句柄;未连则会触发对应 server 的懒连接。 * @returns 找不到时返回 undefined(由调用方决定抛错或降级)。 */ resolve(resourceId: string, toolName: string, signal?: AbortSignal): Promise; } /** * 向上端口:提供本次 run 可用的 MCP server 集合。 * * `resources` 来自 agentConfig 的 {@link IMcpResource}[](配置级抽象);provider * 决定如何连后端(远端网关 / 本地 / 多路)。默认实现 {@link NoopMcpServerProvider} * 返回空数组,组合根默认装配它,确保默认链路零网络。 */ export interface IMcpServerProvider { servers(resources: IMcpResource[]): Promise; } /** * 向上端口:把 {@link ISkillResource} 解析成可注入的技能正文(`string[]`)。 * * 默认实现 {@link NoopSkillLoader} 返回空数组。环境可注入从 OSS / bundle / 文件系统 * 拉取技能正文的实现;返回的正文由下行适配器决定如何并入 agent(默认并入 prompt)。 */ export interface ISkillLoader { load(resources: ISkillResource[]): Promise; }