You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

347 lines
12 KiB
Markdown

---
title: "Chapter 7: Interrupt/Resume"
---
The goal of this chapter is to understand the Interrupt/Resume mechanism and implement a Tool approval flow that lets users confirm before sensitive operations.
## Code Location
- Entry code: [cmd/ch07/main.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/cmd/ch07/main.go)
## Prerequisites
Same as Chapter 1: you need to configure an available ChatModel (OpenAI or Ark). Additionally, you need to set `PROJECT_ROOT` as in Chapter 4:
```bash
export PROJECT_ROOT=/path/to/eino # Eino core library root directory (defaults to the current directory if not set)
```
## Running
In the `examples/quickstart/chatwitheino` directory, run:
```bash
# Set the project root directory
export PROJECT_ROOT=/path/to/your/project
go run ./cmd/ch07
```
Output example:
```text
you> Please execute the command echo hello
Warning: Approval Required
Tool: execute
Arguments: {"command":"echo hello"}
Approve this action? (y/n): y
[tool result] hello
hello
```
## From Auto-Execution to Human Approval: Why We Need Interrupt
In the previous chapters, the Agent automatically executed all Tool calls, but in certain scenarios this is dangerous:
**Risks of auto-execution:**
- Deleting files: Accidentally deleting important data
- Sending emails: Sending incorrect content
- Executing commands: Running dangerous operations
- Modifying configuration: Breaking system settings
**The role of Interrupt:**
- **Interrupt is the Agent's pause mechanism**: Pauses before critical operations, waiting for user confirmation
- **Interrupt can carry information**: Shows the user the operation about to be executed
- **Interrupt is resumable**: Continues execution after user confirmation, returns an error if rejected
**Simple analogy:**
- **Auto-execution** = "autopilot" (fully trusting the system)
- **Interrupt** = "manual override" (critical decisions are made by humans)
## Key Concepts
### Interrupt Mechanism
`Interrupt` is the core mechanism in Eino for implementing human-agent collaboration.
**Core idea: Pause before executing critical operations, and continue after user confirmation.**
A Tool execution that requires approval is split into **two phases**:
1. **First call (trigger interrupt)**: The Tool saves the current arguments, then returns an interrupt signal. The Runner pauses execution and returns an Interrupt event to the caller.
2. **Resume after user approval**: The Runner calls the Tool again. This time the Tool detects it was "previously interrupted", reads the user's approval result, and executes (or rejects).
**Simplified pseudocode:**
```
func myTool(ctx, args):
if first call:
save args
return interrupt signal // Runner pauses, shows approval prompt
else: // Second call after Resume
if user approved:
return execute(saved args)
else:
return "Operation rejected by user"
```
**Full code with key field explanations:**
```go
// Trigger an interrupt in a Tool
func myTool(ctx context.Context, args string) (string, error) {
// wasInterrupted: whether this is the second call after Resume (false on first call, true after Resume)
// storedArgs: arguments saved via StatefulInterrupt during the first call, retrievable after Resume
wasInterrupted, _, storedArgs := tool.GetInterruptState[string](ctx)
if !wasInterrupted {
// First call: trigger interrupt, saving args for use after Resume
return "", tool.StatefulInterrupt(ctx, &ApprovalInfo{
ToolName: "my_tool",
ArgumentsInJSON: args,
}, args) // Third argument is the state to save (retrievable via storedArgs after Resume)
}
// Second call after Resume: read user's approval result
// isTarget: whether this Resume targets the current Tool (each Resume targets only one Tool)
// hasData: whether the Resume carries approval result data
// data: the user's approval result
isTarget, hasData, data := tool.GetResumeContext[*ApprovalResult](ctx)
if isTarget && hasData {
if data.Approved {
return doSomething(storedArgs) // Execute actual operation using saved arguments
}
return "Operation rejected by user", nil
}
// Other cases (isTarget=false means this Resume targets a different Tool): re-interrupt
return "", tool.StatefulInterrupt(ctx, &ApprovalInfo{
ToolName: "my_tool",
ArgumentsInJSON: storedArgs,
}, storedArgs)
}
```
### ApprovalMiddleware
`ApprovalMiddleware` is a general-purpose approval middleware that can intercept specific Tool calls:
```go
type approvalMiddleware struct {
*adk.BaseChatModelAgentMiddleware
}
func (m *approvalMiddleware) WrapInvokableToolCall(
_ context.Context,
endpoint adk.InvokableToolCallEndpoint,
tCtx *adk.ToolContext,
) (adk.InvokableToolCallEndpoint, error) {
// Only intercept Tools that require approval
if tCtx.Name != "execute" {
return endpoint, nil
}
return func(ctx context.Context, args string, opts ...tool.Option) (string, error) {
wasInterrupted, _, storedArgs := tool.GetInterruptState[string](ctx)
if !wasInterrupted {
return "", tool.StatefulInterrupt(ctx, &commontool.ApprovalInfo{
ToolName: tCtx.Name,
ArgumentsInJSON: args,
}, args)
}
isTarget, hasData, data := tool.GetResumeContext[*commontool.ApprovalResult](ctx)
if isTarget && hasData {
if data.Approved {
return endpoint(ctx, storedArgs, opts...)
}
if data.DisapproveReason != nil {
return fmt.Sprintf("tool '%s' disapproved: %s", tCtx.Name, *data.DisapproveReason), nil
}
return fmt.Sprintf("tool '%s' disapproved", tCtx.Name), nil
}
// Re-interrupt
return "", tool.StatefulInterrupt(ctx, &commontool.ApprovalInfo{
ToolName: tCtx.Name,
ArgumentsInJSON: storedArgs,
}, storedArgs)
}, nil
}
func (m *approvalMiddleware) WrapStreamableToolCall(
_ context.Context,
endpoint adk.StreamableToolCallEndpoint,
tCtx *adk.ToolContext,
) (adk.StreamableToolCallEndpoint, error) {
// If the agent is configured with StreamingShell, execute uses streaming calls,
// so this method must be implemented to intercept it
if tCtx.Name != "execute" {
return endpoint, nil
}
return func(ctx context.Context, args string, opts ...tool.Option) (*schema.StreamReader[string], error) {
wasInterrupted, _, storedArgs := tool.GetInterruptState[string](ctx)
if !wasInterrupted {
return nil, tool.StatefulInterrupt(ctx, &commontool.ApprovalInfo{
ToolName: tCtx.Name,
ArgumentsInJSON: args,
}, args)
}
isTarget, hasData, data := tool.GetResumeContext[*commontool.ApprovalResult](ctx)
if isTarget && hasData {
if data.Approved {
return endpoint(ctx, storedArgs, opts...)
}
if data.DisapproveReason != nil {
return singleChunkReader(fmt.Sprintf("tool '%s' disapproved: %s", tCtx.Name, *data.DisapproveReason)), nil
}
return singleChunkReader(fmt.Sprintf("tool '%s' disapproved", tCtx.Name)), nil
}
isTarget, _, _ = tool.GetResumeContext[any](ctx)
if !isTarget {
return nil, tool.StatefulInterrupt(ctx, &commontool.ApprovalInfo{
ToolName: tCtx.Name,
ArgumentsInJSON: storedArgs,
}, storedArgs)
}
return endpoint(ctx, storedArgs, opts...)
}, nil
}
```
### CheckPointStore
`CheckPointStore` is the key component for implementing interrupt recovery:
```go
type CheckPointStore interface {
// Save a checkpoint
Put(ctx context.Context, key string, checkpoint *Checkpoint) error
// Get a checkpoint
Get(ctx context.Context, key string) (*Checkpoint, error)
}
```
**Why do we need CheckPointStore?**
- Save state on interrupt: Tool arguments, execution position, etc.
- Load state on resume: Continue execution from the interrupt point
- Support cross-process recovery: Can resume even after process restart
## Interrupt/Resume Implementation
### 1. Configure Runner with CheckPointStore
```go
runner := adk.NewRunner(ctx, adk.RunnerConfig{
Agent: agent,
EnableStreaming: true,
CheckPointStore: adkstore.NewInMemoryStore(), // In-memory storage
})
```
### 2. Configure Agent with ApprovalMiddleware
```go
agent, err := deep.New(ctx, &deep.Config{
// ... other configuration
Handlers: []adk.ChatModelAgentMiddleware{
&approvalMiddleware{}, // Add approval middleware
&safeToolMiddleware{}, // Convert Tool errors to strings (interrupt errors continue to propagate upward)
},
})
```
### 3. Handle Interrupt Events
```go
checkPointID := sessionID
events := runner.Run(ctx, history, adk.WithCheckPointID(checkPointID))
content, interruptInfo, err := printAndCollectAssistantFromEvents(events)
if err != nil {
return err
}
if interruptInfo != nil {
// Note: it's recommended to use the same stdin reader for both "user input" and "approval y/n"
// to avoid approval input being treated as the next round's you> message
content, err = handleInterrupt(ctx, runner, checkPointID, interruptInfo, reader)
if err != nil {
return err
}
}
_ = session.Append(schema.AssistantMessage(content, nil))
```
## Interrupt/Resume Execution Flow
```
+------------------------------------------+
| User: Execute command echo hello |
+------------------------------------------+
|
+------------------------+
| Agent analyzes intent |
| Decides to call |
| execute |
+------------------------+
|
+------------------------+
| ApprovalMiddleware |
| Intercepts Tool call |
+------------------------+
|
+------------------------+
| Trigger Interrupt |
| Save state to Store |
+------------------------+
|
+------------------------+
| Return Interrupt event |
| Wait for user approval |
+------------------------+
|
+------------------------+
| User inputs y/n |
+------------------------+
|
+------------------------+
| runner.ResumeWith... |
| Resume execution |
+------------------------+
|
+------------------------+
| Execute command |
| or return rejection |
+------------------------+
```
## Chapter Summary
- **Interrupt**: The Agent's pause mechanism — pauses before critical operations to wait for confirmation
- **Resume**: Resumes execution — continues after user confirmation, or returns an error if rejected
- **ApprovalMiddleware**: A general-purpose approval middleware that intercepts specific Tool calls
- **CheckPointStore**: Saves interrupt state, supporting cross-process recovery
- **Human-agent collaboration**: Critical decisions are confirmed by humans, improving safety
## Further Thinking
**Other Interrupt scenarios:**
- Multi-option approval: User selects one of multiple options
- Parameter completion: User provides missing parameters
- Conditional branching: User decides the execution path
**Approval strategies:**
- Allowlist: Only require approval for sensitive operations
- Blocklist: Require approval for all operations, except safe ones
- Dynamic rules: Decide whether to require approval based on argument content