Skip to content

Add REPL host-embedding: external cancellation + live capability events#19

Merged
senamakel merged 2 commits into
tinyhumansai:mainfrom
senamakel:feat/repl-host-embedding
Jul 4, 2026
Merged

Add REPL host-embedding: external cancellation + live capability events#19
senamakel merged 2 commits into
tinyhumansai:mainfrom
senamakel:feat/repl-host-embedding

Conversation

@senamakel

Copy link
Copy Markdown
Member

Summary

Fills the three host-embedding gaps a host needs to drive
repl::session::ReplSession from an async runtime — none of which could be
worked around downstream. Motivated by exposing the .ragsh REPL as a
first-class rlm tool in a downstream host (openhuman); see that host's plan
for the consuming design.

1. External cancellation (ReplCancelFlag)

A running cell could previously only be stopped by its wall-clock timeout. New
ReplCancelFlag (a sticky, cheaply-cloneable Arc<AtomicBool>) is installed
via ReplSession::with_cancel_flag, and enforced fail-closed at the same two
points as the deadline
:

  • the engine on_progress hook terminates a running script at the next
    statement/operation (via a CANCELLED_TOKEN sentinel mapped to
    TinyAgentsError::Cancelled);
  • the blocking bridge (bridge_block_on_raw) grows a watcher thread that polls
    the flag and drops an in-flight (possibly hung) capability future promptly.

eval_cell also fails closed before starting a cell if the flag is already
tripped, without consuming a max_iterations slot.

Note on the error type. The plan proposed a new Cancelled(String)
variant. On re-verification, TinyAgentsError::Cancelled already exists as a
unit variant used in ~15 places (agent loop, graph parallel, steering, e2e
tests); overloading it with a payload would be a breaking change for no added
value here (the phase is conveyed via logs/events). This PR reuses the
existing unit Cancelled.

2. Live capability-call events on the EventSink

ReplResult.calls is only readable after a cell returns, so a long fan-out
looks frozen. New feature-gated AgentEvent::ReplCall { session_id, record, phase } (+ ReplCallPhase::{Started, Completed}) streams each model_query /
tool_call / agent_query / emit on the session EventSink as it starts
and again as it completes, correlated by a call_id threaded from the built-in
through record. A host subscribes an EventListener and forwards REPL
progress mid-cell. Gated behind repl so the default build neither pulls in
Rhai nor references ReplCallRecord.

3. Async-embedding documentation

eval_cell's rustdoc now documents that it blocks internally
(futures::executor::block_on) and must be driven via spawn_blocking / a
dedicated thread from an async host, and deadlocks on a current-thread runtime.

Tests (shipped with the change, per crate convention)

  • cancel-before-eval short-circuits without running the cell or burning an
    iteration; session reusable with a fresh flag;
  • cancel mid-script-loop terminates promptly (on_progress path);
  • cancel during a hanging capability future is dropped promptly (bridge path);
  • bridge unit tests for pre-cancelled and mid-call cancel;
  • start + completion ReplCall events observed and paired by call_id,
    correlated to the session id.

Commands run

  • cargo fmt --check — clean
  • cargo clippy --all-targets --features repl -- -D warnings — clean
  • cargo test --features repl — green
  • cargo test (default features) — green

Note on base

Branched off 357bcc8 (the commit the downstream submodule currently pins), as
called for by the integration plan; main has since advanced, so a rebase may
be needed before merge.

Fills the three host-embedding gaps a host (openhuman) needs to drive
`repl::session::ReplSession` from an async runtime, none of which could be
worked around downstream:

- External cancellation. New `ReplCancelFlag` (sticky `Arc<AtomicBool>`),
  installed via `ReplSession::with_cancel_flag`. Enforced fail-closed at the
  same two points as the deadline: the engine `on_progress` hook terminates a
  running script at the next statement (mapped to `Cancelled` via a
  `CANCELLED_TOKEN` sentinel), and the blocking bridge grows a watcher thread
  that drops an in-flight (possibly hung) capability future promptly on
  cancel. `eval_cell` also fails closed before starting a cell if already
  cancelled, without consuming an iteration. Reuses the existing unit
  `TinyAgentsError::Cancelled` (the plan's proposed `Cancelled(String)` would
  have been a breaking change to ~15 existing call sites for no added value).

- Live capability-call events. New feature-gated `AgentEvent::ReplCall
  { session_id, record, phase }` (+ `ReplCallPhase`) streamed on the session
  `EventSink` at capability-call start and completion, correlated by a
  `call_id` threaded from the built-in through `record`. A host subscribes an
  `EventListener` and forwards REPL progress mid-cell, instead of only reading
  `ReplResult.calls` after the cell returns.

- Async-embedding docs. `eval_cell` rustdoc now states it blocks internally
  (`futures::executor::block_on`) and must be driven via
  `spawn_blocking`/dedicated thread, and deadlocks on a current-thread runtime.

Tests ship with the change (crate convention): cancel-before-eval,
cancel-mid-script-loop (`on_progress` path), cancel-during-hanging-capability
(bridge path), and start+completion events paired by call_id. `cargo fmt
--check`, `cargo clippy --all-targets --features repl -- -D warnings`,
`cargo test --features repl`, and plain `cargo test` all pass.
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 46 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 35924eab-7efa-40eb-b48b-964e7b9d023c

📥 Commits

Reviewing files that changed from the base of the PR and between 146280f and df391c4.

📒 Files selected for processing (9)
  • src/harness/events/types.rs
  • src/lib.rs
  • src/repl/session/builtins/authoring.rs
  • src/repl/session/builtins/batched.rs
  • src/repl/session/builtins/capabilities.rs
  • src/repl/session/builtins/mod.rs
  • src/repl/session/mod.rs
  • src/repl/session/test.rs
  • src/repl/session/types.rs

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 622c9b27eb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

.ok_or_else(|| raise(ctx, TinyAgentsError::ModelNotFound(model_name.clone())))?;
let request = build_model_request(&model_name, params);
let call_id = new_call_id();
emit_call_started(ctx, &call_id, ReplCallKind::Model, &model_name);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Pair started events on failed capability calls

When this model call is cancelled, times out, or the provider returns an error, the later bridge_block_on(...).map_err(...)? exits after this Started event but before record emits a terminal ReplCall. A host that maintains an active-call set from ReplCall events will leave the call active forever in exactly those failure/cancellation paths; emit a completed/failed REPL call event before raising so every started call is paired.

Useful? React with 👍 / 👎.

// Stream a "started" event for every fan-out leg up front, so a live
// observer sees the whole batch dispatch before any leg completes.
let call_id = new_call_id();
emit_call_started(ctx, &call_id, ReplCallKind::Model, &model_name);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Emit batch starts only when legs are dispatched

This sends Started for every batch item during preparation, but buffered(concurrency) only polls up to max_concurrency futures at a time. With max_concurrency = 1 and the first model call hanging or getting cancelled, later items are reported as started even though they were never dispatched and may never run, so live observers overcount in-flight work; move the start emission into each async leg immediately before invoking the capability.

Useful? React with 👍 / 👎.

`with_cancel_flag` consumes `self`, which suits construction but not a
long-lived session held behind a lock by a host session manager. Because a
`ReplCancelFlag` is sticky (once cancelled, stays cancelled), a persistent
session reused after a cancelled cell would otherwise refuse every subsequent
cell. `set_cancel_flag(&mut self, flag)` installs a fresh flag in place and
rebuilds the engine (the persistent variable namespace is untouched, so `let`
bindings survive the swap), letting a host arm a new per-cell flag before each
`eval_cell` and stay resumable after a cancel. Test covers the swap + namespace
preservation.
@senamakel senamakel merged commit 0b275d5 into tinyhumansai:main Jul 4, 2026
2 checks passed
senamakel added a commit to senamakel/openhuman that referenced this pull request Jul 5, 2026
Points `vendor/tinyagents` at the `feat/repl-host-embedding` head (df391c4):
external `ReplCancelFlag` cancellation, live `ReplCall` events, and
`set_cancel_flag`. Depends on tinyhumansai/tinyagents#19; re-pin to the merged
commit once that lands.
senamakel added a commit to senamakel/openhuman that referenced this pull request Jul 5, 2026
Points `vendor/tinyagents` at the `feat/repl-host-embedding` head (df391c4):
external `ReplCancelFlag` cancellation, live `ReplCall` events, and
`set_cancel_flag`. Depends on tinyhumansai/tinyagents#19; re-pin to the merged
commit once that lands.
senamakel added a commit to senamakel/openhuman that referenced this pull request Jul 5, 2026
…nyhumansai#18 started_at_ms)

tinyhumansai/tinyagents#19 (REPL host-embedding: ReplCancelFlag cancellation,
live ReplCall events, set_cancel_flag) is now merged to tinyagents main, which
also carries tinyhumansai#18 (`AgentEvent::ToolCompleted.started_at_ms`) that openhuman's
observability path now requires. Pin the submodule at tinyagents main (e72036d)
so the crate provides both — superseding the earlier pin at the pre-merge
tinyhumansai#19 branch head, which predated tinyhumansai#18 and left `observability.rs` referencing a
field the crate lacked.
senamakel added a commit to senamakel/openhuman that referenced this pull request Jul 5, 2026
tinyhumansai/tinyagents#19 (REPL host-embedding: ReplCancelFlag cancellation,
live ReplCall events, set_cancel_flag) is now merged to tinyagents main
(released v1.6.0, e72036d), which also carries tinyhumansai#18's `ToolCompleted`/
`ModelCompleted` field additions (started_at_ms/duration_ms/output_bytes/error).
Pin the submodule at tinyagents main so the crate provides the REPL surface the
`rlm` domain needs.

Because the `rlm` domain requires tinyagents ≥ tinyhumansai#19 (linearly after tinyhumansai#18), this
also completes openhuman's partial 1.6.0 adoption in `tinyagents/observability.rs`:
the `ToolCompleted` projector match ignores the new fields via `..`, and the
test-only `ToolCompleted`/`ModelCompleted` constructions carry the new fields
(`None`). The projector's `_ => {}` catch-all already handles the new
`ReplCall`/`ModelOverrideSkipped` variants. Regenerated Cargo.lock (1.5.0→1.6.0).
senamakel added a commit to senamakel/openhuman that referenced this pull request Jul 5, 2026
Points `vendor/tinyagents` at the `feat/repl-host-embedding` head (df391c4):
external `ReplCancelFlag` cancellation, live `ReplCall` events, and
`set_cancel_flag`. Depends on tinyhumansai/tinyagents#19; re-pin to the merged
commit once that lands.
senamakel added a commit to senamakel/openhuman that referenced this pull request Jul 5, 2026
Points `vendor/tinyagents` at the `feat/repl-host-embedding` head (df391c4):
external `ReplCancelFlag` cancellation, live `ReplCall` events, and
`set_cancel_flag`. Depends on tinyhumansai/tinyagents#19; re-pin to the merged
commit once that lands.
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.

1 participant