Skip to content

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

Open
zastrowm wants to merge 21 commits intomainfrom
agent-tasks/479
Open

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

Conversation

@zastrowm
Copy link
Copy Markdown
Member

@zastrowm zastrowm commented Apr 2, 2026

Description

Adds an interrupt system for human-in-the-loop workflows. Agents can pause execution within tool callbacks, BeforeToolCallEvent hooks, or BeforeToolsEvent hooks to collect user input before proceeding.

The interrupt() method halts the agent loop on first call, returning stopReason: 'interrupt' with an interrupts array. When the caller resumes with InterruptResponseContent objects, the same interrupt() call returns the user's response instead of halting.

Multiple hook callbacks on the same event can each raise their own interrupt. The registry collects all interrupts across callbacks before halting, and duplicate interrupt names are rejected. This matches the Python SDK's behavior.

Both assistant and tool result messages are appended only after tool execution completes, preventing dangling toolUse blocks without matching results. When an interrupt fires mid-batch, completed tool results are preserved so the agent skips the model call on resume and only executes remaining tools. Hook-level interrupts (from BeforeToolCallEvent/BeforeToolsEvent) also store pending execution state, so resume skips the model call just like tool-level interrupts.

Public API Changes

End-to-end usage

import { Agent, tool } from '@strands-agents/sdk'
import { z } from 'zod'

const transferMoney = tool({
  name: 'transfer_money',
  description: 'Transfer money between accounts',
  inputSchema: z.object({ amount: z.number() }),
  callback: (input, context) => {
    if (input.amount > 1000) {
      const response = context.interrupt({
        name: 'confirm_transfer',
        reason: 'Confirm large transfer?',
      })
      if (!response.confirmed) return 'Transfer cancelled'
    }
    return 'Transfer completed'
  },
})

const agent = new Agent({ model, tools: [transferMoney] })

// First call — agent pauses at interrupt
const result = await agent.invoke('Transfer $5000')
// result.stopReason === 'interrupt'
// result.interrupts === [{ id: '...', name: 'confirm_transfer', reason: 'Confirm large transfer?' }]

// Resume with user response — skips model call, re-executes tool with response
const resumed = await agent.invoke(
  result.interrupts.map((i) => ({
    interruptResponse: { interruptId: i.id, response: { confirmed: true } },
  }))
)
// resumed.stopReason === 'endTurn'

New types and exports

  • Interrupt class — interrupt data returned in AgentResult.interrupts
  • InterruptParams, InterruptResponse, InterruptResponseContent types
  • InvokeArgs now includes InterruptResponseContent[] as a valid input type
  • ToolContext now includes interrupt<T>(params: InterruptParams): T
  • InterruptError and InterruptState are internal and not exported

Related Issues

Documentation PR

Type of Change

New feature

Testing

  • I ran npm run check

Checklist

  • I have read the CONTRIBUTING document
  • I have added any necessary tests that prove my fix is effective or my feature works
  • I have updated the documentation accordingly
  • I have added an appropriate example to the documentation to outline the feature, or no new docs are needed
  • My changes generate no new warnings
  • Any dependent changes have been merged and published

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

strands-agent and others added 7 commits April 1, 2026 16:21
…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
- 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>
…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.
…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 github-actions bot added the strands-running <strands-managed> Whether or not an agent is currently running label Apr 2, 2026
Comment thread src/types/agent.ts
Comment thread src/agent/agent.ts Outdated
@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 2, 2026

Assessment: Request Changes

This is a well-designed implementation of the human-in-the-loop interrupt system with comprehensive tests. The API design is intuitive and the code follows project patterns well.

Review Categories
  • Documentation PR (Blocking): This PR introduces significant new public API surface (classes, types, methods) that users need to understand. A documentation PR is required and must be linked before merging.
  • API Review: This PR introduces new public primitives (Interrupt, InterruptError, InterruptState) and modifies existing interfaces (ToolContext, AgentResult). Consider adding the needs-api-review label.
  • Breaking Changes: Two breaking changes identified that should be explicitly documented: (1) ToolContext.interrupt() requirement, (2) messages array now readonly.

The interrupt mechanism itself is well-thought-out, particularly the handling of partial tool execution and resume semantics.

Comment thread src/tools/tool.ts
@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 strands-running <strands-managed> Whether or not an agent is currently running and removed strands-running <strands-managed> Whether or not an agent is currently running labels Apr 2, 2026
@github-actions github-actions bot added strands-running <strands-managed> Whether or not an agent is currently running and removed strands-running <strands-managed> Whether or not an agent is currently running labels Apr 2, 2026
@strands-agents strands-agents deleted a comment from github-actions bot Apr 2, 2026
@zastrowm zastrowm marked this pull request as ready for review April 2, 2026 23:45
@github-actions github-actions bot removed the strands-running <strands-managed> Whether or not an agent is currently running label Apr 2, 2026
@strands-agents strands-agents deleted a comment from github-actions bot Apr 3, 2026
Comment thread src/multiagent/graph.ts Outdated
// InterruptResponseContent[] passes through to the agent — cannot be merged with deps
if (Array.isArray(input) && input.length > 0 && isInterruptResponseContent(input[0])) {
return input
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would recommend leaving this out for now and handle graph interrupts separately. This is just one piece of a larger change.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Originally it was added to workaround the type-checking, but moving to exclude via type system instead

Comment thread src/hooks/events.ts
* })
* ```
*/
interrupt<T = unknown>(params: InterruptParams): T {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Python, we setup an _Interruptible protocol (here). This allowed us to define shared logic for the hook events to derive. Most notably, it defines the interrupt method. Could we do something similar in TS to help avoid duplication.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding interface + shared method; I don't want to add a new base class for just this method and mixins (or protocols) aren't a thing in TS as much as Python

Comment thread src/hooks/events.ts Outdated
agent: LocalAgent
toolUse: { name: string; toolUseId: string; input: JSONValue }
tool: Tool | undefined
interruptState?: InterruptState
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would make sense to manager this under LocalAgent that way we can access through agent.interruptState. I understand though we would have to make it a public attribute which maybe we want to hold off on. Was that your thinking and the reason why you pass interruptState separately?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand though we would have to make it a public attribute which maybe we want to hold off on.

I don't want to make this public as to me it's an internal implementation detail. We can use duck typing/casting to access it if we want though.

Was that your thinking and the reason why you pass interruptState separately?

It was more of a "We already have access to it here, why not pass it in directly without a cast". That said, I know in other places we use the cast so I think I'll migrate to that instead which leaves us open to a better two way door

Comment thread src/agent/agent.ts Outdated
Comment on lines +450 to +456
} catch (error) {
if (error instanceof InterruptError) {
const interruptResult = this._createInterruptResult()
yield await this._invokeCallbacks(new AgentResultEvent({ agent: this, result: interruptResult }))
return interruptResult
}
throw error
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this catch? Don't we handle interrupt errors gracefully in streamGenerator?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this one; I think this was from an earlier revision when error handling wasn't figured out.

According to Kiro this would be effective for some cases Like BeforeInvocationEvent, but those are not a concern for today

Comment thread src/types/interrupt.ts Outdated
* User's response to the interrupt.
* Can be any value that the hook or tool expects.
*/
response: unknown
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be serializable? Should we use JSONValue?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah; changed it to JSONValue; same for reason

Comment thread src/hooks/registry.ts
if (collectedInterrupts.length > 0) {
const seen = new Set<string>()
for (const interrupt of collectedInterrupts) {
if (seen.has(interrupt.name)) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can we gather all of the duplicates and then raise the error?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep; added

Comment thread src/agent/__tests__/agent.test.ts Outdated

expect(beforeTools).toEqual(
new BeforeToolsEvent({
expect.objectContaining({
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious, why doesnt the class work here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was the private field that was added to BeforeToolsEvent; but with that change gone, we can revert

Comment thread src/hooks/events.ts Outdated
throw new Error('Interrupt state not available')
}

const interruptId = `beforeTools:${params.name}`
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need a more unique id here? Im thinking of an example like this:

Imagine the user set up an interrupt in the BeforeTools event with id of ConsentToTools.

If the agent executes, then decides to invoke a tool, this interrupt with trigger with an id of: beforeTools:ConsentToTools.

If the user responds to this interrupt, the agent loop will get to this BeforeTools event, pass the interrupt, call the tools, and continue the agent loop. Then the model decided to call a tool again. This previous interrupt will still be in interrupt state, so this will be bypassed without another interrupt.

Im thinking of maybe including all of the upcoming tooluseids in this id, so that its unique for each turn. What do you think?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The interrupt state should be cleared before that next loop and so this id would not have to be more unique. You can see here in Python we clear the interrupt state immediately after tool execution so that another internal loop will lead to another interrupt (unless of course the user is caching themselves with agent.state).

I'm scanning the PR right now to see if similar logic is present.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't appear that interrupt state is cleared after tool calls. We clear at the end of invocation. So we either need to clear after tool calls or add a more unique identifier as suggested. Using the batch of tool use ids could work because it is deterministic.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch

So we either need to clear after tool calls or add a more unique identifier as suggested.

Clearing after tool executions. It doesn't make sense to me to add ids given that they should always be cleared and because for other interruptible events in the future (like model invocation, if we supported) we don't have a similar id, so this pattern makes sense to me

Comment thread src/interrupt.ts
return {
id: this.id,
name: this.name,
...(this.reason !== undefined && { reason: this.reason }),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the other comment, but should these be unknown if we want to be able to JSON serialize them?

@Unshure
Copy link
Copy Markdown
Member

Unshure commented Apr 3, 2026

General feedback from my review agent:

Details

Critical Issues

Issue 1: BeforeToolsEvent interrupt ID lacks cycle uniqueness

  • File: src/hooks/events.ts:654

  • Problem: The interrupt ID for BeforeToolsEvent is beforeTools:${params.name}, which has no cycle-specific component. If a hook interrupts in cycle 1, gets resumed, and the agent continues to cycle 2 where the same hook fires with the same name, getOrCreateInterrupt returns the cycle-1 interrupt (which already has a response). The hook silently receives the old response instead of interrupting again.

    This means a second batch of tool calls would be auto-approved without user confirmation — a security concern for approval-type hooks.

    BeforeToolCallEvent and tool callbacks don't have this issue because their IDs include toolUseId, which is unique per tool use block.

  • Suggestion: Include a cycle-distinguishing component in the BeforeToolsEvent interrupt ID. Options:

    • Include the assistant message's tool use IDs: beforeTools:${toolUseIds.join(',')}:${params.name}
    • Clear answered interrupts at the start of each cycle
    • Add a cycle counter to InterruptState

Important Issues

Issue 1: Interrupt state not included in snapshot system

  • File: src/agent/agent.ts:277, src/agent/snapshot.ts
  • Problem: InterruptState has toJSON()/fromJSON() methods suggesting planned persistence, but the snapshot system (ALL_SNAPSHOT_FIELDS, takeSnapshot, loadSnapshot) doesn't include interrupt state. If the agent process restarts between interrupt and resume, the interrupt state is lost and the user cannot resume.
  • Suggestion: Either integrate interruptState into the snapshot system now, or add a // TODO and documentation noting this limitation. Users relying on SessionManager for durability would expect interrupt state to survive restarts.

Issue 2: Duplicate test names in registry.test.ts

  • File: src/hooks/__tests__/registry.test.ts:285 and src/hooks/__tests__/registry.test.ts:310
  • Problem: Two tests share the identical name 'runs all callbacks when only some raise interrupts'. The first tests that a non-interrupt error stops execution mid-way. The second tests that non-interrupting callbacks still run between interrupting ones. Having duplicate names makes test output ambiguous.
  • Suggestion: Rename the first to something like 'stops on non-interrupt error even when interrupts were collected' and keep the second as-is.

Issue 3: isInterruptResponseContent not exported

  • File: src/index.ts:31-33
  • Problem: The InterruptResponseContent type is exported, but the isInterruptResponseContent type guard is not. Users who need to inspect content blocks (e.g., in custom middleware or multi-agent orchestration) have no public way to check if a value is an interrupt response.
  • Suggestion: Export isInterruptResponseContent from src/index.ts alongside the type.

Issue 4: Breaking change to ToolContext interface

  • File: src/tools/tool.ts:25-51
  • Problem: ToolContext gains a required interrupt method. Any existing code that constructs a ToolContext manually (custom tool implementations, tests) will fail type-checking. The PR updates internal tests but external consumers will break.
  • Suggestion: Consider making interrupt optional (interrupt?: ...) on the interface, with the agent providing the implementation. Tools that call context.interrupt?.() or context.interrupt!() would still work, and existing code wouldn't break. Alternatively, document this as a breaking change in the PR description.

Suggestions

  • The response field on Interrupt is the only mutable field (no readonly). Consider a setter or method like setResponse() to make mutation points more explicit and greppable.
  • The _stream method's pattern of catching InterruptError and throwing it back into the generator (streamGenerator.throw(error)) is clever but complex. A brief inline comment explaining why the error is re-thrown into the generator (to let executeTools store pending state) would help future maintainers.
  • InterruptState.getUnansweredInterrupt() returns only the first unanswered interrupt. If multiple interrupts are unanswered, only the first is surfaced. This is fine for the current flow but worth documenting.

@zastrowm
Copy link
Copy Markdown
Member Author

zastrowm commented Apr 5, 2026

General feedback from my review agent:

@Unshure any items you feel should be addressed/prioritized?

Addressed the snapshot one; that was a good catch

@github-actions github-actions bot added the strands-running <strands-managed> Whether or not an agent is currently running label Apr 5, 2026
@github-actions github-actions bot removed the strands-running <strands-managed> Whether or not an agent is currently running label Apr 5, 2026
# Conflicts:
#	src/agent/__tests__/snapshot.test.ts
#	src/agent/snapshot.ts
@github-actions github-actions bot added strands-running <strands-managed> Whether or not an agent is currently running and removed strands-running <strands-managed> Whether or not an agent is currently running labels Apr 5, 2026
Comment thread src/interrupt.ts
* Tool results that were completed before the interrupt.
* Maps toolUseId to serialized ToolResultBlock data.
*/
completedToolResults: Record<string, ContentBlockData>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be ToolResultBlockData.

Comment thread src/interrupt.ts
*
* Interrupt state is cleared after resuming.
*/
export class InterruptState {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Should this implements InterruptStateData

Comment thread src/interrupt.ts

const completedToolResults = new Map<string, ToolResultBlock>()
for (const [toolUseId, resultData] of Object.entries(this.pendingToolExecution.completedToolResults)) {
const block = contentBlockFromData(resultData)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a toolResultContentFromData as well.

Comment thread src/tools/tool.ts
* })
* ```
*/
interrupt<T = unknown>(params: InterruptParams): T
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the default be JSONValue instead of unknown?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this could break users mocking ToolContext for unit tests on custom function tools. I'm not so concerned about that though since we are rc right now.

Comment thread src/multiagent/graph.ts
: input.map((b) => ('type' in b ? (b as ContentBlock) : contentBlockFromData(b)))
: (input as Exclude<typeof input, string>).map((b) =>
'type' in b ? (b as ContentBlock) : contentBlockFromData(b as ContentBlockData)
)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this necessary with the changes to the MultiAgentInput definition?

Comment thread src/hooks/events.ts
* Accesses the agent's interrupt state to register or resume an interrupt.
*/
function _interruptFromAgent<T>(agent: LocalAgent, interruptId: string, params: InterruptParams): T {
const interruptState = (agent as unknown as { _interruptState?: InterruptState })._interruptState
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This awkward casting seems to be a sign that we need to find a way to expose internal state publicly? We can figure that out in a follow up though.

Comment thread src/agent/agent.ts
// If user sends a regular message (not interrupt responses), clear any pending state
// This allows the user to "abandon" an interrupted workflow and start fresh
this._interruptState.clearPendingToolExecution()
this._interruptState.deactivate()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works here in TS because we don't add tool uses until we have the tool results correct? In Python, this would not work because we would have a unanswered tool uses in the messages array.

Comment thread src/agent/agent.ts
let structuredOutputChoice: ToolChoice | undefined

// Emit event before the try block
yield new BeforeInvocationEvent({ agent: this })
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What events fire on interrupt and resume is something we should make more clear in the docs. In Python, we skip the AfterToolCallEvent if interrupting a tool. The motivation here is that we don't have a tool result to feed it. We do however still emit AfterInvocationEvent. We also emit the before events on resume including BeforeToolCallEvent. This means we emit a BeforeToolCallEvent without a pairing AfterToolCallEvent. Maybe we want to change the behavior for TS?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it would also be worth adding some sort of field to the Before events that indicates whether or not the agent is resuming from an interrupt. There might be certain things a users doesn't want done if just resuming from an interrupt. This could be considered for follow up though.

Comment thread src/hooks/events.ts
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should consider emitting an InterruptEvent. In Python, we have a ToolInterruptEvent, but maybe we can generalize. This could be considered for follow up though.

Comment thread src/interrupt.ts
* @param reason - Optional reason for the interrupt
* @returns The interrupt (may have a response if resuming)
*/
getOrCreateInterrupt(id: string, name: string, reason?: JSONValue): Interrupt {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Python, we allow a user to pass in a response from within their hook or tool definitions. This is to skip the interrupt if they already have a response ready. For example, they might raise an interrupt in BeforeToolCallEvent and save the response in state for future invocations ([details](Can users provide a preemptive response)). Not absolutely necessary but customers can get slightly cleaner code:

// not supported
agent.addHook(BeforeToolCallEvent, (event) => {
  if (event.toolUse.name !== 'delete_files') return

  const cached = event.agent.appState.get('approval')
  if (cached) return

  const response = event.interrupt({ name: 'approval', reason: 'Confirm deletion?' })
  event.agent.appState.set('approval', response)

  if (response !== 'y') {
    event.cancel = 'User denied permission'
  }
})
// supported
agent.addHook(BeforeToolCallEvent, (event) => {
  if (event.toolUse.name !== 'delete_files') return

  const cached = event.agent.appState.get('approval')
  const response = event.interrupt({ name: 'approval', reason: 'Confirm deletion?', response: cached })
  event.agent.appState.set('approval', response)

  if (response !== 'y') {
    event.cancel = 'User denied permission'
  }
})

Not sure if it is worth it anymore. Can follow up later. Definitely doesn't need to be considered for this PR.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since interrupts are an internal mechanism, the unit tests should offer sufficient coverage. Still, I think it would be good to at least integ test the happy paths. We do this in Python (here). Could be considered for follow up.

Comment thread src/agent/agent.ts
lastMessage,
traces: this._tracer.localTraces,
metrics: this._meter.metrics,
interrupts: this._interruptState.getInterruptsList(),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should only be returning the interrupts not yet responded to. This is what we do in Python (here). We can get into this situation in a few ways:

  • Customer raises interrupt in BeforeToolsEvent and then again in BeforeToolCallEvent.
  • Customer raises multiple interrupts on a single event (can only be processed one at a time).
  • Customer doesn't respond to all the interrupts the first time around.

Comment thread src/agent/agent.ts
yield new BeforeInvocationEvent({ agent: this })

// Normalize input to get the user messages for telemetry
const inputMessages = this._normalizeInput(args)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like we are allowing users to mix in other content block types with their interrupt responses (in Python we raise an exception). It looks like if interrupt response isn't the first content item, we could end up populating inputMessages which get appended to the messages array down below (through a second call to normalizeInput).

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 Interrupts - Before Tools Event Interrupts - Before Tool Call Event

4 participants