230 lines
6.4 KiB
TypeScript
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;
|
|
}
|