--- 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