@dreb/代理核心
具有工具执行和事件流的状态代理 。 建造在 @dreb/ai。 。 。 .
安装
npm install @dreb/agent-core
快速启动
import { Agent } from "@dreb/agent-core";
import { getModel } from "@dreb/ai";
const agent = new Agent({
initialState: {
systemPrompt: "You are a helpful assistant.",
model: getModel("anthropic", "claude-sonnet-4-20250514"),
},
});
agent.subscribe((event) => {
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
// Stream just the new text chunk
process.stdout.write(event.assistantMessageEvent.delta);
}
});
await agent.prompt("Hello!");
核心概念
代理Message vs LLM 信件
代理与 AgentMessage,灵活类型,可以包括:
- 标准 LLM 消息 (
user, (中文(简体) ),assistant, (中文(简体) ),toolResult(中文(简体) ) - 通过声明合并自定义应用特定消息类型
法学硕士只理解 user, (中文(简体) ), assistant,以及 toolResult编辑 convertToLlm 函数通过在每次LLM调用前过滤和转换消息来弥补这一空白.
信件流动
settled AgentContext → beforeLlmCall() → transformContext() → convertToLlm() → LLM
(optional) (optional) (required)
- 在 LlmCall 之前: 检查已结算的用户/工具-结果上下文,并在请求前在原子上替换消息和/或活动模式
- 转换语句: prune 旧信件或输入请求的外部上下文
- 转换为Llm: 过滤出只包含 UI 的信件,并将自定义类型转换为 LLM 格式
beforeLlmCall 运行后 shouldContinue 允许申请及申请前 turn_start返回 { messages, model } 更新活动循环和 Agent.state; 返回时 undefined 保存他们。 钩子收到快照,所以没有回取替换的突变被忽略. 与 Agent,被抛出的错误会以可见的助手错误终止运行,而不是应用部分替换;低级循环调用器必须自表面出故障并返回 undefined 而不是拒绝.
事件流程
代理为 UI 更新发布事件 。 了解事件序列有助于构建响应界面.
提示( ) 事件顺序
当你打电话 prompt("Hello")数字 :
prompt("Hello")
├─ agent_start
├─ turn_start
├─ message_start { message: userMessage } // Your prompt
├─ message_end { message: userMessage }
├─ message_start { message: assistantMessage } // LLM starts responding
├─ message_update { message: partial... } // Streaming chunks
├─ message_update { message: partial... }
├─ message_end { message: assistantMessage } // Complete response
├─ turn_end { message, toolResults: [] }
└─ agent_end { messages: [...] }
用工具调用
如果助手调用工具,循环继续:
prompt("Read config.json")
├─ agent_start
├─ turn_start
├─ message_start/end { userMessage }
├─ message_start { assistantMessage with toolCall }
├─ message_update...
├─ message_end { assistantMessage }
├─ tool_execution_start { toolCallId, toolName, args }
├─ tool_execution_update { partialResult } // If tool streams
├─ tool_execution_end { toolCallId, result }
├─ message_start/end { toolResultMessage }
├─ turn_end { message, toolResults: [toolResult] }
│
├─ turn_start // Next turn
├─ message_start { assistantMessage } // LLM responds to tool result
├─ message_update...
├─ message_end
├─ turn_end
└─ agent_end
工具执行模式可配置 :
parallel(默认):飞行前工具按顺序调用,同时执行允许的工具,发送最终tool_execution_end财务报告和审定财务报表toolResult消息以助理源顺序排列sequential: 逐一执行工具调用, 匹配历史行为
这个 beforeToolCall 钩子在后面运行 tool_execution_start 并验证论证。 它可以阻止执行。 这个 afterToolCall 工具执行完成后及执行前的勾钩运行 tool_execution_end 发布最后的工具结果信息事件.
当你使用 Agent 班级,助理 message_end 在工具开始飞行前,加工被视为障碍。 这意味着 beforeToolCall 查看已包含请求工具调用助理消息的代理状态 .
持续( ) 事件顺序
continue() 从现有上下文恢复,而不添加新信件。 错误发生后用它进行重试 .
// After an error, retry from current state
await agent.continue();
上下文中的最后信息必须是 user 或者说 toolResult (无) assistant) (中文(简体) ).
事件类型
| 活动 | 说明 |
|---|---|
agent_start | 代理开始处理;包括已解决的模型和有效的思维水平 |
agent_end | 代理完成所有新信件 |
turn_start | 新回合开始( 一个 LLM 调用 + 工具执行) |
turn_end | 以助理消息和工具结果转换完成 |
message_start | 任何消息开始( 用户、 助手、 工具Result) |
message_update | 仅限助理. 包括 assistantMessageEvent 带三角洲 |
message_end | 信件完成 |
stream_retry | 提供者流已丢弃; 丢弃部分响应后重试 |
length_retry | 在符号限制下缩短了响应; 在配置的输出限制下重试 |
tool_execution_start | 工具开始 |
tool_execution_update | 工具流进展 |
tool_execution_end | 工具完成 |
agent_start 将供应商/模型身份和有效思维级别发送给供应商,用于该运行:
{ type: "agent_start", model: { provider: "anthropic", id: "claude-sonnet-4-6" }, thinkingLevel: "high" }
代理选项
const agent = new Agent({
// Initial state
initialState: {
systemPrompt: string,
model: Model<any>,
thinkingLevel: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max",
tools: AgentTool<any>[],
messages: AgentMessage[],
},
// Prepare settled context immediately before each LLM request. Both fields
// are optional; replacements apply to the active loop and Agent.state.
beforeLlmCall: async (context, signal) => ({
messages: compactedMessages,
model: resizedContextModel,
}),
// Convert AgentMessage[] to LLM Message[] (required for custom message types)
convertToLlm: (messages) => messages.filter(...),
// Transform context before convertToLlm (for pruning, compaction)
transformContext: async (messages, signal) => pruneOldMessages(messages),
// Steering mode: "one-at-a-time" (default) or "all"
steeringMode: "one-at-a-time",
// Follow-up mode: "one-at-a-time" (default) or "all"
followUpMode: "one-at-a-time",
// Custom stream function (for proxy backends)
streamFn: streamProxy,
// Session ID for provider caching
sessionId: "session-123",
// Dynamic API key resolution (for expiring OAuth tokens)
getApiKey: async (provider) => refreshToken(),
// Tool execution mode: "parallel" (default) or "sequential"
toolExecution: "parallel",
// Preflight each tool call after args are validated. Can block execution.
beforeToolCall: async ({ toolCall, args, context }) => {
if (toolCall.name === "bash") {
return { block: true, reason: "bash is disabled" };
}
},
// Postprocess each tool result before final tool events are emitted.
afterToolCall: async ({ toolCall, result, isError, context }) => {
if (!isError) {
return { details: { ...result.details, audited: true } };
}
},
// Forward non-fatal provider warnings (e.g. malformed SSE/JSON) to the caller
onWarning: (code, message) => console.warn(`[${code}] ${message}`),
// Stream drop retry: number of retries when a provider stream ends without
// its terminal event (default: 3). Set to 0 to disable. Messages with a
// `refusal` (provider safety refusal) are never retried.
streamRetries: 3,
// Base delay for exponential backoff between stream retries in ms (default: 1000)
streamRetryBaseDelayMs: 1000,
// Length retry: number of retries when a turn ends with stopReason "length"
// (model exhausted its configured output limit). Each retry uses that same limit.
// Separate from streamRetries (default: 2). Set to 0 to disable.
lengthRetries: 2,
// Custom thinking budgets for token-based providers
thinkingBudgets: {
minimal: 128,
low: 512,
medium: 1024,
high: 2048,
},
// Thinking display for adaptive-thinking Anthropic/Bedrock Claude models.
// "summarized" returns thinking text; "omitted" returns only an encrypted
// signature (lower latency). Ignored by other models. Opus 4.7+ default to
// "omitted" at the API, so pass "summarized" to keep thinking visible.
thinkingDisplay: "summarized",
});
代理国
interface AgentState {
systemPrompt: string;
model: Model<any>;
thinkingLevel: ThinkingLevel;
tools: AgentTool<any>[];
messages: AgentMessage[];
isStreaming: boolean;
streamMessage: AgentMessage | null; // Current partial during streaming
pendingToolCalls: Set<string>;
error?: string;
}
通过 agent.state在流出时, streamMessage 包含部分助理消息.
方法
提示
// Text prompt
await agent.prompt("Hello");
// With images
await agent.prompt("What's in this image?", [
{ type: "image", data: base64Data, mimeType: "image/jpeg" }
]);
// AgentMessage directly
await agent.prompt({ role: "user", content: "Hello", timestamp: Date.now() });
// Continue from current context (last message must be user or toolResult)
await agent.continue();
州管理层
agent.setSystemPrompt("New prompt");
agent.setModel(getModel("openai", "gpt-4o"));
agent.setThinkingLevel("medium");
agent.setTools([myTool]);
agent.setToolExecution("sequential");
agent.setBeforeLlmCall(async (context, signal) => ({ messages: context.messages }));
agent.setBeforeToolCall(async ({ toolCall }) => undefined);
agent.setAfterToolCall(async ({ toolCall, result }) => undefined);
agent.replaceMessages(newMessages);
agent.appendMessage(message);
agent.clearMessages();
agent.reset(); // Clear everything
会议和思考预算
agent.sessionId = "session-123";
agent.thinkingBudgets = {
minimal: 128,
low: 512,
medium: 1024,
high: 2048,
};
控制权
agent.abort(); // Cancel current operation
await agent.waitForIdle(); // Wait for completion
活动
const unsubscribe = agent.subscribe((event) => {
console.log(event.type);
});
unsubscribe();
指导和后续行动
引导消息允许您在工具运行时中断代理 。 后续消息允许您排队工作, 否则代理将停止 .
agent.setSteeringMode("one-at-a-time");
agent.setFollowUpMode("one-at-a-time");
// While agent is running tools
agent.steer({
role: "user",
content: "Stop! Do this instead.",
timestamp: Date.now(),
});
// After the agent finishes its current work
agent.followUp({
role: "user",
content: "Also summarize the result.",
timestamp: Date.now(),
});
const steeringMode = agent.getSteeringMode();
const followUpMode = agent.getFollowUpMode();
agent.clearSteeringQueue();
agent.clearFollowUpQueue();
agent.clearAllQueues();
使用清除SteeringQueee, 清除后继Queee, 或清除所有已排队的消息 .
当旋转完成后检测到方向信息时:
- 当前助理消息中的所有工具呼叫都已完成
- 输入引导消息
- LLM 在下一个回合响应
只有在没有更多的工具呼叫和引导消息时,才会检查后续消息. 如果有排队,则进行注射并再转一圈.
自定义信件类型
扩展 AgentMessage 通过声明合并:
declare module "@dreb/agent-core" {
interface CustomAgentMessages {
notification: { role: "notification"; text: string; timestamp: number };
}
}
// Now valid
const msg: AgentMessage = { role: "notification", text: "Info", timestamp: Date.now() };
处理自定义类型 convertToLlm数字 :
const agent = new Agent({
convertToLlm: (messages) => messages.flatMap(m => {
if (m.role === "notification") return []; // Filter out
return [m];
}),
});
工具
使用 AgentTool数字 :
import { Type } from "@sinclair/typebox";
const readFileTool: AgentTool = {
name: "read_file",
label: "Read File", // For UI display
description: "Read a file's contents",
parameters: Type.Object({
path: Type.String({ description: "File path" }),
}),
execute: async (toolCallId, params, signal, onUpdate) => {
const content = await fs.readFile(params.path, "utf-8");
// Optional: stream progress
onUpdate?.({ content: [{ type: "text", text: "Reading..." }], details: {} });
return {
content: [{ type: "text", text: content }],
details: { path: params.path, size: content.length },
};
},
};
agent.setTools([readFileTool]);
处理错误
丢出错误 当工具失败时。 不返回错误消息为内容 .
execute: async (toolCallId, params, signal, onUpdate) => {
if (!fs.existsSync(params.path)) {
throw new Error(`File not found: ${params.path}`);
}
// Return content only on success
return { content: [{ type: "text", text: "..." }] };
}
丢出的错误被代理抓住,并作为工具错误报告到 LLM isError: true。 。 。 .
代理用户
对于通过后端代理的浏览器应用:
import { Agent, streamProxy } from "@dreb/agent-core";
const agent = new Agent({
streamFn: (model, context, options) =>
streamProxy(model, context, {
...options,
authToken: "...",
proxyUrl: "https://your-server.com",
}),
});
代理服务器流 ProxyAssistantMessageEvent编号 AssistantMessageEvents 无带宽重 partial 页:1 内容 error 事件应当前进 errorMessage 而对于安全拒绝,信息是 refusal 对象,所以重建后的信件保留 refusal 也从未被作为短暂的失败而被重审过.
低级 API
对于没有代理类的直接控制:
import { agentLoop, agentLoopContinue } from "@dreb/agent-core";
const context: AgentContext = {
systemPrompt: "You are helpful.",
messages: [],
tools: [],
};
const config: AgentLoopConfig = {
model: getModel("openai", "gpt-4o"),
beforeLlmCall: async (settledContext, signal) => ({ messages: settledContext.messages }),
convertToLlm: (msgs) => msgs.filter(m => ["user", "assistant", "toolResult"].includes(m.role)),
toolExecution: "parallel",
beforeToolCall: async ({ toolCall, args, context }) => undefined,
afterToolCall: async ({ toolCall, result, isError, context }) => undefined,
};
const userMessage = { role: "user", content: "Hello", timestamp: Date.now() };
for await (const event of agentLoop([userMessage], context, config)) {
console.log(event.type);
}
// Continue from existing context
for await (const event of agentLoopContinue(context, config)) {
console.log(event.type);
}
这些低层溪流是可观测的. 它们保存事件顺序, 但是它们不会等待您的 Async 事件处理在以后的制作阶段继续之前解决 . beforeLlmCall 在提出请求之前,将进行解剖。 如果您需要普通消息事件处理,在工具飞行前起到屏障的作用,请使用 Agent 类而不是生 agentLoop() 或者说 agentLoopContinue()。 。 。 .
许可证
麻省理工学院