forked from EduCraft/curriculum-project-hub
3bca137b48
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 通过。
89 lines
3.0 KiB
TypeScript
89 lines
3.0 KiB
TypeScript
/**
|
|
* 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");
|
|
}
|