mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-14 03:30:19 +00:00
🔨 chore: add agent-runtime (#9206)
* add agent runtime * add agent runtime * support finish reason * update workflow * 支持中断 * 支持 token usage 统计 * refactor * add example * add docs * update
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
---
|
||||
description: 包含添加 debug 日志请求时
|
||||
globs:
|
||||
description: 包含添加 console.log 日志请求时
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
# Debug 包使用指南
|
||||
|
||||
@@ -19,6 +19,7 @@ jobs:
|
||||
- electron-server-ipc
|
||||
- utils
|
||||
- context-engine
|
||||
- agent-runtime
|
||||
|
||||
name: Test package ${{ matrix.package }}
|
||||
|
||||
|
||||
@@ -143,6 +143,7 @@
|
||||
"@icons-pack/react-simple-icons": "9.6.0",
|
||||
"@khmyznikov/pwa-install": "0.3.9",
|
||||
"@langchain/community": "^0.3.55",
|
||||
"@lobechat/agent-runtime": "workspace:*",
|
||||
"@lobechat/const": "workspace:*",
|
||||
"@lobechat/context-engine": "workspace:*",
|
||||
"@lobechat/database": "workspace:*",
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
// @ts-nocheck
|
||||
import OpenAI from 'openai';
|
||||
|
||||
import { AgentRuntime } from '../src';
|
||||
import type { Agent, AgentState, RuntimeContext } from '../src';
|
||||
|
||||
// OpenAI 模型运行时
|
||||
async function* openaiRuntime(payload: any) {
|
||||
const openai = new OpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY || '',
|
||||
});
|
||||
|
||||
const { messages, tools } = payload;
|
||||
|
||||
const stream = await openai.chat.completions.create({
|
||||
messages,
|
||||
model: 'gpt-4.1-mini',
|
||||
stream: true,
|
||||
tools,
|
||||
});
|
||||
|
||||
let content = '';
|
||||
let toolCalls: any[] = [];
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta;
|
||||
|
||||
if (delta?.content) {
|
||||
content += delta.content;
|
||||
yield { content: delta.content };
|
||||
}
|
||||
|
||||
if (delta?.tool_calls) {
|
||||
for (const toolCall of delta.tool_calls) {
|
||||
if (!toolCalls[toolCall.index]) {
|
||||
toolCalls[toolCall.index] = {
|
||||
function: { arguments: '', name: '' },
|
||||
id: toolCall.id,
|
||||
type: 'function',
|
||||
};
|
||||
}
|
||||
if (toolCall.function?.name) {
|
||||
toolCalls[toolCall.index].function.name += toolCall.function.name;
|
||||
}
|
||||
if (toolCall.function?.arguments) {
|
||||
toolCalls[toolCall.index].function.arguments += toolCall.function.arguments;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (toolCalls.length > 0) {
|
||||
yield { tool_calls: toolCalls.filter(Boolean) };
|
||||
}
|
||||
}
|
||||
|
||||
// 简单的 Agent 实现
|
||||
class SimpleAgent implements Agent {
|
||||
private conversationState: 'waiting_user' | 'processing_llm' | 'executing_tools' | 'done' =
|
||||
'waiting_user';
|
||||
private pendingToolCalls: any[] = [];
|
||||
|
||||
// Agent 拥有自己的模型运行时
|
||||
modelRuntime = openaiRuntime;
|
||||
|
||||
// 定义可用工具
|
||||
tools = {
|
||||
calculate: async ({ expression }: { expression: string }) => {
|
||||
try {
|
||||
// 注意:实际应用中应使用安全的数学解析器
|
||||
const result = new Function(`"use strict"; return (${expression})`)();
|
||||
return { expression, result };
|
||||
} catch {
|
||||
return { error: 'Invalid expression', expression };
|
||||
}
|
||||
},
|
||||
|
||||
get_time: async () => {
|
||||
return {
|
||||
current_time: new Date().toISOString(),
|
||||
formatted_time: new Date().toLocaleString(),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
// 获取工具定义
|
||||
private getToolDefinitions() {
|
||||
return [
|
||||
{
|
||||
function: {
|
||||
description: 'Get current date and time',
|
||||
name: 'get_time',
|
||||
parameters: { properties: {}, type: 'object' },
|
||||
},
|
||||
type: 'function' as const,
|
||||
},
|
||||
{
|
||||
function: {
|
||||
description: 'Calculate mathematical expressions',
|
||||
name: 'calculate',
|
||||
parameters: {
|
||||
properties: {
|
||||
expression: { description: 'Math expression', type: 'string' },
|
||||
},
|
||||
required: ['expression'],
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
type: 'function' as const,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// Agent 决策逻辑 - 基于执行阶段和上下文
|
||||
async runner(context: RuntimeContext, state: AgentState) {
|
||||
console.log(`[${context.phase}] 对话状态: ${this.conversationState}`);
|
||||
|
||||
switch (context.phase) {
|
||||
case 'init': {
|
||||
// 初始化阶段
|
||||
this.conversationState = 'waiting_user';
|
||||
return { reason: 'No action needed', type: 'finish' as const };
|
||||
}
|
||||
|
||||
case 'user_input': {
|
||||
// 用户输入阶段
|
||||
const userPayload = context.payload as { isFirstMessage: boolean; message: any };
|
||||
console.log(`👤 用户消息: ${userPayload.message.content}`);
|
||||
|
||||
// 只有在等待用户输入状态时才处理
|
||||
if (this.conversationState === 'waiting_user') {
|
||||
this.conversationState = 'processing_llm';
|
||||
return {
|
||||
payload: {
|
||||
messages: state.messages,
|
||||
tools: this.getToolDefinitions(),
|
||||
},
|
||||
type: 'call_llm' as const,
|
||||
};
|
||||
}
|
||||
|
||||
// 其他状态下不处理用户输入,结束对话
|
||||
console.log(`⚠️ 忽略用户输入,当前状态: ${this.conversationState}`);
|
||||
return {
|
||||
reason: `Not in waiting_user state: ${this.conversationState}`,
|
||||
type: 'finish' as const,
|
||||
};
|
||||
}
|
||||
|
||||
case 'llm_result': {
|
||||
// LLM 结果阶段,检查是否需要工具调用
|
||||
const llmPayload = context.payload as { hasToolCalls: boolean; result: any };
|
||||
|
||||
// 手动添加 assistant 消息到状态中(修复 Runtime 的问题)
|
||||
const assistantMessage: any = {
|
||||
content: llmPayload.result.content || null,
|
||||
role: 'assistant',
|
||||
};
|
||||
|
||||
if (llmPayload.hasToolCalls) {
|
||||
const toolCalls = llmPayload.result.tool_calls;
|
||||
assistantMessage.tool_calls = toolCalls;
|
||||
this.pendingToolCalls = toolCalls;
|
||||
this.conversationState = 'executing_tools';
|
||||
|
||||
console.log(
|
||||
'🔧 需要执行工具:',
|
||||
toolCalls.map((call: any) => call.function.name),
|
||||
);
|
||||
|
||||
// 添加包含 tool_calls 的 assistant 消息
|
||||
state.messages.push(assistantMessage);
|
||||
|
||||
// 执行第一个工具调用
|
||||
return {
|
||||
toolCall: toolCalls[0],
|
||||
type: 'call_tool' as const,
|
||||
};
|
||||
}
|
||||
|
||||
// 没有工具调用,添加普通 assistant 消息
|
||||
state.messages.push(assistantMessage);
|
||||
this.conversationState = 'done';
|
||||
return { reason: 'LLM response completed', type: 'finish' as const };
|
||||
}
|
||||
|
||||
case 'tool_result': {
|
||||
// 工具执行结果阶段
|
||||
const toolPayload = context.payload as { result: any; toolMessage: any };
|
||||
console.log(`🛠️ 工具执行完成: ${JSON.stringify(toolPayload.result)}`);
|
||||
|
||||
// 移除已执行的工具
|
||||
this.pendingToolCalls = this.pendingToolCalls.slice(1);
|
||||
|
||||
// 如果还有未执行的工具,继续执行
|
||||
if (this.pendingToolCalls.length > 0) {
|
||||
return {
|
||||
toolCall: this.pendingToolCalls[0],
|
||||
type: 'call_tool' as const,
|
||||
};
|
||||
}
|
||||
|
||||
// 所有工具执行完成,调用 LLM 处理结果
|
||||
this.conversationState = 'processing_llm';
|
||||
return {
|
||||
payload: {
|
||||
messages: state.messages,
|
||||
tools: this.getToolDefinitions(),
|
||||
},
|
||||
type: 'call_llm' as const,
|
||||
};
|
||||
}
|
||||
|
||||
case 'human_response': {
|
||||
// 人机交互响应阶段(简化示例中不使用)
|
||||
return { reason: 'Human interaction not supported', type: 'finish' as const };
|
||||
}
|
||||
|
||||
case 'error': {
|
||||
// 错误阶段
|
||||
const errorPayload = context.payload as { error: any };
|
||||
console.error('❌ 错误状态:', errorPayload.error);
|
||||
return { reason: 'Error occurred', type: 'finish' as const };
|
||||
}
|
||||
|
||||
default: {
|
||||
return { reason: 'Unknown phase', type: 'finish' as const };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 主函数
|
||||
async function main() {
|
||||
console.log('🚀 简单的 OpenAI Tools Agent 示例\n');
|
||||
|
||||
if (!process.env.OPENAI_API_KEY) {
|
||||
console.error('❌ 请设置 OPENAI_API_KEY 环境变量');
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建 Agent 和 Runtime
|
||||
const agent = new SimpleAgent();
|
||||
const runtime = new AgentRuntime(agent); // modelRuntime 现在在 Agent 中
|
||||
|
||||
// 测试消息
|
||||
const testMessage = process.argv[2] || 'What time is it? Also calculate 15 * 8 + 7';
|
||||
console.log(`💬 用户: ${testMessage}\n`);
|
||||
|
||||
// 创建初始状态
|
||||
let state = AgentRuntime.createInitialState({
|
||||
maxSteps: 10,
|
||||
messages: [{ content: testMessage, role: 'user' }],
|
||||
sessionId: 'simple-test',
|
||||
});
|
||||
|
||||
console.log('🤖 AI: ');
|
||||
|
||||
// 执行对话循环
|
||||
let nextContext: RuntimeContext | undefined = undefined;
|
||||
|
||||
while (state.status !== 'done' && state.status !== 'error') {
|
||||
const result = await runtime.step(state, nextContext);
|
||||
|
||||
// 处理事件
|
||||
for (const event of result.events) {
|
||||
switch (event.type) {
|
||||
case 'llm_stream': {
|
||||
if ((event as any).chunk.content) {
|
||||
process.stdout.write((event as any).chunk.content);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'llm_result': {
|
||||
if ((event as any).result.tool_calls) {
|
||||
console.log('\n\n🔧 需要调用工具...');
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'tool_result': {
|
||||
console.log(`\n🛠️ 工具执行结果:`, event.result);
|
||||
console.log('\n🤖 AI: ');
|
||||
break;
|
||||
}
|
||||
case 'done': {
|
||||
console.log('\n\n✅ 对话完成');
|
||||
break;
|
||||
}
|
||||
case 'error': {
|
||||
console.error('\n❌ 错误:', event.error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state = result.newState;
|
||||
nextContext = result.nextContext; // 使用返回的 nextContext
|
||||
}
|
||||
|
||||
console.log(`\n📊 总共执行了 ${state.stepCount} 个步骤`);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "@lobechat/agent-runtime",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
"scripts": {
|
||||
"simple": "tsx examples/tools-calling.ts",
|
||||
"test": "vitest",
|
||||
"test:coverage": "vitest --coverage"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"openai": "^4.0.0"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
export * from './runtime';
|
||||
@@ -0,0 +1,656 @@
|
||||
import type {
|
||||
Agent,
|
||||
AgentEvent,
|
||||
AgentInstruction,
|
||||
AgentState,
|
||||
Cost,
|
||||
InstructionExecutor,
|
||||
RuntimeConfig,
|
||||
RuntimeContext,
|
||||
ToolRegistry,
|
||||
ToolsCalling,
|
||||
Usage,
|
||||
} from '../types';
|
||||
|
||||
/**
|
||||
* Simplified Agent Runtime - The "Engine" that executes instructions from an "Agent" (Brain).
|
||||
* Now includes built-in call_llm support and allows full executor customization.
|
||||
*/
|
||||
export class AgentRuntime {
|
||||
private executors: Record<AgentInstruction['type'], InstructionExecutor>;
|
||||
|
||||
constructor(
|
||||
private agent: Agent,
|
||||
private config: RuntimeConfig = {},
|
||||
) {
|
||||
// Build executors with priority: agent.executors > config.executors > built-in
|
||||
this.executors = {
|
||||
call_llm: this.createCallLLMExecutor(),
|
||||
call_tool: this.createCallToolExecutor(),
|
||||
finish: this.createFinishExecutor(),
|
||||
request_human_approve: this.createHumanApproveExecutor(),
|
||||
request_human_prompt: this.createHumanPromptExecutor(),
|
||||
request_human_select: this.createHumanSelectExecutor(),
|
||||
// Config executors override built-in
|
||||
...config.executors,
|
||||
// Agent provided executors have highest priority
|
||||
...(agent.executors as any),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a single step of the Plan -> Execute loop.
|
||||
* @param state - Current agent state
|
||||
* @param context - Runtime context for this step (required for proper phase detection)
|
||||
*/
|
||||
async step(
|
||||
state: AgentState,
|
||||
context?: RuntimeContext,
|
||||
): Promise<{ events: AgentEvent[]; newState: AgentState; nextContext?: RuntimeContext }> {
|
||||
try {
|
||||
// Increment step count and check limits
|
||||
const newState = structuredClone(state);
|
||||
newState.stepCount += 1;
|
||||
newState.lastModified = new Date().toISOString();
|
||||
|
||||
// Check maximum steps limit
|
||||
if (newState.maxSteps && newState.stepCount > newState.maxSteps) {
|
||||
// Finish execution when maxSteps is exceeded
|
||||
newState.status = 'done';
|
||||
const finishEvent = {
|
||||
finalState: newState,
|
||||
reason: 'max_steps_exceeded' as const,
|
||||
reasonDetail: `Maximum steps exceeded: ${newState.maxSteps}`,
|
||||
type: 'done' as const,
|
||||
};
|
||||
newState.events = [...newState.events, finishEvent];
|
||||
|
||||
return {
|
||||
events: [finishEvent],
|
||||
newState,
|
||||
nextContext: undefined, // No next context when done
|
||||
};
|
||||
}
|
||||
|
||||
// Use provided context or create initial context
|
||||
const runtimeContext = context || this.createInitialContext(newState);
|
||||
|
||||
let result: { events: AgentEvent[]; newState: AgentState; nextContext?: RuntimeContext };
|
||||
|
||||
// Handle human approved tool calls
|
||||
if (runtimeContext.phase === 'human_approved_tool') {
|
||||
const approvedPayload = runtimeContext.payload as { approvedToolCall: ToolsCalling };
|
||||
result = await this.executors.call_tool(
|
||||
{ toolCall: approvedPayload.approvedToolCall, type: 'call_tool' },
|
||||
newState,
|
||||
);
|
||||
} else {
|
||||
// Standard flow: Plan -> Execute
|
||||
const instruction = await this.agent.runner(runtimeContext, newState);
|
||||
result = await this.executors[instruction.type](instruction, newState);
|
||||
}
|
||||
|
||||
// Ensure stepCount is preserved in the result
|
||||
result.newState.stepCount = newState.stepCount;
|
||||
result.newState.lastModified = newState.lastModified;
|
||||
|
||||
// Accumulate events to state history
|
||||
result.newState.events = [...result.newState.events, ...result.events];
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
const errorState = structuredClone(state);
|
||||
errorState.stepCount += 1;
|
||||
errorState.lastModified = new Date().toISOString();
|
||||
return this.createErrorResult(errorState, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method for approving and executing a tool call
|
||||
*/
|
||||
async approveToolCall(
|
||||
state: AgentState,
|
||||
approvedToolCall: ToolsCalling,
|
||||
): Promise<{ events: AgentEvent[]; newState: AgentState; nextContext?: RuntimeContext }> {
|
||||
const context: RuntimeContext = {
|
||||
payload: { approvedToolCall },
|
||||
phase: 'human_approved_tool',
|
||||
session: this.createSessionContext(state),
|
||||
};
|
||||
|
||||
return this.step(state, context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Interrupt the current execution
|
||||
* @param state - Current agent state
|
||||
* @param reason - Reason for interruption
|
||||
* @param canResume - Whether the interruption can be resumed later
|
||||
* @param metadata - Additional metadata about the interruption
|
||||
*/
|
||||
interrupt(
|
||||
state: AgentState,
|
||||
reason: string,
|
||||
canResume: boolean = true,
|
||||
metadata?: Record<string, unknown>,
|
||||
): { events: AgentEvent[]; newState: AgentState } {
|
||||
const newState = structuredClone(state);
|
||||
const interruptedAt = new Date().toISOString();
|
||||
|
||||
newState.status = 'interrupted';
|
||||
newState.lastModified = interruptedAt;
|
||||
newState.interruption = {
|
||||
canResume,
|
||||
interruptedAt,
|
||||
// Store the current step for potential resumption
|
||||
interruptedInstruction: undefined,
|
||||
|
||||
reason, // Could be enhanced to store current instruction
|
||||
};
|
||||
|
||||
const interruptEvent: AgentEvent = {
|
||||
canResume,
|
||||
interruptedAt,
|
||||
metadata,
|
||||
reason,
|
||||
type: 'interrupted',
|
||||
};
|
||||
|
||||
newState.events = [...newState.events, interruptEvent];
|
||||
|
||||
return {
|
||||
events: [interruptEvent],
|
||||
newState,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume execution from an interrupted state
|
||||
* @param state - Interrupted agent state
|
||||
* @param reason - Reason for resumption
|
||||
* @param context - Optional context to resume with
|
||||
*/
|
||||
async resume(
|
||||
state: AgentState,
|
||||
reason: string = 'User resumed execution',
|
||||
context?: RuntimeContext,
|
||||
): Promise<{ events: AgentEvent[]; newState: AgentState; nextContext?: RuntimeContext }> {
|
||||
if (state.status !== 'interrupted') {
|
||||
throw new Error('Cannot resume: state is not interrupted');
|
||||
}
|
||||
|
||||
if (state.interruption && !state.interruption.canResume) {
|
||||
throw new Error('Cannot resume: interruption is not resumable');
|
||||
}
|
||||
|
||||
const newState = structuredClone(state);
|
||||
const resumedAt = new Date().toISOString();
|
||||
const resumedFromStep = state.stepCount;
|
||||
|
||||
// Clear interruption context and set status back to running
|
||||
newState.status = 'running';
|
||||
newState.lastModified = resumedAt;
|
||||
newState.interruption = undefined;
|
||||
|
||||
const resumeEvent: AgentEvent = {
|
||||
reason,
|
||||
resumedAt,
|
||||
resumedFromStep,
|
||||
type: 'resumed',
|
||||
};
|
||||
|
||||
newState.events = [...newState.events, resumeEvent];
|
||||
|
||||
// If context is provided, continue with that context
|
||||
if (context) {
|
||||
const result = await this.step(newState, context);
|
||||
return {
|
||||
events: [resumeEvent, ...result.events],
|
||||
newState: result.newState,
|
||||
nextContext: result.nextContext,
|
||||
};
|
||||
}
|
||||
|
||||
// Otherwise, just return the resumed state
|
||||
return {
|
||||
events: [resumeEvent],
|
||||
newState,
|
||||
nextContext: this.createInitialContext(newState),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new agent state with flexible initialization
|
||||
* @param partialState - Partial state to override defaults
|
||||
* @returns Complete AgentState with defaults filled in
|
||||
*/
|
||||
static createInitialState(partialState: Partial<AgentState> & { sessionId: string }): AgentState {
|
||||
const now = new Date().toISOString();
|
||||
|
||||
// Default usage statistics
|
||||
const defaultUsage: Usage = {
|
||||
humanInteraction: {
|
||||
approvalRequests: 0,
|
||||
promptRequests: 0,
|
||||
selectRequests: 0,
|
||||
totalWaitingTimeMs: 0,
|
||||
},
|
||||
llm: {
|
||||
apiCalls: 0,
|
||||
processingTimeMs: 0,
|
||||
tokens: { input: 0, output: 0, total: 0 },
|
||||
},
|
||||
tools: {
|
||||
byTool: {},
|
||||
totalCalls: 0,
|
||||
totalTimeMs: 0,
|
||||
},
|
||||
};
|
||||
|
||||
// Default cost structure
|
||||
const defaultCost: Cost = {
|
||||
calculatedAt: now,
|
||||
currency: 'USD',
|
||||
llm: {
|
||||
byModel: {},
|
||||
currency: 'USD',
|
||||
total: 0,
|
||||
},
|
||||
tools: {
|
||||
byTool: {},
|
||||
currency: 'USD',
|
||||
total: 0,
|
||||
},
|
||||
total: 0,
|
||||
};
|
||||
|
||||
return {
|
||||
cost: defaultCost,
|
||||
// Default values
|
||||
createdAt: now,
|
||||
events: [],
|
||||
lastModified: now,
|
||||
messages: [],
|
||||
status: 'idle',
|
||||
stepCount: 0,
|
||||
usage: defaultUsage,
|
||||
// User provided values override defaults
|
||||
...partialState,
|
||||
};
|
||||
}
|
||||
|
||||
// ============ Executor Factory Methods ============
|
||||
|
||||
/** Create call_llm executor with streaming support */
|
||||
private createCallLLMExecutor(): InstructionExecutor {
|
||||
return async (instruction, state) => {
|
||||
const { payload } = instruction as Extract<AgentInstruction, { type: 'call_llm' }>;
|
||||
const newState = structuredClone(state);
|
||||
const events: AgentEvent[] = [];
|
||||
|
||||
newState.status = 'running';
|
||||
newState.lastModified = new Date().toISOString();
|
||||
|
||||
events.push({ payload, type: 'llm_start' });
|
||||
|
||||
// Use Agent's modelRuntime first, fallback to config
|
||||
const modelRuntime = this.agent.modelRuntime;
|
||||
if (!modelRuntime) {
|
||||
throw new Error(
|
||||
'Model Runtime is required for call_llm instruction. Provide it via Agent.modelRuntime or RuntimeConfig.modelRuntime',
|
||||
);
|
||||
}
|
||||
|
||||
let assistantContent = '';
|
||||
let toolCalls: ToolsCalling[] = [];
|
||||
|
||||
try {
|
||||
// Stream LLM response
|
||||
for await (const chunk of modelRuntime(payload)) {
|
||||
events.push({ chunk, type: 'llm_stream' });
|
||||
|
||||
// Accumulate content and tool calls from chunks
|
||||
if (chunk.content) {
|
||||
assistantContent += chunk.content;
|
||||
}
|
||||
|
||||
if (chunk.tool_calls) {
|
||||
toolCalls = chunk.tool_calls;
|
||||
}
|
||||
}
|
||||
|
||||
events.push({
|
||||
result: { content: assistantContent, tool_calls: toolCalls },
|
||||
type: 'llm_result',
|
||||
});
|
||||
|
||||
// Update usage and cost if agent provides calculation methods
|
||||
if (this.agent.calculateUsage) {
|
||||
newState.usage = this.agent.calculateUsage(
|
||||
'llm',
|
||||
{ content: assistantContent, tool_calls: toolCalls },
|
||||
newState.usage,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.agent.calculateCost) {
|
||||
newState.cost = this.agent.calculateCost({
|
||||
costLimit: newState.costLimit,
|
||||
previousCost: newState.cost,
|
||||
usage: newState.usage,
|
||||
});
|
||||
}
|
||||
|
||||
// Check cost limits
|
||||
if (newState.costLimit && newState.cost.total > newState.costLimit.maxTotalCost) {
|
||||
return this.handleCostLimitExceeded(newState);
|
||||
}
|
||||
|
||||
// Provide next context based on LLM result
|
||||
const nextContext: RuntimeContext = {
|
||||
payload: {
|
||||
hasToolCalls: toolCalls.length > 0,
|
||||
result: { content: assistantContent, tool_calls: toolCalls },
|
||||
toolCalls,
|
||||
},
|
||||
phase: 'llm_result',
|
||||
session: this.createSessionContext(newState),
|
||||
};
|
||||
|
||||
return { events, newState, nextContext };
|
||||
} catch (error) {
|
||||
return this.createErrorResult(state, error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** Create call_tool executor */
|
||||
private createCallToolExecutor(): InstructionExecutor {
|
||||
return async (instruction, state) => {
|
||||
const { toolCall } = instruction as Extract<AgentInstruction, { type: 'call_tool' }>;
|
||||
const newState = structuredClone(state);
|
||||
const events: AgentEvent[] = [];
|
||||
|
||||
newState.lastModified = new Date().toISOString();
|
||||
newState.status = 'running';
|
||||
|
||||
const tools = this.agent.tools || ({} as ToolRegistry);
|
||||
const handler = tools[toolCall.function.name];
|
||||
if (!handler) throw new Error(`Tool not found: ${toolCall.function.name}`);
|
||||
|
||||
const args = JSON.parse(toolCall.function.arguments);
|
||||
const result = await handler(args);
|
||||
|
||||
newState.messages.push({
|
||||
content: JSON.stringify(result),
|
||||
role: 'tool',
|
||||
tool_call_id: toolCall.id,
|
||||
});
|
||||
|
||||
events.push({ id: toolCall.id, result, type: 'tool_result' });
|
||||
|
||||
// Update usage and cost if agent provides calculation methods
|
||||
if (this.agent.calculateUsage) {
|
||||
newState.usage = this.agent.calculateUsage(
|
||||
'tool',
|
||||
{ executionTime: 0, result, toolCall }, // Could track actual execution time
|
||||
newState.usage,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.agent.calculateCost) {
|
||||
newState.cost = this.agent.calculateCost({
|
||||
costLimit: newState.costLimit,
|
||||
previousCost: newState.cost,
|
||||
usage: newState.usage,
|
||||
});
|
||||
}
|
||||
|
||||
// Check cost limits
|
||||
if (newState.costLimit && newState.cost.total > newState.costLimit.maxTotalCost) {
|
||||
return this.handleCostLimitExceeded(newState);
|
||||
}
|
||||
|
||||
// Provide next context for tool result
|
||||
const nextContext: RuntimeContext = {
|
||||
payload: {
|
||||
result,
|
||||
toolCall,
|
||||
toolCallId: toolCall.id,
|
||||
},
|
||||
phase: 'tool_result',
|
||||
session: this.createSessionContext(newState),
|
||||
};
|
||||
|
||||
return { events, newState, nextContext };
|
||||
};
|
||||
}
|
||||
|
||||
/** Create human approve executor */
|
||||
private createHumanApproveExecutor(): InstructionExecutor {
|
||||
return async (instruction, state) => {
|
||||
const { pendingToolsCalling } = instruction as Extract<
|
||||
AgentInstruction,
|
||||
{ type: 'request_human_approve' }
|
||||
>;
|
||||
const newState = structuredClone(state);
|
||||
|
||||
newState.lastModified = new Date().toISOString();
|
||||
newState.status = 'waiting_for_human_input';
|
||||
newState.pendingToolsCalling = pendingToolsCalling;
|
||||
|
||||
const events: AgentEvent[] = [
|
||||
{
|
||||
pendingToolsCalling,
|
||||
sessionId: newState.sessionId,
|
||||
type: 'human_approve_required',
|
||||
},
|
||||
{ toolCalls: pendingToolsCalling, type: 'tool_pending' },
|
||||
];
|
||||
|
||||
return { events, newState };
|
||||
};
|
||||
}
|
||||
|
||||
/** Create human prompt executor */
|
||||
private createHumanPromptExecutor(): InstructionExecutor {
|
||||
return async (instruction, state) => {
|
||||
const { metadata, prompt } = instruction as Extract<
|
||||
AgentInstruction,
|
||||
{ type: 'request_human_prompt' }
|
||||
>;
|
||||
const newState = structuredClone(state);
|
||||
|
||||
newState.lastModified = new Date().toISOString();
|
||||
newState.status = 'waiting_for_human_input';
|
||||
newState.pendingHumanPrompt = { metadata, prompt };
|
||||
|
||||
const events: AgentEvent[] = [
|
||||
{
|
||||
metadata,
|
||||
prompt,
|
||||
sessionId: newState.sessionId,
|
||||
type: 'human_prompt_required',
|
||||
},
|
||||
];
|
||||
|
||||
return { events, newState };
|
||||
};
|
||||
}
|
||||
|
||||
/** Create human select executor */
|
||||
private createHumanSelectExecutor(): InstructionExecutor {
|
||||
return async (instruction, state) => {
|
||||
const { metadata, multi, options, prompt } = instruction as Extract<
|
||||
AgentInstruction,
|
||||
{ type: 'request_human_select' }
|
||||
>;
|
||||
const newState = structuredClone(state);
|
||||
|
||||
newState.lastModified = new Date().toISOString();
|
||||
newState.status = 'waiting_for_human_input';
|
||||
newState.pendingHumanSelect = { metadata, multi, options, prompt };
|
||||
|
||||
const events: AgentEvent[] = [
|
||||
{
|
||||
metadata,
|
||||
multi,
|
||||
options,
|
||||
prompt,
|
||||
sessionId: newState.sessionId,
|
||||
type: 'human_select_required',
|
||||
},
|
||||
];
|
||||
|
||||
return { events, newState };
|
||||
};
|
||||
}
|
||||
|
||||
/** Create finish executor */
|
||||
private createFinishExecutor(): InstructionExecutor {
|
||||
return async (instruction, state) => {
|
||||
const { reason, reasonDetail } = instruction as Extract<AgentInstruction, { type: 'finish' }>;
|
||||
const newState = structuredClone(state);
|
||||
|
||||
newState.lastModified = new Date().toISOString();
|
||||
newState.status = 'done';
|
||||
|
||||
const events: AgentEvent[] = [
|
||||
{
|
||||
finalState: newState,
|
||||
reason,
|
||||
reasonDetail,
|
||||
type: 'done',
|
||||
},
|
||||
];
|
||||
return { events, newState };
|
||||
};
|
||||
}
|
||||
|
||||
// ============ Helper Methods ============
|
||||
|
||||
/**
|
||||
* Handle cost limit exceeded scenario
|
||||
*/
|
||||
private handleCostLimitExceeded(state: AgentState): {
|
||||
events: AgentEvent[];
|
||||
newState: AgentState;
|
||||
nextContext?: RuntimeContext;
|
||||
} {
|
||||
const newState = structuredClone(state);
|
||||
const costLimit = newState.costLimit!;
|
||||
|
||||
switch (costLimit.onExceeded) {
|
||||
case 'stop': {
|
||||
newState.status = 'done';
|
||||
const finishEvent = {
|
||||
finalState: newState,
|
||||
reason: 'cost_limit_exceeded' as const,
|
||||
reasonDetail: `Cost limit exceeded: ${newState.cost.total} ${newState.cost.currency} > ${costLimit.maxTotalCost} ${costLimit.currency}`,
|
||||
type: 'done' as const,
|
||||
};
|
||||
newState.events = [...newState.events, finishEvent];
|
||||
return {
|
||||
events: [finishEvent],
|
||||
newState,
|
||||
nextContext: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
case 'interrupt': {
|
||||
return {
|
||||
...this.interrupt(
|
||||
newState,
|
||||
`Cost limit exceeded: ${newState.cost.total} ${newState.cost.currency}`,
|
||||
true,
|
||||
{
|
||||
costExceeded: true,
|
||||
currentCost: newState.cost.total,
|
||||
limitCost: costLimit.maxTotalCost,
|
||||
},
|
||||
),
|
||||
nextContext: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
default: {
|
||||
// Continue execution but emit warning event
|
||||
const warningEvent = {
|
||||
error: new Error(
|
||||
`Warning: Cost limit exceeded: ${newState.cost.total} ${newState.cost.currency}`,
|
||||
),
|
||||
type: 'error' as const,
|
||||
};
|
||||
newState.events = [...newState.events, warningEvent];
|
||||
return {
|
||||
events: [warningEvent],
|
||||
newState,
|
||||
nextContext: {
|
||||
payload: { error: warningEvent.error, isCostWarning: true },
|
||||
phase: 'error' as const,
|
||||
session: this.createSessionContext(newState),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create session context metadata - reusable helper
|
||||
*/
|
||||
private createSessionContext(state: AgentState) {
|
||||
return {
|
||||
eventCount: state.events.length,
|
||||
messageCount: state.messages.length,
|
||||
sessionId: state.sessionId,
|
||||
status: state.status,
|
||||
stepCount: state.stepCount,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create initial context for the first step (fallback for backward compatibility)
|
||||
*/
|
||||
private createInitialContext(state: AgentState): RuntimeContext {
|
||||
const lastMessage = state.messages.at(-1);
|
||||
|
||||
if (lastMessage?.role === 'user') {
|
||||
return {
|
||||
payload: {
|
||||
isFirstMessage: state.messages.length === 1,
|
||||
message: lastMessage,
|
||||
},
|
||||
phase: 'user_input',
|
||||
session: this.createSessionContext(state),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
payload: undefined,
|
||||
phase: 'init',
|
||||
session: this.createSessionContext(state),
|
||||
};
|
||||
}
|
||||
|
||||
/** Create error state and events */
|
||||
private createErrorResult(
|
||||
state: AgentState,
|
||||
error: any,
|
||||
): { events: AgentEvent[]; newState: AgentState } {
|
||||
const errorState = structuredClone(state);
|
||||
errorState.status = 'error';
|
||||
errorState.error = error;
|
||||
errorState.lastModified = new Date().toISOString();
|
||||
|
||||
const errorEvent = { error, type: 'error' } as const;
|
||||
|
||||
// Accumulate error event to state history
|
||||
errorState.events = [...errorState.events, errorEvent];
|
||||
|
||||
return {
|
||||
events: [errorEvent],
|
||||
newState: errorState,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './core';
|
||||
export * from './types';
|
||||
@@ -0,0 +1,121 @@
|
||||
/* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/interface */
|
||||
import type { AgentState, ToolsCalling } from './state';
|
||||
|
||||
export interface AgentEventInit {
|
||||
type: 'init';
|
||||
}
|
||||
|
||||
export interface AgentEventLlmStart {
|
||||
type: 'llm_start';
|
||||
payload: unknown;
|
||||
}
|
||||
|
||||
export interface AgentEventLlmStream {
|
||||
type: 'llm_stream';
|
||||
chunk: unknown;
|
||||
}
|
||||
|
||||
export interface AgentEventLlmResult {
|
||||
type: 'llm_result';
|
||||
result: unknown;
|
||||
}
|
||||
|
||||
export interface AgentEventToolPending {
|
||||
type: 'tool_pending';
|
||||
toolCalls: ToolsCalling[];
|
||||
}
|
||||
|
||||
export interface AgentEventToolResult {
|
||||
type: 'tool_result';
|
||||
id: string;
|
||||
result: any;
|
||||
}
|
||||
|
||||
export interface AgentEventHumanApproveRequired {
|
||||
type: 'human_approve_required';
|
||||
pendingToolsCalling: ToolsCalling[];
|
||||
sessionId: string;
|
||||
}
|
||||
|
||||
export interface AgentEventHumanPromptRequired {
|
||||
type: 'human_prompt_required';
|
||||
metadata?: Record<string, unknown>;
|
||||
prompt: string;
|
||||
sessionId: string;
|
||||
}
|
||||
|
||||
export interface AgentEventHumanSelectRequired {
|
||||
type: 'human_select_required';
|
||||
metadata?: Record<string, unknown>;
|
||||
multi?: boolean;
|
||||
options: { label: string; value: string }[];
|
||||
prompt?: string;
|
||||
sessionId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Standardized finish reasons
|
||||
*/
|
||||
export type FinishReason =
|
||||
| 'completed' // Normal completion
|
||||
| 'user_requested' // User requested to end
|
||||
| 'max_steps_exceeded' // Reached maximum steps limit
|
||||
| 'cost_limit_exceeded' // Reached cost limit
|
||||
| 'timeout' // Execution timeout
|
||||
| 'agent_decision' // Agent decided to finish
|
||||
| 'error_recovery' // Finished due to unrecoverable error
|
||||
| 'system_shutdown'; // System is shutting down
|
||||
|
||||
export interface AgentEventDone {
|
||||
type: 'done';
|
||||
finalState: AgentState;
|
||||
reason: FinishReason;
|
||||
reasonDetail?: string;
|
||||
}
|
||||
|
||||
export interface AgentEventError {
|
||||
type: 'error';
|
||||
error: any;
|
||||
}
|
||||
|
||||
export interface AgentEventInterrupted {
|
||||
type: 'interrupted';
|
||||
reason: string;
|
||||
interruptedAt: string;
|
||||
interruptedInstruction?: any;
|
||||
canResume: boolean;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface AgentEventResumed {
|
||||
type: 'resumed';
|
||||
reason: string;
|
||||
resumedAt: string;
|
||||
resumedFromStep: number;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Events emitted by the AgentRuntime during execution
|
||||
*/
|
||||
export type AgentEvent =
|
||||
// Initialization
|
||||
| AgentEventInit
|
||||
// LLM streaming output
|
||||
| AgentEventLlmStart
|
||||
| AgentEventLlmStream
|
||||
| AgentEventLlmResult
|
||||
// Tool invocation
|
||||
| AgentEventToolPending
|
||||
| AgentEventToolResult
|
||||
// Normal completion
|
||||
| AgentEventDone
|
||||
// Error thrown
|
||||
| AgentEventError
|
||||
// Human-in-the-loop (HIL)
|
||||
| AgentEventHumanApproveRequired
|
||||
| AgentEventHumanPromptRequired
|
||||
| AgentEventHumanSelectRequired
|
||||
// Interruption and resumption
|
||||
| AgentEventInterrupted
|
||||
| AgentEventResumed;
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from './event';
|
||||
export * from './instruction';
|
||||
export * from './runtime';
|
||||
export * from './state';
|
||||
export * from './usage';
|
||||
@@ -0,0 +1,125 @@
|
||||
import type { FinishReason } from './event';
|
||||
import { AgentState, ToolRegistry, ToolsCalling } from './state';
|
||||
import type { Cost, CostCalculationContext, Usage } from './usage';
|
||||
|
||||
/**
|
||||
* Runtime execution context passed to Agent runner
|
||||
*/
|
||||
export interface RuntimeContext {
|
||||
/** Phase-specific payload/context */
|
||||
payload?: unknown;
|
||||
/** Current execution phase */
|
||||
phase:
|
||||
| 'init'
|
||||
| 'user_input'
|
||||
| 'llm_result'
|
||||
| 'tool_result'
|
||||
| 'human_response'
|
||||
| 'human_approved_tool'
|
||||
| 'error';
|
||||
/** Session metadata */
|
||||
session: {
|
||||
eventCount: number;
|
||||
messageCount: number;
|
||||
sessionId: string;
|
||||
status: AgentState['status'];
|
||||
stepCount: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the "Brain" of an agent.
|
||||
* It contains all the decision-making logic and is completely stateless.
|
||||
*/
|
||||
export interface Agent {
|
||||
/**
|
||||
* Calculate cost from usage statistics
|
||||
* @param context - Cost calculation context with usage and limits
|
||||
* @returns Updated cost information
|
||||
*/
|
||||
calculateCost?(context: CostCalculationContext): Cost;
|
||||
|
||||
/**
|
||||
* Calculate usage statistics from operation results
|
||||
* @param operationType - Type of operation that was performed
|
||||
* @param operationResult - Result data from the operation
|
||||
* @param previousUsage - Previous usage statistics
|
||||
* @returns Updated usage statistics
|
||||
*/
|
||||
calculateUsage?(
|
||||
operationType: 'llm' | 'tool' | 'human_interaction',
|
||||
operationResult: any,
|
||||
previousUsage: Usage,
|
||||
): Usage;
|
||||
|
||||
/** Optional custom executors mapping to extend runtime behaviors */
|
||||
executors?: Partial<Record<AgentInstruction['type'], any>>;
|
||||
|
||||
/**
|
||||
* Model runtime function for LLM calls - Agent owns its model integration
|
||||
* @param payload - LLM call payload (messages, tools, etc.)
|
||||
* @returns Async iterable of streaming response chunks
|
||||
*/
|
||||
modelRuntime?: (payload: unknown) => AsyncIterable<any>;
|
||||
|
||||
/**
|
||||
* The core runner method. Based on the current execution context and state,
|
||||
* it decides what the next action should be.
|
||||
* @param context - Current runtime context with phase and payload
|
||||
* @param state - Complete agent state for reference
|
||||
*/
|
||||
runner(context: RuntimeContext, state: AgentState): Promise<AgentInstruction>;
|
||||
|
||||
/** Optional tools registry held by the agent */
|
||||
tools?: ToolRegistry;
|
||||
}
|
||||
|
||||
export interface AgentInstructionCallLlm {
|
||||
payload: unknown;
|
||||
type: 'call_llm';
|
||||
}
|
||||
|
||||
export interface AgentInstructionCallTool {
|
||||
toolCall: ToolsCalling;
|
||||
type: 'call_tool';
|
||||
}
|
||||
|
||||
export interface AgentInstructionRequestHumanPrompt {
|
||||
metadata?: Record<string, unknown>;
|
||||
prompt: string;
|
||||
reason?: string;
|
||||
type: 'request_human_prompt';
|
||||
}
|
||||
|
||||
export interface AgentInstructionRequestHumanSelect {
|
||||
metadata?: Record<string, unknown>;
|
||||
multi?: boolean;
|
||||
options: Array<{ label: string; value: string }>;
|
||||
prompt?: string;
|
||||
reason?: string;
|
||||
type: 'request_human_select';
|
||||
}
|
||||
|
||||
export interface AgentInstructionRequestHumanApprove {
|
||||
pendingToolsCalling: ToolsCalling[];
|
||||
reason?: string;
|
||||
type: 'request_human_approve';
|
||||
}
|
||||
|
||||
export interface AgentInstructionFinish {
|
||||
reason: FinishReason;
|
||||
reasonDetail?: string;
|
||||
type: 'finish';
|
||||
}
|
||||
|
||||
/**
|
||||
* A serializable instruction object that the "Agent" (Brain) returns
|
||||
* to the "AgentRuntime" (Engine) to execute.
|
||||
*/
|
||||
export type AgentInstruction =
|
||||
| AgentInstructionCallLlm
|
||||
| AgentInstructionCallTool
|
||||
| AgentInstructionRequestHumanPrompt
|
||||
| AgentInstructionRequestHumanSelect
|
||||
| AgentInstructionRequestHumanApprove
|
||||
| AgentInstructionFinish;
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { AgentEvent } from './event';
|
||||
import { AgentInstruction, RuntimeContext } from './instruction';
|
||||
import { AgentState } from './state';
|
||||
|
||||
export type InstructionExecutor = (
|
||||
instruction: AgentInstruction,
|
||||
state: AgentState,
|
||||
) => Promise<{
|
||||
events: AgentEvent[];
|
||||
newState: AgentState;
|
||||
/** Next context to pass to Agent runner (if execution should continue) */
|
||||
nextContext?: RuntimeContext;
|
||||
}>;
|
||||
|
||||
export interface RuntimeConfig {
|
||||
/** Custom executors for specific instruction types */
|
||||
executors?: Partial<Record<AgentInstruction['type'], InstructionExecutor>>;
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/interface */
|
||||
import type { AgentEvent } from './event';
|
||||
import type { Cost, CostLimit, Usage } from './usage';
|
||||
|
||||
/**
|
||||
* Agent's serializable state.
|
||||
* This is the "passport" that can be persisted and transferred.
|
||||
*/
|
||||
export interface AgentState {
|
||||
sessionId: string;
|
||||
// --- State Machine ---
|
||||
status: 'idle' | 'running' | 'waiting_for_human_input' | 'done' | 'error' | 'interrupted';
|
||||
|
||||
// --- Core Context ---
|
||||
messages: any[];
|
||||
systemRole?: string;
|
||||
|
||||
// --- Event History ---
|
||||
/**
|
||||
* Complete event trace for this agent session.
|
||||
* Useful for debugging, auditing, and state replay.
|
||||
*/
|
||||
events: AgentEvent[];
|
||||
|
||||
// --- Execution Tracking ---
|
||||
/**
|
||||
* Number of execution steps in this session.
|
||||
* Incremented on each runtime.step() call.
|
||||
*/
|
||||
stepCount: number;
|
||||
/**
|
||||
* Optional maximum number of steps allowed.
|
||||
* If set, execution will stop with error when exceeded.
|
||||
*/
|
||||
maxSteps?: number;
|
||||
|
||||
// --- Usage and Cost Tracking ---
|
||||
/**
|
||||
* Accumulated usage statistics for this session.
|
||||
* Tracks tokens, API calls, tool usage, etc.
|
||||
*/
|
||||
usage: Usage;
|
||||
/**
|
||||
* Current calculated cost for this session.
|
||||
* Updated after each billable operation.
|
||||
*/
|
||||
cost: Cost;
|
||||
/**
|
||||
* Optional cost limits configuration.
|
||||
* If set, execution will stop when limits are exceeded.
|
||||
*/
|
||||
costLimit?: CostLimit;
|
||||
|
||||
// --- HIL ---
|
||||
/**
|
||||
* When status is 'waiting_for_human_input', this stores pending requests
|
||||
* for human-in-the-loop operations.
|
||||
*/
|
||||
pendingToolsCalling?: ToolsCalling[];
|
||||
pendingHumanPrompt?: { metadata?: Record<string, unknown>; prompt: string };
|
||||
pendingHumanSelect?: {
|
||||
metadata?: Record<string, unknown>;
|
||||
multi?: boolean;
|
||||
options: Array<{ label: string; value: string }>;
|
||||
prompt?: string;
|
||||
};
|
||||
|
||||
// --- Interruption Handling ---
|
||||
/**
|
||||
* When status is 'interrupted', this stores the interruption context
|
||||
* for potential resumption or cleanup.
|
||||
*/
|
||||
interruption?: {
|
||||
/** Reason for interruption */
|
||||
reason: string;
|
||||
/** Timestamp when interruption occurred */
|
||||
interruptedAt: string;
|
||||
/** The instruction that was being executed when interrupted */
|
||||
interruptedInstruction?: any;
|
||||
/** Whether the interruption can be resumed */
|
||||
canResume: boolean;
|
||||
};
|
||||
|
||||
// --- Metadata ---
|
||||
createdAt: string;
|
||||
error?: any;
|
||||
lastModified: string;
|
||||
|
||||
// --- Extensible metadata ---
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenAI Tool Call
|
||||
*/
|
||||
export interface ToolsCalling {
|
||||
function: {
|
||||
arguments: string;
|
||||
name: string; // A JSON string of arguments
|
||||
};
|
||||
id: string;
|
||||
type: 'function';
|
||||
}
|
||||
|
||||
/**
|
||||
* A registry for tools, mapping tool names to their implementation.
|
||||
*/
|
||||
export type ToolRegistry = Record<string, (args: any) => Promise<any>>;
|
||||
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* Token usage tracking for different types of operations
|
||||
*/
|
||||
export interface TokenUsage {
|
||||
/** Input tokens consumed */
|
||||
input: number;
|
||||
/** Output tokens generated */
|
||||
output: number;
|
||||
/** Total tokens (input + output) */
|
||||
total: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Usage statistics for different operation types
|
||||
*/
|
||||
export interface Usage {
|
||||
/** Human interaction statistics */
|
||||
humanInteraction: {
|
||||
/** Number of approval requests */
|
||||
approvalRequests: number;
|
||||
/** Number of prompt requests */
|
||||
promptRequests: number;
|
||||
/** Number of selection requests */
|
||||
selectRequests: number;
|
||||
/** Total waiting time for human input */
|
||||
totalWaitingTimeMs: number;
|
||||
};
|
||||
|
||||
/** LLM model usage */
|
||||
llm: {
|
||||
/** Number of LLM API calls made */
|
||||
apiCalls: number;
|
||||
/** Total processing time in milliseconds */
|
||||
processingTimeMs: number;
|
||||
/** Total token usage across all LLM calls */
|
||||
tokens: TokenUsage;
|
||||
};
|
||||
|
||||
/** Tool usage statistics */
|
||||
tools: {
|
||||
/** Usage breakdown by tool name */
|
||||
byTool: Record<
|
||||
string,
|
||||
{
|
||||
calls: number;
|
||||
errors: number;
|
||||
totalTimeMs: number;
|
||||
}
|
||||
>;
|
||||
/** Number of tool calls executed */
|
||||
totalCalls: number;
|
||||
/** Total tool execution time */
|
||||
totalTimeMs: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Cost calculation result
|
||||
*/
|
||||
export interface Cost {
|
||||
/** Cost calculation timestamp */
|
||||
calculatedAt: string;
|
||||
|
||||
currency: string;
|
||||
|
||||
/** LLM API costs */
|
||||
llm: {
|
||||
/** Cost per model used */
|
||||
byModel: Record<
|
||||
string,
|
||||
{
|
||||
currency: string;
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
totalCost: number;
|
||||
}
|
||||
>;
|
||||
currency: string;
|
||||
/** Total LLM cost */
|
||||
total: number;
|
||||
};
|
||||
/** Tool execution costs */
|
||||
tools: {
|
||||
/** Cost per tool (if tool has associated costs) */
|
||||
byTool: Record<
|
||||
string,
|
||||
{
|
||||
calls: number;
|
||||
currency: string;
|
||||
totalCost: number;
|
||||
}
|
||||
>;
|
||||
currency: string;
|
||||
/** Total tool cost */
|
||||
total: number;
|
||||
};
|
||||
|
||||
/** Total session cost */
|
||||
total: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cost limit configuration
|
||||
*/
|
||||
export interface CostLimit {
|
||||
/** Currency for cost limits */
|
||||
currency: string;
|
||||
/** Maximum LLM cost allowed */
|
||||
maxLlmCost?: number;
|
||||
/** Maximum tool cost allowed */
|
||||
maxToolCost?: number;
|
||||
/** Maximum total cost allowed */
|
||||
maxTotalCost: number;
|
||||
/** Action to take when limit is exceeded */
|
||||
onExceeded: 'stop' | 'warn' | 'interrupt';
|
||||
}
|
||||
|
||||
/**
|
||||
* Cost calculation context passed to Agent
|
||||
*/
|
||||
export interface CostCalculationContext {
|
||||
/** Cost limits configuration */
|
||||
costLimit?: CostLimit;
|
||||
/** Previous cost calculation (if any) */
|
||||
previousCost?: Cost;
|
||||
/** Current usage statistics */
|
||||
usage: Usage;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
coverage: {
|
||||
reporter: ['text', 'json', 'lcov', 'text-summary'],
|
||||
},
|
||||
environment: 'happy-dom',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user