HookのPod側への移動・Interceptorの実装

This commit is contained in:
2026-04-11 17:19:20 +09:00
parent 496038307f
commit fc8ff9362e
22 changed files with 675 additions and 898 deletions
+1
View File
@@ -5,6 +5,7 @@ edition.workspace = true
license.workspace = true
[dependencies]
async-trait = "0.1.89"
clap = { version = "4.6.0", features = ["derive"] }
llm-worker = { version = "0.2.1", path = "../llm-worker" }
llm-worker-persistence = { version = "0.1.0", path = "../llm-worker-persistence" }
+160
View File
@@ -0,0 +1,160 @@
//! Pod-layer hook infrastructure
//!
//! Provides the `Hook<E>` trait and `HookRegistry` for orchestration hooks
//! that govern control-flow decisions in the Worker execution loop.
//!
//! The type system (`HookEventKind` / `Hook<E>`) mirrors the pattern
//! originally in llm-worker, now at the insomnia layer where orchestration
//! concerns belong.
use async_trait::async_trait;
use llm_worker::interceptor::{
PostToolAction, PreRequestAction, PreToolAction, PromptAction, ToolCallInfo, ToolResultInfo,
TurnEndAction,
};
use llm_worker::Item;
// =============================================================================
// Hook Event Kinds
// =============================================================================
/// Marker trait for hook event kinds.
///
/// Each event kind specifies its input (passed mutably to hooks) and
/// output (the control-flow action returned by hooks).
pub trait HookEventKind: Send + Sync + 'static {
/// Mutable input passed to the hook.
type Input;
/// Control-flow action returned by the hook.
type Output;
}
// --- Event kind markers ---
/// After receiving user input, before adding to history.
pub struct OnPromptSubmit;
/// Before each LLM request.
pub struct PreLlmRequest;
/// Before each tool is executed.
pub struct PreToolCall;
/// After each tool completes.
pub struct PostToolCall;
/// When a turn ends with no tool calls.
pub struct OnTurnEnd;
/// When execution is interrupted.
pub struct OnAbort;
impl HookEventKind for OnPromptSubmit {
type Input = Item;
type Output = PromptAction;
}
impl HookEventKind for PreLlmRequest {
type Input = Vec<Item>;
type Output = PreRequestAction;
}
impl HookEventKind for PreToolCall {
type Input = ToolCallInfo;
type Output = PreToolAction;
}
impl HookEventKind for PostToolCall {
type Input = ToolResultInfo;
type Output = PostToolAction;
}
impl HookEventKind for OnTurnEnd {
type Input = Vec<Item>;
type Output = TurnEndAction;
}
impl HookEventKind for OnAbort {
type Input = String;
type Output = ();
}
// =============================================================================
// Hook Trait
// =============================================================================
/// Async hook for a specific event kind.
///
/// Hooks receive mutable access to the event's input and return a
/// control-flow action. Multiple hooks can be registered per event;
/// they are evaluated in registration order and short-circuit on the
/// first non-Continue result.
#[async_trait]
pub trait Hook<E: HookEventKind>: Send + Sync {
async fn call(&self, input: &mut E::Input) -> E::Output;
}
// =============================================================================
// Hook Registry
// =============================================================================
/// Builder for constructing a frozen `HookRegistry`.
///
/// Hooks are added during setup, then `build()` produces an immutable
/// registry that can be shared via `Arc`.
#[derive(Default)]
pub struct HookRegistryBuilder {
on_prompt_submit: Vec<Box<dyn Hook<OnPromptSubmit>>>,
pre_llm_request: Vec<Box<dyn Hook<PreLlmRequest>>>,
pre_tool_call: Vec<Box<dyn Hook<PreToolCall>>>,
post_tool_call: Vec<Box<dyn Hook<PostToolCall>>>,
on_turn_end: Vec<Box<dyn Hook<OnTurnEnd>>>,
on_abort: Vec<Box<dyn Hook<OnAbort>>>,
}
impl HookRegistryBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn add_on_prompt_submit(&mut self, hook: impl Hook<OnPromptSubmit> + 'static) {
self.on_prompt_submit.push(Box::new(hook));
}
pub fn add_pre_llm_request(&mut self, hook: impl Hook<PreLlmRequest> + 'static) {
self.pre_llm_request.push(Box::new(hook));
}
pub fn add_pre_tool_call(&mut self, hook: impl Hook<PreToolCall> + 'static) {
self.pre_tool_call.push(Box::new(hook));
}
pub fn add_post_tool_call(&mut self, hook: impl Hook<PostToolCall> + 'static) {
self.post_tool_call.push(Box::new(hook));
}
pub fn add_on_turn_end(&mut self, hook: impl Hook<OnTurnEnd> + 'static) {
self.on_turn_end.push(Box::new(hook));
}
pub fn add_on_abort(&mut self, hook: impl Hook<OnAbort> + 'static) {
self.on_abort.push(Box::new(hook));
}
/// Freeze the builder into an immutable registry.
pub fn build(self) -> HookRegistry {
HookRegistry {
on_prompt_submit: self.on_prompt_submit,
pre_llm_request: self.pre_llm_request,
pre_tool_call: self.pre_tool_call,
post_tool_call: self.post_tool_call,
on_turn_end: self.on_turn_end,
on_abort: self.on_abort,
}
}
}
/// Frozen registry of hooks. Constructed via [`HookRegistryBuilder::build()`].
pub struct HookRegistry {
pub(crate) on_prompt_submit: Vec<Box<dyn Hook<OnPromptSubmit>>>,
pub(crate) pre_llm_request: Vec<Box<dyn Hook<PreLlmRequest>>>,
pub(crate) pre_tool_call: Vec<Box<dyn Hook<PreToolCall>>>,
pub(crate) post_tool_call: Vec<Box<dyn Hook<PostToolCall>>>,
pub(crate) on_turn_end: Vec<Box<dyn Hook<OnTurnEnd>>>,
pub(crate) on_abort: Vec<Box<dyn Hook<OnAbort>>>,
}
+87
View File
@@ -0,0 +1,87 @@
//! HookInterceptor — bridges Pod-layer hooks to Worker's Interceptor trait.
use std::sync::Arc;
use async_trait::async_trait;
use llm_worker::interceptor::{
Interceptor, PostToolAction, PreRequestAction, PreToolAction, PromptAction, ToolCallInfo,
ToolResultInfo, TurnEndAction,
};
use llm_worker::Item;
use crate::hook::HookRegistry;
/// An `Interceptor` implementation that delegates to a `HookRegistry`.
///
/// Each method iterates the registered hooks in order and short-circuits
/// on the first non-Continue (or non-Finish) result.
pub(crate) struct HookInterceptor {
registry: Arc<HookRegistry>,
}
impl HookInterceptor {
pub(crate) fn new(registry: Arc<HookRegistry>) -> Self {
Self { registry }
}
}
#[async_trait]
impl Interceptor for HookInterceptor {
async fn on_prompt_submit(&self, item: &mut Item) -> PromptAction {
for hook in &self.registry.on_prompt_submit {
let action = hook.call(item).await;
if !matches!(action, PromptAction::Continue) {
return action;
}
}
PromptAction::Continue
}
async fn pre_llm_request(&self, context: &mut Vec<Item>) -> PreRequestAction {
for hook in &self.registry.pre_llm_request {
let action = hook.call(context).await;
if !matches!(action, PreRequestAction::Continue) {
return action;
}
}
PreRequestAction::Continue
}
async fn pre_tool_call(&self, info: &mut ToolCallInfo) -> PreToolAction {
for hook in &self.registry.pre_tool_call {
let action = hook.call(info).await;
if !matches!(action, PreToolAction::Continue) {
return action;
}
}
PreToolAction::Continue
}
async fn post_tool_call(&self, info: &mut ToolResultInfo) -> PostToolAction {
for hook in &self.registry.post_tool_call {
let action = hook.call(info).await;
if !matches!(action, PostToolAction::Continue) {
return action;
}
}
PostToolAction::Continue
}
async fn on_turn_end(&self, history: &[Item]) -> TurnEndAction {
let mut history_vec = history.to_vec();
for hook in &self.registry.on_turn_end {
let action = hook.call(&mut history_vec).await;
if !matches!(action, TurnEndAction::Finish) {
return action;
}
}
TurnEndAction::Finish
}
async fn on_abort(&self, reason: &str) {
let mut reason_string = reason.to_string();
for hook in &self.registry.on_abort {
hook.call(&mut reason_string).await;
}
}
}
+3
View File
@@ -1,12 +1,15 @@
pub mod controller;
pub mod hook;
pub mod runtime_dir;
pub mod shared_state;
pub mod socket_server;
mod hook_interceptor;
mod pod;
pub use controller::{PodController, PodHandle};
pub use manifest::{PodManifest, ProviderConfig, ProviderKind, Scope};
pub use hook::{Hook, HookEventKind, HookRegistryBuilder};
pub use pod::{Pod, PodError, PodRunResult, apply_worker_manifest};
pub use protocol::{ErrorCode, Event, Method, TurnResult};
pub use provider::{ProviderError, build_client};
+77
View File
@@ -1,3 +1,5 @@
use std::sync::Arc;
use llm_worker::llm_client::client::LlmClient;
use llm_worker::llm_client::RequestConfig;
use llm_worker::Worker;
@@ -7,6 +9,12 @@ use llm_worker_persistence::{
use manifest::{PodManifest, Scope, WorkerManifest};
use crate::hook::{
Hook, HookRegistryBuilder, OnAbort, OnPromptSubmit, OnTurnEnd, PostToolCall, PreLlmRequest,
PreToolCall,
};
use crate::hook_interceptor::HookInterceptor;
/// An independent agent execution unit.
///
/// Wraps a persistent [`Session`] with manifest metadata and an optional
@@ -15,6 +23,8 @@ pub struct Pod<C: LlmClient, St: Store> {
manifest: PodManifest,
session: Session<C, St>,
scope: Option<Scope>,
hook_builder: HookRegistryBuilder,
interceptor_installed: bool,
}
impl<C: LlmClient, St: Store> Pod<C, St> {
@@ -34,6 +44,8 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
manifest,
session,
scope,
hook_builder: HookRegistryBuilder::new(),
interceptor_installed: false,
})
}
@@ -50,6 +62,8 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
manifest,
session,
scope,
hook_builder: HookRegistryBuilder::new(),
interceptor_installed: false,
})
}
@@ -76,14 +90,75 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
&mut self.session
}
// --- Hook registration ---
//
// Hooks must be registered before the first call to `run()` or `resume()`.
// Attempting to add a hook after execution has started will panic.
fn assert_hooks_open(&self) {
assert!(
!self.interceptor_installed,
"cannot add hooks after run() or resume() has been called"
);
}
/// Register a hook that runs after receiving user input.
pub fn add_on_prompt_submit_hook(&mut self, hook: impl Hook<OnPromptSubmit> + 'static) {
self.assert_hooks_open();
self.hook_builder.add_on_prompt_submit(hook);
}
/// Register a hook that runs before each LLM request.
pub fn add_pre_llm_request_hook(&mut self, hook: impl Hook<PreLlmRequest> + 'static) {
self.assert_hooks_open();
self.hook_builder.add_pre_llm_request(hook);
}
/// Register a hook that runs before each tool call.
pub fn add_pre_tool_call_hook(&mut self, hook: impl Hook<PreToolCall> + 'static) {
self.assert_hooks_open();
self.hook_builder.add_pre_tool_call(hook);
}
/// Register a hook that runs after each tool call.
pub fn add_post_tool_call_hook(&mut self, hook: impl Hook<PostToolCall> + 'static) {
self.assert_hooks_open();
self.hook_builder.add_post_tool_call(hook);
}
/// Register a hook that runs at the end of a turn.
pub fn add_on_turn_end_hook(&mut self, hook: impl Hook<OnTurnEnd> + 'static) {
self.assert_hooks_open();
self.hook_builder.add_on_turn_end(hook);
}
/// Register a hook that runs when execution is aborted.
pub fn add_on_abort_hook(&mut self, hook: impl Hook<OnAbort> + 'static) {
self.assert_hooks_open();
self.hook_builder.add_on_abort(hook);
}
/// Install the hook-based interceptor on the Worker if not already done.
fn ensure_interceptor_installed(&mut self) {
if !self.interceptor_installed {
let builder = std::mem::take(&mut self.hook_builder);
let registry = Arc::new(builder.build());
let interceptor = HookInterceptor::new(registry);
self.session.worker.set_interceptor(interceptor);
self.interceptor_installed = true;
}
}
/// Send user input and run until the LLM turn completes.
pub async fn run(&mut self, input: impl Into<String>) -> Result<PodRunResult, PodError> {
self.ensure_interceptor_installed();
let result = self.session.run(input).await?;
Ok(result.into())
}
/// Resume from a paused state.
pub async fn resume(&mut self) -> Result<PodRunResult, PodError> {
self.ensure_interceptor_installed();
let result = self.session.resume().await?;
Ok(result.into())
}
@@ -107,6 +182,8 @@ impl<St: Store> Pod<Box<dyn LlmClient>, St> {
manifest,
session,
scope,
hook_builder: HookRegistryBuilder::new(),
interceptor_installed: false,
})
}
}