Skip to content

feat(interrupt): implement interrupt system for human-in-the-loop workflows#777

Closed
zastrowm wants to merge 7 commits intomainfrom
agent-tasks/479
Closed

feat(interrupt): implement interrupt system for human-in-the-loop workflows#777
zastrowm wants to merge 7 commits intomainfrom
agent-tasks/479

Conversation

@zastrowm
Copy link
Copy Markdown
Member

@zastrowm zastrowm commented Apr 1, 2026

Motivation

Agents executing tools often need to pause for human approval or input before proceeding with sensitive operations like financial transactions, data modifications, or external API calls. Without a built-in interrupt mechanism, developers must implement custom solutions to handle these human-in-the-loop workflows.

This implementation adds a comprehensive interrupt system that allows agents to:

  • Pause execution at specific points (before individual tool calls, before batch tool execution, or within tool callbacks)
  • Return interrupt details to the caller for user interaction
  • Resume execution with user responses

Resolves #479

Public API Changes

New interrupt() method on hook events and tool context

Tools can now call interrupt() to pause execution:

const tool = new FunctionTool({
  name: 'transfer_money',
  description: 'Transfer money between accounts',
  callback: async (input, context) => {
    if (input.amount > 1000) {
      context.interrupt({
        name: 'confirm_transfer',
        reason: `Confirm transfer of $${input.amount}?`
      })
    }
    return { success: true }
  },
})

Hooks can also trigger interrupts:

agent.addHook(BeforeToolCallEvent, (event) => {
  if (event.toolUse.name === 'dangerous_operation') {
    event.interrupt({
      name: 'safety_approval',
      reason: 'This operation requires approval'
    })
  }
})

New stopReason: 'interrupt' and interrupts array on AgentResult

When an interrupt is triggered, the agent returns immediately:

const result = await agent.invoke('Transfer $5000')
if (result.stopReason === 'interrupt') {
  console.log(result.interrupts) // [{ name: 'confirm_transfer', reason: '...' }]
}

Use Cases

  • Financial transactions: Require approval before processing large transfers
  • Data modifications: Confirm destructive operations before execution
  • External integrations: Validate API calls to third-party services
  • Security-sensitive operations: Add human verification for privileged actions

…kflows

Add comprehensive interrupt support for agent execution pausing:

- Implement Interrupt and InterruptState classes to manage interrupt lifecycle
- Add interrupt() method to BeforeToolCallEvent and BeforeToolsEvent hooks
- Add interrupt() method to ToolContext for use in tool callbacks
- Handle InterruptError in agent loop to return stopReason: 'interrupt'
- Return interrupts array in AgentResult for inspection

The interrupt system allows agents to pause execution at specific points
and resume with user responses, enabling human-in-the-loop workflows
for approvals, confirmations, and other interactive scenarios.

Resolves #479
@github-actions github-actions Bot added the strands-running <strands-managed> Whether or not an agent is currently running label Apr 1, 2026
Comment thread src/hooks/events.ts
Comment thread src/agent/agent.ts
@github-actions

This comment was marked as off-topic.

@github-actions github-actions Bot removed the strands-running <strands-managed> Whether or not an agent is currently running label Apr 1, 2026
@zastrowm

This comment was marked as resolved.

@github-actions github-actions Bot added the strands-running <strands-managed> Whether or not an agent is currently running label Apr 1, 2026
- Export interrupt types from src/index.ts (Interrupt, InterruptError,
  InterruptState, InterruptParams, InterruptResponse, InterruptResponseContent)
- Extract duplicate InterruptError handling into _createInterruptResult() helper
- Add interrupt state check after BeforeToolsEvent/BeforeToolCallEvent yields
  to properly propagate hook interrupts
- Implement name-based interrupt matching in getOrCreateInterrupt() for resume
  across model calls with different tool use IDs
- Update jsdoc for _interruptCounter to document serialization exclusion
- Add e2e tests for interrupt → response → continue resume flow
- Add tests for name-matching behavior in InterruptState

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@github-actions

This comment was marked as resolved.

@github-actions github-actions Bot removed the strands-running <strands-managed> Whether or not an agent is currently running label Apr 1, 2026
Comment thread src/agent/agent.ts
@zastrowm

This comment was marked as resolved.

@github-actions github-actions Bot added the strands-running <strands-managed> Whether or not an agent is currently running label Apr 2, 2026
…l results

When resuming from an interrupt, the agent now:
- Skips re-calling the model and uses the stored assistant message
- Preserves completed tool results to avoid re-executing successful tools
- Only executes the tool that was interrupted (and any remaining tools)

Example scenario: If tools A, B, C are requested and A & B succeed but C
interrupts, on resume only C executes - A and B are skipped.

Implementation:
- Add PendingToolExecution interface to store assistant message and completed results
- Store pending state when interrupt occurs during tool execution
- On resume, check for pending state and skip model invocation
- Pass completed tool results to executeTools to skip already-completed tools
- Clear pending state when user sends a new message (abandons interrupted flow)

This addresses the reviewer feedback that resume should jump directly to
tool execution without re-calling the model.
@github-actions

This comment was marked as resolved.

@github-actions github-actions Bot removed the strands-running <strands-managed> Whether or not an agent is currently running label Apr 2, 2026
Comment thread src/agent/agent.ts Outdated
@zastrowm

This comment was marked as resolved.

@github-actions github-actions Bot added the strands-running <strands-managed> Whether or not an agent is currently running label Apr 2, 2026
…ptState

Move the logic for reconstructing assistant message and completed tool
results from agent.ts into InterruptState.getPendingExecution() method.

This provides better encapsulation and makes the agent code cleaner:
- Before: inline reconstruction with Message.fromMessageData() and loop
- After: this._interruptState.getPendingExecution()

The method returns { assistantMessage, completedToolResults } or undefined
if no pending execution exists.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 2, 2026

Encapsulation Refactor Complete

Addressed the second feedback item in commit 070b5de.

Change

Moved the pending execution reconstruction logic into InterruptState.getPendingExecution() method:

// New method signature
getPendingExecution(): { 
  assistantMessage: Message
  completedToolResults: Map<string, ToolResultBlock> 
} | undefined

Result

The agent code is now cleaner and the reconstruction logic is encapsulated in the InterruptState class where it belongs.

All 1862 tests pass.

@github-actions github-actions Bot removed the strands-running <strands-managed> Whether or not an agent is currently running label Apr 2, 2026
@github-actions github-actions Bot added the strands-running <strands-managed> Whether or not an agent is currently running label Apr 2, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 2, 2026

Review Summary - Follow-up Review

Assessment: Approve

All previous feedback has been addressed. The implementation is now complete with proper exports, comprehensive resume flow tests, and the maintainer-requested improvements for skipping model re-invocation on resume.

Changes Since Previous Review

Addressed Issues:

  • Exports: Interrupt, InterruptError, InterruptState, and related types now exported from src/index.ts
  • Resume Tests: Comprehensive E2E tests added covering interrupt → response → continue flow, including partial execution resume
  • Code Duplication: _createInterruptResult() helper method consolidates interrupt result creation
  • Maintainer Feedback: Resume without model re-invocation implemented with getPendingExecution() encapsulation
Documentation Note

This feature adds significant public API surface. Consider a follow-up documentation PR covering:

  • interrupt() method on tool context and hook events
  • stopReason: 'interrupt' and interrupts array on AgentResult
  • Resume flow with InterruptResponseContent

Well-designed interrupt system that aligns with SDK tenets—simple API entry points, extensible through hooks, and composable with existing patterns.

@github-actions github-actions Bot removed the strands-running <strands-managed> Whether or not an agent is currently running label Apr 2, 2026
@github-actions github-actions Bot added the strands-running <strands-managed> Whether or not an agent is currently running label Apr 2, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 2, 2026

Review Summary - Third Review

Assessment: Approve (No changes needed)

The changes since my last review are all improvements - test consolidation following guidelines, better encapsulation (_interruptState now private), generic interrupt<T>() for type safety, and the new isInterruptResponseContent type guard.

All previous feedback remains addressed. Ready to merge.

@github-actions github-actions Bot removed the strands-running <strands-managed> Whether or not an agent is currently running label Apr 2, 2026
@zastrowm zastrowm closed this Apr 2, 2026
@zastrowm
Copy link
Copy Markdown
Member Author

zastrowm commented Apr 2, 2026

Moved over to #784

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Interrupts - Tool Call

2 participants