surprisingly complex Interrupt shit

we found a way tho lets go
This commit is contained in:
lda
2026-04-28 09:27:55 +07:00 Verified
parent 60f2af9cb3
commit dcdd2294cc
2 changed files with 158 additions and 0 deletions
+94
View File
@@ -145,6 +145,20 @@ Conditions are structured JSON, not freeform code strings.
.type: "join"
```
#### **InterruptNode**
```text
.id
.type: "interrupt"
.kind // approval | text_input | choice | tool_auth | ...
.request_map // graph state/input path -> interrupt payload field
.out_map // resume payload field -> graph state path
.outcomes[] // ex: submitted, cancelled
```
Interrupt nodes are explicit graph nodes, not arbitrary line-level pauses inside Python code.
This keeps pause points visible, typed, traceable, and easier to validate.
### Edges
Routing is outcome-based.
@@ -313,6 +327,86 @@ Two useful execution entry points fall out of this:
That keeps the main execution loop small and makes future interrupt/re-invoke semantics easier to model.
### Interrupts
Interrupts should be graph-native and typed.
Preferred model:
1. A normal business node returns an outcome such as `needs_input`
2. The graph routes to an `InterruptNode`
3. The interrupt node produces a typed interrupt request
4. The runtime marks the run interrupted and surfaces the request externally
5. External code supplies a typed resume payload
6. The interrupt node maps resume payload back into state
7. The graph continues via a declared interrupt outcome such as `submitted` or `cancelled`
This is deliberately stricter than line-level dynamic interrupts inside arbitrary node code.
Why:
- pause points stay visible in the graph
- interrupt payloads can be validated by `kind`
- traces stay clean
- child graph interruption is easier to surface to parent graphs
- `foreach` and nested execution are less likely to become cursed
#### Interrupt request shape
At runtime, an interrupt should become a structured request attached to run state.
```text
InterruptRequest
.id
.node_id
.kind
.payload
.resumable
```
Notes:
- `kind` chooses the interrupt contract
- `payload` is still JSON-like, but should be typed by `kind`
- v1 should allow only one active interrupt per run at a time
- the whole run pauses in v1, even if the interrupt originated inside a child graph or future foreach frame
#### Resume semantics
Resume should not jump back into the middle of arbitrary Python code.
Instead:
- resume data is delivered to the interrupt node
- the interrupt node maps resume fields back into state through explicit `out_map`
- graph routing continues normally from that node
This means the primary interrupt design is node-based, not line-based.
#### Child graph behavior
If a child graph interrupts:
- the parent run is considered interrupted in v1
- the parent only needs to know that the child graph node interrupted
- the child interrupt payload should still be surfaced to the external caller/UI
#### Foreach compatibility
Interrupt design must be compatible with future foreach support.
Bad designs to avoid:
- one anonymous global interrupt payload with no origin
- treating interrupt like a normal business outcome only
- resuming from arbitrary instruction pointers
Better design:
- interrupt belongs to a specific execution frame / node
- v1 may still pause the whole run
- future foreach support should record which iteration/frame produced the interrupt
### Retry
Default retry behavior should be strict.
+64
View File
@@ -276,6 +276,70 @@ Default retry behavior should be strict and deterministic.
- failfast is the default engine behavior
- more permissive collection behavior should be opt-in at graph level, especially in `foreach`
### Interrupt semantics
Interrupt should primarily be a graph node, not a random Python line.
Preferred shape:
- business node returns something like `needs_input`
- graph routes to an `InterruptNode`
- interrupt node raises/surfaces a typed interrupt request
- runtime marks run interrupted
- caller provides resume payload later
- interrupt node maps resumed payload back into state
- graph continues from declared next outcome
This is cleaner than loose line-level interrupt for this engine.
Why:
- graph can see interrupt points
- planner can reason about them
- trace is cleaner
- validation is easier
- child graphs and future foreach are less cursed
Interrupt node shape idea:
```text
InterruptNode
.id
.type: "interrupt"
.kind
.request_map // input/state -> interrupt payload
.out_map // resume payload -> state
.outcomes[] // submitted, cancelled, ...
```
Interrupt request runtime shape:
```text
InterruptRequest
.id
.node_id
.kind
.payload
.resumable
```
Rules:
- one active interrupt per run in v1
- whole run pauses in v1
- if child graph interrupts, parent sees child graph node interrupted
- external caller/UI should still receive the child interrupt payload
- resume should continue from interrupt node semantics, not arbitrary instruction pointer
Kinds are explicit and expandable:
- `approval`
- `text_input`
- `choice`
- `tool_auth`
Generic now, specialized later is easy because `kind` already exists.
### Reuse and composition
- workflow input/output separation makes a workflow reusable as a node