Skip to content

fix(infinity-agent-core): return tool failures to the agent - #88

Merged
shadaj merged 1 commit into
mainfrom
sandbox-86595edd-0387-40e8-aa40-3f1862988c82
Aug 10, 2026
Merged

fix(infinity-agent-core): return tool failures to the agent#88
shadaj merged 1 commit into
mainfrom
sandbox-86595edd-0387-40e8-aa40-3f1862988c82

Conversation

@shadaj

@shadaj shadaj commented Jul 29, 2026

Copy link
Copy Markdown
Member
  • Add a shared send_tool_error helper that enqueues an error ToolResult with the original tool and call IDs
  • Convert thread-tool argument and relationship validation failures, including missing close_thread.thread_id, from propagated errors or duplicated message construction into queued tool results
  • Make the batch processor enqueue a generic Error: Tool call failed result when asynchronous tool execution returns an error, while retaining the detailed display event and logging delivery failures
  • Add regressions covering missing close_thread arguments and generic failed-tool fallback delivery

* Add a shared `send_tool_error` helper that enqueues an error `ToolResult` with the original tool and call IDs
* Convert thread-tool argument and relationship validation failures, including missing `close_thread.thread_id`, from propagated errors or duplicated message construction into queued tool results
* Make the batch processor enqueue a generic `Error: Tool call failed` result when asynchronous tool execution returns an error, while retaining the detailed display event and logging delivery failures
* Add regressions covering missing `close_thread` arguments and generic failed-tool fallback delivery

Co-authored-by: Infinity 🤖 <infinity@hydro.run>
PR: #88
@shadaj
shadaj force-pushed the sandbox-86595edd-0387-40e8-aa40-3f1862988c82 branch from 65a0f98 to 127f90b Compare July 29, 2026 00:06
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 29, 2026

Copy link
Copy Markdown

Deploying infinity with  Cloudflare Pages  Cloudflare Pages

Latest commit: 127f90b
Status: ✅  Deploy successful!
Preview URL: https://5088ef94.infinity-dc7.pages.dev
Branch Preview URL: https://sandbox-86595edd-0387-40e8-a.infinity-dc7.pages.dev

View logs

@shadaj
shadaj marked this pull request as ready for review July 29, 2026 21:37

@MingweiSamuel MingweiSamuel left a comment

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.

Nice, didn't realize this wasn't already the case, will help a lot

@shadaj
shadaj merged commit 448dbed into main Aug 10, 2026
9 checks passed
@shadaj
shadaj deleted the sandbox-86595edd-0387-40e8-aa40-3f1862988c82 branch August 10, 2026 21:32
shadaj added a commit that referenced this pull request Aug 11, 2026
…on + lambda onto it

Introduce `infinity_agent_core::system`, a builder-based, object-oriented API for
running agents, rebuild both production embeddings on top of it, unify the parallel
implementations that had grown around the old architecture, and rewrite the
`infinity-runtime` docs around the new API.

Core (`infinity_agent_core::system`):
- `AgentSystemBuilder`: `new(stores, model, sender)` builds a step-mode `AgentSystem`
  for serverless platforms; `new_local(stores, model)` builds a `LocalAgentSystem`
  with an internal in-process queue (`ChannelSender`).
- Step mode's public surface is `AgentSystem::step(inputs, observer, defer)`: the
  batch may span multiple threads (SQS FIFO batches can interleave message groups);
  `step` partitions by `group_id`, applies the deferral policy per thread, joins the
  per-thread slices concurrently (prepare → completion → history sync → observer
  commit barrier → tool dispatch), and returns each thread's `StepOutcome`. `&mut
  self` serializes calls per system; the internal `Thread` type is not public and
  `AgentSystem` is not `Clone`, so two live in-memory views of one thread cannot
  exist through the public API. Per-thread configuration resolves lazily on first
  step so loading a thread never boots tool servers.
- `LocalAgentSystem::start` runs an actor-system-style runtime: a router plus one
  driver per active thread, with input batching, `<interrupt>` handling, deferral of
  synthetic events while a non-passive tool call is pending, auto-compaction above
  75% of the context window, and race-free idle exit/respawn. `RunningSystem`
  exposes senders, ack'd `subscribe`, `active_threads`, a `thread_exits` channel for
  per-conversation resource teardown, and `begin_shutdown` for process exit.
- `ThreadHandle` (`start_with_handles()` + `thread_handle(id)`): attach to a thread
  (even before it exists), get `replay()` plus an exactly-once `events` stream, and
  send input; subscriptions survive driver respawns via a shared registry.
- `ThreadObserver`: synchronous `on_event(AgentEvent)` fan-out (exactly-once relative
  to `on_subscribe` replays) plus awaited durability hooks (`on_user_choice_required`
  /`_dismissed`, `on_commit` before tool dispatch). `EventCollector` buffers
  `(thread_id, event)` pairs.
- `ModelSource` (per-round resolution; enables mid-session model switching),
  `ThreadConfigSource` (per-thread tools/prompt/notifier; enables per-session RAP
  servers and toolsets), `DeferQueue` (`InMemoryDeferQueue`/`NoDeferral`),
  `Tool::is_passive()`, tokio sleep tools (`with_tokio_sleep_tools`).
- **`batch_processor` deleted**: `process_batch` had no production callers and its
  semantics violated the documented durability barrier (swallowed sync errors, then
  dispatched; emitted ResponseDone before sync). The documented low-level API is now
  `HistoryManager` + `prepare_input`/`run_completion`/`execute_action`;
  `execute_action_with_error_result` (the #88 error-fallback) lives in
  `event_processor`. `DisplayEvent` moved out of core: `AgentEvent` is core's only
  event vocabulary; the CLI keeps a local, non-generic `DisplayEvent` as its
  terminal-rendering enum. All unique regression coverage ported.
- **Shared slice internals**: `event_processor::input_echo` is the single
  computation of accepted-input echoes (was duplicated between the system path and
  the old batch path); `SyntheticKind::is_compaction_complete()`;
  `InputMessage::user_text()`; driver's in-flight step modeled as
  `InFlightStep { fut, cancel_tx }` with a consuming `cancel()`.
- **`rap_callback` module**: the RAP callback → `InputMessage` conversion
  (multimodal content, display segments, tagged synthetics incl. subscription
  final/associative flags, user_choice, oauth) now lives in core with 11 unit
  tests; the daemon delegates to it.
- In-memory `ConversationStore`/`StateStore` in `infinity_agent_core::stores` with a
  serializable snapshot/restore API; the daemon's stores are now thin wrappers
  (extras + JSON persistence) so the subtle ancestor/compaction/dedup semantics
  exist in exactly one place. `spawn_thread_with_id` supports deterministic IDs.
- Platform-neutral core: `input_queue_arn` removed from `ToolContext`/builder
  (Lambda sleep tools carry it as a field).
- All 20 daemon agent-loop tests migrated into core `system/tests.rs` with
  byte-identical snapshots, plus new coverage: `ThreadHandle` streaming across
  tool-call rounds (CompletionFinished is per round; consumers keep pulling while a
  call is pending), multi-group step batches, failed-dispatch fallback, OAuth
  surfacing, prepare-level user-choice, handle respawn survival and pruning.

Daemon (`infinity-daemon`):
- One daemon-lifetime agent system replaces per-session agent loops; sessions are
  root threads; a `thread_exits` watcher (via the single
  `session_has_active_threads` predicate) marks sessions idle and shuts down their
  RAP servers when no keep-alive client is attached.
- `rap_servers.rs`: `ManagedRapServer` boots lazily per session via
  `ThreadConfigSource` and reboots transparently after idle shutdown; `Up` state
  caches the manifest (fetch failure tears the boot down — no half-up state); MCP
  proxy tasks are owned (stdio children `kill_on_drop`) but proxies stay up across
  idles since MCP servers may be stateful; migration flows ride the same
  `collect_server_specs` + `ensure_up` path (`MigrationServer`;
  `boot_rap_servers`/`BootedRapServers`/`rap_tools.rs` deleted); RAP config
  reading/merging unified in `config::load_merged_rap_config`.
- `DaemonObserver` implements the observer hooks; subscriber broadcasting unified in
  `observer::broadcast_pruning`/`broadcast_to_thread` (fixing `handle_view_update`
  never pruning dead subscribers); re-sent Connects replace rather than stack
  subscriptions; replay rendering now matches live rendering (strips `<interrupt>`
  prefixes, uses `rap_protocol::build_display_segments`).
- `CatalogModelSource` resolves each thread's persisted model per round. Fix the
  pre-existing double `ModelSwitched` delivery (`switch_model` tracks whether the
  broadcast reached the requester via `same_channel`).
- `send_input` takes `user_driven: bool`; `SessionManager` methods that only touch
  interior-mutable state take `&self`.
- Web goldens `chat-image-result`/`chat-diff-result` regenerated (old goldens
  captured an eager-boot layout artifact).
- Deleted: `session/thread_worker.rs`, `session/agent_loop.rs`, `sleep_tools.rs`,
  `InMemoryMessageSender`, `Session`, `spawn_session`, `AgentMessage`.

Lambda (`infinity-agent-lambda`):
- Event handler rewritten on the new API: one system per invocation with a
  `ThreadConfigSource` resolving each thread's RAP toolsets (DynamoDB manifest
  cache) plus platform sleep tools; one batch-shaped `step`; per-thread outputs
  aggregated in a `BTreeMap`. Sleep tools share a `WakeupScheduler` (SQS delay /
  EventBridge dispatch deduplicated).
- **New `rap-receiver` binary replaces `agent/lib/infinity-agents/rap-receiver/
  index.mjs`** (deleted): same Function-URL contract, but full parity with the
  daemon's conversion via `infinity_agent_core::rap_callback` — user_choice
  callbacks accepted (JS returned 400), multimodal content and `display_as`
  preserved, subscription `final` flag and tagged synthetics honored, and stable
  dedup IDs for results/oauth/choices (per-delivery IDs only for subscription
  events, which share a tool_call_id). CDK switches the receiver to a
  cargo-lambda `RustFunction`. Deploy note: the Function URL is recreated, so
  persisted callback URLs from long-lived subscriptions must be re-subscribed.

Docs (`docs/docs/infinity-runtime`): the single "Rust API" page is replaced by two
sections rewritten in a usage-oriented style (example-first walkthroughs rather than
API inventories): "The Agent System API" (overview, building-a-system,
running-locally, step-mode, observers) and "The Low-Level API" (overview,
history-manager, completion-loop). Cross-references updated repo-wide; `cargo doc`
is warning-free.

BREAKING CHANGE: `infinity-agent-core` removes `batch_processor` (`process_batch`,
`process_input_item`, `DisplayEvent`); `Thread` is no longer public (use
`AgentSystem::step` or `ThreadHandle`); `AgentSystem` is not `Clone`;
`EventCollector::take` returns `(thread_id, event)` pairs; builder tools are stored
as `Rc<dyn Tool<M>>`; `Tool` gains defaulted `is_passive`; `ToolContext`/builder
lose `input_queue_arn`. `infinity-daemon` no longer exports `sleep_tools`,
`AgentMessage`, `thread_worker`, `agent_loop`, `Session`, `InMemoryMessageSender`,
`boot_rap_servers`, or `rap_tools`; `SessionManager::switch_model` takes the
requester's sender and returns `Result<(), String>`; `send_input` takes
`user_driven: bool`; `SharedSessionManager` is `Rc<tokio::Mutex<...>>`. The CLI's
`DisplayEvent` now lives in `infinity_agent_cli::display`.

Co-authored-by: Infinity 🤖 <infinity@hydro.run>
PR: #92
shadaj added a commit that referenced this pull request Aug 11, 2026
…on + lambda onto it

Introduce `infinity_agent_core::system`, a builder-based, object-oriented API for
running agents, rebuild both production embeddings on top of it, unify the parallel
implementations that had grown around the old architecture, and rewrite the
`infinity-runtime` docs around the new API.

Core (`infinity_agent_core::system`):
- `AgentSystemBuilder`: `new(stores, model, sender)` builds a step-mode `AgentSystem`
  for serverless platforms; `new_local(stores, model)` builds a `LocalAgentSystem`
  with an internal in-process queue (`ChannelSender`).
- Step mode's public surface is `AgentSystem::step(inputs, observer, defer)`: the
  batch may span multiple threads (SQS FIFO batches can interleave message groups);
  `step` partitions by `group_id`, applies the deferral policy per thread, joins the
  per-thread slices concurrently (prepare → completion → history sync → observer
  commit barrier → tool dispatch), and returns each thread's `StepOutcome`. `&mut
  self` serializes calls per system; the internal `Thread` type is not public and
  `AgentSystem` is not `Clone`, so two live in-memory views of one thread cannot
  exist through the public API. Per-thread configuration resolves lazily on first
  step so loading a thread never boots tool servers.
- `LocalAgentSystem::start` runs an actor-system-style runtime: a router plus one
  driver per active thread, with input batching, `<interrupt>` handling, deferral of
  synthetic events while a non-passive tool call is pending, auto-compaction above
  75% of the context window, and race-free idle exit/respawn. `RunningSystem`
  exposes senders, ack'd `subscribe`, `active_threads`, a `thread_exits` channel for
  per-conversation resource teardown, and `begin_shutdown` for process exit.
- `ThreadHandle` (`start_with_handles()` + `thread_handle(id)`): attach to a thread
  (even before it exists), get `replay()` plus an exactly-once `events` stream, and
  send input; subscriptions survive driver respawns via a shared registry.
- `ThreadObserver`: synchronous `on_event(AgentEvent)` fan-out (exactly-once relative
  to `on_subscribe` replays) plus awaited durability hooks (`on_user_choice_required`
  /`_dismissed`, `on_commit` before tool dispatch). `EventCollector` buffers
  `(thread_id, event)` pairs.
- `ModelSource` (per-round resolution; enables mid-session model switching),
  `ThreadConfigSource` (per-thread tools/prompt/notifier; enables per-session RAP
  servers and toolsets), `DeferQueue` (`InMemoryDeferQueue`/`NoDeferral`),
  `Tool::is_passive()`, tokio sleep tools (`with_tokio_sleep_tools`).
- **`batch_processor` deleted**: `process_batch` had no production callers and its
  semantics violated the documented durability barrier (swallowed sync errors, then
  dispatched; emitted ResponseDone before sync). The documented low-level API is now
  `HistoryManager` + `prepare_input`/`run_completion`/`execute_action`;
  `execute_action_with_error_result` (the #88 error-fallback) lives in
  `event_processor`. `DisplayEvent` moved out of core: `AgentEvent` is core's only
  event vocabulary; the CLI keeps a local, non-generic `DisplayEvent` as its
  terminal-rendering enum. All unique regression coverage ported.
- **Shared slice internals**: `event_processor::input_echo` is the single
  computation of accepted-input echoes (was duplicated between the system path and
  the old batch path); `SyntheticKind::is_compaction_complete()`;
  `InputMessage::user_text()`; driver's in-flight step modeled as
  `InFlightStep { fut, cancel_tx }` with a consuming `cancel()`.
- **`rap_callback` module**: the RAP callback → `InputMessage` conversion
  (multimodal content, display segments, tagged synthetics incl. subscription
  final/associative flags, user_choice, oauth) now lives in core with 11 unit
  tests; the daemon delegates to it.
- In-memory `ConversationStore`/`StateStore` in `infinity_agent_core::stores` with a
  serializable snapshot/restore API; the daemon's stores are now thin wrappers
  (extras + JSON persistence) so the subtle ancestor/compaction/dedup semantics
  exist in exactly one place. `spawn_thread_with_id` supports deterministic IDs.
- Platform-neutral core: `input_queue_arn` removed from `ToolContext`/builder
  (Lambda sleep tools carry it as a field).
- All 20 daemon agent-loop tests migrated into core `system/tests.rs` with
  byte-identical snapshots, plus new coverage: `ThreadHandle` streaming across
  tool-call rounds (CompletionFinished is per round; consumers keep pulling while a
  call is pending), multi-group step batches, failed-dispatch fallback, OAuth
  surfacing, prepare-level user-choice, handle respawn survival and pruning.

Daemon (`infinity-daemon`):
- One daemon-lifetime agent system replaces per-session agent loops; sessions are
  root threads; a `thread_exits` watcher (via the single
  `session_has_active_threads` predicate) marks sessions idle and shuts down their
  RAP servers when no keep-alive client is attached.
- `rap_servers.rs`: `ManagedRapServer` boots lazily per session via
  `ThreadConfigSource` and reboots transparently after idle shutdown; `Up` state
  caches the manifest (fetch failure tears the boot down — no half-up state); MCP
  proxy tasks are owned (stdio children `kill_on_drop`) but proxies stay up across
  idles since MCP servers may be stateful; migration flows ride the same
  `collect_server_specs` + `ensure_up` path (`MigrationServer`;
  `boot_rap_servers`/`BootedRapServers`/`rap_tools.rs` deleted); RAP config
  reading/merging unified in `config::load_merged_rap_config`.
- `DaemonObserver` implements the observer hooks; subscriber broadcasting unified in
  `observer::broadcast_pruning`/`broadcast_to_thread` (fixing `handle_view_update`
  never pruning dead subscribers); re-sent Connects replace rather than stack
  subscriptions; replay rendering now matches live rendering (strips `<interrupt>`
  prefixes, uses `rap_protocol::build_display_segments`).
- `CatalogModelSource` resolves each thread's persisted model per round. Fix the
  pre-existing double `ModelSwitched` delivery (`switch_model` tracks whether the
  broadcast reached the requester via `same_channel`).
- `send_input` takes `user_driven: bool`; `SessionManager` methods that only touch
  interior-mutable state take `&self`.
- Web goldens `chat-image-result`/`chat-diff-result` regenerated (old goldens
  captured an eager-boot layout artifact).
- Deleted: `session/thread_worker.rs`, `session/agent_loop.rs`, `sleep_tools.rs`,
  `InMemoryMessageSender`, `Session`, `spawn_session`, `AgentMessage`.

Lambda (`infinity-agent-lambda`):
- Event handler rewritten on the new API: one system per invocation with a
  `ThreadConfigSource` resolving each thread's RAP toolsets (DynamoDB manifest
  cache) plus platform sleep tools; one batch-shaped `step`; per-thread outputs
  aggregated in a `BTreeMap`. Sleep tools share a `WakeupScheduler` (SQS delay /
  EventBridge dispatch deduplicated).
- **New `rap-receiver` binary replaces `agent/lib/infinity-agents/rap-receiver/
  index.mjs`** (deleted): same Function-URL contract, but full parity with the
  daemon's conversion via `infinity_agent_core::rap_callback` — user_choice
  callbacks accepted (JS returned 400), multimodal content and `display_as`
  preserved, subscription `final` flag and tagged synthetics honored, and stable
  dedup IDs for results/oauth/choices (per-delivery IDs only for subscription
  events, which share a tool_call_id). CDK switches the receiver to a
  cargo-lambda `RustFunction`. Deploy note: the Function URL is recreated, so
  persisted callback URLs from long-lived subscriptions must be re-subscribed.

Docs (`docs/docs/infinity-runtime`): the single "Rust API" page is replaced by two
sections rewritten in a usage-oriented style (example-first walkthroughs rather than
API inventories): "The Agent System API" (overview, building-a-system,
running-locally, step-mode, observers) and "The Low-Level API" (overview,
history-manager, completion-loop). Cross-references updated repo-wide; `cargo doc`
is warning-free.

BREAKING CHANGE: `infinity-agent-core` removes `batch_processor` (`process_batch`,
`process_input_item`, `DisplayEvent`); `Thread` is no longer public (use
`AgentSystem::step` or `ThreadHandle`); `AgentSystem` is not `Clone`;
`EventCollector::take` returns `(thread_id, event)` pairs; builder tools are stored
as `Rc<dyn Tool<M>>`; `Tool` gains defaulted `is_passive`; `ToolContext`/builder
lose `input_queue_arn`. `infinity-daemon` no longer exports `sleep_tools`,
`AgentMessage`, `thread_worker`, `agent_loop`, `Session`, `InMemoryMessageSender`,
`boot_rap_servers`, or `rap_tools`; `SessionManager::switch_model` takes the
requester's sender and returns `Result<(), String>`; `send_input` takes
`user_driven: bool`; `SharedSessionManager` is `Rc<tokio::Mutex<...>>`. The CLI's
`DisplayEvent` now lives in `infinity_agent_cli::display`.

Co-authored-by: Infinity 🤖 <infinity@hydro.run>
PR: #92
shadaj added a commit that referenced this pull request Aug 12, 2026
…on + lambda onto it

Introduce `infinity_agent_core::system`, a builder-based, object-oriented API for
running agents, rebuild both production embeddings on top of it, unify the parallel
implementations that had grown around the old architecture, and rewrite the
`infinity-runtime` docs around the new API.

Core (`infinity_agent_core::system`):
- `AgentSystemBuilder`: `new(stores, model, sender)` builds a step-mode `AgentSystem`
  for serverless platforms; `new_local(stores, model)` builds a `LocalAgentSystem`
  with an internal in-process queue (`ChannelSender`).
- Step mode's public surface is `AgentSystem::step(inputs, observer, defer)`: the
  batch may span multiple threads (SQS FIFO batches can interleave message groups);
  `step` partitions by `group_id`, applies the deferral policy per thread, joins the
  per-thread slices concurrently (prepare → completion → history sync → observer
  commit barrier → tool dispatch), and returns each thread's `StepOutcome`. `&mut
  self` serializes calls per system; the internal `Thread` type is not public and
  `AgentSystem` is not `Clone`, so two live in-memory views of one thread cannot
  exist through the public API. Per-thread configuration resolves lazily on first
  step so loading a thread never boots tool servers.
- `LocalAgentSystem` is mode-typed (`Handles` by default, `Launcher` after
  `with_thread_launcher()`), with one `start()` per mode:
  - Handles mode: `start()` returns a `RunningSystem` running an actor-system-style
    runtime (router + one driver per active thread, input batching, `<interrupt>`
    handling, deferral while a non-passive tool call is pending, auto-compaction
    above 75% of the context window, race-free idle exit/respawn) with ack'd
    `subscribe`, a `thread_exits` channel, and `thread_handle(id)` attaching to any
    thread. `start_with_observer(factory)` swaps in a custom `ThreadObserver`.
  - Launcher mode: `start()` returns a `LaunchingSystem`. `thread_builder()`
    configures and launches new threads (generated IDs) with their own tools and
    prompt, unioned onto the system-wide configuration (static or
    `ThreadConfigSource`) via an internal `UnionConfigSource`; `thread_handle(id)`
    re-attaches to existing threads only (launched in-process or with history).
    Launch configurations are process-lifetime (not persisted).
- `ThreadHandle`: `replay()` plus an exactly-once `events` stream and input sending;
  subscriptions survive driver respawns via a shared registry.
- `ThreadObserver`: synchronous `on_event(AgentEvent)` fan-out (exactly-once relative
  to `on_subscribe` replays) plus awaited durability hooks (`on_user_choice_required`
  /`_dismissed`, `on_commit` before tool dispatch). `EventCollector` buffers
  `(thread_id, event)` pairs.
- `ModelSource` (per-round resolution; enables mid-session model switching),
  `ThreadConfigSource` (per-thread tools/prompt/notifier; enables per-session RAP
  servers and toolsets), `DeferQueue` (`InMemoryDeferQueue`/`NoDeferral`),
  `Tool::is_passive()`, tokio sleep tools (`with_tokio_sleep_tools`).
- `batch_processor` deleted: `process_batch` had no production callers and its
  semantics violated the documented durability barrier (swallowed sync errors, then
  dispatched; emitted ResponseDone before sync). The documented low-level API is now
  `HistoryManager` + `prepare_input`/`run_completion`/`execute_action`;
  `execute_action_with_error_result` (the #88 error-fallback) lives in
  `event_processor`. `DisplayEvent` moved out of core: `AgentEvent` is core's only
  event vocabulary; the CLI keeps a local, non-generic `DisplayEvent` as its
  terminal-rendering enum. All unique regression coverage ported.
- Shared slice internals: `event_processor::input_echo` is the single computation of
  accepted-input echoes; `SyntheticKind::is_compaction_complete()`;
  `InputMessage::user_text()`; driver's in-flight step modeled as
  `InFlightStep { fut, cancel_tx }` with a consuming `cancel()`.
- `rap_callback` module: the RAP callback → `InputMessage` conversion (multimodal
  content, display segments, tagged synthetics incl. subscription final/associative
  flags, user_choice, oauth) lives in core with 11 unit tests; the daemon delegates.
- In-memory `ConversationStore`/`StateStore` in `infinity_agent_core::stores` with a
  serializable snapshot/restore API; the daemon's stores are thin wrappers (extras +
  JSON persistence) so the ancestor/compaction/dedup semantics exist in exactly one
  place. `spawn_thread_with_id` supports deterministic IDs.
- Platform-neutral core: `input_queue_arn` removed from `ToolContext`/builder.
- All 20 daemon agent-loop tests migrated into core `system/tests.rs` with
  byte-identical snapshots, plus new coverage: launcher tool/prompt unioning and
  existing-only attachment, `ThreadHandle` streaming across tool-call rounds,
  multi-group step batches, failed-dispatch fallback, OAuth surfacing, prepare-level
  user-choice, handle respawn survival and pruning.

Daemon (`infinity-daemon`):
- One daemon-lifetime agent system replaces per-session agent loops; sessions are
  root threads; a `thread_exits` watcher marks sessions idle and shuts down their
  RAP servers when no keep-alive client is attached.
- `rap_servers.rs`: `ManagedRapServer` boots lazily per session via
  `ThreadConfigSource` and reboots transparently after idle shutdown; `Up` caches
  the manifest (fetch failure tears the boot down); MCP proxy tasks are owned
  (stdio children `kill_on_drop`) but proxies stay up across idles since MCP servers
  may be stateful; migration flows ride the same path (`MigrationServer`;
  `boot_rap_servers`/`rap_tools.rs` deleted); config merging unified in
  `config::load_merged_rap_config`.
- `DaemonObserver` implements the observer hooks; broadcasting unified in
  `observer::broadcast_pruning`/`broadcast_to_thread` (fixing `handle_view_update`
  never pruning dead subscribers); re-sent Connects replace rather than stack
  subscriptions; replay rendering matches live rendering.
- `CatalogModelSource` resolves each thread's persisted model per round; fixed the
  pre-existing double `ModelSwitched` delivery.
- `send_input` takes `user_driven: bool`; interior-mutability methods take `&self`.
- Web goldens `chat-image-result`/`chat-diff-result` regenerated (old goldens
  captured an eager-boot layout artifact).
- Deleted: `session/thread_worker.rs`, `session/agent_loop.rs`, `sleep_tools.rs`,
  `InMemoryMessageSender`, `Session`, `spawn_session`, `AgentMessage`.

Lambda (`infinity-agent-lambda`):
- Event handler rewritten on the new API: one system per invocation with a
  `ThreadConfigSource` resolving each thread's RAP toolsets (DynamoDB manifest
  cache) plus platform sleep tools (shared `WakeupScheduler` backend); one
  batch-shaped `step`; per-thread outputs aggregated in a `BTreeMap`.
- New `rap-receiver` binary replaces the drifted JS receiver (deleted): same
  Function-URL contract, full parity with the daemon's conversion via
  `infinity_agent_core::rap_callback` (user_choice accepted, multimodal and
  `display_as` preserved, tagged synthetics honored, stable dedup IDs where the
  wire has identity). CDK switches to a cargo-lambda `RustFunction`; the Function
  URL is recreated on deploy, so persisted callback URLs must be re-subscribed.

Docs (`docs/docs/infinity-runtime`): the single "Rust API" page is replaced by two
sections, written in the Hydro docs register (definitional openings, bolded key
terms, cause-and-effect guarantees, code-first examples, admonitions for caveats):
"The Agent System API" (overview, building-a-system, running-locally, step-mode,
observers, including launcher mode) and "The Low-Level API" (overview,
history-manager, completion-loop). Cross-references updated repo-wide; `cargo doc`
is warning-free.

BREAKING CHANGE: `infinity-agent-core` removes `batch_processor` (`process_batch`,
`process_input_item`, `DisplayEvent`); `Thread` is no longer public (use
`AgentSystem::step` or `ThreadHandle`); `AgentSystem` is not `Clone`;
`LocalAgentSystem::start_with_handles` is now `start()` and custom observers use
`start_with_observer` (`LocalAgentSystem` gained a mode type parameter, default
`Handles`); `EventCollector::take` returns `(thread_id, event)` pairs; builder tools
are stored as `Rc<dyn Tool<M>>`; `Tool` gains defaulted `is_passive`;
`ToolContext`/builder lose `input_queue_arn`. `infinity-daemon` no longer exports
`sleep_tools`, `AgentMessage`, `thread_worker`, `agent_loop`, `Session`,
`InMemoryMessageSender`, `boot_rap_servers`, or `rap_tools`;
`SessionManager::switch_model` takes the requester's sender and returns
`Result<(), String>`; `send_input` takes `user_driven: bool`;
`SharedSessionManager` is `Rc<tokio::Mutex<...>>`. The CLI's `DisplayEvent` lives in
`infinity_agent_cli::display`.

Co-authored-by: Infinity 🤖 <infinity@hydro.run>
PR: #92
shadaj added a commit that referenced this pull request Aug 12, 2026
…on + lambda onto it

Introduce `infinity_agent_core::system`, a builder-based, object-oriented API for
running agents, rebuild both production embeddings on top of it, unify the parallel
implementations that had grown around the old architecture, and rewrite the
`infinity-runtime` docs around the new API.

Core (`infinity_agent_core::system`):
- `AgentSystemBuilder`: `new(stores, model, sender)` builds a step-mode `AgentSystem`
  for serverless platforms; `new_local(stores, model)` builds a `LocalAgentSystem`
  with an internal in-process queue (`ChannelSender`).
- Step mode's public surface is `AgentSystem::step(inputs, observer, defer)`: the
  batch may span multiple threads (SQS FIFO batches can interleave message groups);
  `step` partitions by `group_id`, applies the deferral policy per thread, joins the
  per-thread slices concurrently (prepare → completion → history sync → observer
  commit barrier → tool dispatch), and returns each thread's `StepOutcome`. `&mut
  self` serializes calls per system; the internal `Thread` type is not public and
  `AgentSystem` is not `Clone`, so two live in-memory views of one thread cannot
  exist through the public API. Per-thread configuration resolves lazily on first
  step so loading a thread never boots tool servers.
- `LocalAgentSystem` is mode-typed (`Handles` by default, `Launcher` after
  `with_thread_launcher()`), with one `start()` per mode:
  - Handles mode: `start()` returns a `RunningSystem` running an actor-system-style
    runtime (router + one driver per active thread, input batching, `<interrupt>`
    handling, deferral while a non-passive tool call is pending, auto-compaction
    above 75% of the context window, race-free idle exit/respawn) with ack'd
    `subscribe`, a `thread_exits` channel, and `thread_handle(id)` attaching to any
    thread. `start_with_observer(factory)` swaps in a custom `ThreadObserver`.
  - Launcher mode: `start()` returns a `LaunchingSystem`. `thread_builder()`
    configures and launches new threads (generated IDs) with their own tools and
    prompt, unioned onto the system-wide configuration (static or
    `ThreadConfigSource`) via an internal `UnionConfigSource`; `thread_handle(id)`
    re-attaches to existing threads only (launched in-process or with history).
    Launch configurations are process-lifetime (not persisted).
- `ThreadHandle`: `replay()` plus an exactly-once `events` stream and input sending;
  subscriptions survive driver respawns via a shared registry.
- `ThreadObserver`: synchronous `on_event(AgentEvent)` fan-out (exactly-once relative
  to `on_subscribe` replays) plus awaited durability hooks (`on_user_choice_required`
  /`_dismissed`, `on_commit` before tool dispatch). `EventCollector` buffers
  `(thread_id, event)` pairs.
- `ModelSource` (per-round resolution; enables mid-session model switching),
  `ThreadConfigSource` (per-thread tools/prompt/notifier; enables per-session RAP
  servers and toolsets), `DeferQueue` (`InMemoryDeferQueue`/`NoDeferral`),
  `Tool::is_passive()`, tokio sleep tools (`with_tokio_sleep_tools`).
- `batch_processor` deleted: `process_batch` had no production callers and its
  semantics violated the documented durability barrier (swallowed sync errors, then
  dispatched; emitted ResponseDone before sync). The documented low-level API is now
  `HistoryManager` + `prepare_input`/`run_completion`/`execute_action`;
  `execute_action_with_error_result` (the #88 error-fallback) lives in
  `event_processor`. `DisplayEvent` moved out of core: `AgentEvent` is core's only
  event vocabulary; the CLI keeps a local, non-generic `DisplayEvent` as its
  terminal-rendering enum. All unique regression coverage ported.
- Shared slice internals: `event_processor::input_echo` is the single computation of
  accepted-input echoes; `SyntheticKind::is_compaction_complete()`;
  `InputMessage::user_text()`; driver's in-flight step modeled as
  `InFlightStep { fut, cancel_tx }` with a consuming `cancel()`.
- `rap_callback` module: the RAP callback → `InputMessage` conversion (multimodal
  content, display segments, tagged synthetics incl. subscription final/associative
  flags, user_choice, oauth) lives in core with 11 unit tests; the daemon delegates.
- In-memory `ConversationStore`/`StateStore` in `infinity_agent_core::stores` with a
  serializable snapshot/restore API; the daemon's stores are thin wrappers (extras +
  JSON persistence) so the ancestor/compaction/dedup semantics exist in exactly one
  place. `spawn_thread_with_id` supports deterministic IDs.
- Platform-neutral core: `input_queue_arn` removed from `ToolContext`/builder.
- All 20 daemon agent-loop tests migrated into core `system/tests.rs` with
  byte-identical snapshots, plus new coverage: launcher tool/prompt unioning and
  existing-only attachment, `ThreadHandle` streaming across tool-call rounds,
  multi-group step batches, failed-dispatch fallback, OAuth surfacing, prepare-level
  user-choice, handle respawn survival and pruning.

Daemon (`infinity-daemon`):
- One daemon-lifetime agent system replaces per-session agent loops; sessions are
  root threads; a `thread_exits` watcher marks sessions idle and shuts down their
  RAP servers when no keep-alive client is attached.
- `rap_servers.rs`: `ManagedRapServer` boots lazily per session via
  `ThreadConfigSource` and reboots transparently after idle shutdown; `Up` caches
  the manifest (fetch failure tears the boot down); MCP proxy tasks are owned
  (stdio children `kill_on_drop`) but proxies stay up across idles since MCP servers
  may be stateful; migration flows ride the same path (`MigrationServer`;
  `boot_rap_servers`/`rap_tools.rs` deleted); config merging unified in
  `config::load_merged_rap_config`.
- `DaemonObserver` implements the observer hooks; broadcasting unified in
  `observer::broadcast_pruning`/`broadcast_to_thread` (fixing `handle_view_update`
  never pruning dead subscribers); re-sent Connects replace rather than stack
  subscriptions; replay rendering matches live rendering.
- `CatalogModelSource` resolves each thread's persisted model per round; fixed the
  pre-existing double `ModelSwitched` delivery.
- `send_input` takes `user_driven: bool`; interior-mutability methods take `&self`.
- Web goldens `chat-image-result`/`chat-diff-result` regenerated (old goldens
  captured an eager-boot layout artifact).
- Deleted: `session/thread_worker.rs`, `session/agent_loop.rs`, `sleep_tools.rs`,
  `InMemoryMessageSender`, `Session`, `spawn_session`, `AgentMessage`.

Lambda (`infinity-agent-lambda`):
- Event handler rewritten on the new API: one system per invocation with a
  `ThreadConfigSource` resolving each thread's RAP toolsets (DynamoDB manifest
  cache) plus platform sleep tools (shared `WakeupScheduler` backend); one
  batch-shaped `step`; per-thread outputs aggregated in a `BTreeMap`.
- New `rap-receiver` binary replaces the drifted JS receiver (deleted): same
  Function-URL contract, full parity with the daemon's conversion via
  `infinity_agent_core::rap_callback` (user_choice accepted, multimodal and
  `display_as` preserved, tagged synthetics honored, stable dedup IDs where the
  wire has identity). CDK switches to a cargo-lambda `RustFunction`; the Function
  URL is recreated on deploy, so persisted callback URLs must be re-subscribed.

Docs (`docs/docs/infinity-runtime`): the single "Rust API" page is replaced by two
sections, written in the Hydro docs register (definitional openings, bolded key
terms, cause-and-effect guarantees, code-first examples, admonitions for caveats):
"The Agent System API" (overview, building-a-system, running-locally, step-mode,
observers, including launcher mode) and "The Low-Level API" (overview,
history-manager, completion-loop). Cross-references updated repo-wide; `cargo doc`
is warning-free.

BREAKING CHANGE: `infinity-agent-core` removes `batch_processor` (`process_batch`,
`process_input_item`, `DisplayEvent`); `Thread` is no longer public (use
`AgentSystem::step` or `ThreadHandle`); `AgentSystem` is not `Clone`;
`LocalAgentSystem::start_with_handles` is now `start()` and custom observers use
`start_with_observer` (`LocalAgentSystem` gained a mode type parameter, default
`Handles`); `EventCollector::take` returns `(thread_id, event)` pairs; builder tools
are stored as `Rc<dyn Tool<M>>`; `Tool` gains defaulted `is_passive`;
`ToolContext`/builder lose `input_queue_arn`. `infinity-daemon` no longer exports
`sleep_tools`, `AgentMessage`, `thread_worker`, `agent_loop`, `Session`,
`InMemoryMessageSender`, `boot_rap_servers`, or `rap_tools`;
`SessionManager::switch_model` takes the requester's sender and returns
`Result<(), String>`; `send_input` takes `user_driven: bool`;
`SharedSessionManager` is `Rc<tokio::Mutex<...>>`. The CLI's `DisplayEvent` lives in
`infinity_agent_cli::display`.

Co-authored-by: Infinity 🤖 <infinity@hydro.run>
PR: #92
shadaj added a commit that referenced this pull request Aug 12, 2026
…on + lambda onto it

Introduce `infinity_agent_core::system`, a builder-based, object-oriented API for
running agents, rebuild both production embeddings on top of it, unify the parallel
implementations that had grown around the old architecture, and rewrite the
`infinity-runtime` docs around the new API.

Core (`infinity_agent_core::system`):
- `AgentSystemBuilder`: `new(stores, model, sender)` builds a step-mode `AgentSystem`
  for serverless platforms; `new_local(stores, model)` builds a `LocalAgentSystem`
  with an internal in-process queue (`ChannelSender`).
- Step mode's public surface is `AgentSystem::step(inputs, observer, defer)`: the
  batch may span multiple threads (SQS FIFO batches can interleave message groups);
  `step` partitions by `group_id`, applies the deferral policy per thread, joins the
  per-thread slices concurrently (prepare → completion → history sync → observer
  commit barrier → tool dispatch), and returns each thread's `StepOutcome`. `&mut
  self` serializes calls per system; the internal `Thread` type is not public and
  `AgentSystem` is not `Clone`, so two live in-memory views of one thread cannot
  exist through the public API. Per-thread configuration resolves lazily on first
  step so loading a thread never boots tool servers.
- `LocalAgentSystem` is mode-typed (`Handles` by default, `Launcher` after
  `with_thread_launcher()`), with one `start()` per mode:
  - Handles mode: `start()` returns a `RunningSystem` running an actor-system-style
    runtime (router + one driver per active thread, input batching, `<interrupt>`
    handling, deferral while a non-passive tool call is pending, auto-compaction
    above 75% of the context window, race-free idle exit/respawn) with ack'd
    `subscribe`, a `thread_exits` channel, and `thread_handle(id)` attaching to any
    thread. `start_with_observer(factory)` swaps in a custom `ThreadObserver`.
  - Launcher mode: `start()` returns a `LaunchingSystem`. `thread_builder()`
    configures and launches new threads (generated IDs) with their own tools and
    prompt, unioned onto the system-wide configuration (static or
    `ThreadConfigSource`) via an internal `UnionConfigSource`; `thread_handle(id)`
    re-attaches to existing threads only (launched in-process or with history).
    Launch configurations are process-lifetime (not persisted).
- `ThreadHandle`: `replay()` plus an exactly-once `events` stream and input sending;
  subscriptions survive driver respawns via a shared registry.
- `ThreadObserver`: synchronous `on_event(AgentEvent)` fan-out (exactly-once relative
  to `on_subscribe` replays) plus awaited durability hooks (`on_user_choice_required`
  /`_dismissed`, `on_commit` before tool dispatch). `EventCollector` buffers
  `(thread_id, event)` pairs.
- `ModelSource` (per-round resolution; enables mid-session model switching),
  `ThreadConfigSource` (per-thread tools/prompt/notifier; enables per-session RAP
  servers and toolsets), `DeferQueue` (`InMemoryDeferQueue`/`NoDeferral`),
  `Tool::is_passive()`, tokio sleep tools (`with_tokio_sleep_tools`).
- `batch_processor` deleted: `process_batch` had no production callers and its
  semantics violated the documented durability barrier (swallowed sync errors, then
  dispatched; emitted ResponseDone before sync). The documented low-level API is now
  `HistoryManager` + `prepare_input`/`run_completion`/`execute_action`;
  `execute_action_with_error_result` (the #88 error-fallback) lives in
  `event_processor`. `DisplayEvent` moved out of core: `AgentEvent` is core's only
  event vocabulary; the CLI keeps a local, non-generic `DisplayEvent` as its
  terminal-rendering enum. All unique regression coverage ported.
- Shared slice internals: `event_processor::input_echo` is the single computation of
  accepted-input echoes; `SyntheticKind::is_compaction_complete()`;
  `InputMessage::user_text()`; driver's in-flight step modeled as
  `InFlightStep { fut, cancel_tx }` with a consuming `cancel()`.
- `rap_callback` module: the RAP callback → `InputMessage` conversion (multimodal
  content, display segments, tagged synthetics incl. subscription final/associative
  flags, user_choice, oauth) lives in core with 11 unit tests; the daemon delegates.
- In-memory `ConversationStore`/`StateStore` in `infinity_agent_core::stores` with a
  serializable snapshot/restore API; the daemon's stores are thin wrappers (extras +
  JSON persistence) so the ancestor/compaction/dedup semantics exist in exactly one
  place. `spawn_thread_with_id` supports deterministic IDs.
- Platform-neutral core: `input_queue_arn` removed from `ToolContext`/builder.
- All 20 daemon agent-loop tests migrated into core `system/tests.rs` with
  byte-identical snapshots, plus new coverage: launcher tool/prompt unioning and
  existing-only attachment, `ThreadHandle` streaming across tool-call rounds,
  multi-group step batches, failed-dispatch fallback, OAuth surfacing, prepare-level
  user-choice, handle respawn survival and pruning.

Daemon (`infinity-daemon`):
- One daemon-lifetime agent system replaces per-session agent loops; sessions are
  root threads; a `thread_exits` watcher marks sessions idle and shuts down their
  RAP servers when no keep-alive client is attached.
- `rap_servers.rs`: `ManagedRapServer` boots lazily per session via
  `ThreadConfigSource` and reboots transparently after idle shutdown; `Up` caches
  the manifest (fetch failure tears the boot down); MCP proxy tasks are owned
  (stdio children `kill_on_drop`) but proxies stay up across idles since MCP servers
  may be stateful; migration flows ride the same path (`MigrationServer`;
  `boot_rap_servers`/`rap_tools.rs` deleted); config merging unified in
  `config::load_merged_rap_config`.
- `DaemonObserver` implements the observer hooks; broadcasting unified in
  `observer::broadcast_pruning`/`broadcast_to_thread` (fixing `handle_view_update`
  never pruning dead subscribers); re-sent Connects replace rather than stack
  subscriptions; replay rendering matches live rendering.
- `CatalogModelSource` resolves each thread's persisted model per round; fixed the
  pre-existing double `ModelSwitched` delivery.
- `send_input` takes `user_driven: bool`; interior-mutability methods take `&self`.
- Web goldens `chat-image-result`/`chat-diff-result` regenerated (old goldens
  captured an eager-boot layout artifact).
- Deleted: `session/thread_worker.rs`, `session/agent_loop.rs`, `sleep_tools.rs`,
  `InMemoryMessageSender`, `Session`, `spawn_session`, `AgentMessage`.

Lambda (`infinity-agent-lambda`):
- Event handler rewritten on the new API: one system per invocation with a
  `ThreadConfigSource` resolving each thread's RAP toolsets (DynamoDB manifest
  cache) plus platform sleep tools (shared `WakeupScheduler` backend); one
  batch-shaped `step`; per-thread outputs aggregated in a `BTreeMap`.
- New `rap-receiver` binary replaces the drifted JS receiver (deleted): same
  Function-URL contract, full parity with the daemon's conversion via
  `infinity_agent_core::rap_callback` (user_choice accepted, multimodal and
  `display_as` preserved, tagged synthetics honored, stable dedup IDs where the
  wire has identity). CDK switches to a cargo-lambda `RustFunction`; the Function
  URL is recreated on deploy, so persisted callback URLs must be re-subscribed.

Docs (`docs/docs/infinity-runtime`): the single "Rust API" page is replaced by two
sections, written in the Hydro docs register (definitional openings, bolded key
terms, cause-and-effect guarantees, code-first examples, admonitions for caveats):
"The Agent System API" (overview, building-a-system, running-locally, step-mode,
observers, including launcher mode) and "The Low-Level API" (overview,
history-manager, completion-loop). Cross-references updated repo-wide; `cargo doc`
is warning-free.

BREAKING CHANGE: `infinity-agent-core` removes `batch_processor` (`process_batch`,
`process_input_item`, `DisplayEvent`); `Thread` is no longer public (use
`AgentSystem::step` or `ThreadHandle`); `AgentSystem` is not `Clone`;
`LocalAgentSystem::start_with_handles` is now `start()` and custom observers use
`start_with_observer` (`LocalAgentSystem` gained a mode type parameter, default
`Handles`); `EventCollector::take` returns `(thread_id, event)` pairs; builder tools
are stored as `Rc<dyn Tool<M>>`; `Tool` gains defaulted `is_passive`;
`ToolContext`/builder lose `input_queue_arn`. `infinity-daemon` no longer exports
`sleep_tools`, `AgentMessage`, `thread_worker`, `agent_loop`, `Session`,
`InMemoryMessageSender`, `boot_rap_servers`, or `rap_tools`;
`SessionManager::switch_model` takes the requester's sender and returns
`Result<(), String>`; `send_input` takes `user_driven: bool`;
`SharedSessionManager` is `Rc<tokio::Mutex<...>>`. The CLI's `DisplayEvent` lives in
`infinity_agent_cli::display`.

Co-authored-by: Infinity 🤖 <infinity@hydro.run>
PR: #92
shadaj added a commit that referenced this pull request Aug 12, 2026
…on + lambda onto it

Introduce `infinity_agent_core::system`, a builder-based, object-oriented API for
running agents, rebuild both production embeddings on top of it, unify the parallel
implementations that had grown around the old architecture, and rewrite the
`infinity-runtime` docs around the new API.

Core (`infinity_agent_core::system`, resident runtime under `system::local`):
- `AgentSystemBuilder`: `new(stores, model, sender)` builds a step-mode `AgentSystem`
  for serverless platforms; `new_local(stores, model)` builds a `LocalAgentSystem`
  with an internal in-process queue (`ChannelSender`).
- Step mode's public surface is `AgentSystem::step(inputs, observer, defer)`: the
  batch may span multiple threads (SQS FIFO batches can interleave message groups);
  `step` partitions by `group_id`, applies the deferral policy per thread, joins the
  per-thread slices concurrently (prepare → completion → history sync → observer
  commit barrier → tool dispatch), and returns each thread's `StepOutcome`. `&mut
  self` serializes calls per system; the internal `Thread` type is not public and
  `AgentSystem` is not `Clone`, so two live in-memory views of one thread cannot
  exist through the public API. Per-thread configuration resolves lazily on first
  step so loading a thread never boots tool servers.
- `LocalAgentSystem` is mode-typed (`Handles` by default, `Launcher` after
  `with_thread_launcher()`), with one `start()` per mode:
  - Handles mode: `start()` returns a `RunningSystem` running an actor-system-style
    runtime (router + one driver per active thread, input batching, `<interrupt>`
    handling, deferral while a non-passive tool call is pending, auto-compaction
    above 75% of the context window, race-free idle exit/respawn) with ack'd
    `subscribe`, a `thread_exits` channel, and `thread_handle(id)` attaching to any
    thread. `start_with_observer(factory)` swaps in a custom `ThreadObserver`.
  - Launcher mode: `start()` returns a `LaunchingSystem`. `thread_builder()`
    configures and launches new threads (generated IDs) with their own tools and
    prompt, unioned onto the system-wide configuration (static or
    `ThreadConfigSource`); `thread_handle(id)` re-attaches to existing threads only.
    Launch configurations are process-lifetime (not persisted).
- `ThreadHandle`: `replay()` plus an exactly-once `events` stream and input sending;
  subscriptions survive driver respawns via a shared registry.
- `ThreadObserver`: synchronous `on_event(AgentEvent)` fan-out (exactly-once relative
  to `on_subscribe` replays) plus awaited durability hooks (`on_user_choice_required`
  /`_dismissed`, `on_commit` before tool dispatch). `EventCollector` buffers
  `(thread_id, event)` pairs.
- `ModelSource` (per-round resolution; enables mid-session model switching),
  `ThreadConfigSource` (per-thread tools/prompt/notifier; enables per-session RAP
  servers and toolsets), `DeferQueue` (`InMemoryDeferQueue`/`NoDeferral`),
  `Tool::is_passive()`, tokio sleep tools (`with_tokio_sleep_tools`).
- `batch_processor` deleted: `process_batch` had no production callers and its
  semantics violated the documented durability barrier (swallowed sync errors, then
  dispatched; emitted ResponseDone before sync). The documented low-level API is now
  `HistoryManager` + `prepare_input`/`run_completion`/`execute_action`;
  `execute_action_with_error_result` (the #88 error-fallback) lives in
  `event_processor`. `DisplayEvent` moved out of core: `AgentEvent` is core's only
  event vocabulary; the CLI keeps a local, non-generic `DisplayEvent` as its
  terminal-rendering enum. All unique regression coverage ported.
- Shared slice internals: `event_processor::input_echo` is the single computation of
  accepted-input echoes; `SyntheticKind::is_compaction_complete()`;
  `InputMessage::user_text()`; driver's in-flight step modeled as
  `InFlightStep { fut, cancel_tx }` with a consuming `cancel()`.
- `rap_callback` module: the RAP callback → `InputMessage` conversion (multimodal
  content, display segments, tagged synthetics incl. subscription final/associative
  flags, user_choice, oauth) lives in core with 11 unit tests; the daemon delegates.
- In-memory `ConversationStore`/`StateStore` in `infinity_agent_core::stores` with a
  serializable snapshot/restore API; the daemon's stores are thin wrappers (extras +
  JSON persistence) so the ancestor/compaction/dedup semantics exist in exactly one
  place. `spawn_thread_with_id` supports deterministic IDs.
- Platform-neutral core: `input_queue_arn` removed from `ToolContext`/builder.
- All 20 daemon agent-loop tests migrated into core `system/tests.rs` with
  byte-identical snapshots, plus new coverage: launcher tool/prompt unioning and
  existing-only attachment, `ThreadHandle` streaming across tool-call rounds,
  multi-group step batches, failed-dispatch fallback, OAuth surfacing, prepare-level
  user-choice, handle respawn survival and pruning.

Daemon (`infinity-daemon`):
- One daemon-lifetime agent system replaces per-session agent loops; sessions are
  root threads; a `thread_exits` watcher marks sessions idle and shuts down their
  RAP servers when no keep-alive client is attached.
- `rap_servers.rs`: `ManagedRapServer` boots lazily per session via
  `ThreadConfigSource` and reboots transparently after idle shutdown; `Up` caches
  the manifest (fetch failure tears the boot down); MCP proxy tasks are owned
  (stdio children `kill_on_drop`) but proxies stay up across idles since MCP servers
  may be stateful; migration flows ride the same path (`MigrationServer`;
  `boot_rap_servers`/`rap_tools.rs` deleted); config merging unified in
  `config::load_merged_rap_config`.
- `DaemonObserver` implements the observer hooks; broadcasting unified in
  `observer::broadcast_pruning`/`broadcast_to_thread` (fixing `handle_view_update`
  never pruning dead subscribers); re-sent Connects replace rather than stack
  subscriptions; replay rendering matches live rendering.
- `CatalogModelSource` resolves each thread's persisted model per round; fixed the
  pre-existing double `ModelSwitched` delivery.
- `send_input` takes `user_driven: bool`; interior-mutability methods take `&self`.
- Web goldens `chat-image-result`/`chat-diff-result` regenerated (old goldens
  captured an eager-boot layout artifact).
- Deleted: `session/thread_worker.rs`, `session/agent_loop.rs`, `sleep_tools.rs`,
  `InMemoryMessageSender`, `Session`, `spawn_session`, `AgentMessage`.

Lambda (`infinity-agent-lambda`):
- Event handler rewritten on the new API: one system per invocation with a
  `ThreadConfigSource` resolving each thread's RAP toolsets (DynamoDB manifest
  cache) plus platform sleep tools (shared `WakeupScheduler` backend); one
  batch-shaped `step`; per-thread outputs aggregated in a `BTreeMap`.
- New `rap-receiver` binary replaces the drifted JS receiver (deleted): same
  Function-URL contract, full parity with the daemon's conversion via
  `infinity_agent_core::rap_callback` (user_choice accepted, multimodal and
  `display_as` preserved, tagged synthetics honored, stable dedup IDs where the
  wire has identity). CDK switches to a cargo-lambda `RustFunction`; the Function
  URL is recreated on deploy, so persisted callback URLs must be re-subscribed.

Docs (`docs/docs/infinity-runtime`): the single "Rust API" page is replaced by two
sections, written in the Hydro docs register (definitional openings, bolded key
terms, cause-and-effect guarantees, code-first examples, admonitions for caveats):
"The Agent System API" (overview, building-a-system, running-locally, step-mode,
observers, including launcher mode) and "The Low-Level API" (overview,
history-manager, completion-loop). Cross-references updated repo-wide; `cargo doc`
is warning-free.

BREAKING CHANGE: `infinity-agent-core` removes `batch_processor` (`process_batch`,
`process_input_item`, `DisplayEvent`); `Thread` is no longer public (use
`AgentSystem::step` or `ThreadHandle`); `AgentSystem` is not `Clone`;
`LocalAgentSystem::start_with_handles` is now `start()` and custom observers use
`start_with_observer` (`LocalAgentSystem` gained a mode type parameter, default
`Handles`); the resident-runtime types (`RunningSystem`, `SubscribeHandle`,
`ThreadHandle`, `HandleObserver`, `HandleSubscribeRequest`, `LaunchingSystem`,
`ThreadBuilder`, `ChannelSender`, `ChannelSendError`, `ActiveThreads`) live under
`system::local`; `EventCollector::take` returns `(thread_id, event)` pairs; builder
tools are stored as `Rc<dyn Tool<M>>`; `Tool` gains defaulted `is_passive`;
`ToolContext`/builder lose `input_queue_arn`. `infinity-daemon` no longer exports
`sleep_tools`, `AgentMessage`, `thread_worker`, `agent_loop`, `Session`,
`InMemoryMessageSender`, `boot_rap_servers`, or `rap_tools`;
`SessionManager::switch_model` takes the requester's sender and returns
`Result<(), String>`; `send_input` takes `user_driven: bool`;
`SharedSessionManager` is `Rc<tokio::Mutex<...>>`. The CLI's `DisplayEvent` lives in
`infinity_agent_cli::display`.

Co-authored-by: Infinity 🤖 <infinity@hydro.run>
PR: #92
shadaj added a commit that referenced this pull request Aug 12, 2026
…on + lambda onto it

Introduce `infinity_agent_core::system`, a builder-based, object-oriented API for
running agents, rebuild both production embeddings on top of it, unify the parallel
implementations that had grown around the old architecture, and rewrite the
`infinity-runtime` docs around the new API.

Core (`infinity_agent_core::system`, resident runtime under `system::local`):
- `AgentSystemBuilder`: `new(stores, model, sender)` builds a step-mode `AgentSystem`
  for serverless platforms; `new_local(stores, model)` builds a `LocalAgentSystem`
  with an internal in-process queue (`ChannelSender`).
- Step mode's public surface is `AgentSystem::step(inputs, observer, defer)`: the
  batch may span multiple threads (SQS FIFO batches can interleave message groups);
  `step` partitions by `group_id`, applies the deferral policy per thread, joins the
  per-thread slices concurrently (prepare → completion → history sync → tool
  dispatch), and returns each thread's `StepOutcome`. `&mut self` serializes calls
  per system; the internal `Thread` type is not public and `AgentSystem` is not
  `Clone`, so two live in-memory views of one thread cannot exist through the
  public API. Per-thread configuration resolves lazily on first step so loading a
  thread never boots tool servers.
- `LocalAgentSystem` is mode-typed (`Handles` by default, `Launcher` after
  `with_thread_launcher()`), with one `start()` per mode:
  - Handles mode: `start()` returns a `RunningSystem` running an actor-system-style
    runtime (router + one driver per active thread, input batching, `<interrupt>`
    handling, deferral while a non-passive tool call is pending, auto-compaction
    above 75% of the context window, race-free idle exit/respawn) with ack'd
    `subscribe`, a `thread_exits` channel, and `thread_handle(id)` attaching to any
    thread. `start_with_observer(factory)` swaps in a custom `ThreadObserver`.
  - Launcher mode: `start()` returns a `LaunchingSystem`. `thread_builder()`
    configures and launches new threads (generated IDs) with their own tools and
    prompt, unioned onto the system-wide configuration; launch configurations
    attach to the launched (root) thread, so spawned subagent threads inherit them
    via root resolution. `thread_handle(id)` re-attaches to existing threads only.
    Launch configurations are process-lifetime (not persisted).
- `ThreadHandle`: `replay()` plus an exactly-once `events` stream and input sending;
  subscriptions survive driver respawns via a shared registry.
- `ThreadObserver`: synchronous `on_event(AgentEvent)` fan-out (exactly-once relative
  to `on_subscribe` replays) plus awaited user-choice durability hooks
  (`on_user_choice_required`/`_dismissed`). Tool dispatch happens strictly after
  history sync, so turns are durable before their effects are observable.
  `EventCollector` buffers `(thread_id, event)` pairs.
- `ModelSource` (per-round resolution; enables mid-session model switching),
  `ThreadConfigSource` (per-thread tools/prompt/notifier; enables per-session RAP
  servers and toolsets), `DeferQueue` (`InMemoryDeferQueue`/`NoDeferral`),
  `Tool::is_passive()`, tokio sleep tools (`with_tokio_sleep_tools`).
- `batch_processor` deleted: `process_batch` had no production callers and its
  semantics violated the documented durability barrier (swallowed sync errors, then
  dispatched; emitted ResponseDone before sync). The documented low-level API is now
  `HistoryManager` + `prepare_input`/`run_completion`/`execute_action`;
  `execute_action_with_error_result` (the #88 error-fallback) lives in
  `event_processor`. `DisplayEvent` moved out of core: `AgentEvent` is core's only
  event vocabulary; the CLI keeps a local, non-generic `DisplayEvent` as its
  terminal-rendering enum. All unique regression coverage ported.
- Shared slice internals: `event_processor::input_echo` is the single computation of
  accepted-input echoes; `SyntheticKind::is_compaction_complete()`;
  `InputMessage::user_text()`; driver's in-flight step modeled as
  `InFlightStep { fut, cancel_tx }` with a consuming `cancel()`.
- `rap_callback` module: the RAP callback → `InputMessage` conversion (multimodal
  content, display segments, tagged synthetics incl. subscription final/associative
  flags, user_choice, oauth) lives in core with 11 unit tests; the daemon delegates.
- In-memory `ConversationStore`/`StateStore` in `infinity_agent_core::stores` with a
  serializable snapshot/restore API; the daemon's stores are thin wrappers (extras +
  JSON persistence) so the ancestor/compaction/dedup semantics exist in exactly one
  place. `spawn_thread_with_id` supports deterministic IDs.
- Platform-neutral core: `input_queue_arn` removed from `ToolContext`/builder.
  Unused refactor leftovers pruned: `ToolSet`/`VecToolSet`,
  `ToolsConfig::{add_command, toolset_commands, mcp_servers, http_mcp_servers}`,
  `HistoryManager::state_store()`.
- All 20 daemon agent-loop tests migrated into core `system/tests.rs` with
  byte-identical snapshots, plus new coverage: launcher tool/prompt unioning,
  launched-thread child inheritance, existing-only attachment, `ThreadHandle`
  streaming across tool-call rounds, multi-group step batches, failed-dispatch
  fallback, OAuth surfacing, prepare-level user-choice, handle respawn survival and
  pruning.

Daemon (`infinity-daemon`):
- One daemon-lifetime agent system replaces per-session agent loops and the
  restart-on-send path (`needs_restart` → `start_session`): idle threads respawn a
  driver transparently instead. Sessions are root threads; a `thread_exits` watcher
  marks sessions idle and shuts down their RAP servers when no keep-alive client is
  attached.
- `rap_servers.rs`: RAP server lifecycle moves out of session start — boot happens
  lazily when a thread's config first resolves (loading/replaying a thread never
  boots servers), shutdown is driven by session idleness, and a shut-down server
  reboots on the next tool interaction. `Up` caches the manifest (fetch failure
  tears the boot down); MCP proxy tasks are owned (stdio children `kill_on_drop`)
  but proxies stay up across idles since MCP servers may be stateful; migration
  flows ride the same path (`MigrationServer`; `boot_rap_servers`/`rap_tools.rs`
  deleted); config merging unified in `config::load_merged_rap_config`.
- `DaemonObserver` implements the observer hooks; broadcasting unified in
  `observer::broadcast_pruning`/`broadcast_to_thread` (fixing `handle_view_update`
  never pruning dead subscribers); re-sent Connects replace rather than stack
  subscriptions; replay rendering matches live rendering.
- `CatalogModelSource` resolves each thread's persisted model per round; fixed the
  pre-existing double `ModelSwitched` delivery.
- `send_input` takes `user_driven: bool`; interior-mutability methods take `&self`.
- Web goldens `chat-image-result`/`chat-diff-result` regenerated (old goldens
  captured an eager-boot layout artifact).
- Deleted: `session/thread_worker.rs`, `session/agent_loop.rs`, `sleep_tools.rs`,
  `InMemoryMessageSender`, `Session`, `spawn_session`, `AgentMessage`.

Lambda (`infinity-agent-lambda`):
- Event handler rewritten on the new API: one system per invocation with a
  `ThreadConfigSource` resolving each thread's RAP toolsets (DynamoDB manifest
  cache) plus platform sleep tools (shared `WakeupScheduler` backend); one
  batch-shaped `step`; per-thread outputs aggregated in a `BTreeMap`.
- New `rap-receiver` binary replaces the drifted JS receiver (deleted): same
  Function-URL contract, full parity with the daemon's conversion via
  `infinity_agent_core::rap_callback` (user_choice accepted, multimodal and
  `display_as` preserved, tagged synthetics honored, stable dedup IDs where the
  wire has identity). CDK switches to a cargo-lambda `RustFunction`; the Function
  URL is recreated on deploy, so persisted callback URLs must be re-subscribed.

Docs (`docs/docs/infinity-runtime`): the single "Rust API" page is replaced by two
sections, written in the Hydro docs register (definitional openings, bolded key
terms, cause-and-effect guarantees, code-first examples, admonitions for caveats):
"The Agent System API" (overview, building-a-system, running-locally, step-mode,
observers, including launcher mode) and "The Low-Level API" (overview,
history-manager, completion-loop). Cross-references updated repo-wide; `cargo doc`
is warning-free.

BREAKING CHANGE: `infinity-agent-core` removes `batch_processor` (`process_batch`,
`process_input_item`, `DisplayEvent`); `Thread` is no longer public (use
`AgentSystem::step` or `ThreadHandle`); `AgentSystem` is not `Clone`;
`LocalAgentSystem::start_with_handles` is now `start()` and custom observers use
`start_with_observer` (`LocalAgentSystem` gained a mode type parameter, default
`Handles`); the resident-runtime types (`RunningSystem`, `SubscribeHandle`,
`ThreadHandle`, `HandleObserver`, `HandleSubscribeRequest`, `LaunchingSystem`,
`ThreadBuilder`, `ChannelSender`, `ChannelSendError`, `ActiveThreads`) live under
`system::local`; `ThreadObserver` has no `on_commit` (persist in `on_event` or
externally; the turn is already durable before dispatch); `EventCollector::take`
returns `(thread_id, event)` pairs; builder tools are stored as `Rc<dyn Tool<M>>`;
`Tool` gains defaulted `is_passive`; `ToolContext`/builder lose `input_queue_arn`;
`ToolSet`/`VecToolSet` and `ToolsConfig::{add_command, toolset_commands,
mcp_servers, http_mcp_servers}` are removed. `infinity-daemon` no longer exports
`sleep_tools`, `AgentMessage`, `thread_worker`, `agent_loop`, `Session`,
`InMemoryMessageSender`, `boot_rap_servers`, or `rap_tools`;
`SessionManager::switch_model` takes the requester's sender and returns
`Result<(), String>`; `send_input` takes `user_driven: bool`;
`SharedSessionManager` is `Rc<tokio::Mutex<...>>`. The CLI's `DisplayEvent` lives in
`infinity_agent_cli::display`.

Co-authored-by: Infinity 🤖 <infinity@hydro.run>
PR: #92
shadaj added a commit that referenced this pull request Aug 12, 2026
…on + lambda onto it

Introduce `infinity_agent_core::system`, a builder-based, object-oriented API for
running agents, rebuild both production embeddings on top of it, unify the parallel
implementations that had grown around the old architecture, and rewrite the
`infinity-runtime` docs around the new API.

Core (`infinity_agent_core::system`, resident runtime under `system::local`):
- `AgentSystemBuilder`: `new(stores, model, sender)` builds a step-mode `AgentSystem`
  for serverless platforms; `new_local(stores, model)` builds a `LocalAgentSystem`
  with an internal in-process queue (`ChannelSender`).
- Step mode's public surface is `AgentSystem::step(inputs, observer, defer)`: the
  batch may span multiple threads (SQS FIFO batches can interleave message groups);
  `step` partitions by `group_id`, applies the deferral policy per thread, joins the
  per-thread slices concurrently (prepare → completion → history sync → tool
  dispatch), and returns each thread's `StepOutcome`. `&mut self` serializes calls
  per system; the internal `Thread` type is not public and `AgentSystem` is not
  `Clone`, so two live in-memory views of one thread cannot exist through the
  public API. Per-thread configuration resolves lazily on first step so loading a
  thread never boots tool servers.
- `LocalAgentSystem` is mode-typed (`Handles` by default, `Launcher` after
  `with_thread_launcher()`), with one `start()` per mode:
  - Handles mode: `start()` returns a `RunningSystem` running an actor-system-style
    runtime (router + one driver per active thread, input batching, `<interrupt>`
    handling, deferral while a non-passive tool call is pending, auto-compaction
    above 75% of the context window, race-free idle exit/respawn) with ack'd
    `subscribe`, a `thread_exits` channel, and `thread_handle(id)` attaching to any
    thread. `start_with_observer(factory)` swaps in a custom `ThreadObserver`.
  - Launcher mode: `start()` returns a `LaunchingSystem`. `thread_builder()`
    configures and launches new threads (generated IDs) with their own tools and
    prompt, unioned onto the system-wide configuration; launch configurations
    attach to the launched (root) thread, so spawned subagent threads inherit them
    via root resolution. `thread_handle(id)` re-attaches to existing threads only.
    Launch configurations are process-lifetime (not persisted).
- `ThreadHandle`: `replay()` plus an exactly-once `events` stream and input sending;
  subscriptions survive driver respawns via a shared registry.
- `ThreadObserver`: synchronous `on_event(AgentEvent)` fan-out (exactly-once relative
  to `on_subscribe` replays) plus awaited user-choice durability hooks
  (`on_user_choice_required`/`_dismissed`). Tool dispatch happens strictly after
  history sync, so turns are durable before their effects are observable.
  `EventCollector` buffers `(thread_id, event)` pairs.
- `ModelSource` (per-round resolution; enables mid-session model switching),
  `ThreadConfigSource` (per-thread tools/prompt/notifier; enables per-session RAP
  servers and toolsets), `DeferQueue` (`InMemoryDeferQueue`/`NoDeferral`),
  `Tool::is_passive()`, tokio sleep tools (`with_tokio_sleep_tools`).
- `batch_processor` deleted: `process_batch` had no production callers and its
  semantics violated the documented durability barrier (swallowed sync errors, then
  dispatched; emitted ResponseDone before sync). The documented low-level API is now
  `HistoryManager` + `prepare_input`/`run_completion`/`execute_action`;
  `execute_action_with_error_result` (the #88 error-fallback) lives in
  `event_processor`. `DisplayEvent` moved out of core: `AgentEvent` is core's only
  event vocabulary; the CLI keeps a local, non-generic `DisplayEvent` as its
  terminal-rendering enum. All unique regression coverage ported.
- Shared slice internals: `event_processor::input_echo` is the single computation of
  accepted-input echoes; `SyntheticKind::is_compaction_complete()`;
  `InputMessage::user_text()`; driver's in-flight step modeled as
  `InFlightStep { fut, cancel_tx }` with a consuming `cancel()`.
- `rap_callback` module: the RAP callback → `InputMessage` conversion (multimodal
  content, display segments, tagged synthetics incl. subscription final/associative
  flags, user_choice, oauth) lives in core with 11 unit tests; the daemon delegates.
- In-memory `ConversationStore`/`StateStore` in `infinity_agent_core::stores` with a
  serializable snapshot/restore API; the daemon's stores are thin wrappers (extras +
  JSON persistence) so the ancestor/compaction/dedup semantics exist in exactly one
  place. `spawn_thread_with_id` supports deterministic IDs.
- Platform-neutral core: `input_queue_arn` removed from `ToolContext`/builder.
  Unused refactor leftovers pruned: `ToolSet`/`VecToolSet`,
  `ToolsConfig::{add_command, toolset_commands, mcp_servers, http_mcp_servers}`,
  `HistoryManager::state_store()`.
- All 20 daemon agent-loop tests migrated into core `system/tests.rs` with
  byte-identical snapshots, plus new coverage: launcher tool/prompt unioning,
  launched-thread child inheritance, existing-only attachment, `ThreadHandle`
  streaming across tool-call rounds, multi-group step batches, failed-dispatch
  fallback, OAuth surfacing, prepare-level user-choice, handle respawn survival and
  pruning.

Daemon (`infinity-daemon`):
- One daemon-lifetime agent system replaces per-session agent loops and the
  restart-on-send path (`needs_restart` → `start_session`): idle threads respawn a
  driver transparently instead. Sessions are root threads; a `thread_exits` watcher
  marks sessions idle and shuts down their RAP servers when no keep-alive client is
  attached.
- `rap_servers.rs`: RAP server lifecycle moves out of session start — boot happens
  lazily when a thread's config first resolves (loading/replaying a thread never
  boots servers), shutdown is driven by session idleness, and a shut-down server
  reboots on the next tool interaction. `Up` caches the manifest (fetch failure
  tears the boot down); MCP proxy tasks are owned (stdio children `kill_on_drop`)
  but proxies stay up across idles since MCP servers may be stateful; migration
  flows ride the same path (`MigrationServer`; `boot_rap_servers`/`rap_tools.rs`
  deleted); config merging unified in `config::load_merged_rap_config`.
- `DaemonObserver` implements the observer hooks; broadcasting unified in
  `observer::broadcast_pruning`/`broadcast_to_thread` (fixing `handle_view_update`
  never pruning dead subscribers); re-sent Connects replace rather than stack
  subscriptions; replay rendering matches live rendering.
- `CatalogModelSource` resolves each thread's persisted model per round; fixed the
  pre-existing double `ModelSwitched` delivery.
- `send_input` takes `user_driven: bool`; interior-mutability methods take `&self`.
- Web goldens `chat-image-result`/`chat-diff-result` regenerated (old goldens
  captured an eager-boot layout artifact).
- Deleted: `session/thread_worker.rs`, `session/agent_loop.rs`, `sleep_tools.rs`,
  `InMemoryMessageSender`, `Session`, `spawn_session`, `AgentMessage`.

Lambda (`infinity-agent-lambda`):
- Event handler rewritten on the new API: one system per invocation with a
  `ThreadConfigSource` resolving each thread's RAP toolsets (DynamoDB manifest
  cache) plus platform sleep tools (shared `WakeupScheduler` backend); one
  batch-shaped `step`; per-thread outputs aggregated in a `BTreeMap`.
- New `rap-receiver` binary replaces the drifted JS receiver (deleted): same
  Function-URL contract, full parity with the daemon's conversion via
  `infinity_agent_core::rap_callback` (user_choice accepted, multimodal and
  `display_as` preserved, tagged synthetics honored, stable dedup IDs where the
  wire has identity). CDK switches to a cargo-lambda `RustFunction`; the Function
  URL is recreated on deploy, so persisted callback URLs must be re-subscribed.

Docs (`docs/docs/infinity-runtime`): the single "Rust API" page is replaced by two
sections, written in the Hydro docs register (definitional openings, bolded key
terms, cause-and-effect guarantees, code-first examples, admonitions for caveats):
"The Agent System API" (overview, building-a-system, running-locally, step-mode,
observers, including launcher mode) and "The Low-Level API" (overview,
history-manager, completion-loop). Cross-references updated repo-wide; `cargo doc`
is warning-free.

BREAKING CHANGE: `infinity-agent-core` removes `batch_processor` (`process_batch`,
`process_input_item`, `DisplayEvent`); `Thread` is no longer public (use
`AgentSystem::step` or `ThreadHandle`); `AgentSystem` is not `Clone`;
`LocalAgentSystem::start_with_handles` is now `start()` and custom observers use
`start_with_observer` (`LocalAgentSystem` gained a mode type parameter, default
`Handles`); the resident-runtime types (`RunningSystem`, `SubscribeHandle`,
`ThreadHandle`, `HandleObserver`, `HandleSubscribeRequest`, `LaunchingSystem`,
`ThreadBuilder`, `ChannelSender`, `ChannelSendError`, `ActiveThreads`) live under
`system::local`; `ThreadObserver` has no `on_commit` (persist in `on_event` or
externally; the turn is already durable before dispatch); `EventCollector::take`
returns `(thread_id, event)` pairs; builder tools are stored as `Rc<dyn Tool<M>>`;
`Tool` gains defaulted `is_passive`; `ToolContext`/builder lose `input_queue_arn`;
`ToolSet`/`VecToolSet` and `ToolsConfig::{add_command, toolset_commands,
mcp_servers, http_mcp_servers}` are removed. `infinity-daemon` no longer exports
`sleep_tools`, `AgentMessage`, `thread_worker`, `agent_loop`, `Session`,
`InMemoryMessageSender`, `boot_rap_servers`, or `rap_tools`;
`SessionManager::switch_model` takes the requester's sender and returns
`Result<(), String>`; `send_input` takes `user_driven: bool`;
`SharedSessionManager` is `Rc<tokio::Mutex<...>>`. The CLI's `DisplayEvent` lives in
`infinity_agent_cli::display`.

Co-authored-by: Infinity 🤖 <infinity@hydro.run>
PR: #92
shadaj added a commit that referenced this pull request Aug 12, 2026
…on + lambda onto it

Introduce `infinity_agent_core::system`, a builder-based, object-oriented API for
running agents, rebuild both production embeddings on top of it, unify the parallel
implementations that had grown around the old architecture, and rewrite the
`infinity-runtime` docs around the new API.

Core (`infinity_agent_core::system`, resident runtime under `system::local`):
- `AgentSystemBuilder`: `new(stores, model, sender)` builds a step-mode `AgentSystem`
  for serverless platforms; `new_local(stores, model)` builds a `LocalAgentSystem`
  with an internal in-process queue (`ChannelSender`).
- Step mode's public surface is `AgentSystem::step(inputs, observer, defer)`: the
  batch may span multiple threads (SQS FIFO batches can interleave message groups);
  `step` partitions by `group_id`, applies the deferral policy per thread, joins the
  per-thread slices concurrently (prepare → completion → history sync → tool
  dispatch), and returns each thread's `StepOutcome`. `&mut self` serializes calls
  per system; the internal `Thread` type is not public and `AgentSystem` is not
  `Clone`, so two live in-memory views of one thread cannot exist through the
  public API. Per-thread configuration resolves lazily on first step so loading a
  thread never boots tool servers.
- `LocalAgentSystem` is mode-typed (`Handles` by default, `Launcher` after
  `with_thread_launcher()`), with one `start()` per mode:
  - Handles mode: `start()` returns a `RunningSystem` running an actor-system-style
    runtime (router + one driver per active thread, input batching, `<interrupt>`
    handling, deferral while a non-passive tool call is pending, auto-compaction
    above 75% of the context window, race-free idle exit/respawn) with ack'd
    `subscribe`, a `thread_exits` channel, and `thread_handle(id)` attaching to any
    thread. `start_with_observer(factory)` swaps in a custom `ThreadObserver`.
  - Launcher mode: `start()` returns a `LaunchingSystem`. `thread_builder()`
    configures and launches new threads (generated IDs) with their own tools,
    prompt, and optionally their own `ModelSource`, unioned onto the system-wide
    configuration; launch configurations attach to the launched (root) thread, so
    spawned subagent threads inherit tools, prompt, and model via root resolution.
    `thread_handle(id)` re-attaches to existing threads only (`None` means the
    thread does not exist). Launch configurations are process-lifetime.
- Shutdown is ownership-based: `RunningSystem::shutdown(self)` (and
  `LaunchingSystem::shutdown(self)`) cancels every driver, flushes in-flight turns,
  and awaits the router. Because shutting down consumes the system, `&self` methods
  are infallible: `thread_handle`/`launch` return handles directly, `subscribe`
  resolves once installed, and `send`/`send_user_text` return `()`. Independent
  handles that can outlive the system (`SubscribeHandle`, `ThreadHandle`) keep
  fallible signatures.
- `ThreadHandle`: `replay()` plus an exactly-once `events` stream and input sending;
  subscriptions survive driver respawns via a shared registry.
- `ThreadObserver`: synchronous `on_event(AgentEvent)` fan-out (exactly-once relative
  to `on_subscribe` replays) plus awaited user-choice durability hooks
  (`on_user_choice_required`/`_dismissed`). Tool dispatch happens strictly after
  history sync, so turns are durable before their effects are observable.
  `EventCollector` buffers `(thread_id, event)` pairs.
- `ModelSource` (per-round resolution; enables mid-session model switching),
  `ThreadConfigSource` (per-thread tools/prompt/notifier; enables per-session RAP
  servers and toolsets), `DeferQueue` (`InMemoryDeferQueue`/`NoDeferral`),
  `Tool::is_passive()`, tokio sleep tools (`with_tokio_sleep_tools`).
- `batch_processor` deleted: `process_batch` had no production callers and its
  semantics violated the documented durability barrier (swallowed sync errors, then
  dispatched; emitted ResponseDone before sync). The documented low-level API is now
  `HistoryManager` + `prepare_input`/`run_completion`/`execute_action`;
  `execute_action_with_error_result` (the #88 error-fallback) lives in
  `event_processor`. `DisplayEvent` moved out of core: `AgentEvent` is core's only
  event vocabulary; the CLI keeps a local, non-generic `DisplayEvent` as its
  terminal-rendering enum. All unique regression coverage ported.
- Shared slice internals: `event_processor::input_echo` is the single computation of
  accepted-input echoes; `SyntheticKind::is_compaction_complete()`;
  `InputMessage::user_text()`; driver's in-flight step modeled as
  `InFlightStep { fut, cancel_tx }` with a consuming `cancel()`.
- `rap_callback` module: the RAP callback → `InputMessage` conversion (multimodal
  content, display segments, tagged synthetics incl. subscription final/associative
  flags, user_choice, oauth) lives in core with 11 unit tests; the daemon delegates.
- In-memory `ConversationStore`/`StateStore` in `infinity_agent_core::stores` with a
  serializable snapshot/restore API; the daemon's stores are thin wrappers (extras +
  JSON persistence) so the ancestor/compaction/dedup semantics exist in exactly one
  place. `spawn_thread_with_id` supports deterministic IDs.
- Platform-neutral core: `input_queue_arn` removed from `ToolContext`/builder.
  Unused refactor leftovers pruned: `ToolSet`/`VecToolSet`,
  `ToolsConfig::{add_command, toolset_commands, mcp_servers, http_mcp_servers}`,
  `HistoryManager::state_store()`.
- All 20 daemon agent-loop tests migrated into core `system/tests.rs` with
  byte-identical snapshots, plus new coverage: launcher tool/prompt/model unioning
  and child inheritance, existing-only attachment, `ThreadHandle` streaming across
  tool-call rounds, multi-group step batches, failed-dispatch fallback, OAuth
  surfacing, prepare-level user-choice, handle respawn survival and pruning.

Daemon (`infinity-daemon`):
- One daemon-lifetime agent system replaces per-session agent loops and the
  restart-on-send path (`needs_restart` → `start_session`): idle threads respawn a
  driver transparently instead. Sessions are root threads; a `thread_exits` watcher
  marks sessions idle and shuts down their RAP servers when no keep-alive client is
  attached.
- `rap_servers.rs`: RAP server lifecycle moves out of session start — boot happens
  lazily when a thread's config first resolves (loading/replaying a thread never
  boots servers), shutdown is driven by session idleness, and a shut-down server
  reboots on the next tool interaction. `Up` caches the manifest (fetch failure
  tears the boot down); MCP proxy tasks are owned (stdio children `kill_on_drop`)
  but proxies stay up across idles since MCP servers may be stateful; migration
  flows ride the same path (`MigrationServer`; `boot_rap_servers`/`rap_tools.rs`
  deleted); config merging unified in `config::load_merged_rap_config`.
- `DaemonObserver` implements the observer hooks; broadcasting unified in
  `observer::broadcast_pruning`/`broadcast_to_thread` (fixing `handle_view_update`
  never pruning dead subscribers); re-sent Connects replace rather than stack
  subscriptions; replay rendering matches live rendering.
- `CatalogModelSource` resolves each thread's persisted model per round; fixed the
  pre-existing double `ModelSwitched` delivery.
- `send_input` takes `user_driven: bool`; interior-mutability methods take `&self`.
- Web goldens `chat-image-result`/`chat-diff-result` regenerated (old goldens
  captured an eager-boot layout artifact).
- Deleted: `session/thread_worker.rs`, `session/agent_loop.rs`, `sleep_tools.rs`,
  `InMemoryMessageSender`, `Session`, `spawn_session`, `AgentMessage`.

Lambda (`infinity-agent-lambda`):
- Event handler rewritten on the new API: one system per invocation with a
  `ThreadConfigSource` resolving each thread's RAP toolsets (DynamoDB manifest
  cache) plus platform sleep tools (shared `WakeupScheduler` backend); one
  batch-shaped `step`; per-thread outputs aggregated in a `BTreeMap`.
- New `rap-receiver` binary replaces the drifted JS receiver (deleted): same
  Function-URL contract, full parity with the daemon's conversion via
  `infinity_agent_core::rap_callback` (user_choice accepted, multimodal and
  `display_as` preserved, tagged synthetics honored, stable dedup IDs where the
  wire has identity). CDK switches to a cargo-lambda `RustFunction`; the Function
  URL is recreated on deploy, so persisted callback URLs must be re-subscribed.

Docs (`docs/docs/infinity-runtime`): the single "Rust API" page is replaced by two
sections, written in the Hydro docs register (definitional openings, bolded key
terms, cause-and-effect guarantees, code-first examples, admonitions for caveats):
"The Agent System API" (overview, building-a-system, running-locally, step-mode,
observers, including launcher mode) and "The Low-Level API" (overview,
history-manager, completion-loop). Cross-references updated repo-wide; `cargo doc`
is warning-free.

BREAKING CHANGE: `infinity-agent-core` removes `batch_processor` (`process_batch`,
`process_input_item`, `DisplayEvent`); `Thread` is no longer public (use
`AgentSystem::step` or `ThreadHandle`); `AgentSystem` is not `Clone`;
`LocalAgentSystem::start_with_handles` is now `start()` and custom observers use
`start_with_observer` (`LocalAgentSystem` gained a mode type parameter, default
`Handles`); the resident-runtime types (`RunningSystem`, `SubscribeHandle`,
`ThreadHandle`, `HandleObserver`, `HandleSubscribeRequest`, `LaunchingSystem`,
`ThreadBuilder`, `ChannelSender`, `ChannelSendError`, `ActiveThreads`) live under
`system::local`; `RunningSystem::begin_shutdown`/`task` are replaced by owned
`shutdown(self)`, and `RunningSystem::{send, send_user_text, subscribe,
thread_handle}` are infallible; `ThreadObserver` has no `on_commit` (persist in
`on_event` or externally; the turn is already durable before dispatch);
`EventCollector::take` returns `(thread_id, event)` pairs; builder tools are stored
as `Rc<dyn Tool<M>>`; `Tool` gains defaulted `is_passive`; `ToolContext`/builder
lose `input_queue_arn`; `ToolSet`/`VecToolSet` and `ToolsConfig::{add_command,
toolset_commands, mcp_servers, http_mcp_servers}` are removed. `infinity-daemon` no
longer exports `sleep_tools`, `AgentMessage`, `thread_worker`, `agent_loop`,
`Session`, `InMemoryMessageSender`, `boot_rap_servers`, or `rap_tools`;
`SessionManager::switch_model` takes the requester's sender and returns
`Result<(), String>`; `send_input` takes `user_driven: bool`;
`SharedSessionManager` is `Rc<tokio::Mutex<...>>`. The CLI's `DisplayEvent` lives in
`infinity_agent_cli::display`.

Co-authored-by: Infinity 🤖 <infinity@hydro.run>
PR: #92
shadaj added a commit that referenced this pull request Aug 12, 2026
…on + lambda onto it

Introduce `infinity_agent_core::system`, a builder-based, object-oriented API for
running agents, rebuild both production embeddings on top of it, unify the parallel
implementations that had grown around the old architecture, and rewrite the
`infinity-runtime` docs around the new API.

Core (`infinity_agent_core::system`, resident runtime under `system::local`):
- `AgentSystemBuilder`: `new(stores, model, sender)` builds a step-mode `AgentSystem`
  for serverless platforms; `new_local(stores, model)` builds a `LocalAgentSystem`
  with an internal in-process queue (`ChannelSender`).
- Step mode's public surface is `AgentSystem::step(inputs, observer, defer)`: the
  batch may span multiple threads (SQS FIFO batches can interleave message groups);
  `step` partitions by `group_id`, applies the deferral policy per thread, joins the
  per-thread slices concurrently (prepare → completion → history sync → tool
  dispatch), and returns each thread's `StepOutcome`. `&mut self` serializes calls
  per system; the internal `Thread` type is not public and `AgentSystem` is not
  `Clone`, so two live in-memory views of one thread cannot exist through the
  public API. Per-thread configuration resolves lazily on first step so loading a
  thread never boots tool servers.
- `LocalAgentSystem` is mode-typed (`Handles` by default, `Launcher` after
  `with_thread_launcher()`), with one `start()` per mode:
  - Handles mode: `start()` returns a `RunningSystem` running an actor-system-style
    runtime (router + one driver per active thread, input batching, `<interrupt>`
    handling, deferral while a non-passive tool call is pending, auto-compaction
    above 75% of the context window, race-free idle exit/respawn) with ack'd
    `subscribe`, a `thread_exits` channel, and `thread_handle(id)` attaching to any
    thread. `start_with_observer(factory)` swaps in a custom `ThreadObserver`.
  - Launcher mode: `start()` returns a `LaunchingSystem`. `thread_builder()`
    configures and launches new threads (generated IDs) with their own tools,
    prompt, and optionally their own `ModelSource`, unioned onto the system-wide
    configuration; launch configurations attach to the launched (root) thread, so
    spawned subagent threads inherit tools, prompt, and model via root resolution.
    `thread_handle(id)` re-attaches to existing threads only (`None` means the
    thread does not exist). Launch configurations are process-lifetime.
- Shutdown is ownership-based: `RunningSystem::shutdown(self)` (and
  `LaunchingSystem::shutdown(self)`) cancels every driver, flushes in-flight turns,
  and awaits the router. Because shutting down consumes the system, `&self` methods
  are infallible: `thread_handle`/`launch` return handles directly, `subscribe`
  resolves once installed, and `send`/`send_user_text` return `()`. Independent
  handles that can outlive the system (`SubscribeHandle`, `ThreadHandle`) keep
  fallible signatures.
- `ThreadHandle`: `replay()` plus an exactly-once `events` stream and input sending;
  subscriptions survive driver respawns via a shared registry.
- `ThreadObserver`: synchronous `on_event(AgentEvent)` fan-out (exactly-once relative
  to `on_subscribe` replays) plus awaited user-choice durability hooks
  (`on_user_choice_required`/`_dismissed`). Tool dispatch happens strictly after
  history sync, so turns are durable before their effects are observable.
  `EventCollector` buffers `(thread_id, event)` pairs.
- `ModelSource` (per-round resolution; enables mid-session model switching),
  `ThreadConfigSource` (per-thread tools/prompt/notifier; enables per-session RAP
  servers and toolsets), `DeferQueue` (`InMemoryDeferQueue`/`NoDeferral`),
  `Tool::is_passive()`, tokio sleep tools (`with_tokio_sleep_tools`).
- `batch_processor` deleted: `process_batch` had no production callers and its
  semantics violated the documented durability barrier (swallowed sync errors, then
  dispatched; emitted ResponseDone before sync). The documented low-level API is now
  `HistoryManager` + `prepare_input`/`run_completion`/`execute_action`;
  `execute_action_with_error_result` (the #88 error-fallback) lives in
  `event_processor`. `DisplayEvent` moved out of core: `AgentEvent` is core's only
  event vocabulary; the CLI keeps a local, non-generic `DisplayEvent` as its
  terminal-rendering enum. All unique regression coverage ported.
- Shared slice internals: `event_processor::input_echo` is the single computation of
  accepted-input echoes; `SyntheticKind::is_compaction_complete()`;
  `InputMessage::user_text()`; driver's in-flight step modeled as
  `InFlightStep { fut, cancel_tx }` with a consuming `cancel()`.
- `rap_callback` module: the RAP callback → `InputMessage` conversion (multimodal
  content, display segments, tagged synthetics incl. subscription final/associative
  flags, user_choice, oauth) lives in core with 11 unit tests; the daemon delegates.
- In-memory `ConversationStore`/`StateStore` in `infinity_agent_core::stores` with a
  serializable snapshot/restore API; the daemon's stores are thin wrappers (extras +
  JSON persistence) so the ancestor/compaction/dedup semantics exist in exactly one
  place. `spawn_thread_with_id` supports deterministic IDs.
- Platform-neutral core: `input_queue_arn` removed from `ToolContext`/builder.
  Unused refactor leftovers pruned: `ToolSet`/`VecToolSet`,
  `ToolsConfig::{add_command, toolset_commands, mcp_servers, http_mcp_servers}`,
  `HistoryManager::state_store()`.
- All 20 daemon agent-loop tests migrated into core `system/tests.rs` with
  byte-identical snapshots, plus new coverage: launcher tool/prompt/model unioning
  and child inheritance, existing-only attachment, `ThreadHandle` streaming across
  tool-call rounds, multi-group step batches, failed-dispatch fallback, OAuth
  surfacing, prepare-level user-choice, handle respawn survival and pruning.

Daemon (`infinity-daemon`):
- One daemon-lifetime agent system replaces per-session agent loops and the
  restart-on-send path (`needs_restart` → `start_session`): idle threads respawn a
  driver transparently instead. Sessions are root threads; a `thread_exits` watcher
  marks sessions idle and shuts down their RAP servers when no keep-alive client is
  attached.
- `rap_servers.rs`: RAP server lifecycle moves out of session start — boot happens
  lazily when a thread's config first resolves (loading/replaying a thread never
  boots servers), shutdown is driven by session idleness, and a shut-down server
  reboots on the next tool interaction. `Up` caches the manifest (fetch failure
  tears the boot down); MCP proxy tasks are owned (stdio children `kill_on_drop`)
  but proxies stay up across idles since MCP servers may be stateful; migration
  flows ride the same path (`MigrationServer`; `boot_rap_servers`/`rap_tools.rs`
  deleted); config merging unified in `config::load_merged_rap_config`.
- `DaemonObserver` implements the observer hooks; broadcasting unified in
  `observer::broadcast_pruning`/`broadcast_to_thread` (fixing `handle_view_update`
  never pruning dead subscribers); re-sent Connects replace rather than stack
  subscriptions; replay rendering matches live rendering.
- `CatalogModelSource` resolves each thread's persisted model per round; fixed the
  pre-existing double `ModelSwitched` delivery.
- `send_input` takes `user_driven: bool`; interior-mutability methods take `&self`.
- Web goldens `chat-image-result`/`chat-diff-result` regenerated (old goldens
  captured an eager-boot layout artifact).
- Deleted: `session/thread_worker.rs`, `session/agent_loop.rs`, `sleep_tools.rs`,
  `InMemoryMessageSender`, `Session`, `spawn_session`, `AgentMessage`.

Lambda (`infinity-agent-lambda`):
- Event handler rewritten on the new API: one system per invocation with a
  `ThreadConfigSource` resolving each thread's RAP toolsets (DynamoDB manifest
  cache) plus platform sleep tools (shared `WakeupScheduler` backend); one
  batch-shaped `step`; per-thread outputs aggregated in a `BTreeMap`.
- New `rap-receiver` binary replaces the drifted JS receiver (deleted): same
  Function-URL contract, full parity with the daemon's conversion via
  `infinity_agent_core::rap_callback` (user_choice accepted, multimodal and
  `display_as` preserved, tagged synthetics honored, stable dedup IDs where the
  wire has identity). CDK switches to a cargo-lambda `RustFunction`; the Function
  URL is recreated on deploy, so persisted callback URLs must be re-subscribed.

Docs (`docs/docs/infinity-runtime`): the single "Rust API" page is replaced by two
sections, written in the Hydro docs register (definitional openings, bolded key
terms, cause-and-effect guarantees, code-first examples, admonitions for caveats):
"The Agent System API" (overview, building-a-system, running-locally, step-mode,
observers, including launcher mode) and "The Low-Level API" (overview,
history-manager, completion-loop). Cross-references updated repo-wide; `cargo doc`
is warning-free.

BREAKING CHANGE: `infinity-agent-core` removes `batch_processor` (`process_batch`,
`process_input_item`, `DisplayEvent`); `Thread` is no longer public (use
`AgentSystem::step` or `ThreadHandle`); `AgentSystem` is not `Clone`;
`LocalAgentSystem::start_with_handles` is now `start()` and custom observers use
`start_with_observer` (`LocalAgentSystem` gained a mode type parameter, default
`Handles`); the resident-runtime types (`RunningSystem`, `SubscribeHandle`,
`ThreadHandle`, `HandleObserver`, `HandleSubscribeRequest`, `LaunchingSystem`,
`ThreadBuilder`, `ChannelSender`, `ChannelSendError`, `ActiveThreads`) live under
`system::local`; `RunningSystem::begin_shutdown`/`task` are replaced by owned
`shutdown(self)`, and `RunningSystem::{send, send_user_text, subscribe,
thread_handle}` are infallible; `ThreadObserver` has no `on_commit` (persist in
`on_event` or externally; the turn is already durable before dispatch);
`EventCollector::take` returns `(thread_id, event)` pairs; builder tools are stored
as `Rc<dyn Tool<M>>`; `Tool` gains defaulted `is_passive`; `ToolContext`/builder
lose `input_queue_arn`; `ToolSet`/`VecToolSet` and `ToolsConfig::{add_command,
toolset_commands, mcp_servers, http_mcp_servers}` are removed. `infinity-daemon` no
longer exports `sleep_tools`, `AgentMessage`, `thread_worker`, `agent_loop`,
`Session`, `InMemoryMessageSender`, `boot_rap_servers`, or `rap_tools`;
`SessionManager::switch_model` takes the requester's sender and returns
`Result<(), String>`; `send_input` takes `user_driven: bool`;
`SharedSessionManager` is `Rc<tokio::Mutex<...>>`. The CLI's `DisplayEvent` lives in
`infinity_agent_cli::display`.

Co-authored-by: Infinity 🤖 <infinity@hydro.run>
PR: #92
shadaj added a commit that referenced this pull request Aug 12, 2026
…on + lambda onto it

Introduce `infinity_agent_core::system`, a builder-based, object-oriented API for
running agents, rebuild both production embeddings on top of it, unify the parallel
implementations that had grown around the old architecture, and rewrite the
`infinity-runtime` docs around the new API.

Core (`infinity_agent_core::system`, resident runtime under `system::local`):
- `AgentSystemBuilder`: `new(stores, model, sender)` builds a step-mode `AgentSystem`
  for serverless platforms; `new_local(stores, model)` builds a `LocalAgentSystem`
  with an internal in-process queue (`ChannelSender`).
- Step mode's public surface is `AgentSystem::step(inputs, observer, defer)`: the
  batch may span multiple threads (SQS FIFO batches can interleave message groups);
  `step` partitions by `group_id`, applies the deferral policy per thread, joins the
  per-thread slices concurrently (prepare → completion → history sync → tool
  dispatch), and returns each thread's `StepOutcome`. `&mut self` serializes calls
  per system; the internal `Thread` type is not public and `AgentSystem` is not
  `Clone`, so two live in-memory views of one thread cannot exist through the
  public API. Per-thread configuration resolves lazily on first step so loading a
  thread never boots tool servers.
- `LocalAgentSystem` is mode-typed (`Handles` by default, `Launcher` after
  `with_thread_launcher()`), with one `start()` per mode:
  - Handles mode: `start()` returns a `RunningSystem` running an actor-system-style
    runtime (router + one driver per active thread, input batching, `<interrupt>`
    handling, deferral while a non-passive tool call is pending, auto-compaction
    above 75% of the context window, race-free idle exit/respawn) with ack'd
    `subscribe`, a `thread_exits` channel, and `thread_handle(id)` attaching to any
    thread. `start_with_observer(factory)` swaps in a custom `ThreadObserver`.
  - Launcher mode: `start()` returns a `LaunchingSystem`. `thread_builder()`
    configures and launches new threads (generated IDs) with their own tools,
    prompt, and optionally their own `ModelSource`, unioned onto the system-wide
    configuration; launch configurations attach to the launched (root) thread, so
    spawned subagent threads inherit tools, prompt, and model via root resolution.
    `thread_handle(id)` re-attaches to existing threads only (`None` means the
    thread does not exist). Launch configurations are process-lifetime.
- Shutdown is ownership-based: `RunningSystem::shutdown(self)` (and
  `LaunchingSystem::shutdown(self)`) cancels every driver, flushes in-flight turns,
  and awaits the router. Because shutting down consumes the system, `&self` methods
  are infallible: `thread_handle`/`launch` return handles directly, `subscribe`
  resolves once installed, and `send`/`send_user_text` return `()`. Independent
  handles that can outlive the system (`SubscribeHandle`, `ThreadHandle`) keep
  fallible signatures.
- `ThreadHandle`: `replay()` plus an exactly-once `events` stream and input sending;
  subscriptions survive driver respawns via a shared registry.
- `ThreadObserver`: synchronous `on_event(AgentEvent)` fan-out (exactly-once relative
  to `on_subscribe` replays) plus awaited user-choice durability hooks
  (`on_user_choice_required`/`_dismissed`). Tool dispatch happens strictly after
  history sync, so turns are durable before their effects are observable.
  `EventCollector` buffers `(thread_id, event)` pairs.
- `ModelSource` (per-round resolution; enables mid-session model switching),
  `ThreadConfigSource` (per-thread tools/prompt/notifier; enables per-session RAP
  servers and toolsets), `DeferQueue` (`InMemoryDeferQueue`/`NoDeferral`),
  `Tool::is_passive()`, tokio sleep tools (`with_tokio_sleep_tools`).
- `batch_processor` deleted: `process_batch` had no production callers and its
  semantics violated the documented durability barrier (swallowed sync errors, then
  dispatched; emitted ResponseDone before sync). The documented low-level API is now
  `HistoryManager` + `prepare_input`/`run_completion`/`execute_action`;
  `execute_action_with_error_result` (the #88 error-fallback) lives in
  `event_processor`. `DisplayEvent` moved out of core: `AgentEvent` is core's only
  event vocabulary; the CLI keeps a local, non-generic `DisplayEvent` as its
  terminal-rendering enum. All unique regression coverage ported.
- Shared slice internals: `event_processor::input_echo` is the single computation of
  accepted-input echoes; `SyntheticKind::is_compaction_complete()`;
  `InputMessage::user_text()`; driver's in-flight step modeled as
  `InFlightStep { fut, cancel_tx }` with a consuming `cancel()`.
- `rap_callback` module: the RAP callback → `InputMessage` conversion (multimodal
  content, display segments, tagged synthetics incl. subscription final/associative
  flags, user_choice, oauth) lives in core with 11 unit tests; the daemon delegates.
- In-memory `ConversationStore`/`StateStore` in `infinity_agent_core::stores` with a
  serializable snapshot/restore API; the daemon's stores are thin wrappers (extras +
  JSON persistence) so the ancestor/compaction/dedup semantics exist in exactly one
  place. `spawn_thread_with_id` supports deterministic IDs.
- Platform-neutral core: `input_queue_arn` removed from `ToolContext`/builder.
  Unused refactor leftovers pruned: `ToolSet`/`VecToolSet`,
  `ToolsConfig::{add_command, toolset_commands, mcp_servers, http_mcp_servers}`,
  `HistoryManager::state_store()`.
- All 20 daemon agent-loop tests migrated into core `system/tests.rs` with
  byte-identical snapshots, plus new coverage: launcher tool/prompt/model unioning
  and child inheritance, existing-only attachment, `ThreadHandle` streaming across
  tool-call rounds, multi-group step batches, failed-dispatch fallback, OAuth
  surfacing, prepare-level user-choice, handle respawn survival and pruning.

Daemon (`infinity-daemon`):
- One daemon-lifetime agent system replaces per-session agent loops and the
  restart-on-send path (`needs_restart` → `start_session`): idle threads respawn a
  driver transparently instead. Sessions are root threads; a `thread_exits` watcher
  marks sessions idle and shuts down their RAP servers when no keep-alive client is
  attached.
- `rap_servers.rs`: RAP server lifecycle moves out of session start — boot happens
  lazily when a thread's config first resolves (loading/replaying a thread never
  boots servers), shutdown is driven by session idleness, and a shut-down server
  reboots on the next tool interaction. `Up` caches the manifest (fetch failure
  tears the boot down); MCP proxy tasks are owned (stdio children `kill_on_drop`)
  but proxies stay up across idles since MCP servers may be stateful; migration
  flows ride the same path (`MigrationServer`; `boot_rap_servers`/`rap_tools.rs`
  deleted); config merging unified in `config::load_merged_rap_config`.
- `DaemonObserver` implements the observer hooks; broadcasting unified in
  `observer::broadcast_pruning`/`broadcast_to_thread` (fixing `handle_view_update`
  never pruning dead subscribers); re-sent Connects replace rather than stack
  subscriptions; replay rendering matches live rendering.
- `CatalogModelSource` resolves each thread's persisted model per round; fixed the
  pre-existing double `ModelSwitched` delivery.
- `send_input` takes `user_driven: bool`; interior-mutability methods take `&self`.
- Web goldens `chat-image-result`/`chat-diff-result` regenerated (old goldens
  captured an eager-boot layout artifact).
- Deleted: `session/thread_worker.rs`, `session/agent_loop.rs`, `sleep_tools.rs`,
  `InMemoryMessageSender`, `Session`, `spawn_session`, `AgentMessage`.

Lambda (`infinity-agent-lambda`):
- Event handler rewritten on the new API: one system per invocation with a
  `ThreadConfigSource` resolving each thread's RAP toolsets (DynamoDB manifest
  cache) plus platform sleep tools (shared `WakeupScheduler` backend); one
  batch-shaped `step`; per-thread outputs aggregated in a `BTreeMap`.
- New `rap-receiver` binary replaces the drifted JS receiver (deleted): same
  Function-URL contract, full parity with the daemon's conversion via
  `infinity_agent_core::rap_callback` (user_choice accepted, multimodal and
  `display_as` preserved, tagged synthetics honored, stable dedup IDs where the
  wire has identity). CDK switches to a cargo-lambda `RustFunction`; the Function
  URL is recreated on deploy, so persisted callback URLs must be re-subscribed.

Docs (`docs/docs/infinity-runtime`): the single "Rust API" page is replaced by two
sections, written in the Hydro docs register (definitional openings, bolded key
terms, cause-and-effect guarantees, code-first examples, admonitions for caveats):
"The Agent System API" (overview, building-a-system, running-locally, step-mode,
observers, including launcher mode) and "The Low-Level API" (overview,
history-manager, completion-loop). Cross-references updated repo-wide; `cargo doc`
is warning-free.

BREAKING CHANGE: `infinity-agent-core` removes `batch_processor` (`process_batch`,
`process_input_item`, `DisplayEvent`); `Thread` is no longer public (use
`AgentSystem::step` or `ThreadHandle`); `AgentSystem` is not `Clone`;
`LocalAgentSystem::start_with_handles` is now `start()` and custom observers use
`start_with_observer` (`LocalAgentSystem` gained a mode type parameter, default
`Handles`); the resident-runtime types (`RunningSystem`, `SubscribeHandle`,
`ThreadHandle`, `HandleObserver`, `HandleSubscribeRequest`, `LaunchingSystem`,
`ThreadBuilder`, `ChannelSender`, `ChannelSendError`, `ActiveThreads`) live under
`system::local`; `RunningSystem::begin_shutdown`/`task` are replaced by owned
`shutdown(self)`, and `RunningSystem::{send, send_user_text, subscribe,
thread_handle}` are infallible; `ThreadObserver` has no `on_commit` (persist in
`on_event` or externally; the turn is already durable before dispatch);
`EventCollector::take` returns `(thread_id, event)` pairs; builder tools are stored
as `Rc<dyn Tool<M>>`; `Tool` gains defaulted `is_passive`; `ToolContext`/builder
lose `input_queue_arn`; `ToolSet`/`VecToolSet` and `ToolsConfig::{add_command,
toolset_commands, mcp_servers, http_mcp_servers}` are removed. `infinity-daemon` no
longer exports `sleep_tools`, `AgentMessage`, `thread_worker`, `agent_loop`,
`Session`, `InMemoryMessageSender`, `boot_rap_servers`, or `rap_tools`;
`SessionManager::switch_model` takes the requester's sender and returns
`Result<(), String>`; `send_input` takes `user_driven: bool`;
`SharedSessionManager` is `Rc<tokio::Mutex<...>>`. The CLI's `DisplayEvent` lives in
`infinity_agent_cli::display`.

Co-authored-by: Infinity 🤖 <infinity@hydro.run>
PR: #92
shadaj added a commit that referenced this pull request Aug 12, 2026
…on + lambda onto it

Introduce `infinity_agent_core::system`, a builder-based, object-oriented API for
running agents, rebuild both production embeddings on top of it, unify the parallel
implementations that had grown around the old architecture, and rewrite the
`infinity-runtime` docs around the new API.

Core (`infinity_agent_core::system`, resident runtime under `system::local`):
- `AgentSystemBuilder`: `new(stores, model, sender)` builds a step-mode `AgentSystem`
  for serverless platforms; `new_local(stores, model)` builds a `LocalAgentSystem`
  with an internal in-process queue (`ChannelSender`).
- Step mode's public surface is `AgentSystem::step(inputs, observer, defer)`: the
  batch may span multiple threads (SQS FIFO batches can interleave message groups);
  `step` partitions by `group_id`, applies the deferral policy per thread, joins the
  per-thread slices concurrently (prepare → completion → history sync → tool
  dispatch), and returns each thread's `StepOutcome`. `&mut self` serializes calls
  per system; the internal `Thread` type is not public and `AgentSystem` is not
  `Clone`, so two live in-memory views of one thread cannot exist through the
  public API. Per-thread configuration resolves lazily on first step so loading a
  thread never boots tool servers.
- `LocalAgentSystem` is mode-typed (`Handles` by default, `Launcher` after
  `with_thread_launcher()`), with one `start()` per mode:
  - Handles mode: `start()` returns a `RunningSystem` running an actor-system-style
    runtime (router + one driver per active thread, input batching, `<interrupt>`
    handling, deferral while a non-passive tool call is pending, auto-compaction
    above 75% of the context window, race-free idle exit/respawn) with ack'd
    `subscribe`, a `thread_exits` channel, and `thread_handle(id)` attaching to any
    thread. `start_with_observer(factory)` swaps in a custom `ThreadObserver`.
  - Launcher mode: `start()` returns a `LaunchingSystem`. `thread_builder()`
    configures and launches new threads (generated IDs) with their own tools,
    prompt, and optionally their own `ModelSource`, unioned onto the system-wide
    configuration; launch configurations attach to the launched (root) thread, so
    spawned subagent threads inherit tools, prompt, and model via root resolution.
    `thread_handle(id)` re-attaches to existing threads only (`None` means the
    thread does not exist). Launch configurations are process-lifetime.
- Shutdown is ownership-based: `RunningSystem::shutdown(self)` (and
  `LaunchingSystem::shutdown(self)`) cancels every driver, flushes in-flight turns,
  and awaits the router. Because shutting down consumes the system, `&self` methods
  are infallible: `thread_handle`/`launch` return handles directly, `subscribe`
  resolves once installed, and `send`/`send_user_text` return `()`. Independent
  handles that can outlive the system (`SubscribeHandle`, `ThreadHandle`) keep
  fallible signatures.
- `ThreadHandle`: `replay()` plus an exactly-once `events` stream and input sending;
  subscriptions survive driver respawns via a shared registry.
- `ThreadObserver`: synchronous `on_event(AgentEvent)` fan-out (exactly-once relative
  to `on_subscribe` replays) plus awaited user-choice durability hooks
  (`on_user_choice_required`/`_dismissed`). Tool dispatch happens strictly after
  history sync, so turns are durable before their effects are observable.
  `EventCollector` buffers `(thread_id, event)` pairs.
- `ModelSource` (per-round resolution; enables mid-session model switching),
  `ThreadConfigSource` (per-thread tools/prompt/notifier; enables per-session RAP
  servers and toolsets), `DeferQueue` (`InMemoryDeferQueue`/`NoDeferral`),
  `Tool::is_passive()`, tokio sleep tools (`with_tokio_sleep_tools`).
- `batch_processor` deleted: `process_batch` had no production callers and its
  semantics violated the documented durability barrier (swallowed sync errors, then
  dispatched; emitted ResponseDone before sync). The documented low-level API is now
  `HistoryManager` + `prepare_input`/`run_completion`/`execute_action`;
  `execute_action_with_error_result` (the #88 error-fallback) lives in
  `event_processor`. `DisplayEvent` moved out of core: `AgentEvent` is core's only
  event vocabulary; the CLI keeps a local, non-generic `DisplayEvent` as its
  terminal-rendering enum. All unique regression coverage ported.
- Shared slice internals: `event_processor::input_echo` is the single computation of
  accepted-input echoes; `SyntheticKind::is_compaction_complete()`;
  `InputMessage::user_text()`; driver's in-flight step modeled as
  `InFlightStep { fut, cancel_tx }` with a consuming `cancel()`.
- `rap_callback` module: the RAP callback → `InputMessage` conversion (multimodal
  content, display segments, tagged synthetics incl. subscription final/associative
  flags, user_choice, oauth) lives in core with 11 unit tests; the daemon delegates.
- In-memory `ConversationStore`/`StateStore` in `infinity_agent_core::stores` with a
  serializable snapshot/restore API; the daemon's stores are thin wrappers (extras +
  JSON persistence) so the ancestor/compaction/dedup semantics exist in exactly one
  place. `spawn_thread_with_id` supports deterministic IDs.
- Platform-neutral core: `input_queue_arn` removed from `ToolContext`/builder.
  Unused refactor leftovers pruned: `ToolSet`/`VecToolSet`,
  `ToolsConfig::{add_command, toolset_commands, mcp_servers, http_mcp_servers}`,
  `HistoryManager::state_store()`.
- All 20 daemon agent-loop tests migrated into core `system/tests.rs` with
  byte-identical snapshots, plus new coverage: launcher tool/prompt/model unioning
  and child inheritance, existing-only attachment, `ThreadHandle` streaming across
  tool-call rounds, multi-group step batches, failed-dispatch fallback, OAuth
  surfacing, prepare-level user-choice, handle respawn survival and pruning.

Daemon (`infinity-daemon`):
- One daemon-lifetime agent system replaces per-session agent loops and the
  restart-on-send path (`needs_restart` → `start_session`): idle threads respawn a
  driver transparently instead. Sessions are root threads; a `thread_exits` watcher
  marks sessions idle and shuts down their RAP servers when no keep-alive client is
  attached.
- `rap_servers.rs`: RAP server lifecycle moves out of session start — boot happens
  lazily when a thread's config first resolves (loading/replaying a thread never
  boots servers), shutdown is driven by session idleness, and a shut-down server
  reboots on the next tool interaction. `Up` caches the manifest (fetch failure
  tears the boot down); MCP proxy tasks are owned (stdio children `kill_on_drop`)
  but proxies stay up across idles since MCP servers may be stateful; migration
  flows ride the same path (`MigrationServer`; `boot_rap_servers`/`rap_tools.rs`
  deleted); config merging unified in `config::load_merged_rap_config`.
- `DaemonObserver` implements the observer hooks; broadcasting unified in
  `observer::broadcast_pruning`/`broadcast_to_thread` (fixing `handle_view_update`
  never pruning dead subscribers); re-sent Connects replace rather than stack
  subscriptions; replay rendering matches live rendering.
- `CatalogModelSource` resolves each thread's persisted model per round; fixed the
  pre-existing double `ModelSwitched` delivery.
- `send_input` takes `user_driven: bool`; interior-mutability methods take `&self`.
- Web goldens `chat-image-result`/`chat-diff-result` regenerated (old goldens
  captured an eager-boot layout artifact).
- Deleted: `session/thread_worker.rs`, `session/agent_loop.rs`, `sleep_tools.rs`,
  `InMemoryMessageSender`, `Session`, `spawn_session`, `AgentMessage`.

Lambda (`infinity-agent-lambda`):
- Event handler rewritten on the new API: one system per invocation with a
  `ThreadConfigSource` resolving each thread's RAP toolsets (DynamoDB manifest
  cache) plus platform sleep tools (shared `WakeupScheduler` backend); one
  batch-shaped `step`; per-thread outputs aggregated in a `BTreeMap`.
- New `rap-receiver` binary replaces the drifted JS receiver (deleted): same
  Function-URL contract, full parity with the daemon's conversion via
  `infinity_agent_core::rap_callback` (user_choice accepted, multimodal and
  `display_as` preserved, tagged synthetics honored, stable dedup IDs where the
  wire has identity). CDK switches to a cargo-lambda `RustFunction`; the Function
  URL is recreated on deploy, so persisted callback URLs must be re-subscribed.

Docs (`docs/docs/infinity-runtime`): the single "Rust API" page is replaced by two
sections, written in the Hydro docs register (definitional openings, bolded key
terms, cause-and-effect guarantees, code-first examples, admonitions for caveats):
"The Agent System API" (overview, building-a-system, running-locally, step-mode,
observers, including launcher mode) and "The Low-Level API" (overview,
history-manager, completion-loop). Cross-references updated repo-wide; `cargo doc`
is warning-free.

BREAKING CHANGE: `infinity-agent-core` removes `batch_processor` (`process_batch`,
`process_input_item`, `DisplayEvent`); `Thread` is no longer public (use
`AgentSystem::step` or `ThreadHandle`); `AgentSystem` is not `Clone`;
`LocalAgentSystem::start_with_handles` is now `start()` and custom observers use
`start_with_observer` (`LocalAgentSystem` gained a mode type parameter, default
`Handles`); the resident-runtime types (`RunningSystem`, `SubscribeHandle`,
`ThreadHandle`, `HandleObserver`, `HandleSubscribeRequest`, `LaunchingSystem`,
`ThreadBuilder`, `ChannelSender`, `ChannelSendError`, `ActiveThreads`) live under
`system::local`; `RunningSystem::begin_shutdown`/`task` are replaced by owned
`shutdown(self)`, and `RunningSystem::{send, send_user_text, subscribe,
thread_handle}` are infallible; `ThreadObserver` has no `on_commit` (persist in
`on_event` or externally; the turn is already durable before dispatch);
`EventCollector::take` returns `(thread_id, event)` pairs; builder tools are stored
as `Rc<dyn Tool<M>>`; `Tool` gains defaulted `is_passive`; `ToolContext`/builder
lose `input_queue_arn`; `ToolSet`/`VecToolSet` and `ToolsConfig::{add_command,
toolset_commands, mcp_servers, http_mcp_servers}` are removed. `infinity-daemon` no
longer exports `sleep_tools`, `AgentMessage`, `thread_worker`, `agent_loop`,
`Session`, `InMemoryMessageSender`, `boot_rap_servers`, or `rap_tools`;
`SessionManager::switch_model` takes the requester's sender and returns
`Result<(), String>`; `send_input` takes `user_driven: bool`;
`SharedSessionManager` is `Rc<tokio::Mutex<...>>`. The CLI's `DisplayEvent` lives in
`infinity_agent_cli::display`.

Co-authored-by: Infinity 🤖 <infinity@hydro.run>
PR: #92
shadaj added a commit that referenced this pull request Aug 12, 2026
…on + lambda onto it

Introduce `infinity_agent_core::system`, a builder-based, object-oriented API for
running agents, rebuild both production embeddings on top of it, unify the parallel
implementations that had grown around the old architecture, and rewrite the
`infinity-runtime` docs around the new API.

Core (`infinity_agent_core::system`, resident runtime under `system::local`):
- `AgentSystemBuilder`: `new(stores, model, sender)` builds a step-mode `AgentSystem`
  for serverless platforms; `new_local(stores, model)` builds a `LocalAgentSystem`
  with an internal in-process queue (`ChannelSender`).
- Step mode's public surface is `AgentSystem::step(inputs, observer, defer)`: the
  batch may span multiple threads (SQS FIFO batches can interleave message groups);
  `step` partitions by `group_id`, applies the deferral policy per thread, joins the
  per-thread slices concurrently (prepare → completion → history sync → tool
  dispatch), and returns each thread's `StepOutcome`. `&mut self` serializes calls
  per system; the internal `Thread` type is not public and `AgentSystem` is not
  `Clone`, so two live in-memory views of one thread cannot exist through the
  public API. Per-thread configuration resolves lazily on first step so loading a
  thread never boots tool servers.
- `LocalAgentSystem` is mode-typed (`Handles` by default, `Launcher` after
  `with_thread_launcher()`), with one `start()` per mode:
  - Handles mode: `start()` returns a `RunningSystem` running an actor-system-style
    runtime (router + one driver per active thread, input batching, `<interrupt>`
    handling, deferral while a non-passive tool call is pending, auto-compaction
    above 75% of the context window, race-free idle exit/respawn) with ack'd
    `subscribe`, a `thread_exits` channel, and `thread_handle(id)` attaching to any
    thread. `start_with_observer(factory)` swaps in a custom `ThreadObserver`.
  - Launcher mode: `start()` returns a `LaunchingSystem`. `thread_builder()`
    configures and launches new threads (generated IDs) with their own tools,
    prompt, and optionally their own `ModelSource`, unioned onto the system-wide
    configuration; launch configurations attach to the launched (root) thread, so
    spawned subagent threads inherit tools, prompt, and model via root resolution.
    `thread_handle(id)` re-attaches to existing threads only (`None` means the
    thread does not exist). Launch configurations are process-lifetime.
- Shutdown is ownership-based: `RunningSystem::shutdown(self)` (and
  `LaunchingSystem::shutdown(self)`) cancels every driver, flushes in-flight turns,
  and awaits the router. Because shutting down consumes the system, `&self` methods
  are infallible: `thread_handle`/`launch` return handles directly, `subscribe`
  resolves once installed, and `send`/`send_user_text` return `()`. Independent
  handles that can outlive the system (`SubscribeHandle`, `ThreadHandle`) keep
  fallible signatures.
- `ThreadHandle`: `replay()` plus an exactly-once `events` stream and input sending;
  subscriptions survive driver respawns via a shared registry.
- `ThreadObserver`: synchronous `on_event(AgentEvent)` fan-out (exactly-once relative
  to `on_subscribe` replays) plus awaited user-choice durability hooks
  (`on_user_choice_required`/`_dismissed`). Tool dispatch happens strictly after
  history sync, so turns are durable before their effects are observable.
  `EventCollector` buffers `(thread_id, event)` pairs.
- `ModelSource` (per-round resolution; enables mid-session model switching),
  `ThreadConfigSource` (per-thread tools/prompt/notifier; enables per-session RAP
  servers and toolsets), `DeferQueue` (`InMemoryDeferQueue`/`NoDeferral`),
  `Tool::is_passive()`, tokio sleep tools (`with_tokio_sleep_tools`).
- `batch_processor` deleted: `process_batch` had no production callers and its
  semantics violated the documented durability barrier (swallowed sync errors, then
  dispatched; emitted ResponseDone before sync). The documented low-level API is now
  `HistoryManager` + `prepare_input`/`run_completion`/`execute_action`;
  `execute_action_with_error_result` (the #88 error-fallback) lives in
  `event_processor`. `DisplayEvent` moved out of core: `AgentEvent` is core's only
  event vocabulary; the CLI keeps a local, non-generic `DisplayEvent` as its
  terminal-rendering enum. All unique regression coverage ported.
- Shared slice internals: `event_processor::input_echo` is the single computation of
  accepted-input echoes; `SyntheticKind::is_compaction_complete()`;
  `InputMessage::user_text()`; driver's in-flight step modeled as
  `InFlightStep { fut, cancel_tx }` with a consuming `cancel()`.
- `rap_callback` module: the RAP callback → `InputMessage` conversion (multimodal
  content, display segments, tagged synthetics incl. subscription final/associative
  flags, user_choice, oauth) lives in core with 11 unit tests; the daemon delegates.
- In-memory `ConversationStore`/`StateStore` in `infinity_agent_core::stores` with a
  serializable snapshot/restore API; the daemon's stores are thin wrappers (extras +
  JSON persistence) so the ancestor/compaction/dedup semantics exist in exactly one
  place. `spawn_thread_with_id` supports deterministic IDs.
- Platform-neutral core: `input_queue_arn` removed from `ToolContext`/builder.
  Unused refactor leftovers pruned: `ToolSet`/`VecToolSet`,
  `ToolsConfig::{add_command, toolset_commands, mcp_servers, http_mcp_servers}`,
  `HistoryManager::state_store()`.
- All 20 daemon agent-loop tests migrated into core `system/tests.rs` with
  byte-identical snapshots, plus new coverage: launcher tool/prompt/model unioning
  and child inheritance, existing-only attachment, `ThreadHandle` streaming across
  tool-call rounds, multi-group step batches, failed-dispatch fallback, OAuth
  surfacing, prepare-level user-choice, handle respawn survival and pruning.

Daemon (`infinity-daemon`):
- One daemon-lifetime agent system replaces per-session agent loops and the
  restart-on-send path (`needs_restart` → `start_session`): idle threads respawn a
  driver transparently instead. Sessions are root threads; a `thread_exits` watcher
  marks sessions idle and shuts down their RAP servers when no keep-alive client is
  attached.
- `rap_servers.rs`: RAP server lifecycle moves out of session start — boot happens
  lazily when a thread's config first resolves (loading/replaying a thread never
  boots servers), shutdown is driven by session idleness, and a shut-down server
  reboots on the next tool interaction. `Up` caches the manifest (fetch failure
  tears the boot down); MCP proxy tasks are owned (stdio children `kill_on_drop`)
  but proxies stay up across idles since MCP servers may be stateful; migration
  flows ride the same path (`MigrationServer`; `boot_rap_servers`/`rap_tools.rs`
  deleted); config merging unified in `config::load_merged_rap_config`.
- `DaemonObserver` implements the observer hooks; broadcasting unified in
  `observer::broadcast_pruning`/`broadcast_to_thread` (fixing `handle_view_update`
  never pruning dead subscribers); re-sent Connects replace rather than stack
  subscriptions; replay rendering matches live rendering.
- `CatalogModelSource` resolves each thread's persisted model per round; fixed the
  pre-existing double `ModelSwitched` delivery.
- `send_input` takes `user_driven: bool`; interior-mutability methods take `&self`.
- Web goldens `chat-image-result`/`chat-diff-result` regenerated (old goldens
  captured an eager-boot layout artifact).
- Deleted: `session/thread_worker.rs`, `session/agent_loop.rs`, `sleep_tools.rs`,
  `InMemoryMessageSender`, `Session`, `spawn_session`, `AgentMessage`.

Lambda (`infinity-agent-lambda`):
- Event handler rewritten on the new API: one system per invocation with a
  `ThreadConfigSource` resolving each thread's RAP toolsets (DynamoDB manifest
  cache) plus platform sleep tools (shared `WakeupScheduler` backend); one
  batch-shaped `step`; per-thread outputs aggregated in a `BTreeMap`.
- New `rap-receiver` binary replaces the drifted JS receiver (deleted): same
  Function-URL contract, full parity with the daemon's conversion via
  `infinity_agent_core::rap_callback` (user_choice accepted, multimodal and
  `display_as` preserved, tagged synthetics honored, stable dedup IDs where the
  wire has identity). CDK switches to a cargo-lambda `RustFunction`; the Function
  URL is recreated on deploy, so persisted callback URLs must be re-subscribed.

Docs (`docs/docs/infinity-runtime`): the single "Rust API" page is replaced by two
sections, written in the Hydro docs register (definitional openings, bolded key
terms, cause-and-effect guarantees, code-first examples, admonitions for caveats):
"The Agent System API" (overview, building-a-system, running-locally, step-mode,
observers, including launcher mode) and "The Low-Level API" (overview,
history-manager, completion-loop). Cross-references updated repo-wide; `cargo doc`
is warning-free.

BREAKING CHANGE: `infinity-agent-core` removes `batch_processor` (`process_batch`,
`process_input_item`, `DisplayEvent`); `Thread` is no longer public (use
`AgentSystem::step` or `ThreadHandle`); `AgentSystem` is not `Clone`;
`LocalAgentSystem::start_with_handles` is now `start()` and custom observers use
`start_with_observer` (`LocalAgentSystem` gained a mode type parameter, default
`Handles`); the resident-runtime types (`RunningSystem`, `SubscribeHandle`,
`ThreadHandle`, `HandleObserver`, `HandleSubscribeRequest`, `LaunchingSystem`,
`ThreadBuilder`, `ChannelSender`, `ChannelSendError`, `ActiveThreads`) live under
`system::local`; `RunningSystem::begin_shutdown`/`task` are replaced by owned
`shutdown(self)`, and `RunningSystem::{send, send_user_text, subscribe,
thread_handle}` are infallible; `ThreadObserver` has no `on_commit` (persist in
`on_event` or externally; the turn is already durable before dispatch);
`EventCollector::take` returns `(thread_id, event)` pairs; builder tools are stored
as `Rc<dyn Tool<M>>`; `Tool` gains defaulted `is_passive`; `ToolContext`/builder
lose `input_queue_arn`; `ToolSet`/`VecToolSet` and `ToolsConfig::{add_command,
toolset_commands, mcp_servers, http_mcp_servers}` are removed. `infinity-daemon` no
longer exports `sleep_tools`, `AgentMessage`, `thread_worker`, `agent_loop`,
`Session`, `InMemoryMessageSender`, `boot_rap_servers`, or `rap_tools`;
`SessionManager::switch_model` takes the requester's sender and returns
`Result<(), String>`; `send_input` takes `user_driven: bool`;
`SharedSessionManager` is `Rc<tokio::Mutex<...>>`. The CLI's `DisplayEvent` lives in
`infinity_agent_cli::display`.

Co-authored-by: Infinity 🤖 <infinity@hydro.run>
PR: #92
shadaj added a commit that referenced this pull request Aug 12, 2026
…on + lambda onto it

Introduce `infinity_agent_core::system`, a builder-based, object-oriented API for
running agents, rebuild both production embeddings on top of it, unify the parallel
implementations that had grown around the old architecture, and rewrite the
`infinity-runtime` docs around the new API.

Core (`infinity_agent_core::system`, resident runtime under `system::local`):
- `AgentSystemBuilder`: `new(stores, model, sender)` builds a step-mode `AgentSystem`
  for serverless platforms; `new_local(stores, model)` builds a `LocalAgentSystem`
  with an internal in-process queue (`ChannelSender`).
- Step mode's public surface is `AgentSystem::step(inputs, observer, defer)`: the
  batch may span multiple threads (SQS FIFO batches can interleave message groups);
  `step` partitions by `group_id`, applies the deferral policy per thread, joins the
  per-thread slices concurrently (prepare → completion → history sync → tool
  dispatch), and returns each thread's `StepOutcome`. `&mut self` serializes calls
  per system; the internal `Thread` type is not public and `AgentSystem` is not
  `Clone`, so two live in-memory views of one thread cannot exist through the
  public API. Per-thread configuration resolves lazily on first step so loading a
  thread never boots tool servers.
- `LocalAgentSystem` is mode-typed (`Handles` by default, `Launcher` after
  `with_thread_launcher()`), with one `start()` per mode:
  - Handles mode: `start()` returns a `RunningSystem` running an actor-system-style
    runtime (router + one driver per active thread, input batching, `<interrupt>`
    handling, deferral while a non-passive tool call is pending, auto-compaction
    above 75% of the context window, race-free idle exit/respawn) with ack'd
    `subscribe`, a `thread_exits` channel, and `thread_handle(id)` attaching to any
    thread. `start_with_observer(factory)` swaps in a custom `ThreadObserver`.
  - Launcher mode: `start()` returns a `LaunchingSystem`. `thread_builder()`
    configures and launches new threads (generated IDs) with their own tools,
    prompt, and optionally their own `ModelSource`, unioned onto the system-wide
    configuration; launch configurations attach to the launched (root) thread, so
    spawned subagent threads inherit tools, prompt, and model via root resolution.
    `thread_handle(id)` re-attaches to existing threads only (`None` means the
    thread does not exist). Launch configurations are process-lifetime.
- Shutdown is ownership-based: `RunningSystem::shutdown(self)` (and
  `LaunchingSystem::shutdown(self)`) cancels every driver, flushes in-flight turns,
  and awaits the router. Because shutting down consumes the system, `&self` methods
  are infallible: `thread_handle`/`launch` return handles directly, `subscribe`
  resolves once installed, and `send`/`send_user_text` return `()`. Independent
  handles that can outlive the system (`SubscribeHandle`, `ThreadHandle`) keep
  fallible signatures.
- `ThreadHandle`: `replay()` plus an exactly-once `events` stream and input sending;
  subscriptions survive driver respawns via a shared registry.
- `ThreadObserver`: synchronous `on_event(AgentEvent)` fan-out (exactly-once relative
  to `on_subscribe` replays) plus awaited user-choice durability hooks
  (`on_user_choice_required`/`_dismissed`). Tool dispatch happens strictly after
  history sync, so turns are durable before their effects are observable.
  `EventCollector` buffers `(thread_id, event)` pairs.
- `ModelSource` (per-round resolution; enables mid-session model switching),
  `ThreadConfigSource` (per-thread tools/prompt/notifier; enables per-session RAP
  servers and toolsets), `DeferQueue` (`InMemoryDeferQueue`/`NoDeferral`),
  `Tool::is_passive()`, tokio sleep tools (`with_tokio_sleep_tools`).
- `batch_processor` deleted: `process_batch` had no production callers and its
  semantics violated the documented durability barrier (swallowed sync errors, then
  dispatched; emitted ResponseDone before sync). The documented low-level API is now
  `HistoryManager` + `prepare_input`/`run_completion`/`execute_action`;
  `execute_action_with_error_result` (the #88 error-fallback) lives in
  `event_processor`. `DisplayEvent` moved out of core: `AgentEvent` is core's only
  event vocabulary; the CLI keeps a local, non-generic `DisplayEvent` as its
  terminal-rendering enum. All unique regression coverage ported.
- Shared slice internals: `event_processor::input_echo` is the single computation of
  accepted-input echoes; `SyntheticKind::is_compaction_complete()`;
  `InputMessage::user_text()`; driver's in-flight step modeled as
  `InFlightStep { fut, cancel_tx }` with a consuming `cancel()`.
- `rap_callback` module: the RAP callback → `InputMessage` conversion (multimodal
  content, display segments, tagged synthetics incl. subscription final/associative
  flags, user_choice, oauth) lives in core with 11 unit tests; the daemon delegates.
- In-memory `ConversationStore`/`StateStore` in `infinity_agent_core::stores` with a
  serializable snapshot/restore API; the daemon's stores are thin wrappers (extras +
  JSON persistence) so the ancestor/compaction/dedup semantics exist in exactly one
  place. `spawn_thread_with_id` supports deterministic IDs.
- Platform-neutral core: `input_queue_arn` removed from `ToolContext`/builder.
  Unused refactor leftovers pruned: `ToolSet`/`VecToolSet`,
  `ToolsConfig::{add_command, toolset_commands, mcp_servers, http_mcp_servers}`,
  `HistoryManager::state_store()`.
- All 20 daemon agent-loop tests migrated into core `system/tests.rs` with
  byte-identical snapshots, plus new coverage: launcher tool/prompt/model unioning
  and child inheritance, existing-only attachment, `ThreadHandle` streaming across
  tool-call rounds, multi-group step batches, failed-dispatch fallback, OAuth
  surfacing, prepare-level user-choice, handle respawn survival and pruning.

Daemon (`infinity-daemon`):
- One daemon-lifetime agent system replaces per-session agent loops and the
  restart-on-send path (`needs_restart` → `start_session`): idle threads respawn a
  driver transparently instead. Sessions are root threads; a `thread_exits` watcher
  marks sessions idle and shuts down their RAP servers when no keep-alive client is
  attached.
- `rap_servers.rs`: RAP server lifecycle moves out of session start — boot happens
  lazily when a thread's config first resolves (loading/replaying a thread never
  boots servers), shutdown is driven by session idleness, and a shut-down server
  reboots on the next tool interaction. `Up` caches the manifest (fetch failure
  tears the boot down); MCP proxy tasks are owned (stdio children `kill_on_drop`)
  but proxies stay up across idles since MCP servers may be stateful; migration
  flows ride the same path (`MigrationServer`; `boot_rap_servers`/`rap_tools.rs`
  deleted); config merging unified in `config::load_merged_rap_config`.
- `DaemonObserver` implements the observer hooks; broadcasting unified in
  `observer::broadcast_pruning`/`broadcast_to_thread` (fixing `handle_view_update`
  never pruning dead subscribers); re-sent Connects replace rather than stack
  subscriptions; replay rendering matches live rendering.
- `CatalogModelSource` resolves each thread's persisted model per round; fixed the
  pre-existing double `ModelSwitched` delivery.
- `send_input` takes `user_driven: bool`; interior-mutability methods take `&self`.
- Web goldens `chat-image-result`/`chat-diff-result` regenerated (old goldens
  captured an eager-boot layout artifact).
- Deleted: `session/thread_worker.rs`, `session/agent_loop.rs`, `sleep_tools.rs`,
  `InMemoryMessageSender`, `Session`, `spawn_session`, `AgentMessage`.

Lambda (`infinity-agent-lambda`):
- Event handler rewritten on the new API: one system per invocation with a
  `ThreadConfigSource` resolving each thread's RAP toolsets (DynamoDB manifest
  cache) plus platform sleep tools (shared `WakeupScheduler` backend); one
  batch-shaped `step`; per-thread outputs aggregated in a `BTreeMap`.
- New `rap-receiver` binary replaces the drifted JS receiver (deleted): same
  Function-URL contract, full parity with the daemon's conversion via
  `infinity_agent_core::rap_callback` (user_choice accepted, multimodal and
  `display_as` preserved, tagged synthetics honored, stable dedup IDs where the
  wire has identity). CDK switches to a cargo-lambda `RustFunction`; the Function
  URL is recreated on deploy, so persisted callback URLs must be re-subscribed.

Docs (`docs/docs/infinity-runtime`): the single "Rust API" page is replaced by two
sections, written in the Hydro docs register (definitional openings, bolded key
terms, cause-and-effect guarantees, code-first examples, admonitions for caveats):
"The Agent System API" (overview, building-a-system, running-locally, step-mode,
observers, including launcher mode) and "The Low-Level API" (overview,
history-manager, completion-loop). Cross-references updated repo-wide; `cargo doc`
is warning-free.

BREAKING CHANGE: `infinity-agent-core` removes `batch_processor` (`process_batch`,
`process_input_item`, `DisplayEvent`); `Thread` is no longer public (use
`AgentSystem::step` or `ThreadHandle`); `AgentSystem` is not `Clone`;
`LocalAgentSystem::start_with_handles` is now `start()` and custom observers use
`start_with_observer` (`LocalAgentSystem` gained a mode type parameter, default
`Handles`); the resident-runtime types (`RunningSystem`, `SubscribeHandle`,
`ThreadHandle`, `HandleObserver`, `HandleSubscribeRequest`, `LaunchingSystem`,
`ThreadBuilder`, `ChannelSender`, `ChannelSendError`, `ActiveThreads`) live under
`system::local`; `RunningSystem::begin_shutdown`/`task` are replaced by owned
`shutdown(self)`, and `RunningSystem::{send, send_user_text, subscribe,
thread_handle}` are infallible; `ThreadObserver` has no `on_commit` (persist in
`on_event` or externally; the turn is already durable before dispatch);
`EventCollector::take` returns `(thread_id, event)` pairs; builder tools are stored
as `Rc<dyn Tool<M>>`; `Tool` gains defaulted `is_passive`; `ToolContext`/builder
lose `input_queue_arn`; `ToolSet`/`VecToolSet` and `ToolsConfig::{add_command,
toolset_commands, mcp_servers, http_mcp_servers}` are removed. `infinity-daemon` no
longer exports `sleep_tools`, `AgentMessage`, `thread_worker`, `agent_loop`,
`Session`, `InMemoryMessageSender`, `boot_rap_servers`, or `rap_tools`;
`SessionManager::switch_model` takes the requester's sender and returns
`Result<(), String>`; `send_input` takes `user_driven: bool`;
`SharedSessionManager` is `Rc<tokio::Mutex<...>>`. The CLI's `DisplayEvent` lives in
`infinity_agent_cli::display`.

Co-authored-by: Infinity 🤖 <infinity@hydro.run>
PR: #92
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.

2 participants