feat(loop): agent loop(8 轮 + checkpoint + 流式 + 记忆注入/提取 + trace + 工具超时回喂)
This commit is contained in:
1 parent
01a4e29f84
commit
71697ca4f8
12 files changed
+1292
No files matched your search
@@ -0,0 +1,75 @@
|
||||
package loop
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/agent"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/trace"
|
||||
)
|
||||
|
||||
// Run drives the agent loop: inject relevant memory into the first-turn context,
|
||||
// then run up to MaxRounds steps. Each step runs the model (streaming through
|
||||
// OnStream when configured) and any requested tool calls are executed under a
|
||||
// per-invoke timeout and fed back as RoleTool messages so the model can continue.
|
||||
// A checkpoint is persisted at every step, every step is traced as a
|
||||
// state_transition span (with an llm_call child and tool_call spans for each
|
||||
// invocation), and after each step the memory Extractor runs with errors logged
|
||||
// rather than fatal.
|
||||
//
|
||||
// It returns the final answer, or a partial Result with StopReasonMaxRounds when
|
||||
// the budget is exhausted without a final answer. Step errors are wrapped with %w
|
||||
// so callers can classify them with errors.Is.
|
||||
func (l *Loop) Run(ctx context.Context, sessionID string, messages []model.Message) (*Result, error) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if l.agent == nil {
|
||||
return nil, fmt.Errorf("loop: run: %w", ErrNilAgent)
|
||||
}
|
||||
|
||||
msgs := l.injectMemory(ctx, messages)
|
||||
res := &Result{}
|
||||
for step := 1; step <= l.maxRounds; step++ {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, fmt.Errorf("loop: run: step %d: %w", step, err)
|
||||
}
|
||||
|
||||
rc, roundSpan := l.span(ctx, "loop.step", trace.SpanKindStateTransition)
|
||||
outcome, err := l.step(rc, sessionID, msgs)
|
||||
if err != nil {
|
||||
l.spanProp(roundSpan, "error", err.Error())
|
||||
l.finishSpan(roundSpan, trace.SpanStatusError)
|
||||
return nil, fmt.Errorf("loop: run: step %d: %w", step, err)
|
||||
}
|
||||
l.spanProp(roundSpan, "step", step)
|
||||
l.spanProp(roundSpan, "stop_reason", outcome.stopReason)
|
||||
l.finishSpan(roundSpan, trace.SpanStatusOK)
|
||||
|
||||
res.Steps = step
|
||||
if outcome.final {
|
||||
res.Text = outcome.text
|
||||
res.StopReason = outcome.stopReason
|
||||
l.saveCheckpoint(ctx, sessionID, step, msgs, outcome.stopReason)
|
||||
l.extractMemory(ctx, msgs, step)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
newMsgs, executed, err := l.runToolCalls(rc, msgs, outcome.toolCalls)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("loop: run: step %d: tools: %w", step, err)
|
||||
}
|
||||
msgs = newMsgs
|
||||
res.ToolCallsExecuted += executed
|
||||
l.saveCheckpoint(ctx, sessionID, step, msgs, agent.StopReasonToolCalls)
|
||||
l.extractMemory(ctx, msgs, step)
|
||||
}
|
||||
|
||||
// Budget exhausted without a final answer.
|
||||
res.Steps = l.maxRounds
|
||||
res.StopReason = StopReasonMaxRounds
|
||||
l.saveCheckpoint(ctx, sessionID, l.maxRounds, msgs, StopReasonMaxRounds)
|
||||
l.extractMemory(ctx, msgs, l.maxRounds)
|
||||
return res, nil
|
||||
}
|
||||
Reference in new issue
Block a user