@ ashx- j/ lunr- 代理服务器
具有工具执行和事件流的状态代理 。 建造在 @ashx-j/lunr-ai。 。 。 .
安装
npm install @ashx-j/lunr-agent
快速启动
import { Agent } from "@ashx-j/lunr-agent";
import { getBuiltinModel } from "@ashx-j/lunr-ai/providers/all";
const agent = new Agent({
initialState: {
systemPrompt: "You are a helpful assistant.",
model: getBuiltinModel("anthropic", "claude-sonnet-4-5"),
},
});
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调用前过滤和转换消息来弥补这一空白.
信件流动
AgentMessage[] → transformContext() → AgentMessage[] → convertToLlm() → Message[] → LLM
(optional) (required)
- 转换语句: Prune 旧消息, 注入外部上下文
- 转换为Llm: 过滤出仅UI 信件, 将自定义类型转换为 LLM 格式
事件流程
代理为 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在每个工具定稿后立即发送工具Result信息和turn_end.toolResults以助理源排序sequential: 逐一执行工具调用, 匹配历史行为
在平行模式下,工具补全事件遵循工具补全顺序,但持续存在的工具Result消息仍然遵循助理源命令.
该模式可通过 toolExecution 在代理配置中,或每个工具通过 executionMode 打开 AgentTool。如果任何工具在批次中调用,则以工具为目标 executionMode: "sequential",整个批次不分全局设置顺序执行.
这个 beforeToolCall 钩子在后面运行 tool_execution_start 并验证论证。 它可以阻止执行。 这个 afterToolCall 工具执行完成后及执行前的勾钩运行 tool_execution_end 发布最后的工具结果信息事件.
工具也可以返回 terminate: true 提示应该跳过自动后续 LLM 呼叫。 当每个最终完成的工具都出现这批货时,循环才提前停止 terminate: true混合批次继续正常进行.
低级循环呼叫器可以设置 shouldStopAfterTurn 在当前转折完成后优雅地停止:
const stream = agentLoop(prompts, context, {
model,
convertToLlm,
shouldStopAfterTurn: async ({ message, toolResults, context, newMessages }) => {
return shouldCompactBeforeNextTurn(context.messages);
},
});
shouldStopAfterTurn 运行后 turn_end 在助理答复和任何工具的处决正常完成后,即行释放。 如果它回来了 true,循环发射 agent_end 并在投票指导或后续排队前退出,并在启动另一个LLM呼叫之前退出。 它不会中止提供者流,不会取消运行的工具,也不会改变助手消息停止理性.
当你使用 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 | 信件完成 |
tool_execution_start | 工具开始 |
tool_execution_update | 工具流进展 |
tool_execution_end | 工具完成 |
Agent.subscribe() 等待听众登记. agent_end 意味着不再出现循环事件,但 await agent.waitForIdle() 财务报告和审定财务报表 await agent.prompt(...) 在等待之后才和解 agent_end 听众结束.
代理选项
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[],
},
// 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 (toolCall.name === "notify_done" && !isError) {
return { terminate: true };
}
if (!isError) {
return { details: { ...result.details, audited: true } };
}
},
// Custom thinking budgets for token-based providers
thinkingBudgets: {
minimal: 128,
low: 512,
medium: 1024,
high: 2048,
},
});
代理国
interface AgentState {
systemPrompt: string;
model: Model<any>;
thinkingLevel: ThinkingLevel;
tools: AgentTool<any>[];
messages: AgentMessage[];
readonly isStreaming: boolean;
readonly streamingMessage?: AgentMessage;
readonly pendingToolCalls: ReadonlySet<string>;
readonly errorMessage?: string;
}
访问状态通过 agent.state。 。 。 .
指派 agent.state.tools = [...] 或者说 agent.state.messages = [...] 在存储前复制顶级数组。 变换返回的数组会变换当前代理状态 .
在流出期间, agent.state.streamingMessage 包含当前部分助理消息.
agent.state.isStreaming 遗 物 true 直到运行完全平息,包括等待 agent_end 订户.
方法
提示
// 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.state.systemPrompt = "New prompt";
agent.state.model = getModel("openai", "gpt-4o");
agent.state.thinkingLevel = "medium";
agent.state.tools = [myTool];
agent.toolExecution = "sequential";
agent.beforeToolCall = async ({ toolCall }) => undefined;
agent.afterToolCall = async ({ toolCall, result }) => undefined;
agent.state.messages = newMessages; // top-level array is copied
agent.state.messages.push(message);
agent.reset();
会议和思考预算
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(async (event, signal) => {
if (event.type === "agent_end") {
// Final barrier work for the run
await flushSessionState(signal);
}
});
unsubscribe();
指导和后续行动
引导消息允许您在工具运行时中断代理 。 后续消息允许您排队工作, 否则代理将停止 .
agent.steeringMode = "one-at-a-time";
agent.followUpMode = "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.steeringMode;
const followUpMode = agent.followUpMode;
agent.clearSteeringQueue();
agent.clearFollowUpQueue();
agent.clearAllQueues();
使用清除SteeringQueee, 清除后继Queee, 或清除所有已排队的消息 .
当旋转完成后检测到方向信息时:
- 当前助理消息中的所有工具呼叫都已完成
- 输入引导消息
- LLM 在下一个回合响应
只有在没有更多的工具呼叫和引导消息时,才会检查后续消息. 如果有排队,则进行注射并再转一圈.
自定义信件类型
扩展 AgentMessage 通过声明合并:
declare module "@ashx-j/lunr-agent" {
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 "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" }),
}),
// Override execution mode for this tool (optional).
// "sequential" forces the entire batch to run one at a time.
// "parallel" allows concurrent execution with other tool calls.
// If omitted, the global toolExecution config applies.
executionMode: "sequential",
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: {} });
// Optional: add `terminate: true` here to skip the automatic follow-up LLM call
// when every finalized tool result in the batch does the same.
return {
content: [{ type: "text", text: content }],
details: { path: params.path, size: content.length },
};
},
};
agent.state.tools = [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。 。 。 .
返回时 terminate: true 从 execute() 或者说 afterToolCall 提示代理在当前工具批次后应当停止。 这只有在每个最终完成的工具都导致批量终止时才会生效. 提示是只运行时间; 释放 toolResult 记录信息仍然是标准的LLM工具结果.
代理用户
对于通过后端代理的浏览器应用:
import { Agent, streamProxy } from "@ashx-j/lunr-agent";
const agent = new Agent({
streamFn: (model, context, options) =>
streamProxy(model, context, {
...options,
authToken: "...",
proxyUrl: "https://your-server.com",
}),
});
低级 API
对于没有代理类的直接控制:
import { agentLoop, agentLoopContinue } from "@ashx-j/lunr-agent";
const context: AgentContext = {
systemPrompt: "You are helpful.",
messages: [],
tools: [],
};
const config: AgentLoopConfig = {
model: getBuiltinModel("openai", "gpt-4o"),
convertToLlm: (msgs) => msgs.filter(m => ["user", "assistant", "toolResult"].includes(m.role)),
toolExecution: "parallel", // overridden by per-tool executionMode if set
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);
}
如果准备或执行拒绝,这些包装会发出一个助理信件 stopReason: "error" 或者说 "aborted",然后是 turn_end 财务报告和审定财务报表 agent_end。迭代结束和 .result() 解决已完成的运行消息加上该失败消息。 直接 runAgentLoop() 财务报告和审定财务报表 runAgentLoopContinue() 函数仍然拒绝执行失败.
这些低层溪流是可观测的. 它们保存事件顺序, 但是它们不会等待您的 Async 事件处理在以后的制作阶段继续之前解决 。 如果您需要信息处理在工具飞行前起到屏障作用,请使用 Agent 类而不是生 agentLoop() 或者说 agentLoopContinue()。 。 。 .
许可证
麻省理工学院