forked from EduCraft/curriculum-project-hub
feat(hub): provider-agnostic agent seam 骨架(ADR-0001/0003/0017)
hub/ TS 新部件,平级于 spec/。本仓从零 TS 重写(非迁移旧服务),栈: Fastify+Prisma 待补;本提交落 provider-agnostic agent runtime 骨架: - provider.ts: AgentProvider seam,OpenAI-compatible messages+tools+tool_calls 形状(语言通解,非 OpenAI 承诺);@Claude 仅为触发品牌(ADR-0017) - openrouter-provider.ts: openai SDK 指向 OpenRouter 的具体适配器,仅做 Message↔SDK 翻译;agent loop 不 import SDK,换 provider 不改 loop - runner.ts: 自有 agent loop(dispatch tools/循环到 stop/budget),per-run model(ADR-0017 role-based routing,run 带其 model) - tools.ts: 窄工具面 + ADR-0003 McpReadRequest.Authorized 落代码: feishu_read_context 拒绝非本项目绑定 chat 的读取(实测错 chat 抛 UnauthorizedChatRead,对 chat 放行) - models.ts: ModelRegistry seam(role→default 映射策略 OPEN) - server.ts: 接线入口(stub Feishu read,无活凭证即可编译) tsc --noEmit rc=0;ADR-0003 不变式 smoke 通过。
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Provider-agnostic agent seam.
|
||||
*
|
||||
* The Hub's agent loop talks to models through this interface, not through any
|
||||
* vendor SDK. The request/response shape follows the OpenAI chat-completions
|
||||
* de-facto standard (messages + tools + tool_calls) because that is the common
|
||||
* surface OpenRouter and most OpenAI-compatible providers expose; it is the
|
||||
* lingua franca, not an OpenAI commitment.
|
||||
*
|
||||
* ADR-0017: the agent layer is provider-agnostic. `@Claude` is a trigger brand,
|
||||
* not a provider contract. Switching model = new session (provider-bound), so
|
||||
* the seam never assumes cross-provider session continuity.
|
||||
*
|
||||
* This file defines the contract; see {@link OpenRouterProvider} for the
|
||||
* concrete adapter that points an OpenAI-compatible client at OpenRouter.
|
||||
*/
|
||||
|
||||
/** Why a run terminated. Mirrors the common provider surface. */
|
||||
export type FinishReason = "stop" | "tool_calls" | "length" | "error";
|
||||
|
||||
/** A contiguous text span in a message. */
|
||||
export interface TextPart {
|
||||
readonly type: "text";
|
||||
readonly text: string;
|
||||
}
|
||||
|
||||
/** A tool call the model wants the runner to execute. `arguments` is a JSON string. */
|
||||
export interface ToolCallPart {
|
||||
readonly type: "tool_call";
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly arguments: string;
|
||||
}
|
||||
|
||||
/** The result of a tool execution, addressed back to the originating call. */
|
||||
export interface ToolResultPart {
|
||||
readonly type: "tool_result";
|
||||
readonly tool_call_id: string;
|
||||
/** Free-form string content; structured payloads are JSON-encoded here. */
|
||||
readonly content: string;
|
||||
}
|
||||
|
||||
export type MessagePart = TextPart | ToolCallPart | ToolResultPart;
|
||||
|
||||
export type Role = "system" | "user" | "assistant" | "tool";
|
||||
|
||||
/** A single chat message: a role plus an ordered list of parts. */
|
||||
export interface Message {
|
||||
readonly role: Role;
|
||||
readonly parts: readonly MessagePart[];
|
||||
}
|
||||
|
||||
/** JSON-schema description of a tool the model may call. */
|
||||
export interface ToolSpec {
|
||||
readonly name: string;
|
||||
readonly description: string;
|
||||
/** A JSON Schema object describing the tool's parameters. */
|
||||
readonly parameters: unknown;
|
||||
}
|
||||
|
||||
export interface ChatRequest {
|
||||
readonly model: string;
|
||||
readonly messages: readonly Message[];
|
||||
readonly tools: readonly ToolSpec[];
|
||||
}
|
||||
|
||||
export interface ChatResponse {
|
||||
readonly finishReason: FinishReason;
|
||||
/** The assistant message produced. May contain tool_calls. */
|
||||
readonly message: Message;
|
||||
readonly usage?: { readonly inputTokens: number; readonly outputTokens: number };
|
||||
}
|
||||
|
||||
/**
|
||||
* The seam a model provider implements. Implementations are expected to be
|
||||
* OpenAI-compatible HTTP clients (OpenRouter, direct vendor APIs, local
|
||||
* gateways); the Hub does not hard-depend on any single-vendor agent SDK.
|
||||
*/
|
||||
export interface AgentProvider {
|
||||
readonly id: string;
|
||||
chat(req: ChatRequest): Promise<ChatResponse>;
|
||||
}
|
||||
|
||||
/** Extract tool calls from an assistant message, if any. */
|
||||
export function toolCallsOf(msg: Message): readonly ToolCallPart[] {
|
||||
if (msg.role !== "assistant") return [];
|
||||
return msg.parts.filter((p): p is ToolCallPart => p.type === "tool_call");
|
||||
}
|
||||
Reference in New Issue
Block a user