Initial commit: sage:TypeScript AI Agent 框架 monorepo(llm / memory / tui 三包)
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "@sage/llm",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": { ".": "./src/index.ts" },
|
||||
"dependencies": {
|
||||
"@sage/core": "*",
|
||||
"ai": "^4.3.0",
|
||||
"@ai-sdk/anthropic": "^1.0.0",
|
||||
"@ai-sdk/openai": "^1.0.0",
|
||||
"@ai-sdk/google": "^1.0.0",
|
||||
"zod": "^3.24.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"typescript": "^5.8.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import { streamText, tool, type CoreMessage, type CoreTool, type TextStreamPart } from "ai"
|
||||
import { z } from "zod"
|
||||
import type { SageConfig, AnyToolDef, ToolContext } from "@sage/core"
|
||||
import { ALL_TOOLS, getTool, addMessage, updateMessage } from "@sage/core"
|
||||
import { getLanguageModel, type ProviderID } from "./providers/index.js"
|
||||
import type { LLMEventEmitter } from "./types.js"
|
||||
|
||||
const SYSTEM_PROMPT = `You are Sage, an expert AI coding assistant. You help users write, debug, and understand code.
|
||||
|
||||
You have access to tools to read/write files, run shell commands, search code, and more.
|
||||
Always prefer using tools to gather information before answering.
|
||||
When editing files, use the edit tool for small changes and write for large rewrites.
|
||||
Be concise but complete. Show your reasoning when working through complex problems.`
|
||||
|
||||
export interface RunOptions {
|
||||
sessionId: string
|
||||
messages: CoreMessage[]
|
||||
model: string
|
||||
provider: string
|
||||
cwd: string
|
||||
config: SageConfig
|
||||
memoryContext?: string
|
||||
onEvent: LLMEventEmitter
|
||||
signal?: AbortSignal
|
||||
}
|
||||
|
||||
export async function runLLM(opts: RunOptions): Promise<void> {
|
||||
const { sessionId, messages, model, provider, cwd, config, onEvent, signal } = opts
|
||||
|
||||
const toolContext: ToolContext = {
|
||||
sessionId,
|
||||
cwd,
|
||||
onProgress: (text) => {
|
||||
/* incremental tool output — currently unused at this level */
|
||||
},
|
||||
}
|
||||
|
||||
const aiTools: Record<string, CoreTool> = {}
|
||||
for (const t of ALL_TOOLS) {
|
||||
aiTools[t.name] = tool({
|
||||
description: t.description,
|
||||
parameters: t.inputSchema as z.ZodType<Record<string, unknown>>,
|
||||
execute: async (input) => {
|
||||
const result = await t.execute(input as Record<string, unknown>, toolContext)
|
||||
return result
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const systemParts: string[] = [SYSTEM_PROMPT]
|
||||
if (opts.memoryContext) {
|
||||
systemParts.push(`\n## Recalled Memories\n${opts.memoryContext}`)
|
||||
}
|
||||
systemParts.push(`\nCurrent working directory: ${cwd}`)
|
||||
|
||||
const lm = getLanguageModel(provider as ProviderID, model, config)
|
||||
|
||||
const currentMessages = [...messages]
|
||||
let assistantMsgId: string | null = null
|
||||
let accText = ""
|
||||
|
||||
const stream = streamText({
|
||||
model: lm,
|
||||
system: systemParts.join("\n"),
|
||||
messages: currentMessages,
|
||||
tools: aiTools,
|
||||
maxSteps: 20,
|
||||
abortSignal: signal,
|
||||
})
|
||||
|
||||
for await (const part of stream.fullStream) {
|
||||
if (signal?.aborted) break
|
||||
|
||||
switch (part.type) {
|
||||
case "text-delta": {
|
||||
if (!assistantMsgId) {
|
||||
const msg = await addMessage({
|
||||
sessionId,
|
||||
role: "assistant",
|
||||
content: "",
|
||||
})
|
||||
assistantMsgId = msg.id
|
||||
}
|
||||
accText += part.textDelta
|
||||
await updateMessage(assistantMsgId, { content: accText })
|
||||
onEvent({ type: "text-delta", delta: part.textDelta })
|
||||
break
|
||||
}
|
||||
|
||||
case "tool-call": {
|
||||
onEvent({
|
||||
type: "tool-call",
|
||||
toolCall: {
|
||||
id: part.toolCallId,
|
||||
name: part.toolName,
|
||||
input: part.args as Record<string, unknown>,
|
||||
},
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case "tool-result": {
|
||||
const output = typeof part.result === "string"
|
||||
? part.result
|
||||
: JSON.stringify(part.result, null, 2)
|
||||
|
||||
await addMessage({
|
||||
sessionId,
|
||||
role: "tool",
|
||||
content: output,
|
||||
toolCallId: part.toolCallId,
|
||||
})
|
||||
|
||||
onEvent({
|
||||
type: "tool-result",
|
||||
toolResult: {
|
||||
toolCallId: part.toolCallId,
|
||||
toolName: part.toolName,
|
||||
output,
|
||||
},
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case "finish": {
|
||||
const usage = part.usage
|
||||
if (assistantMsgId && usage) {
|
||||
await updateMessage(assistantMsgId, {
|
||||
tokensInput: usage.promptTokens,
|
||||
tokensOutput: usage.completionTokens,
|
||||
})
|
||||
}
|
||||
onEvent({
|
||||
type: "finish",
|
||||
usage: usage
|
||||
? { inputTokens: usage.promptTokens, outputTokens: usage.completionTokens }
|
||||
: undefined,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case "error": {
|
||||
onEvent({ type: "error", error: part.error as Error })
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./client.js"
|
||||
export * from "./providers/index.js"
|
||||
export * from "./types.js"
|
||||
@@ -0,0 +1,75 @@
|
||||
import { createAnthropic } from "@ai-sdk/anthropic"
|
||||
import { createOpenAI } from "@ai-sdk/openai"
|
||||
import { createGoogleGenerativeAI } from "@ai-sdk/google"
|
||||
import type { LanguageModelV1 } from "ai"
|
||||
import type { SageConfig } from "@sage/core"
|
||||
|
||||
export type ProviderID = "anthropic" | "openai" | "google" | "openrouter"
|
||||
|
||||
export interface ProviderModel {
|
||||
id: string
|
||||
name: string
|
||||
contextWindow: number
|
||||
supportsTools: boolean
|
||||
}
|
||||
|
||||
export const PROVIDER_MODELS: Record<ProviderID, ProviderModel[]> = {
|
||||
anthropic: [
|
||||
{ id: "claude-opus-4-8", name: "Claude Opus 4.8", contextWindow: 200_000, supportsTools: true },
|
||||
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6", contextWindow: 200_000, supportsTools: true },
|
||||
{ id: "claude-haiku-4-5-20251001", name: "Claude Haiku 4.5", contextWindow: 200_000, supportsTools: true },
|
||||
],
|
||||
openai: [
|
||||
{ id: "gpt-4o", name: "GPT-4o", contextWindow: 128_000, supportsTools: true },
|
||||
{ id: "gpt-4o-mini", name: "GPT-4o Mini", contextWindow: 128_000, supportsTools: true },
|
||||
{ id: "o3", name: "o3", contextWindow: 200_000, supportsTools: true },
|
||||
],
|
||||
google: [
|
||||
{ id: "gemini-2.5-pro", name: "Gemini 2.5 Pro", contextWindow: 1_000_000, supportsTools: true },
|
||||
{ id: "gemini-2.5-flash", name: "Gemini 2.5 Flash", contextWindow: 1_000_000, supportsTools: true },
|
||||
],
|
||||
openrouter: [
|
||||
{ id: "anthropic/claude-sonnet-4-6", name: "Claude Sonnet 4.6 (OR)", contextWindow: 200_000, supportsTools: true },
|
||||
{ id: "openai/gpt-4o", name: "GPT-4o (OR)", contextWindow: 128_000, supportsTools: true },
|
||||
{ id: "google/gemini-2.5-pro", name: "Gemini 2.5 Pro (OR)", contextWindow: 1_000_000, supportsTools: true },
|
||||
{ id: "deepseek/deepseek-r1", name: "DeepSeek R1 (OR)", contextWindow: 64_000, supportsTools: true },
|
||||
],
|
||||
}
|
||||
|
||||
export function getLanguageModel(provider: ProviderID, modelId: string, config: SageConfig): LanguageModelV1 {
|
||||
const providerCfg = config.providers[provider] ?? {}
|
||||
|
||||
switch (provider) {
|
||||
case "anthropic": {
|
||||
const apiKey = (providerCfg as { apiKey?: string }).apiKey ?? process.env.ANTHROPIC_API_KEY
|
||||
const client = createAnthropic({ apiKey })
|
||||
return client(modelId) as LanguageModelV1
|
||||
}
|
||||
case "openai": {
|
||||
const cfg = providerCfg as { apiKey?: string; baseUrl?: string }
|
||||
const apiKey = cfg.apiKey ?? process.env.OPENAI_API_KEY
|
||||
const client = createOpenAI({ apiKey, baseURL: cfg.baseUrl })
|
||||
return client(modelId) as LanguageModelV1
|
||||
}
|
||||
case "google": {
|
||||
const apiKey = (providerCfg as { apiKey?: string }).apiKey ?? process.env.GOOGLE_API_KEY
|
||||
const client = createGoogleGenerativeAI({ apiKey })
|
||||
return client(modelId) as LanguageModelV1
|
||||
}
|
||||
case "openrouter": {
|
||||
const apiKey = (providerCfg as { apiKey?: string }).apiKey ?? process.env.OPENROUTER_API_KEY
|
||||
const client = createOpenAI({
|
||||
apiKey,
|
||||
baseURL: "https://openrouter.ai/api/v1",
|
||||
headers: { "HTTP-Referer": "https://github.com/sage-ai", "X-Title": "Sage" },
|
||||
})
|
||||
return client(modelId) as LanguageModelV1
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown provider: ${provider}`)
|
||||
}
|
||||
}
|
||||
|
||||
export function listProviders(): ProviderID[] {
|
||||
return ["anthropic", "openai", "google", "openrouter"]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { CoreMessage, CoreTool, StreamTextResult } from "ai"
|
||||
import type { ToolCall, ToolResult } from "@sage/core"
|
||||
|
||||
export interface LLMRequest {
|
||||
messages: CoreMessage[]
|
||||
tools?: Record<string, CoreTool>
|
||||
model: string
|
||||
provider: string
|
||||
maxSteps?: number
|
||||
systemPrompt?: string
|
||||
}
|
||||
|
||||
export interface LLMStreamEvent {
|
||||
type: "text-delta" | "tool-call" | "tool-result" | "finish" | "error"
|
||||
delta?: string
|
||||
toolCall?: { id: string; name: string; input: Record<string, unknown> }
|
||||
toolResult?: ToolResult
|
||||
usage?: { inputTokens: number; outputTokens: number }
|
||||
error?: Error
|
||||
}
|
||||
|
||||
export type LLMEventEmitter = (event: LLMStreamEvent) => void
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Reference in New Issue
Block a user