HookのPod側への移動・Interceptorの実装
This commit is contained in:
@@ -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" }
|
||||
|
||||
@@ -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>>>,
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user