Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
223d1c2
Added: RunConfigHook contract and dispatch for amending run config
Sewer56 Aug 18, 2026
679c853
Changed: run hooks observe a read-only view of the run config
Sewer56 Aug 18, 2026
d1d71fb
Changed: adopt the RunConfigHook contract in the serdesai agent runtime
Sewer56 Aug 18, 2026
18bfcbd
Changed: apply run-config hooks on the streaming path with shared pro…
Sewer56 Aug 18, 2026
65e89ea
Added: run hook examples for preamble injection and read-only lifecycle
Sewer56 Aug 18, 2026
968db5d
Fixed: drop redundant rustdoc link target in run_stream docs
Sewer56 Aug 18, 2026
5ec3c05
Changed: document why Debug count tests pin hook-trait formatting
Sewer56 Aug 18, 2026
f75e179
Changed: trim dispatch_run doc to the core hook flow
Sewer56 Aug 18, 2026
2eecd18
Changed: replace run-config hook test doubles with closure adapters
Sewer56 Aug 18, 2026
34efb87
Changed: drop RunConfigHook note from run-event module docs
Sewer56 Aug 18, 2026
19a9bbf
Changed: fold redundant dispatch_run config test into the wrap test
Sewer56 Aug 18, 2026
dfa5f15
Remove verbose re-export doc comment in serdesai lib
Sewer56 Aug 18, 2026
8972685
Changed: restore serdesai-run-hook as a single RunHook example
Sewer56 Aug 18, 2026
67ab8cd
Changed: tighten run_hook module and trait docs
Sewer56 Aug 18, 2026
f490d95
Remove outdated mock-model echo note from run config hook example
Sewer56 Aug 18, 2026
bfde8f2
Changed: trim and clarify HookedAgent run-path docs
Sewer56 Aug 18, 2026
a905a32
Changed: sync hooks docs with the RunConfigHook API
Sewer56 Aug 18, 2026
2d75126
Fixed: stream run-config head includes prompt separator like run()
Sewer56 Aug 18, 2026
ac0cb97
Changed: build empty-chain RunConfig test input as a struct literal
Sewer56 Aug 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ serdes-ai-models = { version = "0.2.6", default-features = false }
serdes-ai-streaming = "0.2"

# Internal crates
reloaded-code-core = { version = "0.2.3", path = "reloaded-code-core", default-features = false }
reloaded-code-core = { version = "0.3.0", path = "reloaded-code-core", default-features = false }
reloaded-code-bubblewrap = { version = "0.1.0", path = "reloaded-code-bubblewrap" }
reloaded-code-agents = { version = "0.1.0", path = "reloaded-code-agents" }
reloaded-code-models-dev = { version = "0.1.0", path = "reloaded-code-models-dev" }
Expand Down
117 changes: 29 additions & 88 deletions src/docs/src/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,6 @@

Hooks let your code see, change, or stop things the agent does.

Tool, run, and run-event hooks are wired into the [SerdesAI] agent
pipeline: registered hooks intercept real tool calls, agent runs, and
streamed run events end to end.

Tool hooks work like game mods.
Each hook gets an `original` function.
`original` calls the next hook or the real tool.
Expand Down Expand Up @@ -166,48 +162,55 @@ let hooks = HookSet::builder()
Full example: [serdesai-tool-chain]
(`cargo run --example serdesai-tool-chain -p reloaded-code-serdesai --features mock`).

### Intercept a run
### Amend run config

Run hooks wrap the whole agent run. Mutate `RunConfig` to change the system
prompt, preambles, or parameters, then call `original` to continue:
`RunConfigHook` changes a run's config before the run starts. `RunConfig`
holds the system prompt, preamble messages, and model settings overrides
(temperature, top_p). `configure` mutates the config in place:

```rust
use reloaded_code_core::{
HookRunContext, HookSet, PreambleMessage, PreambleRole, RunConfig, RunHook,
RunHookFuture, RunOriginal,
HookRunContext, HookSet, PreambleMessage, PreambleRole, RunConfig,
RunConfigHook, RunConfigHookFuture,
};

struct PreambleInjector;

impl RunHook for PreambleInjector {
fn hook<'a>(
impl RunConfigHook for PreambleInjector {
fn configure<'a>(
&'a self,
ctx: &'a HookRunContext<'a>,
mut config: RunConfig,
original: RunOriginal<'a>,
) -> RunHookFuture<'a> {
_ctx: &'a HookRunContext<'a>,
config: &'a mut RunConfig,
) -> RunConfigHookFuture<'a> {
Box::pin(async move {
config.preamble_messages.push(PreambleMessage {
role: PreambleRole::System,
content: "You are a helpful assistant.".into(),
});
original.call(ctx, config).await
Ok(())
})
}
}

let hooks = HookSet::builder()
.run_hook(PreambleInjector)
.run_config_hook(PreambleInjector)
.build();
```

Full example: [serdesai-run-hook]
(`cargo run --example serdesai-run-hook -p reloaded-code-serdesai --features mock`).
A `RunConfigHook` runs before the request is made. Hooks run in
registration order; the chain stops at the first error, and the run does
not start.

Full example: [serdesai-run-config-hook]
(`cargo run --example serdesai-run-config-hook -p reloaded-code-serdesai --features mock`).

### Observe run start and end

A `RunHook` observes without changing anything: log before calling
`original`, inspect the result after:
`original`, inspect the result after.

The `config` argument is a read-only view of the final `RunConfig`.
Register a `RunConfigHook` to change it:

```rust
use reloaded_code_core::{
Expand All @@ -221,12 +224,12 @@ impl RunHook for RunObserver {
fn hook<'a>(
&'a self,
ctx: &'a HookRunContext<'a>,
config: RunConfig,
_config: &'a RunConfig,
original: RunOriginal<'a>,
) -> RunHookFuture<'a> {
Box::pin(async move {
println!("run starting for {}", ctx.agent_name);
let result = original.call(ctx, config).await;
let result = original.call(ctx).await;
let reason = match &result {
Ok(output) => output.reason,
Err(_) => EndReason::Failed,
Expand All @@ -248,6 +251,9 @@ error still propagates to the caller. An outer hook that skips
`original` never reaches this hook, so do not rely on it for cleanup
that must run on every path.

Full example: [serdesai-run-hook]
(`cargo run --example serdesai-run-hook -p reloaded-code-serdesai --features mock`).

### Intercept streamed events

Run-event hooks fire only on `run_stream()`. Each event passes the
Expand Down Expand Up @@ -306,47 +312,6 @@ Full example: [serdesai-run-event-hook] rewrites text deltas to
uppercase and suppresses the output-ready milestone
(`cargo run --example serdesai-run-event-hook -p reloaded-code-serdesai --features mock`).

## Available types

### Tool hook types

| Type | Purpose |
| ------------------- | ---------------------------------------------------------- |
| [`ToolHook`] | Intercepts a tool call and may call [`ToolOriginal`]. |
| [`ToolOriginal`] | Pointer to next hook or the real tool. |
| [`ToolHookFuture`] | Boxed future returned by tool hooks. |
| [`ToolCallContext`] | Tool name, agent name, run id. |
| [`ToolRequest`] | JSON arguments carried through the hook chain. |
| [`ToolOutput`] | Tool call result wrapping content and truncation metadata. |

### Run hook types

| Type | Purpose |
| ----------------- | ------------------------------------------------------------ |
| [`RunHook`] | Intercepts a run and may call [`RunOriginal`]. |
| [`RunOriginal`] | Pointer to next hook or the real run executor. |
| [`RunHookFuture`] | Boxed future returned by run hooks. |
| [`RunConfig`] | Mutable config a RunHook can change before calling original. |
| [`RunOutput`] | Framework-agnostic result of a completed run. |
| [`RunExecutor`] | Final callable used at the end of the run hook chain. |
| [`RunUsage`] | Token usage for a completed run. |

### Run event hook types

| Type | Purpose |
| ---------------------- | ----------------------------------------------------- |
| [`RunEventHook`] | Observes, rewrites, or suppresses one streamed event. |
| [`RunEventContext`] | Agent and model names for the event's stream. |
| [`RunEvent`] | Framework-owned event yielded by a run stream. |
| [`RunEventHookResult`] | Publish, rewrite, suppress, or Err(ToolError). |

### Container types

| Type | Purpose |
| ------------------ | ----------------------------------------------------------- |
| [`HookSet`] | Stores tool, run, and run-event hooks, plus compact events. |
| [`HookSetBuilder`] | Builder for [`HookSet`]. |

## How tool hooks stack

This diagram assumes you register two hooks. If you set no hooks, the
Expand Down Expand Up @@ -408,33 +373,9 @@ passes `HookSet::default()`.
- **Empty fast path.** `dispatch_tool` calls the real tool directly when you
set no hooks.

- **Mode-scoped run hooks.** `RunHook` fires only on `run()`;
`RunEventHook` fires only on `run_stream()`. Each hook point stays
inert on the other path.


[`ToolHook`]: https://docs.rs/reloaded-code-core/latest/reloaded_code_core/trait.ToolHook.html
[`ToolOriginal`]: https://docs.rs/reloaded-code-core/latest/reloaded_code_core/struct.ToolOriginal.html
[`ToolHookFuture`]: https://docs.rs/reloaded-code-core/latest/reloaded_code_core/type.ToolHookFuture.html
[`ToolCallContext`]: https://docs.rs/reloaded-code-core/latest/reloaded_code_core/struct.ToolCallContext.html
[`ToolRequest`]: https://docs.rs/reloaded-code-core/latest/reloaded_code_core/struct.ToolRequest.html
[`ToolOutput`]: https://docs.rs/reloaded-code-core/latest/reloaded_code_core/struct.ToolOutput.html
[`HookSet`]: https://docs.rs/reloaded-code-core/latest/reloaded_code_core/struct.HookSet.html
[`HookSetBuilder`]: https://docs.rs/reloaded-code-core/latest/reloaded_code_core/struct.HookSetBuilder.html
[`RunHook`]: https://docs.rs/reloaded-code-core/latest/reloaded_code_core/trait.RunHook.html
[`RunOriginal`]: https://docs.rs/reloaded-code-core/latest/reloaded_code_core/struct.RunOriginal.html
[`RunHookFuture`]: https://docs.rs/reloaded-code-core/latest/reloaded_code_core/type.RunHookFuture.html
[`RunConfig`]: https://docs.rs/reloaded-code-core/latest/reloaded_code_core/struct.RunConfig.html
[`RunOutput`]: https://docs.rs/reloaded-code-core/latest/reloaded_code_core/struct.RunOutput.html
[`RunExecutor`]: https://docs.rs/reloaded-code-core/latest/reloaded_code_core/trait.RunExecutor.html
[`RunUsage`]: https://docs.rs/reloaded-code-core/latest/reloaded_code_core/struct.RunUsage.html
[`RunEventHook`]: https://docs.rs/reloaded-code-core/latest/reloaded_code_core/trait.RunEventHook.html
[`RunEventContext`]: https://docs.rs/reloaded-code-core/latest/reloaded_code_core/struct.RunEventContext.html
[`RunEvent`]: https://docs.rs/reloaded-code-core/latest/reloaded_code_core/enum.RunEvent.html
[`RunEventHookResult`]: https://docs.rs/reloaded-code-core/latest/reloaded_code_core/type.RunEventHookResult.html
[SerdesAI]: https://crates.io/crates/serdes-ai
[serdesai-tool-hook]: https://github.com/Reloaded-Project/ReloadedCode/blob/main/src/reloaded-code-serdesai/examples/hooks/tool/serdesai-tool-hook.rs
[serdesai-tool-block]: https://github.com/Reloaded-Project/ReloadedCode/blob/main/src/reloaded-code-serdesai/examples/hooks/tool/serdesai-tool-block.rs
[serdesai-tool-chain]: https://github.com/Reloaded-Project/ReloadedCode/blob/main/src/reloaded-code-serdesai/examples/hooks/tool/serdesai-tool-chain.rs
[serdesai-run-config-hook]: https://github.com/Reloaded-Project/ReloadedCode/blob/main/src/reloaded-code-serdesai/examples/hooks/run/serdesai-run-config-hook.rs
[serdesai-run-hook]: https://github.com/Reloaded-Project/ReloadedCode/blob/main/src/reloaded-code-serdesai/examples/hooks/run/serdesai-run-hook.rs
[serdesai-run-event-hook]: https://github.com/Reloaded-Project/ReloadedCode/blob/main/src/reloaded-code-serdesai/examples/hooks/run/serdesai-run-event-hook.rs
4 changes: 2 additions & 2 deletions src/reloaded-code-agents/src/runtime/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,10 +322,10 @@ mod tests {
fn hook<'a>(
&'a self,
ctx: &'a reloaded_code_core::HookRunContext<'a>,
config: reloaded_code_core::RunConfig,
_config: &'a reloaded_code_core::RunConfig,
original: reloaded_code_core::RunOriginal<'a>,
) -> reloaded_code_core::RunHookFuture<'a> {
original.call(ctx, config)
original.call(ctx)
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/reloaded-code-core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "reloaded-code-core"
version = "0.2.3"
version = "0.3.0"
edition = "2021"
description = "Lightweight, high-performance core types and utilities for coding tools - framework agnostic"
repository = "https://github.com/Reloaded-Project/ReloadedCode"
Expand Down
95 changes: 89 additions & 6 deletions src/reloaded-code-core/src/hooks/builder.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
//! HookSetBuilder — builder for constructing a [`HookSet`].

use crate::hooks::{HookSet, RunEventHook, RunHook, SessionCompactFn, ToolHook, INLINE_CAP};
use crate::hooks::{
HookSet, RunConfigHook, RunEventHook, RunHook, SessionCompactFn, ToolHook, INLINE_CAP,
};
use std::fmt;
use std::sync::Arc;
use tinyvec::TinyVec;
Expand All @@ -9,6 +11,7 @@ use tinyvec::TinyVec;
#[derive(Default)]
pub struct HookSetBuilder {
pub(super) tool_hooks: Vec<Arc<dyn ToolHook>>,
pub(super) run_config_hooks: Vec<Arc<dyn RunConfigHook>>,
pub(super) run_hooks: Vec<Arc<dyn RunHook>>,
pub(super) run_event_hooks: Vec<Arc<dyn RunEventHook>>,
pub(super) session_compact: TinyVec<[Option<SessionCompactFn>; INLINE_CAP]>,
Expand Down Expand Up @@ -49,6 +52,25 @@ impl HookSetBuilder {
self
}

/// Registers a run-config hook.
///
/// Hooks run in registration order before the run hook chain,
/// each mutating the run config in place.
#[inline]
#[must_use]
pub fn run_config_hook(mut self, hook: impl RunConfigHook) -> Self {
self.run_config_hooks.push(Arc::new(hook));
self
}

/// Registers an already shared run-config hook.
#[inline]
#[must_use]
pub fn shared_run_config_hook(mut self, hook: Arc<dyn RunConfigHook>) -> Self {
self.run_config_hooks.push(hook);
self
}

/// Registers a game-style run hook.
///
/// Hooks run in registration order. Each hook's `original` handle calls
Expand Down Expand Up @@ -94,6 +116,7 @@ impl HookSetBuilder {
pub fn build(self) -> HookSet {
HookSet {
tool_hooks: self.tool_hooks,
run_config_hooks: self.run_config_hooks,
run_hooks: self.run_hooks,
run_event_hooks: self.run_event_hooks,
session_compact: self.session_compact,
Expand All @@ -105,6 +128,7 @@ impl fmt::Debug for HookSetBuilder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("HookSetBuilder")
.field("tool_hooks", &self.tool_hooks.len())
.field("run_config_hooks", &self.run_config_hooks.len())
.field("run_hooks", &self.run_hooks.len())
.field("run_event_hooks", &self.run_event_hooks.len())
.field("session_compact", &self.session_compact.len())
Expand All @@ -116,7 +140,9 @@ impl fmt::Debug for HookSetBuilder {
mod tests {
use super::*;
use crate::hooks::run_event::{RunEvent, RunEventContext, RunEventHook, RunEventHookResult};
use crate::hooks::run_hook::{HookRunContext, RunConfig, RunHookFuture, RunOriginal};
use crate::hooks::run_hook::{
HookRunContext, RunConfig, RunConfigHook, RunConfigHookFuture, RunHookFuture, RunOriginal,
};
use crate::hooks::tool_hook::{ToolCallContext, ToolHookFuture, ToolOriginal, ToolRequest};

#[test]
Expand Down Expand Up @@ -159,10 +185,10 @@ mod tests {
fn hook<'a>(
&'a self,
ctx: &'a HookRunContext<'a>,
config: RunConfig,
_config: &'a RunConfig,
original: RunOriginal<'a>,
) -> RunHookFuture<'a> {
original.call(ctx, config)
original.call(ctx)
}
}
let hooks = HookSetBuilder::new().run_hook(NoopRun).build();
Expand All @@ -178,10 +204,10 @@ mod tests {
fn hook<'a>(
&'a self,
ctx: &'a HookRunContext<'a>,
config: RunConfig,
_config: &'a RunConfig,
original: RunOriginal<'a>,
) -> RunHookFuture<'a> {
original.call(ctx, config)
original.call(ctx)
}
}
let shared: Arc<dyn RunHook> = Arc::new(NoopRun);
Expand Down Expand Up @@ -218,6 +244,63 @@ mod tests {
assert!(!hooks.run_event_hooks_is_empty());
}

#[test]
fn run_config_hook_registration_makes_hook_set_non_empty() {
struct NoopConfig;
impl RunConfigHook for NoopConfig {
fn configure<'a>(
&'a self,
_ctx: &'a HookRunContext<'a>,
_config: &'a mut RunConfig,
) -> RunConfigHookFuture<'a> {
Box::pin(async { Ok(()) })
}
}

let hooks = HookSetBuilder::new().run_config_hook(NoopConfig).build();
assert!(!hooks.is_empty());
assert!(!hooks.run_config_hooks_is_empty());
assert_eq!(hooks.run_config_hooks().len(), 1);
}

#[test]
fn shared_run_config_hook_registration() {
struct NoopConfig;
impl RunConfigHook for NoopConfig {
fn configure<'a>(
&'a self,
_ctx: &'a HookRunContext<'a>,
_config: &'a mut RunConfig,
) -> RunConfigHookFuture<'a> {
Box::pin(async { Ok(()) })
}
}

let shared: Arc<dyn RunConfigHook> = Arc::new(NoopConfig);
let hooks = HookSetBuilder::new().shared_run_config_hook(shared).build();
assert!(!hooks.run_config_hooks_is_empty());
assert_eq!(hooks.run_config_hooks().len(), 1);
}

#[test]
// Pins manual Debug: counts only, never hook contents (traits lack Debug).
fn builder_debug_includes_run_config_hooks() {
struct NoopConfig;
impl RunConfigHook for NoopConfig {
fn configure<'a>(
&'a self,
_ctx: &'a HookRunContext<'a>,
_config: &'a mut RunConfig,
) -> RunConfigHookFuture<'a> {
Box::pin(async { Ok(()) })
}
}

let builder = HookSetBuilder::new().run_config_hook(NoopConfig);
let debug = format!("{builder:?}");
assert!(debug.contains("run_config_hooks: 1"));
}

#[test]
fn builder_debug_includes_run_hooks() {
let builder = HookSetBuilder::new();
Expand Down
Loading
Loading