Initial commit: sage:TypeScript AI Agent 框架 monorepo(llm / memory / tui 三包)

This commit is contained in:
WpyQwq
2026-09-19 12:10:51 +08:00
commit eb29eb3fa1
47 changed files with 4429 additions and 0 deletions
+19
View File
@@ -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"
}
}
+148
View File
@@ -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
}
}
}
}
+3
View File
@@ -0,0 +1,3 @@
export * from "./client.js"
export * from "./providers/index.js"
export * from "./types.js"
+75
View File
@@ -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"]
}
+22
View File
@@ -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
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}