Add checkpoint persistence to agent core loop
coreLoop now saves incremental checkpoints after each tool-call turn and checks a cooperative stop signal at turn boundaries. SuspendedError is handled in finishRun, executeParallel, and executeSingleTool. Approval-interrupted checkpoints are persisted for both flat and nested interruptions. Introduce RunOption, WithCheckpointStore, RunWithOpts, ResumeWithOpts, and RunStreamedWithOpts so callers can provide checkpoint storage. Add StreamEventSuspended and OnRunRestore hook. Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
This commit is contained in:
@@ -24,6 +24,7 @@ import (
|
|||||||
type RunHooks interface {
|
type RunHooks interface {
|
||||||
OnRunStart(ctx context.Context, agent *Agent, messages []llm.Message)
|
OnRunStart(ctx context.Context, agent *Agent, messages []llm.Message)
|
||||||
OnRunEnd(ctx context.Context, agent *Agent, result *Result, err error)
|
OnRunEnd(ctx context.Context, agent *Agent, result *Result, err error)
|
||||||
|
OnRunRestore(ctx context.Context, agent *Agent, checkpoint *Checkpoint)
|
||||||
OnLLMStart(ctx context.Context, agent *Agent, messages []llm.Message)
|
OnLLMStart(ctx context.Context, agent *Agent, messages []llm.Message)
|
||||||
OnLLMEnd(ctx context.Context, agent *Agent, response *llm.ChatCompletionResponse, err error)
|
OnLLMEnd(ctx context.Context, agent *Agent, response *llm.ChatCompletionResponse, err error)
|
||||||
OnToolStart(ctx context.Context, agent *Agent, tool Tool, arguments string)
|
OnToolStart(ctx context.Context, agent *Agent, tool Tool, arguments string)
|
||||||
@@ -39,6 +40,7 @@ var _ RunHooks = NoOpHooks{}
|
|||||||
|
|
||||||
func (NoOpHooks) OnRunStart(context.Context, *Agent, []llm.Message) {}
|
func (NoOpHooks) OnRunStart(context.Context, *Agent, []llm.Message) {}
|
||||||
func (NoOpHooks) OnRunEnd(context.Context, *Agent, *Result, error) {}
|
func (NoOpHooks) OnRunEnd(context.Context, *Agent, *Result, error) {}
|
||||||
|
func (NoOpHooks) OnRunRestore(context.Context, *Agent, *Checkpoint) {}
|
||||||
func (NoOpHooks) OnLLMStart(context.Context, *Agent, []llm.Message) {}
|
func (NoOpHooks) OnLLMStart(context.Context, *Agent, []llm.Message) {}
|
||||||
func (NoOpHooks) OnLLMEnd(context.Context, *Agent, *llm.ChatCompletionResponse, error) {}
|
func (NoOpHooks) OnLLMEnd(context.Context, *Agent, *llm.ChatCompletionResponse, error) {}
|
||||||
func (NoOpHooks) OnToolStart(context.Context, *Agent, Tool, string) {}
|
func (NoOpHooks) OnToolStart(context.Context, *Agent, Tool, string) {}
|
||||||
|
|||||||
273
pkg/agent/run.go
273
pkg/agent/run.go
@@ -40,6 +40,8 @@ const (
|
|||||||
type (
|
type (
|
||||||
CallLLMFunc func(ctx context.Context, agent *Agent, req *llm.ChatCompletionRequest) (*llm.ChatCompletionResponse, error)
|
CallLLMFunc func(ctx context.Context, agent *Agent, req *llm.ChatCompletionRequest) (*llm.ChatCompletionResponse, error)
|
||||||
|
|
||||||
|
RunOption func(*runOpts)
|
||||||
|
|
||||||
runOpts struct {
|
runOpts struct {
|
||||||
callLLM CallLLMFunc
|
callLLM CallLLMFunc
|
||||||
onEvent func(ctx context.Context, ev StreamEvent)
|
onEvent func(ctx context.Context, ev StreamEvent)
|
||||||
@@ -47,6 +49,9 @@ type (
|
|||||||
skipSessionLoad bool
|
skipSessionLoad bool
|
||||||
initialUsage llm.Usage
|
initialUsage llm.Usage
|
||||||
initialTurns int
|
initialTurns int
|
||||||
|
checkpointStore CheckpointStore
|
||||||
|
runID string
|
||||||
|
toolUsedInRun bool
|
||||||
}
|
}
|
||||||
|
|
||||||
loopState struct {
|
loopState struct {
|
||||||
@@ -72,6 +77,13 @@ type (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func WithCheckpointStore(store CheckpointStore, runID string) RunOption {
|
||||||
|
return func(o *runOpts) {
|
||||||
|
o.checkpointStore = store
|
||||||
|
o.runID = runID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func noopEvent(_ context.Context, _ StreamEvent) {}
|
func noopEvent(_ context.Context, _ StreamEvent) {}
|
||||||
|
|
||||||
func blockingCallLLM(ctx context.Context, agent *Agent, req *llm.ChatCompletionRequest) (*llm.ChatCompletionResponse, error) {
|
func blockingCallLLM(ctx context.Context, agent *Agent, req *llm.ChatCompletionRequest) (*llm.ChatCompletionResponse, error) {
|
||||||
@@ -104,15 +116,19 @@ func blockingCallLLM(ctx context.Context, agent *Agent, req *llm.ChatCompletionR
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *Agent) Run(ctx context.Context, messages []llm.Message) (*Result, error) {
|
func (a *Agent) Run(ctx context.Context, messages []llm.Message) (*Result, error) {
|
||||||
return coreLoop(
|
return a.RunWithOpts(ctx, messages)
|
||||||
ctx,
|
}
|
||||||
a,
|
|
||||||
messages,
|
func (a *Agent) RunWithOpts(ctx context.Context, messages []llm.Message, opts ...RunOption) (*Result, error) {
|
||||||
runOpts{
|
ro := runOpts{
|
||||||
callLLM: blockingCallLLM,
|
callLLM: blockingCallLLM,
|
||||||
onEvent: noopEvent,
|
onEvent: noopEvent,
|
||||||
},
|
}
|
||||||
)
|
for _, opt := range opts {
|
||||||
|
opt(&ro)
|
||||||
|
}
|
||||||
|
|
||||||
|
return coreLoop(ctx, a, messages, ro)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *loopState) resolveAgentTools(ctx context.Context) error {
|
func (s *loopState) resolveAgentTools(ctx context.Context) error {
|
||||||
@@ -138,6 +154,20 @@ func (s *loopState) finishRun(ctx context.Context, result *Result, err error) (*
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if _, ok := errors.AsType[*SuspendedError](err); ok {
|
||||||
|
s.runSpan.SetAttributes(attribute.Bool("agent.suspended", true))
|
||||||
|
s.opts.onEvent(ctx, StreamEvent{Type: StreamEventSuspended, Agent: s.agent})
|
||||||
|
|
||||||
|
s.logger.InfoCtx(
|
||||||
|
ctx,
|
||||||
|
"agent run suspended",
|
||||||
|
log.String("agent", s.agent.name),
|
||||||
|
log.Int("turns", s.turns),
|
||||||
|
)
|
||||||
|
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
s.runSpan.RecordError(err)
|
s.runSpan.RecordError(err)
|
||||||
s.runSpan.SetStatus(codes.Error, err.Error())
|
s.runSpan.SetStatus(codes.Error, err.Error())
|
||||||
s.opts.onEvent(ctx, StreamEvent{Type: StreamEventError, Agent: s.agent, Err: err})
|
s.opts.onEvent(ctx, StreamEvent{Type: StreamEventError, Agent: s.agent, Err: err})
|
||||||
@@ -192,6 +222,21 @@ func (s *loopState) finishRun(ctx context.Context, result *Result, err error) (*
|
|||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *loopState) buildCheckpoint(status CheckpointStatus) *Checkpoint {
|
||||||
|
msgsCopy := make([]llm.Message, len(s.messages))
|
||||||
|
copy(msgsCopy, s.messages)
|
||||||
|
|
||||||
|
return &Checkpoint{
|
||||||
|
Version: CheckpointVersion,
|
||||||
|
Status: status,
|
||||||
|
AgentName: s.agent.name,
|
||||||
|
Messages: msgsCopy,
|
||||||
|
Usage: s.totalUsage,
|
||||||
|
Turns: s.turns,
|
||||||
|
ToolUsedInRun: s.toolUsedInRun,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *loopState) applyHandoff(ctx context.Context, handoffTarget *Handoff) error {
|
func (s *loopState) applyHandoff(ctx context.Context, handoffTarget *Handoff) error {
|
||||||
emitHook(s.agent, func(h RunHooks) { h.OnHandoff(ctx, s.agent, handoffTarget.Agent) })
|
emitHook(s.agent, func(h RunHooks) { h.OnHandoff(ctx, s.agent, handoffTarget.Agent) })
|
||||||
emitAgentHook(handoffTarget.Agent, func(h AgentHooks) { h.OnHandoff(ctx, handoffTarget.Agent, s.agent) })
|
emitAgentHook(handoffTarget.Agent, func(h AgentHooks) { h.OnHandoff(ctx, handoffTarget.Agent, s.agent) })
|
||||||
@@ -238,6 +283,7 @@ func coreLoop(ctx context.Context, startAgent *Agent, inputMessages []llm.Messag
|
|||||||
systemPrompt: startAgent.buildSystemPrompt(ctx),
|
systemPrompt: startAgent.buildSystemPrompt(ctx),
|
||||||
totalUsage: opts.initialUsage,
|
totalUsage: opts.initialUsage,
|
||||||
turns: opts.initialTurns,
|
turns: opts.initialTurns,
|
||||||
|
toolUsedInRun: opts.toolUsedInRun,
|
||||||
tracer: otel.GetTracerProvider().Tracer(tracerName),
|
tracer: otel.GetTracerProvider().Tracer(tracerName),
|
||||||
opts: opts,
|
opts: opts,
|
||||||
logger: startAgent.logger,
|
logger: startAgent.logger,
|
||||||
@@ -328,6 +374,25 @@ func coreLoop(ctx context.Context, startAgent *Agent, inputMessages []llm.Messag
|
|||||||
return s.finishRun(ctx, nil, fmt.Errorf("cannot complete: %w", err))
|
return s.finishRun(ctx, nil, fmt.Errorf("cannot complete: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ch := stopSignalFrom(ctx); ch != nil {
|
||||||
|
select {
|
||||||
|
case <-ch:
|
||||||
|
cp := s.buildCheckpoint(CheckpointStatusSuspended)
|
||||||
|
se := &SuspendedError{RunID: s.opts.runID}
|
||||||
|
|
||||||
|
if s.opts.checkpointStore != nil && s.opts.runID != "" {
|
||||||
|
if saveErr := s.opts.checkpointStore.Save(ctx, s.opts.runID, cp); saveErr != nil {
|
||||||
|
s.logger.ErrorCtx(ctx, "cannot save suspension checkpoint", log.Error(saveErr))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
se.Checkpoint = cp
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.finishRun(ctx, nil, se)
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if s.turns >= s.agent.maxTurns {
|
if s.turns >= s.agent.maxTurns {
|
||||||
return s.finishRun(ctx, nil, &MaxTurnsExceededError{MaxTurns: s.agent.maxTurns})
|
return s.finishRun(ctx, nil, &MaxTurnsExceededError{MaxTurns: s.agent.maxTurns})
|
||||||
}
|
}
|
||||||
@@ -485,6 +550,21 @@ func coreLoop(ctx context.Context, startAgent *Agent, inputMessages []llm.Messag
|
|||||||
s.messages = append(s.messages, toolMsgs...)
|
s.messages = append(s.messages, toolMsgs...)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if se, ok := errors.AsType[*SuspendedError](err); ok {
|
||||||
|
outerCP := s.buildCheckpoint(CheckpointStatusSuspended)
|
||||||
|
if se.Checkpoint != nil {
|
||||||
|
outerCP.AllToolCalls = se.Checkpoint.AllToolCalls
|
||||||
|
outerCP.InnerCheckpoints = se.Checkpoint.InnerCheckpoints
|
||||||
|
outerCP.CompletedCalls = se.Checkpoint.CompletedCalls
|
||||||
|
}
|
||||||
|
if s.opts.checkpointStore != nil && s.opts.runID != "" {
|
||||||
|
if saveErr := s.opts.checkpointStore.Save(ctx, s.opts.runID, outerCP); saveErr != nil {
|
||||||
|
s.logger.ErrorCtx(ctx, "cannot save checkpoint", log.Error(saveErr))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s.finishRun(ctx, nil, &SuspendedError{RunID: s.opts.runID})
|
||||||
|
}
|
||||||
|
|
||||||
if nae, ok := errors.AsType[*needsApprovalError](err); ok {
|
if nae, ok := errors.AsType[*needsApprovalError](err); ok {
|
||||||
s.logger.InfoCtx(
|
s.logger.InfoCtx(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -495,6 +575,15 @@ func coreLoop(ctx context.Context, startAgent *Agent, inputMessages []llm.Messag
|
|||||||
msgsCopy := make([]llm.Message, len(s.messages))
|
msgsCopy := make([]llm.Message, len(s.messages))
|
||||||
copy(msgsCopy, s.messages)
|
copy(msgsCopy, s.messages)
|
||||||
|
|
||||||
|
if s.opts.checkpointStore != nil && s.opts.runID != "" {
|
||||||
|
cp := s.buildCheckpoint(CheckpointStatusAwaitingApproval)
|
||||||
|
cp.PendingToolCalls = nae.allToolCalls
|
||||||
|
cp.PendingApprovals = nae.pendingApprovals
|
||||||
|
if saveErr := s.opts.checkpointStore.Save(ctx, s.opts.runID, cp); saveErr != nil {
|
||||||
|
s.logger.ErrorCtx(ctx, "cannot save approval checkpoint", log.Error(saveErr))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return s.finishRun(
|
return s.finishRun(
|
||||||
ctx,
|
ctx,
|
||||||
nil,
|
nil,
|
||||||
@@ -520,6 +609,29 @@ func coreLoop(ctx context.Context, startAgent *Agent, inputMessages []llm.Messag
|
|||||||
msgsCopy := make([]llm.Message, len(s.messages))
|
msgsCopy := make([]llm.Message, len(s.messages))
|
||||||
copy(msgsCopy, s.messages)
|
copy(msgsCopy, s.messages)
|
||||||
|
|
||||||
|
if s.opts.checkpointStore != nil && s.opts.runID != "" {
|
||||||
|
cp := s.buildCheckpoint(CheckpointStatusAwaitingApproval)
|
||||||
|
cp.PendingToolCalls = nie.inner.ToolCalls
|
||||||
|
cp.PendingApprovals = nie.inner.PendingApprovals
|
||||||
|
cp.AllToolCalls = nie.allToolCalls
|
||||||
|
cp.CompletedCalls = nie.completedCalls
|
||||||
|
cp.InnerCheckpoints = map[string]*Checkpoint{
|
||||||
|
nie.toolCallID: {
|
||||||
|
Version: CheckpointVersion,
|
||||||
|
Status: CheckpointStatusAwaitingApproval,
|
||||||
|
AgentName: nie.inner.Agent.name,
|
||||||
|
Messages: nie.inner.Messages,
|
||||||
|
Usage: nie.inner.Usage,
|
||||||
|
Turns: nie.inner.Turns,
|
||||||
|
PendingToolCalls: nie.inner.ToolCalls,
|
||||||
|
PendingApprovals: nie.inner.PendingApprovals,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if saveErr := s.opts.checkpointStore.Save(ctx, s.opts.runID, cp); saveErr != nil {
|
||||||
|
s.logger.ErrorCtx(ctx, "cannot save nested approval checkpoint", log.Error(saveErr))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return s.finishRun(
|
return s.finishRun(
|
||||||
ctx,
|
ctx,
|
||||||
nil,
|
nil,
|
||||||
@@ -580,6 +692,14 @@ func coreLoop(ctx context.Context, startAgent *Agent, inputMessages []llm.Messag
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save incremental checkpoint after completed tool-call turn.
|
||||||
|
if s.opts.checkpointStore != nil && s.opts.runID != "" {
|
||||||
|
cp := s.buildCheckpoint(CheckpointStatusSuspended)
|
||||||
|
if saveErr := s.opts.checkpointStore.Save(ctx, s.opts.runID, cp); saveErr != nil {
|
||||||
|
s.logger.ErrorCtx(ctx, "cannot save checkpoint", log.Error(saveErr))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
case llm.FinishReasonContentFilter:
|
case llm.FinishReasonContentFilter:
|
||||||
return s.finishRun(ctx, nil, fmt.Errorf("cannot complete: content was filtered by the provider"))
|
return s.finishRun(ctx, nil, fmt.Errorf("cannot complete: content was filtered by the provider"))
|
||||||
|
|
||||||
@@ -703,13 +823,13 @@ func executeWithHandoff(
|
|||||||
tr, err := executeSingleTool(ctx, tracer, agent, toolCalls[i], descriptors[i].(Tool), onEvent, logger)
|
tr, err := executeSingleTool(ctx, tracer, agent, toolCalls[i], descriptors[i].(Tool), onEvent, logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if ie, ok := errors.AsType[*InterruptedError](err); ok {
|
if ie, ok := errors.AsType[*InterruptedError](err); ok {
|
||||||
var completed []completedCall
|
var completed []CompletedCall
|
||||||
for j := range results {
|
for j := range results {
|
||||||
completed = append(
|
completed = append(
|
||||||
completed,
|
completed,
|
||||||
completedCall{
|
CompletedCall{
|
||||||
toolCallID: toolCalls[j].ID,
|
ToolCallID: toolCalls[j].ID,
|
||||||
result: results[j].Result,
|
Result: results[j].Result,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -803,9 +923,12 @@ func executeParallel(
|
|||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
for i, entry := range entries {
|
for i, entry := range entries {
|
||||||
var ie *InterruptedError
|
ie, ok := errors.AsType[*InterruptedError](entry.err)
|
||||||
if entry.err != nil && errors.As(entry.err, &ie) {
|
if !ok {
|
||||||
var completed []completedCall
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var completed []CompletedCall
|
||||||
for j, other := range entries {
|
for j, other := range entries {
|
||||||
if j == i {
|
if j == i {
|
||||||
continue
|
continue
|
||||||
@@ -813,9 +936,9 @@ func executeParallel(
|
|||||||
if other.err != nil {
|
if other.err != nil {
|
||||||
completed = append(
|
completed = append(
|
||||||
completed,
|
completed,
|
||||||
completedCall{
|
CompletedCall{
|
||||||
toolCallID: toolCalls[j].ID,
|
ToolCallID: toolCalls[j].ID,
|
||||||
result: ToolResult{
|
Result: ToolResult{
|
||||||
Content: fmt.Sprintf("Error: %s", other.err.Error()),
|
Content: fmt.Sprintf("Error: %s", other.err.Error()),
|
||||||
IsError: true,
|
IsError: true,
|
||||||
},
|
},
|
||||||
@@ -825,9 +948,9 @@ func executeParallel(
|
|||||||
}
|
}
|
||||||
completed = append(
|
completed = append(
|
||||||
completed,
|
completed,
|
||||||
completedCall{
|
CompletedCall{
|
||||||
toolCallID: toolCalls[j].ID,
|
ToolCallID: toolCalls[j].ID,
|
||||||
result: other.result,
|
Result: other.result,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -838,6 +961,61 @@ func executeParallel(
|
|||||||
completedCalls: completed,
|
completedCalls: completed,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for suspended inner agents (stop signal propagated).
|
||||||
|
for i, entry := range entries {
|
||||||
|
if entry.err == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
se, ok := errors.AsType[*SuspendedError](entry.err)
|
||||||
|
if ok && se.Checkpoint != nil {
|
||||||
|
innerCheckpoints := make(map[string]*Checkpoint)
|
||||||
|
var completed []CompletedCall
|
||||||
|
|
||||||
|
for j, other := range entries {
|
||||||
|
if j == i {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if other.err == nil {
|
||||||
|
completed = append(
|
||||||
|
completed,
|
||||||
|
CompletedCall{
|
||||||
|
ToolCallID: toolCalls[j].ID,
|
||||||
|
Result: other.result,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
otherSE, ok := errors.AsType[*SuspendedError](other.err)
|
||||||
|
if ok && otherSE.Checkpoint != nil {
|
||||||
|
innerCheckpoints[toolCalls[j].ID] = otherSE.Checkpoint
|
||||||
|
} else {
|
||||||
|
completed = append(
|
||||||
|
completed,
|
||||||
|
CompletedCall{
|
||||||
|
ToolCallID: toolCalls[j].ID,
|
||||||
|
Result: ToolResult{
|
||||||
|
Content: fmt.Sprintf("Error: %s", other.err.Error()),
|
||||||
|
IsError: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
innerCheckpoints[toolCalls[i].ID] = se.Checkpoint
|
||||||
|
|
||||||
|
outerSE := &SuspendedError{
|
||||||
|
Checkpoint: &Checkpoint{
|
||||||
|
Version: CheckpointVersion,
|
||||||
|
Status: CheckpointStatusSuspended,
|
||||||
|
AllToolCalls: toolCalls,
|
||||||
|
InnerCheckpoints: innerCheckpoints,
|
||||||
|
CompletedCalls: completed,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return nil, nil, outerSE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -939,6 +1117,17 @@ func executeSingleTool(
|
|||||||
return ToolResult{}, err
|
return ToolResult{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _, ok := errors.AsType[*SuspendedError](err); ok {
|
||||||
|
toolSpan.SetAttributes(attribute.Bool("tool.suspended", true))
|
||||||
|
toolSpan.End()
|
||||||
|
|
||||||
|
onEvent(ctx, StreamEvent{Type: StreamEventToolEnd, Agent: agent, Tool: tool, Err: err})
|
||||||
|
emitHook(agent, func(h RunHooks) { h.OnToolEnd(ctx, agent, tool, ToolResult{}, err) })
|
||||||
|
emitAgentHook(agent, func(h AgentHooks) { h.OnToolEnd(ctx, agent, tool, ToolResult{}) })
|
||||||
|
|
||||||
|
return ToolResult{}, err
|
||||||
|
}
|
||||||
|
|
||||||
toolSpan.RecordError(err)
|
toolSpan.RecordError(err)
|
||||||
toolSpan.SetStatus(codes.Error, err.Error())
|
toolSpan.SetStatus(codes.Error, err.Error())
|
||||||
toolSpan.End()
|
toolSpan.End()
|
||||||
@@ -1055,8 +1244,24 @@ func runOutputGuardrails(ctx context.Context, agent *Agent, message llm.Message)
|
|||||||
// are not re-evaluated because the messages were already validated in the
|
// are not re-evaluated because the messages were already validated in the
|
||||||
// original Run call.
|
// original Run call.
|
||||||
func Resume(ctx context.Context, interrupted *InterruptedError, input ResumeInput) (*Result, error) {
|
func Resume(ctx context.Context, interrupted *InterruptedError, input ResumeInput) (*Result, error) {
|
||||||
|
return ResumeWithOpts(ctx, interrupted, input)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResumeWithOpts(ctx context.Context, interrupted *InterruptedError, input ResumeInput, opts ...RunOption) (*Result, error) {
|
||||||
|
ro := runOpts{
|
||||||
|
callLLM: blockingCallLLM,
|
||||||
|
onEvent: noopEvent,
|
||||||
|
}
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(&ro)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resumeWithOpts(ctx, interrupted, input, ro)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resumeWithOpts(ctx context.Context, interrupted *InterruptedError, input ResumeInput, ro runOpts) (*Result, error) {
|
||||||
if interrupted.outerState != nil {
|
if interrupted.outerState != nil {
|
||||||
return resumeNested(ctx, interrupted, input)
|
return resumeNested(ctx, interrupted, input, ro)
|
||||||
}
|
}
|
||||||
|
|
||||||
tracer := otel.GetTracerProvider().Tracer(tracerName)
|
tracer := otel.GetTracerProvider().Tracer(tracerName)
|
||||||
@@ -1153,7 +1358,7 @@ func Resume(ctx context.Context, interrupted *InterruptedError, input ResumeInpu
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
tr, execErr := executeSingleTool(ctx, tracer, agent, tc, desc.(Tool), noopEvent, logger)
|
tr, execErr := executeSingleTool(ctx, tracer, agent, tc, desc.(Tool), ro.onEvent, logger)
|
||||||
if execErr != nil {
|
if execErr != nil {
|
||||||
return nil, execErr
|
return nil, execErr
|
||||||
}
|
}
|
||||||
@@ -1202,17 +1407,20 @@ func Resume(ctx context.Context, interrupted *InterruptedError, input ResumeInpu
|
|||||||
resumeAgent,
|
resumeAgent,
|
||||||
messages,
|
messages,
|
||||||
runOpts{
|
runOpts{
|
||||||
callLLM: blockingCallLLM,
|
callLLM: ro.callLLM,
|
||||||
onEvent: noopEvent,
|
onEvent: ro.onEvent,
|
||||||
skipInputGuardrails: true,
|
skipInputGuardrails: true,
|
||||||
skipSessionLoad: true,
|
skipSessionLoad: true,
|
||||||
initialUsage: interrupted.Usage,
|
initialUsage: interrupted.Usage,
|
||||||
initialTurns: interrupted.Turns,
|
initialTurns: interrupted.Turns,
|
||||||
|
checkpointStore: ro.checkpointStore,
|
||||||
|
runID: ro.runID,
|
||||||
|
toolUsedInRun: ro.toolUsedInRun,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func resumeNested(ctx context.Context, interrupted *InterruptedError, input ResumeInput) (*Result, error) {
|
func resumeNested(ctx context.Context, interrupted *InterruptedError, input ResumeInput, ro runOpts) (*Result, error) {
|
||||||
outer := interrupted.outerState
|
outer := interrupted.outerState
|
||||||
logger := outer.agent.logger
|
logger := outer.agent.logger
|
||||||
|
|
||||||
@@ -1223,10 +1431,10 @@ func resumeNested(ctx context.Context, interrupted *InterruptedError, input Resu
|
|||||||
log.String("inner_agent", interrupted.Agent.name),
|
log.String("inner_agent", interrupted.Agent.name),
|
||||||
)
|
)
|
||||||
|
|
||||||
innerResult, err := Resume(ctx, outer.innerInterrupt, input)
|
innerResult, err := resumeWithOpts(ctx, outer.innerInterrupt, input, ro)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var innerIE *InterruptedError
|
innerIE, ok := errors.AsType[*InterruptedError](err)
|
||||||
if errors.As(err, &innerIE) {
|
if ok {
|
||||||
return nil, &InterruptedError{
|
return nil, &InterruptedError{
|
||||||
ToolCalls: innerIE.ToolCalls,
|
ToolCalls: innerIE.ToolCalls,
|
||||||
PendingApprovals: innerIE.PendingApprovals,
|
PendingApprovals: innerIE.PendingApprovals,
|
||||||
@@ -1251,7 +1459,7 @@ func resumeNested(ctx context.Context, interrupted *InterruptedError, input Resu
|
|||||||
|
|
||||||
completedMap := make(map[string]ToolResult, len(outer.completedCalls))
|
completedMap := make(map[string]ToolResult, len(outer.completedCalls))
|
||||||
for _, cc := range outer.completedCalls {
|
for _, cc := range outer.completedCalls {
|
||||||
completedMap[cc.toolCallID] = cc.result
|
completedMap[cc.ToolCallID] = cc.Result
|
||||||
}
|
}
|
||||||
|
|
||||||
messages := make([]llm.Message, len(outer.messages))
|
messages := make([]llm.Message, len(outer.messages))
|
||||||
@@ -1282,12 +1490,15 @@ func resumeNested(ctx context.Context, interrupted *InterruptedError, input Resu
|
|||||||
outer.agent,
|
outer.agent,
|
||||||
messages,
|
messages,
|
||||||
runOpts{
|
runOpts{
|
||||||
callLLM: blockingCallLLM,
|
callLLM: ro.callLLM,
|
||||||
onEvent: noopEvent,
|
onEvent: ro.onEvent,
|
||||||
skipInputGuardrails: true,
|
skipInputGuardrails: true,
|
||||||
skipSessionLoad: true,
|
skipSessionLoad: true,
|
||||||
initialUsage: outer.usage,
|
initialUsage: outer.usage,
|
||||||
initialTurns: outer.turns,
|
initialTurns: outer.turns,
|
||||||
|
checkpointStore: ro.checkpointStore,
|
||||||
|
runID: ro.runID,
|
||||||
|
toolUsedInRun: ro.toolUsedInRun,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ const (
|
|||||||
StreamEventToolEnd StreamEventType = "tool_end"
|
StreamEventToolEnd StreamEventType = "tool_end"
|
||||||
StreamEventHandoff StreamEventType = "handoff"
|
StreamEventHandoff StreamEventType = "handoff"
|
||||||
StreamEventComplete StreamEventType = "complete"
|
StreamEventComplete StreamEventType = "complete"
|
||||||
|
StreamEventSuspended StreamEventType = "suspended"
|
||||||
StreamEventError StreamEventType = "error"
|
StreamEventError StreamEventType = "error"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -59,6 +60,10 @@ func (sr *StreamedRun) Wait() (*Result, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *Agent) RunStreamed(ctx context.Context, messages []llm.Message) *StreamedRun {
|
func (a *Agent) RunStreamed(ctx context.Context, messages []llm.Message) *StreamedRun {
|
||||||
|
return a.RunStreamedWithOpts(ctx, messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) RunStreamedWithOpts(ctx context.Context, messages []llm.Message, opts ...RunOption) *StreamedRun {
|
||||||
events := make(chan StreamEvent, 64)
|
events := make(chan StreamEvent, 64)
|
||||||
sr := &StreamedRun{
|
sr := &StreamedRun{
|
||||||
Events: events,
|
Events: events,
|
||||||
@@ -69,17 +74,17 @@ func (a *Agent) RunStreamed(ctx context.Context, messages []llm.Message) *Stream
|
|||||||
defer close(sr.done)
|
defer close(sr.done)
|
||||||
defer close(events)
|
defer close(events)
|
||||||
|
|
||||||
result, err := coreLoop(
|
ro := runOpts{
|
||||||
ctx,
|
|
||||||
a,
|
|
||||||
messages,
|
|
||||||
runOpts{
|
|
||||||
callLLM: streamingCallLLM(events),
|
callLLM: streamingCallLLM(events),
|
||||||
onEvent: func(ctx context.Context, ev StreamEvent) {
|
onEvent: func(ctx context.Context, ev StreamEvent) {
|
||||||
trySendEvent(ctx, events, ev)
|
trySendEvent(ctx, events, ev)
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
)
|
for _, opt := range opts {
|
||||||
|
opt(&ro)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := coreLoop(ctx, a, messages, ro)
|
||||||
|
|
||||||
sr.result = result
|
sr.result = result
|
||||||
sr.err = err
|
sr.err = err
|
||||||
|
|||||||
Reference in New Issue
Block a user