Repository navigation
feat(infinity-daemon): add input-less Interrupt for in-flight generation - #165
akainth015 wants to merge 1 commit into
Conversation
Deploying infinity with
|
| Latest commit: |
65776c2
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://b1495459.infinity-dc7.pages.dev |
| Branch Preview URL: | https://pr-daemon-interrupt.infinity-dc7.pages.dev |
| // Input-less interrupts cancel the in-flight completion | ||
| // just like user text; the markers themselves are consumed | ||
| // by the shared stripping below (never stepped). | ||
| if has_user_text || batch.iter().any(|(msg, _)| is_interrupt_input(msg)) { |
There was a problem hiding this comment.
Should we do the || inside to avoid a double iteration?
There was a problem hiding this comment.
Folded into a single backwards scan, last_decisive_input. It answers both whether the batch interrupts and which decisive input came last.
| // User text supersedes an interrupt marker in the | ||
| // same batch: folding the text already settles a | ||
| // trailing tool call, so the marker is redundant. | ||
| batch.retain(|(msg, _)| !is_interrupt_input(msg)); |
There was a problem hiding this comment.
?? If there is an interrupt after user text that would mean we should not trigger the model, right? Should think through the semantics carefully.
There was a problem hiding this comment.
Agreed, the order now decides: the last decisive input wins. [text, interrupt] records the text but does not trigger the model; [interrupt, text] cancels and answers; [interrupt] just cancels. One choice to check: the unanswered text in [text, interrupt] is not persisted, since no completion validated it. That matches input cancelled before output on main, and the #159 stance on untrusted input. The tests pin this choice.
| if let InputMessageContent::User(UserContent::Text(text)) = | ||
| &mut user_inputs[0].0.content | ||
| { | ||
| text.text = format!("<interrupt>{}", text.text); |
There was a problem hiding this comment.
I would love to get rid of this code and instead have interrupts be their own message kind in the agent history and derive the <interrupt> tokens at runtime dynamically.
There was a problem hiding this comment.
Done, in a new preparatory PR, #170 (now the base of this one): interrupts are InfinityMessage::Interrupt history entries, and <interrupt> is derived when history is rendered (message::render_history). User text is never rewritten. The provider sees byte-identical output for interrupt+text, and legacy prefixed rows render unchanged with no migration. While restructuring I also fixed a latent bug here: the synthetic interrupted result was never persisted, so a late tool result could restart generation after the driver idled out.
2fe43b6 to
7920231
Compare
d7c64e1 to
b2c57e5
Compare
7920231 to
f2bdfb3
Compare
b2c57e5 to
d2870d3
Compare
f2bdfb3 to
fd5f935
Compare
…ation
Clients can stop a thread's in-flight work without queuing input and
without shutting the session down. Builds on interrupts being recorded as
history entries (`InfinityMessage::Interrupt`, previous commit).
* New `ClientMessage::Interrupt { thread_id }`: interrupts the thread and
its descendant subtree. Old daemons skip unparsable frames, so sending it
to one is a no-op.
* User text and interrupt markers are a batch's *decisive* inputs. One
backwards scan (`last_decisive_input`) decides whether the batch
interrupts the in-flight completion (any decisive input) and what happens
next (the last one):
| batch | in-flight work | new completion? |
|-------------------------|----------------|-----------------------|
| `[interrupt]` | cancelled | no |
| `[text, interrupt]` | cancelled | no |
| `[interrupt, text]` | cancelled | yes, answers the text |
| `[text]` | cancelled | yes (implicit marker) |
* A batch ending on an interrupt is recorded by `Thread::record_interrupt()`
without a completion: inputs are folded in order, a trailing unanswered
tool call is settled as interrupted (RAP cancellation notified, pending
choice dismissed; this settlement is factored out of `step_no_defer`), and
history is persisted. Exactly one `CompletionFinished` (→ `ResponseDone`)
is emitted per interrupted round.
* `HistoryManager::promote_harness_settlements()` persists the leading
interrupt entries that no completion will validate. History then ends on
the interrupt, so the driver idles out and a late tool result is dropped
instead of restarting generation. User text stays unvalidated and
unpersisted, like any input cancelled before model output.
* `SessionManager::interrupt_thread()` delivers interrupt markers to the
target and its active descendants; idle descendants are skipped.
* `ClientMessage::Interrupt` is handled in the client handler, with thread
refs stripped on the remote path like `UserInput`.
d2870d3 to
ae72ccf
Compare
fd5f935 to
65776c2
Compare
Adds
ClientMessage::Interrupt { thread_id }: cancels the in-flight completion on the target thread and its active descendants without queuing input or shutting the session down. Old daemons skip the unknown frame.last_decisive_input) decides what happens:[interrupt]and[text, interrupt]cancel without a new completion;[interrupt, text]and[text]cancel and answer the text.Thread::record_interruptrecords an interrupt-ending batch without a completion: a trailing unanswered tool call gets a synthetic "interrupted" result (late real results are dropped, which stops multi-step loops), and the pending choice is dismissed. OneResponseDoneis sent per interrupted round.promote_harness_settlements. Without this, a late tool result could restart generation once the driver idles out. Unanswered user text stays unpersisted.Stacked on #170.
Replaces #142, which GitHub auto-marked merged when the stack was reordered. Nothing reached
main.