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.
shentong.martin fb76b1c544 feat(graphtool): add InvokableGraphTool and StreamableGraphTool for wrapping compose types as tools
- Add graphtool package with InvokableGraphTool and StreamableGraphTool
- Support compose.Graph, compose.Chain, and compose.Workflow via Compilable interface
- Full interrupt/resume support with checkpoint persistence
- Add 4 examples demonstrating different use cases:
  - 1_chain_summarize: Document summarization with compose.Chain
  - 2_graph_research: Multi-source research with compose.Graph + streaming
  - 3_workflow_order: Order processing with compose.Workflow + approval
  - 4_nested_interrupt: Nested interrupts (outer approval + inner risk check)
- Update InvokableApprovableTool to support nested interrupts
- Add comprehensive README documentation for package and examples

Change-Id: Ia7cceebb9e25baf6ca4db25b36eddc74eec273cb
4 months ago
..
README.md feat(graphtool): add InvokableGraphTool and StreamableGraphTool for wrapping compose types as tools 4 months ago
main.go feat(graphtool): add InvokableGraphTool and StreamableGraphTool for wrapping compose types as tools 4 months ago

README.md

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