Agent/core/agent-graph.ts
2026-08-06 22:17:39 +08:00

230 lines
6.4 KiB
TypeScript

// @ts-nocheck
const MAX_STEPS = 20;
export class AgentGraph {
constructor(opts = {}) {
this.nodes = new Map();
this.edges = new Map();
this.entryPoint = null;
this.checkpointStore = opts.checkpointStore || null;
this.maxSteps = opts.maxSteps || MAX_STEPS;
this.onStep = opts.onStep || null;
}
addNode(name, fn) {
if (typeof fn !== 'function') throw new Error(`节点 ${name} 必须是函数`);
this.nodes.set(name, fn);
return this;
}
addEdge(from, to) {
if (!this.edges.has(from)) this.edges.set(from, []);
this.edges.get(from).push(to);
return this;
}
addConditionalEdge(from, condition) {
if (!this.edges.has(from)) this.edges.set(from, []);
this.edges.get(from).push(condition);
return this;
}
setEntryPoint(name) {
this.entryPoint = name;
return this;
}
async run(initialCtx = {}) {
if (!this.entryPoint) throw new Error('未设置 entry point');
if (!this.nodes.has(this.entryPoint)) throw new Error(`入口节点 ${this.entryPoint} 不存在`);
let state = { step: 0, current: this.entryPoint, done: false, history: [] };
let ctx = { ...initialCtx, graph: this };
if (this.checkpointStore) {
const restored = await this.checkpointStore.load();
if (restored) {
state = restored;
ctx.resumed = true;
}
}
while (!state.done && state.step < this.maxSteps) {
const nodeName = state.current;
const nodeFn = this.nodes.get(nodeName);
if (!nodeFn) {
throw new Error(`节点 ${nodeName} 不存在`);
}
const update = await nodeFn(state, ctx);
if (update && typeof update === 'object') {
Object.assign(state, update);
}
state.step += 1;
state.history.push({ step: state.step, node: nodeName, ts: Date.now() });
if (this.checkpointStore) {
await this.checkpointStore.save(state);
}
if (this.onStep) this.onStep(state, nodeName);
const next = this._decideNext(nodeName, state);
if (next === null) {
state.done = true;
break;
}
state.current = next;
}
if (state.step >= this.maxSteps && !state.done) {
state.error = `超出最大步数 ${this.maxSteps}`;
}
if (state.done && this.checkpointStore) {
await this.checkpointStore.clear();
}
return state;
}
_decideNext(from, state) {
const edges = this.edges.get(from) || [];
for (const edge of edges) {
if (typeof edge === 'function') {
const next = edge(state);
if (next) return next;
} else if (typeof edge === 'string') {
return edge;
}
}
return null;
}
}
export class MemoryCheckpoint {
constructor() {
this._state = null;
}
async save(state) { this._state = JSON.parse(JSON.stringify(state)); }
async load() { return this._state; }
async clear() { this._state = null; }
}
export class LocalStorageCheckpoint {
constructor(key = 'ccsparkle_graph_checkpoint') {
this.key = key;
}
async save(state) {
try {
const safe = JSON.parse(JSON.stringify(state, (key, val) =>
typeof val === 'function' ? '[function]' : val
));
localStorage.setItem(this.key, JSON.stringify(safe));
} catch (e) {
console.warn('[checkpoint] 保存失败:', e);
}
}
async load() {
try {
const s = localStorage.getItem(this.key);
return s ? JSON.parse(s) : null;
} catch (e) { return null; }
}
async clear() {
localStorage.removeItem(this.key);
}
}
export function buildAgentLoopGraph(opts) {
const { chat, callTool, shouldApprove, humanApprove, maxTurns = 8, onStep } = opts;
const graph = new AgentGraph({ maxSteps: maxTurns * 3, onStep });
graph.addNode('agent', async (state, ctx) => {
const result = await chat(state, ctx);
return {
lastResponse: result,
messages: [...(state.messages || []), ...(result.messages || [])],
pendingToolCalls: result.toolCalls || [],
};
});
graph.addNode('tools', async (state, ctx) => {
const calls = state.pendingToolCalls || [];
const results = [];
for (const tc of calls) {
if (shouldApprove) {
const needsApproval = shouldApprove(state, tc);
if (needsApproval) {
state.pendingApproval = tc;
return {};
}
}
const result = await callTool(state, tc, ctx);
results.push({ tc, result });
}
return {
toolResults: results,
pendingToolCalls: [],
messages: [...(state.messages || []), ...results.map(r => ({
role: 'tool',
toolCallId: r.tc.id,
content: JSON.stringify(r.result),
}))],
};
});
graph.addNode('approve', async (state, ctx) => {
if (!humanApprove) return {};
const decision = await humanApprove(state, ctx);
return {
approvalDecisions: [...(state.approvalDecisions || []), {
toolCall: state.pendingApproval,
approved: decision.approved,
}],
pendingApproval: null,
};
});
graph.setEntryPoint('agent');
graph.addConditionalEdge('agent', (state) => {
if (!state.pendingToolCalls || state.pendingToolCalls.length === 0) return null;
return 'tools';
});
graph.addConditionalEdge('tools', (state) => {
if (state.pendingApproval) return 'approve';
return 'agent';
});
graph.addConditionalEdge('approve', (state) => {
const lastDecision = state.approvalDecisions?.[state.approvalDecisions.length - 1];
return lastDecision?.approved ? 'tools' : 'agent';
});
return graph;
}