surprisingly complex Interrupt shit
we found a way tho lets go
This commit is contained in:
@@ -145,6 +145,20 @@ Conditions are structured JSON, not freeform code strings.
|
|||||||
.type: "join"
|
.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
|
### Edges
|
||||||
|
|
||||||
Routing is outcome-based.
|
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.
|
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
|
### Retry
|
||||||
|
|
||||||
Default retry behavior should be strict.
|
Default retry behavior should be strict.
|
||||||
|
|||||||
@@ -276,6 +276,70 @@ Default retry behavior should be strict and deterministic.
|
|||||||
- failfast is the default engine behavior
|
- failfast is the default engine behavior
|
||||||
- more permissive collection behavior should be opt-in at graph level, especially in `foreach`
|
- 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
|
### Reuse and composition
|
||||||
|
|
||||||
- workflow input/output separation makes a workflow reusable as a node
|
- workflow input/output separation makes a workflow reusable as a node
|
||||||
|
|||||||
Reference in New Issue
Block a user