Publish harness and TUI open-source
initial sync from the monorepo
This commit is contained in:
commit
c68e39f604
2734 changed files with 1437016 additions and 0 deletions
17
crates/codegen/xai-agent-lifecycle/src/lib.rs
Normal file
17
crates/codegen/xai-agent-lifecycle/src/lib.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
//! Host-agnostic agent lifecycle hooks shared by multiple agent hosts (e.g. xai-grok-shell).
|
||||
//! Contributors receive data-only per-hook inputs at dispatch time; anything they act through is a
|
||||
//! capability injected at install time, and they never own loop control.
|
||||
|
||||
pub mod local;
|
||||
pub mod send;
|
||||
|
||||
pub use local::{
|
||||
LocalCommandContributor, LocalExtensionRegistry, LocalExtensionRegistryBuilder,
|
||||
LocalSessionLifecycleContributor, LocalTurnInputContributor, LocalTurnLifecycleContributor,
|
||||
};
|
||||
pub use send::{
|
||||
CommandAction, CommandContributor, CommandInvocation, CommandSpec, ExtensionRegistry,
|
||||
ExtensionRegistryBuilder, SessionIdleInput, SessionLifecycleContributor, TurnAbortInput,
|
||||
TurnAbortReason, TurnDoneInput, TurnErrorInput, TurnInputContext, TurnInputContributor,
|
||||
TurnInputFragment, TurnLifecycleContributor, TurnStartInput,
|
||||
};
|
||||
8
crates/codegen/xai-agent-lifecycle/src/local.rs
Normal file
8
crates/codegen/xai-agent-lifecycle/src/local.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
pub mod contributors;
|
||||
pub mod registry;
|
||||
|
||||
pub use contributors::{
|
||||
LocalCommandContributor, LocalSessionLifecycleContributor, LocalTurnInputContributor,
|
||||
LocalTurnLifecycleContributor,
|
||||
};
|
||||
pub use registry::{LocalExtensionRegistry, LocalExtensionRegistryBuilder};
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
pub mod command;
|
||||
pub mod session_lifecycle;
|
||||
pub mod turn_input;
|
||||
pub mod turn_lifecycle;
|
||||
|
||||
pub use command::LocalCommandContributor;
|
||||
pub use session_lifecycle::LocalSessionLifecycleContributor;
|
||||
pub use turn_input::LocalTurnInputContributor;
|
||||
pub use turn_lifecycle::LocalTurnLifecycleContributor;
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
use async_trait::async_trait;
|
||||
|
||||
use crate::send::contributors::command::{
|
||||
CommandAction, CommandContributor, CommandInvocation, CommandSpec,
|
||||
};
|
||||
|
||||
/// `?Send` twin of [`CommandContributor`] for single-threaded hosts like grok build's TUI agent, whose session state is `Rc`/`RefCell`-based and can
|
||||
/// never satisfy the `Send` bounds the send flavor bakes into its boxed hook futures.
|
||||
#[async_trait(?Send)]
|
||||
pub trait LocalCommandContributor {
|
||||
fn advertised_commands(&self) -> Vec<CommandSpec>;
|
||||
|
||||
async fn handle_command(&self, _input: &CommandInvocation<'_>)
|
||||
-> Result<CommandAction, String>;
|
||||
}
|
||||
|
||||
/// Send contributors work in single-threaded hosts as-is, so shared logic implements [`CommandContributor`] once and both hosts can register it.
|
||||
#[async_trait(?Send)]
|
||||
impl<T: CommandContributor> LocalCommandContributor for T {
|
||||
fn advertised_commands(&self) -> Vec<CommandSpec> {
|
||||
CommandContributor::advertised_commands(self)
|
||||
}
|
||||
|
||||
async fn handle_command(&self, input: &CommandInvocation<'_>) -> Result<CommandAction, String> {
|
||||
CommandContributor::handle_command(self, input).await
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
use async_trait::async_trait;
|
||||
|
||||
use crate::send::contributors::session_lifecycle::{SessionIdleInput, SessionLifecycleContributor};
|
||||
|
||||
/// `?Send` twin of [`SessionLifecycleContributor`].
|
||||
#[async_trait(?Send)]
|
||||
pub trait LocalSessionLifecycleContributor {
|
||||
/// Fired when the session settles idle (no running turn or queued work); the host owns the check.
|
||||
async fn on_session_idle(&self, _input: &SessionIdleInput) {}
|
||||
}
|
||||
|
||||
/// Send contributors are usable in single-threaded hosts as-is, so shared logic implements
|
||||
/// [`SessionLifecycleContributor`] once and both hosts can register it.
|
||||
#[async_trait(?Send)]
|
||||
impl<T: SessionLifecycleContributor> LocalSessionLifecycleContributor for T {
|
||||
async fn on_session_idle(&self, input: &SessionIdleInput) {
|
||||
SessionLifecycleContributor::on_session_idle(self, input).await;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
use async_trait::async_trait;
|
||||
|
||||
use crate::send::contributors::turn_input::{
|
||||
TurnInputContext, TurnInputContributor, TurnInputFragment,
|
||||
};
|
||||
|
||||
/// `?Send` twin of [`TurnInputContributor`] for single-threaded hosts like grok build's TUI agent, whose session state is `Rc`/`RefCell`-based
|
||||
/// and can never satisfy the `Send` bounds the send flavor bakes into its boxed hook futures.
|
||||
#[async_trait(?Send)]
|
||||
pub trait LocalTurnInputContributor {
|
||||
async fn contribute_turn_input(&self, _input: &TurnInputContext) -> Vec<TurnInputFragment> {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Send contributors are usable in single-threaded hosts as-is, so shared logic implements [`TurnInputContributor`] once for both hosts.
|
||||
#[async_trait(?Send)]
|
||||
impl<T: TurnInputContributor> LocalTurnInputContributor for T {
|
||||
async fn contribute_turn_input(&self, input: &TurnInputContext) -> Vec<TurnInputFragment> {
|
||||
TurnInputContributor::contribute_turn_input(self, input).await
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
use async_trait::async_trait;
|
||||
|
||||
use crate::send::contributors::turn_lifecycle::{
|
||||
TurnAbortInput, TurnDoneInput, TurnErrorInput, TurnLifecycleContributor, TurnStartInput,
|
||||
};
|
||||
|
||||
/// `?Send` twin of [`TurnLifecycleContributor`] for single-threaded hosts like grok build's TUI
|
||||
/// agent, whose session state is `Rc`/`RefCell`-based and can never satisfy the `Send` bounds the
|
||||
/// send flavor bakes into its boxed hook futures.
|
||||
#[async_trait(?Send)]
|
||||
pub trait LocalTurnLifecycleContributor {
|
||||
async fn on_turn_start(&self, _input: &TurnStartInput) {}
|
||||
|
||||
async fn on_turn_done(&self, _input: &TurnDoneInput) {}
|
||||
|
||||
async fn on_turn_abort(&self, _input: &TurnAbortInput) {}
|
||||
|
||||
async fn on_turn_error(&self, _input: &TurnErrorInput<'_>) {}
|
||||
}
|
||||
|
||||
/// Send contributors are usable in single-threaded hosts as-is, so shared logic implements
|
||||
/// [`TurnLifecycleContributor`] once and both hosts can register it.
|
||||
#[async_trait(?Send)]
|
||||
impl<T: TurnLifecycleContributor> LocalTurnLifecycleContributor for T {
|
||||
async fn on_turn_start(&self, input: &TurnStartInput) {
|
||||
TurnLifecycleContributor::on_turn_start(self, input).await;
|
||||
}
|
||||
|
||||
async fn on_turn_done(&self, input: &TurnDoneInput) {
|
||||
TurnLifecycleContributor::on_turn_done(self, input).await;
|
||||
}
|
||||
|
||||
async fn on_turn_abort(&self, input: &TurnAbortInput) {
|
||||
TurnLifecycleContributor::on_turn_abort(self, input).await;
|
||||
}
|
||||
|
||||
async fn on_turn_error(&self, input: &TurnErrorInput<'_>) {
|
||||
TurnLifecycleContributor::on_turn_error(self, input).await;
|
||||
}
|
||||
}
|
||||
101
crates/codegen/xai-agent-lifecycle/src/local/registry.rs
Normal file
101
crates/codegen/xai-agent-lifecycle/src/local/registry.rs
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::local::contributors::{
|
||||
LocalCommandContributor, LocalSessionLifecycleContributor, LocalTurnInputContributor,
|
||||
LocalTurnLifecycleContributor,
|
||||
};
|
||||
|
||||
/// Mutable registry used while hosts register typed runtime contributions.
|
||||
#[derive(Default)]
|
||||
pub struct LocalExtensionRegistryBuilder {
|
||||
turn_lifecycle_contributors: Vec<Rc<dyn LocalTurnLifecycleContributor>>,
|
||||
session_lifecycle_contributors: Vec<Rc<dyn LocalSessionLifecycleContributor>>,
|
||||
turn_input_contributors: Vec<Rc<dyn LocalTurnInputContributor>>,
|
||||
command_contributors: Vec<Rc<dyn LocalCommandContributor>>,
|
||||
}
|
||||
|
||||
impl LocalExtensionRegistryBuilder {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn turn_lifecycle_contributor(
|
||||
&mut self,
|
||||
contributor: Rc<dyn LocalTurnLifecycleContributor>,
|
||||
) {
|
||||
self.turn_lifecycle_contributors.push(contributor);
|
||||
}
|
||||
|
||||
pub fn session_lifecycle_contributor(
|
||||
&mut self,
|
||||
contributor: Rc<dyn LocalSessionLifecycleContributor>,
|
||||
) {
|
||||
self.session_lifecycle_contributors.push(contributor);
|
||||
}
|
||||
|
||||
pub fn turn_input_contributor(&mut self, contributor: Rc<dyn LocalTurnInputContributor>) {
|
||||
self.turn_input_contributors.push(contributor);
|
||||
}
|
||||
|
||||
pub fn command_contributor(&mut self, contributor: Rc<dyn LocalCommandContributor>) {
|
||||
self.command_contributors.push(contributor);
|
||||
}
|
||||
|
||||
/// Routes each advertised command to its one owner. Duplicate names are a composition bug:
|
||||
/// first registration wins, panics in debug builds, logs in release.
|
||||
pub fn build(self) -> LocalExtensionRegistry {
|
||||
let mut command_handlers: HashMap<String, Rc<dyn LocalCommandContributor>> = HashMap::new();
|
||||
for contributor in &self.command_contributors {
|
||||
for spec in contributor.advertised_commands() {
|
||||
if command_handlers.contains_key(&spec.name) {
|
||||
debug_assert!(false, "duplicate command contributed: /{}", spec.name);
|
||||
tracing::error!(command = %spec.name, "Duplicate command contributed; first registration wins");
|
||||
continue;
|
||||
}
|
||||
command_handlers.insert(spec.name, contributor.clone());
|
||||
}
|
||||
}
|
||||
|
||||
LocalExtensionRegistry {
|
||||
turn_lifecycle_contributors: self.turn_lifecycle_contributors,
|
||||
session_lifecycle_contributors: self.session_lifecycle_contributors,
|
||||
turn_input_contributors: self.turn_input_contributors,
|
||||
command_contributors: self.command_contributors,
|
||||
command_handlers,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Immutable typed registry produced after extensions are installed.
|
||||
#[derive(Default)]
|
||||
pub struct LocalExtensionRegistry {
|
||||
turn_lifecycle_contributors: Vec<Rc<dyn LocalTurnLifecycleContributor>>,
|
||||
session_lifecycle_contributors: Vec<Rc<dyn LocalSessionLifecycleContributor>>,
|
||||
turn_input_contributors: Vec<Rc<dyn LocalTurnInputContributor>>,
|
||||
command_contributors: Vec<Rc<dyn LocalCommandContributor>>,
|
||||
command_handlers: HashMap<String, Rc<dyn LocalCommandContributor>>,
|
||||
}
|
||||
|
||||
impl LocalExtensionRegistry {
|
||||
pub fn turn_lifecycle_contributors(&self) -> &[Rc<dyn LocalTurnLifecycleContributor>] {
|
||||
&self.turn_lifecycle_contributors
|
||||
}
|
||||
|
||||
pub fn session_lifecycle_contributors(&self) -> &[Rc<dyn LocalSessionLifecycleContributor>] {
|
||||
&self.session_lifecycle_contributors
|
||||
}
|
||||
|
||||
pub fn turn_input_contributors(&self) -> &[Rc<dyn LocalTurnInputContributor>] {
|
||||
&self.turn_input_contributors
|
||||
}
|
||||
|
||||
pub fn command_contributors(&self) -> &[Rc<dyn LocalCommandContributor>] {
|
||||
&self.command_contributors
|
||||
}
|
||||
|
||||
/// The one contributor owning `name`, or `None` when no extension advertised it.
|
||||
pub fn command_handler(&self, name: &str) -> Option<&Rc<dyn LocalCommandContributor>> {
|
||||
self.command_handlers.get(name)
|
||||
}
|
||||
}
|
||||
10
crates/codegen/xai-agent-lifecycle/src/send.rs
Normal file
10
crates/codegen/xai-agent-lifecycle/src/send.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
pub mod contributors;
|
||||
pub mod registry;
|
||||
|
||||
pub use contributors::{
|
||||
CommandAction, CommandContributor, CommandInvocation, CommandSpec, SessionIdleInput,
|
||||
SessionLifecycleContributor, TurnAbortInput, TurnAbortReason, TurnDoneInput, TurnErrorInput,
|
||||
TurnInputContext, TurnInputContributor, TurnInputFragment, TurnLifecycleContributor,
|
||||
TurnStartInput,
|
||||
};
|
||||
pub use registry::{ExtensionRegistry, ExtensionRegistryBuilder};
|
||||
12
crates/codegen/xai-agent-lifecycle/src/send/contributors.rs
Normal file
12
crates/codegen/xai-agent-lifecycle/src/send/contributors.rs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
pub mod command;
|
||||
pub mod session_lifecycle;
|
||||
pub mod turn_input;
|
||||
pub mod turn_lifecycle;
|
||||
|
||||
pub use command::{CommandAction, CommandContributor, CommandInvocation, CommandSpec};
|
||||
pub use session_lifecycle::{SessionIdleInput, SessionLifecycleContributor};
|
||||
pub use turn_input::{TurnInputContext, TurnInputContributor, TurnInputFragment};
|
||||
pub use turn_lifecycle::{
|
||||
TurnAbortInput, TurnAbortReason, TurnDoneInput, TurnErrorInput, TurnLifecycleContributor,
|
||||
TurnStartInput,
|
||||
};
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
use async_trait::async_trait;
|
||||
|
||||
/// A slash command a contributor advertises; the host maps it onto its own advertising protocol.
|
||||
pub struct CommandSpec {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub arg_hint: String,
|
||||
}
|
||||
|
||||
/// A parsed `/name args` invocation. The host owns parsing and routes it to the command's one owner.
|
||||
pub struct CommandInvocation<'a> {
|
||||
pub name: &'a str,
|
||||
pub args: &'a str, // Whitespace-trimmed; empty for a bare `/name`.
|
||||
}
|
||||
|
||||
/// What a handled command does to the turn; rejections travel as the `Err` reason.
|
||||
pub enum CommandAction {
|
||||
/// Replace the model-visible copy of the message with `model_text`.
|
||||
Rewrite { model_text: String },
|
||||
/// Side effect performed, nothing to say; the state change surfaces through the host's own rendering.
|
||||
Acted,
|
||||
}
|
||||
|
||||
/// Handles the slash commands the extension advertises. Only invoked for commands this contributor
|
||||
/// owns; the `Err` reason is the only channel for "why not", so hosts must surface it.
|
||||
#[async_trait]
|
||||
pub trait CommandContributor: Send + Sync {
|
||||
fn advertised_commands(&self) -> Vec<CommandSpec>;
|
||||
|
||||
async fn handle_command(&self, _input: &CommandInvocation<'_>)
|
||||
-> Result<CommandAction, String>;
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
use async_trait::async_trait;
|
||||
|
||||
/// Input supplied when the host observes the session settling idle.
|
||||
pub struct SessionIdleInput;
|
||||
|
||||
#[async_trait]
|
||||
pub trait SessionLifecycleContributor: Send + Sync {
|
||||
/// Fired when the session settles idle (no running turn or queued work); the host owns the check.
|
||||
async fn on_session_idle(&self, _input: &SessionIdleInput) {}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
use async_trait::async_trait;
|
||||
|
||||
/// Turn facts supplied when the host pulls extension input at its sampling chokepoint.
|
||||
pub struct TurnInputContext {
|
||||
/// Stable host-owned turn identifier.
|
||||
pub turn_id: String,
|
||||
/// True when the harness produced the turn (auto-wake, drain, cron, continuation), not the user.
|
||||
pub synthetic: bool,
|
||||
}
|
||||
|
||||
/// A model-visible input fragment contributed into the active turn. The host owns wrapping, origin stamping, and placement.
|
||||
pub struct TurnInputFragment {
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
/// Contributes model-visible input fragments into the active turn when the host pulls at its sampling chokepoint.
|
||||
/// Fragments land in the same turn, never a new one.
|
||||
#[async_trait]
|
||||
pub trait TurnInputContributor: Send + Sync {
|
||||
async fn contribute_turn_input(&self, _input: &TurnInputContext) -> Vec<TurnInputFragment> {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
use async_trait::async_trait;
|
||||
|
||||
/// Input supplied when the host starts a turn.
|
||||
pub struct TurnStartInput {
|
||||
/// True when the harness produced the turn (auto-wake, drain, cron, continuation), not the user.
|
||||
pub synthetic: bool,
|
||||
}
|
||||
|
||||
impl TurnStartInput {
|
||||
pub fn new(synthetic: bool) -> Self {
|
||||
TurnStartInput { synthetic }
|
||||
}
|
||||
}
|
||||
|
||||
/// Input supplied when the host completes a turn.
|
||||
pub struct TurnDoneInput;
|
||||
|
||||
/// Why the host aborted the turn instead of completing it.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum TurnAbortReason {
|
||||
/// The client went away mid-turn.
|
||||
Disconnected,
|
||||
/// The user interrupted the turn before it completed.
|
||||
Interrupted,
|
||||
}
|
||||
|
||||
/// Input supplied when the host aborts a turn.
|
||||
pub struct TurnAbortInput {
|
||||
pub reason: TurnAbortReason,
|
||||
}
|
||||
|
||||
impl TurnAbortInput {
|
||||
pub fn new(reason: TurnAbortReason) -> Self {
|
||||
TurnAbortInput { reason }
|
||||
}
|
||||
}
|
||||
|
||||
/// Input supplied when the host observes an error for a turn.
|
||||
pub struct TurnErrorInput<'a> {
|
||||
pub message: &'a str,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait TurnLifecycleContributor: Send + Sync {
|
||||
async fn on_turn_start(&self, _input: &TurnStartInput) {}
|
||||
|
||||
async fn on_turn_done(&self, _input: &TurnDoneInput) {}
|
||||
|
||||
async fn on_turn_abort(&self, _input: &TurnAbortInput) {}
|
||||
|
||||
async fn on_turn_error(&self, _input: &TurnErrorInput<'_>) {}
|
||||
}
|
||||
226
crates/codegen/xai-agent-lifecycle/src/send/registry.rs
Normal file
226
crates/codegen/xai-agent-lifecycle/src/send/registry.rs
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::send::contributors::{
|
||||
CommandContributor, SessionLifecycleContributor, TurnInputContributor, TurnLifecycleContributor,
|
||||
};
|
||||
|
||||
/// Mutable registry used while hosts register typed runtime contributions.
|
||||
#[derive(Default)]
|
||||
pub struct ExtensionRegistryBuilder {
|
||||
turn_lifecycle_contributors: Vec<Arc<dyn TurnLifecycleContributor>>,
|
||||
session_lifecycle_contributors: Vec<Arc<dyn SessionLifecycleContributor>>,
|
||||
turn_input_contributors: Vec<Arc<dyn TurnInputContributor>>,
|
||||
command_contributors: Vec<Arc<dyn CommandContributor>>,
|
||||
}
|
||||
|
||||
impl ExtensionRegistryBuilder {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn turn_lifecycle_contributor(&mut self, contributor: Arc<dyn TurnLifecycleContributor>) {
|
||||
self.turn_lifecycle_contributors.push(contributor);
|
||||
}
|
||||
|
||||
pub fn session_lifecycle_contributor(
|
||||
&mut self,
|
||||
contributor: Arc<dyn SessionLifecycleContributor>,
|
||||
) {
|
||||
self.session_lifecycle_contributors.push(contributor);
|
||||
}
|
||||
|
||||
pub fn turn_input_contributor(&mut self, contributor: Arc<dyn TurnInputContributor>) {
|
||||
self.turn_input_contributors.push(contributor);
|
||||
}
|
||||
|
||||
pub fn command_contributor(&mut self, contributor: Arc<dyn CommandContributor>) {
|
||||
self.command_contributors.push(contributor);
|
||||
}
|
||||
|
||||
/// Routes each advertised command to its one owner. Duplicate names are a composition bug:
|
||||
/// first registration wins, panics in debug builds, logs in release.
|
||||
pub fn build(self) -> ExtensionRegistry {
|
||||
let mut command_handlers: HashMap<String, Arc<dyn CommandContributor>> = HashMap::new();
|
||||
for contributor in &self.command_contributors {
|
||||
for spec in contributor.advertised_commands() {
|
||||
if command_handlers.contains_key(&spec.name) {
|
||||
debug_assert!(false, "duplicate command contributed: /{}", spec.name);
|
||||
tracing::error!(command = %spec.name, "Duplicate command contributed; first registration wins");
|
||||
continue;
|
||||
}
|
||||
command_handlers.insert(spec.name, contributor.clone());
|
||||
}
|
||||
}
|
||||
|
||||
ExtensionRegistry {
|
||||
turn_lifecycle_contributors: self.turn_lifecycle_contributors,
|
||||
session_lifecycle_contributors: self.session_lifecycle_contributors,
|
||||
turn_input_contributors: self.turn_input_contributors,
|
||||
command_contributors: self.command_contributors,
|
||||
command_handlers,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Immutable typed registry produced after extensions are installed.
|
||||
#[derive(Default)]
|
||||
pub struct ExtensionRegistry {
|
||||
turn_lifecycle_contributors: Vec<Arc<dyn TurnLifecycleContributor>>,
|
||||
session_lifecycle_contributors: Vec<Arc<dyn SessionLifecycleContributor>>,
|
||||
turn_input_contributors: Vec<Arc<dyn TurnInputContributor>>,
|
||||
command_contributors: Vec<Arc<dyn CommandContributor>>,
|
||||
command_handlers: HashMap<String, Arc<dyn CommandContributor>>,
|
||||
}
|
||||
|
||||
impl ExtensionRegistry {
|
||||
pub fn turn_lifecycle_contributors(&self) -> &[Arc<dyn TurnLifecycleContributor>] {
|
||||
&self.turn_lifecycle_contributors
|
||||
}
|
||||
|
||||
pub fn session_lifecycle_contributors(&self) -> &[Arc<dyn SessionLifecycleContributor>] {
|
||||
&self.session_lifecycle_contributors
|
||||
}
|
||||
|
||||
pub fn turn_input_contributors(&self) -> &[Arc<dyn TurnInputContributor>] {
|
||||
&self.turn_input_contributors
|
||||
}
|
||||
|
||||
pub fn command_contributors(&self) -> &[Arc<dyn CommandContributor>] {
|
||||
&self.command_contributors
|
||||
}
|
||||
|
||||
/// The one contributor owning `name`, or `None` when no extension advertised it.
|
||||
pub fn command_handler(&self, name: &str) -> Option<&Arc<dyn CommandContributor>> {
|
||||
self.command_handlers.get(name)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use super::*;
|
||||
use crate::send::contributors::{
|
||||
CommandAction, CommandInvocation, CommandSpec, SessionIdleInput, TurnAbortInput,
|
||||
TurnAbortReason, TurnDoneInput, TurnErrorInput, TurnInputContext, TurnInputFragment,
|
||||
TurnStartInput,
|
||||
};
|
||||
|
||||
struct Counter(AtomicUsize);
|
||||
|
||||
#[async_trait]
|
||||
impl TurnLifecycleContributor for Counter {
|
||||
async fn on_turn_done(&self, _input: &TurnDoneInput) {
|
||||
self.0.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SessionLifecycleContributor for Counter {
|
||||
async fn on_session_idle(&self, _input: &SessionIdleInput) {
|
||||
self.0.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl TurnInputContributor for Counter {
|
||||
async fn contribute_turn_input(&self, _input: &TurnInputContext) -> Vec<TurnInputFragment> {
|
||||
self.0.fetch_add(1, Ordering::SeqCst);
|
||||
vec![TurnInputFragment {
|
||||
text: "nudge".to_string(),
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl CommandContributor for Counter {
|
||||
fn advertised_commands(&self) -> Vec<CommandSpec> {
|
||||
vec![CommandSpec {
|
||||
name: "goal".to_string(),
|
||||
description: "Set a goal".to_string(),
|
||||
arg_hint: "<text>".to_string(),
|
||||
}]
|
||||
}
|
||||
|
||||
async fn handle_command(
|
||||
&self,
|
||||
input: &CommandInvocation<'_>,
|
||||
) -> Result<CommandAction, String> {
|
||||
self.0.fetch_add(1, Ordering::SeqCst);
|
||||
Ok(CommandAction::Rewrite {
|
||||
model_text: format!("{} {}", input.name, input.args),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "duplicate command contributed: /goal")]
|
||||
fn build_rejects_duplicate_command_names() {
|
||||
let counter = Arc::new(Counter(AtomicUsize::new(0)));
|
||||
let mut builder = ExtensionRegistryBuilder::new();
|
||||
builder.command_contributor(counter.clone());
|
||||
builder.command_contributor(counter);
|
||||
builder.build();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn builder_freezes_and_registry_dispatches_in_order() {
|
||||
let counter = Arc::new(Counter(AtomicUsize::new(0)));
|
||||
let mut builder = ExtensionRegistryBuilder::new();
|
||||
builder.turn_lifecycle_contributor(counter.clone());
|
||||
builder.turn_lifecycle_contributor(counter.clone());
|
||||
builder.session_lifecycle_contributor(counter.clone());
|
||||
builder.turn_input_contributor(counter.clone());
|
||||
builder.command_contributor(counter.clone());
|
||||
let registry = builder.build();
|
||||
|
||||
for contributor in registry.turn_lifecycle_contributors() {
|
||||
contributor
|
||||
.on_turn_start(&TurnStartInput { synthetic: false })
|
||||
.await;
|
||||
contributor.on_turn_done(&TurnDoneInput).await;
|
||||
contributor
|
||||
.on_turn_abort(&TurnAbortInput {
|
||||
reason: TurnAbortReason::Interrupted,
|
||||
})
|
||||
.await;
|
||||
contributor
|
||||
.on_turn_error(&TurnErrorInput { message: "boom" })
|
||||
.await;
|
||||
}
|
||||
|
||||
for contributor in registry.session_lifecycle_contributors() {
|
||||
contributor.on_session_idle(&SessionIdleInput).await;
|
||||
}
|
||||
|
||||
for contributor in registry.turn_input_contributors() {
|
||||
let fragments = contributor
|
||||
.contribute_turn_input(&TurnInputContext {
|
||||
turn_id: "turn-1".to_string(),
|
||||
synthetic: false,
|
||||
})
|
||||
.await;
|
||||
assert_eq!(1, fragments.len());
|
||||
assert_eq!("nudge", fragments[0].text);
|
||||
}
|
||||
|
||||
assert!(registry.command_handler("nope").is_none());
|
||||
let handler = registry.command_handler("goal").expect("goal has an owner");
|
||||
let action = handler
|
||||
.handle_command(&CommandInvocation {
|
||||
name: "goal",
|
||||
args: "ship it",
|
||||
})
|
||||
.await
|
||||
.expect("command should be handled");
|
||||
let CommandAction::Rewrite { model_text } = action else {
|
||||
panic!("expected a rewrite");
|
||||
};
|
||||
assert_eq!("goal ship it", model_text);
|
||||
|
||||
assert_eq!(5, counter.0.load(Ordering::SeqCst));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue