4_nested_interrupt

command
v0.0.0-...-a80cde9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 21, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

README

Example 4: Nested Interrupts (Outer Approval + Inner Risk Check)

This example demonstrates nested interrupt handling where an InvokableApprovableTool wraps an InvokableGraphTool that contains its own internal interrupt for risk approval.

What This Example Shows

  • Two-level interrupt/resume flow
  • Outer interrupt: Tool-level approval via InvokableApprovableTool
  • Inner interrupt: Workflow-level risk check via compose.StatefulInterrupt
  • Proper interrupt state isolation between layers
  • Sequential approval handling

Architecture

User Request
      │
      ▼
┌─────────────────────────────────────────────┐
│         InvokableApprovableTool             │
│  ┌───────────────────────────────────────┐  │
│  │      InvokableGraphTool               │  │
│  │  ┌─────────────────────────────────┐  │  │
│  │  │         Workflow                │  │  │
│  │  │                                 │  │  │
│  │  │  validate → risk_check_execute  │  │  │
│  │  │              ↓                  │  │  │
│  │  │     [INNER INTERRUPT]           │  │  │  ← If amount > $1000
│  │  │     (risk approval)             │  │  │
│  │  └─────────────────────────────────┘  │  │
│  └───────────────────────────────────────┘  │
│                    ↓                        │
│           [OUTER INTERRUPT]                 │  ← Always (tool approval)
│           (tool approval)                   │
└─────────────────────────────────────────────┘

Interrupt Flow

1. User: "Transfer $1500 from A001 to B002"
         │
         ▼
2. Agent calls transfer_funds tool
         │
         ▼
3. OUTER INTERRUPT (InvokableApprovableTool)
   "tool 'transfer_funds' interrupted... waiting for approval"
         │
         ▼
4. User approves (Y)
         │
         ▼
5. Workflow executes: validate → risk_check_and_execute
         │
         ▼
6. INNER INTERRUPT (amount > $1000)
   "High-value transfer of $1500 requires risk team approval"
         │
         ▼
7. User approves (Y)
         │
         ▼
8. Transfer completes

Key Components

Inner Interrupt (Risk Check)
workflow.AddLambdaNode("risk_check_and_execute", compose.InvokableLambda(func(ctx context.Context, validation *validationResult) (*TransferOutput, error) {
    // Check if resuming from interrupt
    wasInterrupted, _, storedValidation := compose.GetInterruptState[*validationResult](ctx)
    
    if wasInterrupted {
        isTarget, hasData, data := compose.GetResumeContext[*InternalApprovalResult](ctx)
        if isTarget && hasData {
            if data.Approved {
                // Execute transfer
            }
            // Rejected
        }
        // Re-interrupt if not target
    }
    
    // First run - check if high-value
    if validation.Amount > 1000 {
        return nil, compose.StatefulInterrupt(ctx, &InternalApprovalInfo{
            Step:    "risk_check",
            Message: fmt.Sprintf("High-value transfer of $%.2f requires risk team approval", validation.Amount),
        }, validation)
    }
    
    // Low-value - execute directly
}))
Type Registration for Interrupts
func init() {
    schema.Register[*InternalApprovalInfo]()
    schema.Register[*InternalApprovalResult]()
    schema.Register[*validationResult]()  // For interrupt state
}
Handling Multiple Interrupts
interruptCount := 0
for {
    // ... process events ...
    
    if lastEvent.Action.Interrupted != nil {
        interruptCount++
        
        var resumeData any
        if interruptCount == 1 {
            // First interrupt is outer (tool approval)
            resumeData = &tool2.ApprovalResult{Approved: true}
        } else {
            // Second interrupt is inner (risk approval)
            resumeData = &InternalApprovalResult{Approved: true, Comment: "Risk approved"}
        }
        
        iter, _ = runner.ResumeWithParams(ctx, checkpointID, &adk.ResumeParams{
            Targets: map[string]any{
                interruptID: resumeData,
            },
        })
    }
}

Running the Example

# Set your OpenAI API key
export OPENAI_API_KEY=your-api-key

# Run the example
go run main.go

Expected Output

=== Nested Interrupt Test ===

This example tests:
1. InvokableApprovableTool wraps InvokableGraphTool
2. The inner workflow has its own interrupt (risk check)
3. Both interrupts should work independently

User Query: Transfer $1500 from account A001 to account B002

[Agent calls transfer_funds tool]

--- Interrupt #1 detected ---
Interrupt ID: xxx
[Tool approval interrupt]

Your decision (Y/N): Y

--- Resuming (interrupt #1) ---

  [Workflow] Validating transfer...
  [Workflow] Performing risk check...
  [Workflow] High-value transfer detected, triggering INTERNAL interrupt...

--- Interrupt #2 detected ---
Interrupt ID: yyy
[Risk approval interrupt]

Your decision (Y/N): Y

--- Resuming (interrupt #2) ---

  [Workflow] Resuming from interrupt...
  [Workflow] Risk team approved with comment: Risk approved by manager
  [Workflow] Executing transfer...

[Agent returns transfer confirmation]

=== Test Complete (Total interrupts: 2) ===

Key Takeaways

  1. Distinct Interrupt State Types: Outer (string) and inner (*graphToolInterruptState) use different types, preventing conflicts
  2. Sequential Approval: Each interrupt must be resolved before the next can occur
  3. State Preservation: StatefulInterrupt preserves data needed for resume
  4. Type Registration: All interrupt info/result types must be registered with schema.Register
  5. Interrupt Identification: Use interruptID from the event to target the correct interrupt when resuming

Documentation

The Go Gopher

There is no documentation for this package.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL