refactor(claw): assemble the daemon from an extension set
The daemon named every feature twice: once in the interactive handler's sixteen-argument constructor, once across the source list, the tick, and the boot pass. Both assemblies are now loops over one ClawExtension trait, so an integration is a struct plus a line in the standard set. The chain order, the detect order, and the boot order are unchanged; the surface suite pins them. Each extension struct is the boundary a later move into the integration crates would lift wholesale.
This commit is contained in:
1 parent
8ab513d49f
commit
5fdd17b901
5 files changed
+758
-260
No files matched your search
@@ -111,6 +111,11 @@ pub enum ForgeKind {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ForgeKind {
|
impl ForgeKind {
|
||||||
|
/// Every kind the claw ships a source for, in registration order.
|
||||||
|
/// A kind missing here has no watch poller, whatever the table
|
||||||
|
/// holds.
|
||||||
|
pub const ALL: [ForgeKind; 3] = [ForgeKind::GitHub, ForgeKind::Gitea, ForgeKind::GitLab];
|
||||||
|
|
||||||
/// The source name this kind registers and dispatches under.
|
/// The source name this kind registers and dispatches under.
|
||||||
pub fn name(self) -> &'static str {
|
pub fn name(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
|
|||||||
@@ -30,13 +30,11 @@ use rhema_rsd::{
|
|||||||
SystemClock,
|
SystemClock,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::calendar::CalendarSource;
|
use crate::config::{ClawConfig, ConfigError};
|
||||||
use crate::config::{ClawConfig, ConfigError, ForgeKind};
|
use crate::extension::{ExtensionDeps, Extensions, Lanes};
|
||||||
use crate::forge::ForgeSource;
|
|
||||||
use crate::handler::ClawHandler;
|
use crate::handler::ClawHandler;
|
||||||
use crate::http::ClawState;
|
use crate::http::ClawState;
|
||||||
use crate::notifications::NotificationStore;
|
use crate::notifications::NotificationStore;
|
||||||
use crate::task_source::TaskScheduler;
|
|
||||||
use crate::task_store::TaskStore;
|
use crate::task_store::TaskStore;
|
||||||
use rhema_memory::FileMemoryStore;
|
use rhema_memory::FileMemoryStore;
|
||||||
|
|
||||||
@@ -125,15 +123,16 @@ fn interactive_dispatch() -> DispatcherConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The resident daemon: one source per forge kind, the calendar
|
/// The resident daemon: the extension set's sources (the forge watches,
|
||||||
/// reminders, the task scheduler, the coding-job source, the three
|
/// the task scheduler, the coding jobs, the calendar reminders), the
|
||||||
/// drivers, and the HTTP surface.
|
/// three drivers, and the HTTP surface.
|
||||||
pub struct Daemon {
|
pub struct Daemon {
|
||||||
sources: Vec<ForgeSource>,
|
/// The extension set: the boot pass runs its hooks, and the tick
|
||||||
calendar_source: CalendarSource,
|
/// detects through the sources collected from it at assembly.
|
||||||
scheduler: TaskScheduler,
|
extensions: Arc<Extensions>,
|
||||||
job_source: crate::jobs::JobsSource,
|
/// The set's sources, in set order, each registered on the lane it
|
||||||
jobs: Arc<crate::job_store::CodingJobStore>,
|
/// feeds; built once, at assembly.
|
||||||
|
sources: Vec<Box<dyn Source>>,
|
||||||
forge_dispatcher: Arc<Dispatcher>,
|
forge_dispatcher: Arc<Dispatcher>,
|
||||||
interactive_dispatcher: Arc<Dispatcher>,
|
interactive_dispatcher: Arc<Dispatcher>,
|
||||||
jobs_dispatcher: Arc<Dispatcher>,
|
jobs_dispatcher: Arc<Dispatcher>,
|
||||||
@@ -183,6 +182,11 @@ impl Daemon {
|
|||||||
let notifications = Arc::new(NotificationStore::open(&db)?);
|
let notifications = Arc::new(NotificationStore::open(&db)?);
|
||||||
let tasks = Arc::new(TaskStore::open(&db)?);
|
let tasks = Arc::new(TaskStore::open(&db)?);
|
||||||
let turns = Arc::new(crate::turn_store::TurnStore::open(&db)?);
|
let turns = Arc::new(crate::turn_store::TurnStore::open(&db)?);
|
||||||
|
// The hub replays what past turns streamed: every event appends
|
||||||
|
// to the turn-history table as it streams, so a restart's replay
|
||||||
|
// reads the table back — a restart continues the transcript,
|
||||||
|
// whole, with no boot-time seed pass.
|
||||||
|
let hub = Arc::new(EventHub::with_record_store(turns.clone()));
|
||||||
// The coding-job tables: instances the `coding_agent` tool
|
// The coding-job tables: instances the `coding_agent` tool
|
||||||
// drives, jobs the jobs dispatcher answers.
|
// drives, jobs the jobs dispatcher answers.
|
||||||
let jobs = Arc::new(crate::job_store::CodingJobStore::open(&db)?);
|
let jobs = Arc::new(crate::job_store::CodingJobStore::open(&db)?);
|
||||||
@@ -221,23 +225,6 @@ impl Daemon {
|
|||||||
.map_err(|err| ClawError::Backend(err.to_string()))?,
|
.map_err(|err| ClawError::Backend(err.to_string()))?,
|
||||||
);
|
);
|
||||||
|
|
||||||
// One source per known forge kind, whatever the table currently
|
|
||||||
// holds: a watch added later (chat tool, HTTP route) of a kind
|
|
||||||
// nobody watched at startup still has its key function
|
|
||||||
// registered. A source over zero watches detects nothing.
|
|
||||||
let sources = [ForgeKind::GitHub, ForgeKind::Gitea, ForgeKind::GitLab]
|
|
||||||
.into_iter()
|
|
||||||
.map(|kind| {
|
|
||||||
ForgeSource::new(
|
|
||||||
&forge_dispatcher,
|
|
||||||
kind.name(),
|
|
||||||
watches.clone(),
|
|
||||||
accounts.clone(),
|
|
||||||
notifications.clone(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// The forge handler resolves its watches through the table at
|
// The forge handler resolves its watches through the table at
|
||||||
// dispatch time, so a watch added mid-run is answerable without a
|
// dispatch time, so a watch added mid-run is answerable without a
|
||||||
// restart.
|
// restart.
|
||||||
@@ -260,11 +247,6 @@ impl Daemon {
|
|||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
// The hub replays what past turns streamed: every event appends
|
|
||||||
// to the turn-history table as it streams, so a restart's replay
|
|
||||||
// reads the table back — a restart continues the transcript,
|
|
||||||
// whole, with no boot-time seed pass.
|
|
||||||
let hub = Arc::new(EventHub::with_record_store(turns.clone()));
|
|
||||||
// The consent table and its wake bridge: the `ask_secret` tool
|
// The consent table and its wake bridge: the `ask_secret` tool
|
||||||
// asks through them, the consent routes answer through them.
|
// asks through them, the consent routes answer through them.
|
||||||
let consents = Arc::new(crate::consents::ConsentStore::open(&db)?);
|
let consents = Arc::new(crate::consents::ConsentStore::open(&db)?);
|
||||||
@@ -306,23 +288,44 @@ impl Daemon {
|
|||||||
// at every boot by deterministic discovery, never by the model,
|
// at every boot by deterministic discovery, never by the model,
|
||||||
// and regenerated over whatever the last boot wrote.
|
// and regenerated over whatever the last boot wrote.
|
||||||
crate::environment::write_report(config.bootstrap_dir()?.join("ENVIRONMENT.md"))?;
|
crate::environment::write_report(config.bootstrap_dir()?.join("ENVIRONMENT.md"))?;
|
||||||
|
// The extension set: every resident feature beyond the core, one
|
||||||
|
// struct per integration. The handler's tool chain, the tick's
|
||||||
|
// detect passes, and the boot pass below all iterate it — the
|
||||||
|
// daemon names no feature twice.
|
||||||
|
let extensions = Arc::new(Extensions::standard(ExtensionDeps {
|
||||||
|
watches: watches.clone(),
|
||||||
|
accounts: accounts.clone(),
|
||||||
|
notifications: notifications.clone(),
|
||||||
|
tasks: tasks.clone(),
|
||||||
|
jobs: jobs.clone(),
|
||||||
|
consents: consents.clone(),
|
||||||
|
bridge: bridge.clone(),
|
||||||
|
attachments: attachments.clone(),
|
||||||
|
phone: phone.clone(),
|
||||||
|
identity: identity.clone(),
|
||||||
|
hub: hub.clone(),
|
||||||
|
workspace: interactive_workspace.clone(),
|
||||||
|
calendars: config.calendars.clone(),
|
||||||
|
}));
|
||||||
|
eprintln!("[claw] extensions: {}", extensions.names().join(", "));
|
||||||
|
// The sources register on their lanes as they build: a watch of
|
||||||
|
// a kind nobody watched at startup still has its key function
|
||||||
|
// registered, and a source over zero watches detects nothing.
|
||||||
|
let lanes = Lanes {
|
||||||
|
forge: forge_dispatcher.clone(),
|
||||||
|
interactive: interactive_dispatcher.clone(),
|
||||||
|
jobs: jobs_dispatcher.clone(),
|
||||||
|
};
|
||||||
|
let sources = extensions.sources(&lanes)?;
|
||||||
let interactive_handler = InteractiveHandler::new(
|
let interactive_handler = InteractiveHandler::new(
|
||||||
backends.clone(),
|
backends.clone(),
|
||||||
interactive_workspace,
|
interactive_workspace,
|
||||||
watches.clone(),
|
|
||||||
memory,
|
memory,
|
||||||
bootstrap.clone(),
|
bootstrap.clone(),
|
||||||
hub.clone(),
|
hub.clone(),
|
||||||
notifications.clone(),
|
notifications.clone(),
|
||||||
tasks.clone(),
|
|
||||||
jobs.clone(),
|
|
||||||
accounts.clone(),
|
|
||||||
config.max_turn_steps,
|
config.max_turn_steps,
|
||||||
consents.clone(),
|
extensions.clone(),
|
||||||
bridge.clone(),
|
|
||||||
attachments.clone(),
|
|
||||||
phone.clone(),
|
|
||||||
identity.clone(),
|
|
||||||
);
|
);
|
||||||
let interactive_driver = Arc::new(
|
let interactive_driver = Arc::new(
|
||||||
Driver::new(
|
Driver::new(
|
||||||
@@ -350,24 +353,7 @@ impl Daemon {
|
|||||||
crate::jobs::is_job_key(key.as_str())
|
crate::jobs::is_job_key(key.as_str())
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
let job_source = crate::jobs::JobsSource::new(&jobs_dispatcher, jobs.clone());
|
|
||||||
crate::interactive::register_user(&interactive_dispatcher);
|
crate::interactive::register_user(&interactive_dispatcher);
|
||||||
// The scheduler is the resident dispatcher's second own source:
|
|
||||||
// its detect pass (the daemon's tick drives it) is what turns a
|
|
||||||
// due task row into an event, never a timer near the model.
|
|
||||||
let scheduler = TaskScheduler::new(
|
|
||||||
&interactive_dispatcher,
|
|
||||||
Arc::new(SystemClock),
|
|
||||||
tasks.clone(),
|
|
||||||
);
|
|
||||||
// The calendar reminders join them: a due reminder is an
|
|
||||||
// event, never a timer near the model.
|
|
||||||
let calendar_source = CalendarSource::new(
|
|
||||||
&interactive_dispatcher,
|
|
||||||
Arc::new(SystemClock),
|
|
||||||
&config.calendars,
|
|
||||||
)
|
|
||||||
.map_err(ClawError::Backend)?;
|
|
||||||
|
|
||||||
let http = ClawState::new(
|
let http = ClawState::new(
|
||||||
interactive_dispatcher.clone(),
|
interactive_dispatcher.clone(),
|
||||||
@@ -389,11 +375,8 @@ impl Daemon {
|
|||||||
);
|
);
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
extensions,
|
||||||
sources,
|
sources,
|
||||||
calendar_source,
|
|
||||||
scheduler,
|
|
||||||
job_source,
|
|
||||||
jobs,
|
|
||||||
forge_dispatcher,
|
forge_dispatcher,
|
||||||
interactive_dispatcher,
|
interactive_dispatcher,
|
||||||
jobs_dispatcher,
|
jobs_dispatcher,
|
||||||
@@ -435,13 +418,10 @@ impl Daemon {
|
|||||||
/// top of the next dispatch (safe tools re-run, the rest report
|
/// top of the next dispatch (safe tools re-run, the rest report
|
||||||
/// interrupted). Idempotent; called before the first pass.
|
/// interrupted). Idempotent; called before the first pass.
|
||||||
pub async fn bootstrap(&self) -> Result<(), ClawError> {
|
pub async fn bootstrap(&self) -> Result<(), ClawError> {
|
||||||
// The job rows first: a turn the restart cut is failed
|
// The extension boot hooks first: a coding job the restart cut
|
||||||
// interrupted before anything re-runs, and a still-queued row
|
// is failed interrupted before anything re-runs, and a
|
||||||
// is re-armed with a fresh dedup id.
|
// still-queued row is re-armed with a fresh dedup id.
|
||||||
let touched = self.jobs.recover()?;
|
self.extensions.boot()?;
|
||||||
if touched > 0 {
|
|
||||||
eprintln!("[claw] jobs: {touched} row(s) recovered at boot");
|
|
||||||
}
|
|
||||||
self.forge_dispatcher.recover_all()?;
|
self.forge_dispatcher.recover_all()?;
|
||||||
self.jobs_dispatcher.recover_all()?;
|
self.jobs_dispatcher.recover_all()?;
|
||||||
self.interactive_dispatcher.recover_all()?;
|
self.interactive_dispatcher.recover_all()?;
|
||||||
@@ -454,9 +434,10 @@ impl Daemon {
|
|||||||
/// backstop — a message delivered live into a running turn waits at
|
/// backstop — a message delivered live into a running turn waits at
|
||||||
/// the key for a pass like this one.
|
/// the key for a pass like this one.
|
||||||
pub async fn tick(&self) -> Result<TickReport, ClawError> {
|
pub async fn tick(&self) -> Result<TickReport, ClawError> {
|
||||||
let mut detected = self.scheduler.detect().await?;
|
// The tick only detects — every timer here drives
|
||||||
detected += self.calendar_source.detect().await?;
|
// deterministic detection, and the set's order is the contract:
|
||||||
detected += self.job_source.detect().await?;
|
// due tasks before the calendar reminders that fire for them.
|
||||||
|
let mut detected = 0usize;
|
||||||
for source in &self.sources {
|
for source in &self.sources {
|
||||||
detected += source.detect().await?;
|
detected += source.detect().await?;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,494 @@
|
|||||||
|
//! The extension seam: one trait for everything the resident runs
|
||||||
|
//! beyond its core, and the standard set the daemon assembles from it.
|
||||||
|
//!
|
||||||
|
//! The claw's core is small: the shell, memory, the bootstrap prompt,
|
||||||
|
//! the chat surface, and the dispatch lanes. Everything else — the
|
||||||
|
//! watch table, the task scheduler, the coding jobs, the notification
|
||||||
|
//! queue, the consent bridge, attachments, the phone, the accounts
|
||||||
|
//! layer, the presentation identity, the calendar reminders — joins
|
||||||
|
//! the daemon through [`ClawExtension`]: tool resolvers for the
|
||||||
|
//! resident's chain, sources for the tick's detect passes, and a boot
|
||||||
|
//! hook where a previous process's wreckage gets made honest. The
|
||||||
|
//! daemon's assembly loops over the set; it does not name features.
|
||||||
|
//!
|
||||||
|
//! The seam is compile-time, not dynamic. An extension is a struct in
|
||||||
|
//! this crate holding its own store handles; installing one is a
|
||||||
|
//! constructor line in [`Extensions::standard`], and each struct is
|
||||||
|
//! the boundary a future extraction would lift wholesale — the glue
|
||||||
|
//! next to its logic in an integration crate, or, if a second
|
||||||
|
//! deployment shape ever appears, an out-of-process plugin speaking
|
||||||
|
//! for the struct. The HTTP routes are deliberately not part of the
|
||||||
|
//! trait: the surface is one API contract the web UI and the phone
|
||||||
|
//! client both compile against, and it stays whole in [`crate::http`]
|
||||||
|
//! until a second consumer exists.
|
||||||
|
//!
|
||||||
|
//! Order is contract. The resolver chain is the extensions'
|
||||||
|
//! [`ClawExtension::tools`] in [`Extensions::standard`]'s order, and
|
||||||
|
//! the tick detects through [`ClawExtension::sources`] in the same
|
||||||
|
//! order — due tasks before calendar reminders, the toolsets the model
|
||||||
|
//! addresses most first. Reordering the set changes what the model
|
||||||
|
//! sees and what fires first.
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use rhema_autonomous_agent::{Allowlist, Source, ToolRegistry};
|
||||||
|
use rhema_core::ToolResolver;
|
||||||
|
use rhema_rsd::{Dispatcher, ResourceKey, SystemClock};
|
||||||
|
|
||||||
|
use crate::calendar::CalendarSource;
|
||||||
|
use crate::config::{CalendarWatch, ForgeKind};
|
||||||
|
use crate::consents::WaitBridge;
|
||||||
|
use crate::daemon::ClawError;
|
||||||
|
use crate::forge::ForgeSource;
|
||||||
|
use crate::hub::EventHub;
|
||||||
|
|
||||||
|
/// The store handles the standard set is assembled over. One struct,
|
||||||
|
/// so a new extension is a field plus a constructor line, not another
|
||||||
|
/// positional argument down a sixteen-deep call chain.
|
||||||
|
pub struct ExtensionDeps {
|
||||||
|
/// The watch table: the forge extension's tools manage it, its
|
||||||
|
/// sources poll it, and the accounts tools hang account watches
|
||||||
|
/// off it.
|
||||||
|
pub watches: Arc<crate::watch_store::ClawStore>,
|
||||||
|
/// The accounts layer the accounts tools manage and the forge
|
||||||
|
/// sources poll through.
|
||||||
|
pub accounts: Arc<dyn rhema_identity::IdentityStore>,
|
||||||
|
/// The notification queue: the `notify` tool writes it, devices
|
||||||
|
/// poll it, and non-chat replies auto-notify through it.
|
||||||
|
pub notifications: Arc<crate::notifications::NotificationStore>,
|
||||||
|
/// The task table the task tools manage and the scheduler fires.
|
||||||
|
pub tasks: Arc<crate::task_store::TaskStore>,
|
||||||
|
/// The coding-job tables the `coding_agent` tool enqueues into.
|
||||||
|
pub jobs: Arc<crate::job_store::CodingJobStore>,
|
||||||
|
/// The consent table the `ask_secret` tool asks through.
|
||||||
|
pub consents: Arc<crate::consents::ConsentStore>,
|
||||||
|
/// The wake bridge between the consent tools and the routes that
|
||||||
|
/// answer them.
|
||||||
|
pub bridge: Arc<WaitBridge>,
|
||||||
|
/// The attachment store the `attach_file` tool copies into.
|
||||||
|
pub attachments: Arc<crate::attachments::AttachmentStore>,
|
||||||
|
/// The phone tables the `phone` tool reads and queues through.
|
||||||
|
pub phone: Arc<crate::phone::PhoneStore>,
|
||||||
|
/// The presentation-identity store the `identity_set` tool writes.
|
||||||
|
pub identity: Arc<crate::identity::IdentityStore>,
|
||||||
|
/// The event hub: the key-scoped tools (consents, attachments,
|
||||||
|
/// phone) emit their cards and asks onto the turn it streams.
|
||||||
|
pub hub: Arc<EventHub>,
|
||||||
|
/// The interactive workspace the account exec and the identity
|
||||||
|
/// registry resolve against.
|
||||||
|
pub workspace: PathBuf,
|
||||||
|
/// The calendar feeds the reminders source polls.
|
||||||
|
pub calendars: Vec<CalendarWatch>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The dispatch lanes a source can feed, handed to
|
||||||
|
/// [`ClawExtension::sources`] once the daemon has built its
|
||||||
|
/// dispatchers. Which lane an extension's sources feed is fixed by
|
||||||
|
/// what the extension reads here — the forge sources feed the forge
|
||||||
|
/// lane, the task scheduler and the calendar reminders the resident
|
||||||
|
/// lane, the coding jobs their own.
|
||||||
|
pub struct Lanes {
|
||||||
|
/// The forge lane: watch polls and account notifications.
|
||||||
|
pub forge: Arc<Dispatcher>,
|
||||||
|
/// The resident lane: chat, tasks, reminders, ingest.
|
||||||
|
pub interactive: Arc<Dispatcher>,
|
||||||
|
/// The coding-job lane: one leased turn per job.
|
||||||
|
pub jobs: Arc<Dispatcher>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One resident feature beyond the core: the tools it lends the
|
||||||
|
/// resolver chain, the sources it feeds the tick, and the boot hook
|
||||||
|
/// that recovers what a crash left behind.
|
||||||
|
///
|
||||||
|
/// Every method has a default, so an extension contributes only what
|
||||||
|
/// it has; `tools` is called per dispatch and `sources` once at
|
||||||
|
/// assembly.
|
||||||
|
pub trait ClawExtension: Send + Sync {
|
||||||
|
/// The extension's name, as logs and the boot line spell it.
|
||||||
|
fn name(&self) -> &'static str;
|
||||||
|
|
||||||
|
/// The tool resolvers this extension lends the resident's chain,
|
||||||
|
/// in the order they join it. Each is cut permissive — the surface
|
||||||
|
/// is the trust boundary — and rebuilt per dispatch, because the
|
||||||
|
/// key-scoped toolsets (consents, attachments, phone) must bind
|
||||||
|
/// their asks and cards to the session that asked.
|
||||||
|
fn tools(&self, _key: &ResourceKey) -> Vec<Arc<dyn ToolResolver>> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The sources this extension feeds the tick with, each already
|
||||||
|
/// registered on the lane it feeds. Built once, at assembly; a
|
||||||
|
/// source over an empty table simply detects nothing.
|
||||||
|
fn sources(&self, _lanes: &Lanes) -> Result<Vec<Box<dyn Source>>, ClawError> {
|
||||||
|
Ok(Vec::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The boot pass: whatever a previous process's crash left
|
||||||
|
/// inconsistent in this extension's tables, made honest before the
|
||||||
|
/// first tick. Runs before the dispatchers recover their leases.
|
||||||
|
fn boot(&self) -> Result<(), ClawError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The assembled set: the extension list the daemon iterates.
|
||||||
|
pub struct Extensions {
|
||||||
|
items: Vec<Box<dyn ClawExtension>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Extensions {
|
||||||
|
/// The standard set, in the order the resolver chain and the tick
|
||||||
|
/// inherit: notifications, tasks, jobs, forge watches, accounts,
|
||||||
|
/// identity, consents, attachments, phone, calendar reminders.
|
||||||
|
/// Tasks precede the calendar on purpose — a due task is the
|
||||||
|
/// user-facing clock, and the reminder that fires for it should
|
||||||
|
/// find the work already detected.
|
||||||
|
pub fn standard(deps: ExtensionDeps) -> Self {
|
||||||
|
Self {
|
||||||
|
items: vec![
|
||||||
|
Box::new(NotifyExtension {
|
||||||
|
notifications: deps.notifications.clone(),
|
||||||
|
}),
|
||||||
|
Box::new(TasksExtension {
|
||||||
|
tasks: deps.tasks.clone(),
|
||||||
|
}),
|
||||||
|
Box::new(JobsExtension {
|
||||||
|
jobs: deps.jobs.clone(),
|
||||||
|
}),
|
||||||
|
Box::new(ForgeExtension {
|
||||||
|
watches: deps.watches.clone(),
|
||||||
|
accounts: deps.accounts.clone(),
|
||||||
|
notifications: deps.notifications.clone(),
|
||||||
|
}),
|
||||||
|
Box::new(AccountsExtension {
|
||||||
|
accounts: deps.accounts.clone(),
|
||||||
|
watches: deps.watches.clone(),
|
||||||
|
workspace: deps.workspace.clone(),
|
||||||
|
}),
|
||||||
|
Box::new(IdentityExtension {
|
||||||
|
identity: deps.identity.clone(),
|
||||||
|
workspace: deps.workspace.clone(),
|
||||||
|
}),
|
||||||
|
Box::new(ConsentsExtension {
|
||||||
|
consents: deps.consents.clone(),
|
||||||
|
bridge: deps.bridge.clone(),
|
||||||
|
hub: deps.hub.clone(),
|
||||||
|
}),
|
||||||
|
Box::new(AttachmentsExtension {
|
||||||
|
store: deps.attachments.clone(),
|
||||||
|
workspace: deps.workspace.clone(),
|
||||||
|
hub: deps.hub.clone(),
|
||||||
|
}),
|
||||||
|
Box::new(PhoneExtension {
|
||||||
|
store: deps.phone.clone(),
|
||||||
|
consents: deps.consents.clone(),
|
||||||
|
bridge: deps.bridge.clone(),
|
||||||
|
hub: deps.hub.clone(),
|
||||||
|
}),
|
||||||
|
Box::new(CalendarExtension {
|
||||||
|
calendars: deps.calendars,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The full resolver chain one dispatch runs under: every
|
||||||
|
/// extension's tools, in set order, key-scoped toolsets bound to
|
||||||
|
/// `key`.
|
||||||
|
pub fn tools(&self, key: &ResourceKey) -> Vec<Arc<dyn ToolResolver>> {
|
||||||
|
self.items
|
||||||
|
.iter()
|
||||||
|
.flat_map(|extension| extension.tools(key))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every extension's sources, in set order, each registered on the
|
||||||
|
/// lane it feeds. Built once; the daemon holds the result for the
|
||||||
|
/// tick's detect passes.
|
||||||
|
pub fn sources(&self, lanes: &Lanes) -> Result<Vec<Box<dyn Source>>, ClawError> {
|
||||||
|
let mut sources = Vec::new();
|
||||||
|
for extension in &self.items {
|
||||||
|
sources.extend(extension.sources(lanes)?);
|
||||||
|
}
|
||||||
|
Ok(sources)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every extension's boot pass, in set order.
|
||||||
|
pub fn boot(&self) -> Result<(), ClawError> {
|
||||||
|
for extension in &self.items {
|
||||||
|
extension.boot()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The set's names, the order the chain and the tick run in — the
|
||||||
|
/// boot line's inventory.
|
||||||
|
pub fn names(&self) -> Vec<&'static str> {
|
||||||
|
self.items
|
||||||
|
.iter()
|
||||||
|
.map(|extension| extension.name())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wraps a registry in the permissive allowlist — the resident tier's
|
||||||
|
/// standing policy: the surface is the trust boundary, and the shell
|
||||||
|
/// already runs auto-approved.
|
||||||
|
fn permissive(registry: ToolRegistry) -> Arc<dyn ToolResolver> {
|
||||||
|
registry.resolver_for(&Allowlist::permissive())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The notification queue: the `notify` tool the resident writes
|
||||||
|
/// through and the rows a device polls and acks. Nothing detects —
|
||||||
|
/// outbound mail needs no polling.
|
||||||
|
struct NotifyExtension {
|
||||||
|
notifications: Arc<crate::notifications::NotificationStore>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClawExtension for NotifyExtension {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"notify"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tools(&self, _key: &ResourceKey) -> Vec<Arc<dyn ToolResolver>> {
|
||||||
|
vec![permissive(crate::notify_tools::registry(
|
||||||
|
self.notifications.clone(),
|
||||||
|
))]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The task scheduler: the `schedule_task`/`list_tasks`/`cancel_task`
|
||||||
|
/// tools, and the source whose detect pass turns a due row into an
|
||||||
|
/// event on the resident lane — never a timer near the model.
|
||||||
|
struct TasksExtension {
|
||||||
|
tasks: Arc<crate::task_store::TaskStore>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClawExtension for TasksExtension {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"tasks"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tools(&self, _key: &ResourceKey) -> Vec<Arc<dyn ToolResolver>> {
|
||||||
|
vec![permissive(crate::task_tools::registry(self.tasks.clone()))]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sources(&self, lanes: &Lanes) -> Result<Vec<Box<dyn Source>>, ClawError> {
|
||||||
|
Ok(vec![Box::new(crate::task_source::TaskScheduler::new(
|
||||||
|
&lanes.interactive,
|
||||||
|
Arc::new(SystemClock),
|
||||||
|
self.tasks.clone(),
|
||||||
|
))])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The coding jobs: the `coding_agent` tool, the source that ingests
|
||||||
|
/// queued rows onto the jobs lane, and the boot pass that fails the
|
||||||
|
/// turns a crash interrupted.
|
||||||
|
struct JobsExtension {
|
||||||
|
jobs: Arc<crate::job_store::CodingJobStore>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClawExtension for JobsExtension {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"jobs"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tools(&self, _key: &ResourceKey) -> Vec<Arc<dyn ToolResolver>> {
|
||||||
|
vec![permissive(crate::job_tools::registry(self.jobs.clone()))]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sources(&self, lanes: &Lanes) -> Result<Vec<Box<dyn Source>>, ClawError> {
|
||||||
|
Ok(vec![Box::new(crate::jobs::JobsSource::new(
|
||||||
|
&lanes.jobs,
|
||||||
|
self.jobs.clone(),
|
||||||
|
))])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn boot(&self) -> Result<(), ClawError> {
|
||||||
|
let touched = self.jobs.recover().map_err(ClawError::Jobs)?;
|
||||||
|
if touched > 0 {
|
||||||
|
eprintln!("[claw] jobs: {touched} row(s) recovered at boot");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The forge watches: the watch-management tools the resident configures
|
||||||
|
/// on request, and one source per forge kind — repo watches polled
|
||||||
|
/// through the watch table, account notifications triaged through the
|
||||||
|
/// accounts layer.
|
||||||
|
struct ForgeExtension {
|
||||||
|
watches: Arc<crate::watch_store::ClawStore>,
|
||||||
|
accounts: Arc<dyn rhema_identity::IdentityStore>,
|
||||||
|
notifications: Arc<crate::notifications::NotificationStore>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClawExtension for ForgeExtension {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"forge"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tools(&self, _key: &ResourceKey) -> Vec<Arc<dyn ToolResolver>> {
|
||||||
|
vec![permissive(crate::watch_tools::registry(
|
||||||
|
self.watches.clone(),
|
||||||
|
))]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sources(&self, lanes: &Lanes) -> Result<Vec<Box<dyn Source>>, ClawError> {
|
||||||
|
Ok(ForgeKind::ALL
|
||||||
|
.into_iter()
|
||||||
|
.map(|kind| {
|
||||||
|
Box::new(ForgeSource::new(
|
||||||
|
&lanes.forge,
|
||||||
|
kind.name(),
|
||||||
|
self.watches.clone(),
|
||||||
|
self.accounts.clone(),
|
||||||
|
self.notifications.clone(),
|
||||||
|
)) as Box<dyn Source>
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The accounts layer: the account tools that manage identities over
|
||||||
|
/// chat, and the one tool that acts with a stored credential — the
|
||||||
|
/// secret reaches a command's environment and never returns.
|
||||||
|
struct AccountsExtension {
|
||||||
|
accounts: Arc<dyn rhema_identity::IdentityStore>,
|
||||||
|
watches: Arc<crate::watch_store::ClawStore>,
|
||||||
|
workspace: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClawExtension for AccountsExtension {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"accounts"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tools(&self, _key: &ResourceKey) -> Vec<Arc<dyn ToolResolver>> {
|
||||||
|
vec![
|
||||||
|
permissive(crate::accounts_tools::registry(
|
||||||
|
self.accounts.clone(),
|
||||||
|
self.watches.clone(),
|
||||||
|
)),
|
||||||
|
Arc::new(crate::accounts_tools::IdentityExec::new(
|
||||||
|
self.accounts.clone(),
|
||||||
|
self.workspace.clone(),
|
||||||
|
)),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The presentation identity: the `identity_set` tool, so the model
|
||||||
|
/// writes its own name, mark, and images the surface serves.
|
||||||
|
struct IdentityExtension {
|
||||||
|
identity: Arc<crate::identity::IdentityStore>,
|
||||||
|
workspace: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClawExtension for IdentityExtension {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"identity"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tools(&self, _key: &ResourceKey) -> Vec<Arc<dyn ToolResolver>> {
|
||||||
|
vec![permissive(crate::identity::registry(
|
||||||
|
self.identity.clone(),
|
||||||
|
self.workspace.clone(),
|
||||||
|
))]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The consent bridge: the `ask_secret`/`request_permission` tools
|
||||||
|
/// that park a running turn on a human answer, and the routes that
|
||||||
|
/// deliver it. Key-scoped: an ask lands on the session that asked.
|
||||||
|
struct ConsentsExtension {
|
||||||
|
consents: Arc<crate::consents::ConsentStore>,
|
||||||
|
bridge: Arc<WaitBridge>,
|
||||||
|
hub: Arc<EventHub>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClawExtension for ConsentsExtension {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"consents"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tools(&self, key: &ResourceKey) -> Vec<Arc<dyn ToolResolver>> {
|
||||||
|
vec![permissive(crate::consents::registry(
|
||||||
|
self.consents.clone(),
|
||||||
|
self.bridge.clone(),
|
||||||
|
self.hub.clone(),
|
||||||
|
key.clone(),
|
||||||
|
))]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The attachments: the `attach_file` tool that copies a workspace
|
||||||
|
/// file into a fetchable card. Key-scoped: a card lands on the session
|
||||||
|
/// that attached it.
|
||||||
|
struct AttachmentsExtension {
|
||||||
|
store: Arc<crate::attachments::AttachmentStore>,
|
||||||
|
workspace: PathBuf,
|
||||||
|
hub: Arc<EventHub>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClawExtension for AttachmentsExtension {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"attachments"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tools(&self, key: &ResourceKey) -> Vec<Arc<dyn ToolResolver>> {
|
||||||
|
vec![permissive(crate::attachments::registry(
|
||||||
|
self.store.clone(),
|
||||||
|
self.workspace.clone(),
|
||||||
|
self.hub.clone(),
|
||||||
|
key.clone(),
|
||||||
|
))]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The phone: the `phone` tool's consent-gated control actions and the
|
||||||
|
/// status it reads. Key-scoped: a command queued from `phone:desk`
|
||||||
|
/// belongs to that device's session.
|
||||||
|
struct PhoneExtension {
|
||||||
|
store: Arc<crate::phone::PhoneStore>,
|
||||||
|
consents: Arc<crate::consents::ConsentStore>,
|
||||||
|
bridge: Arc<WaitBridge>,
|
||||||
|
hub: Arc<EventHub>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClawExtension for PhoneExtension {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"phone"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tools(&self, key: &ResourceKey) -> Vec<Arc<dyn ToolResolver>> {
|
||||||
|
vec![permissive(crate::phone::registry(
|
||||||
|
self.store.clone(),
|
||||||
|
self.consents.clone(),
|
||||||
|
self.bridge.clone(),
|
||||||
|
self.hub.clone(),
|
||||||
|
key.clone(),
|
||||||
|
))]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The calendar reminders: a due occurrence is an event on the resident
|
||||||
|
/// lane, never a timer near the model.
|
||||||
|
struct CalendarExtension {
|
||||||
|
calendars: Vec<CalendarWatch>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClawExtension for CalendarExtension {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"calendar"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sources(&self, lanes: &Lanes) -> Result<Vec<Box<dyn Source>>, ClawError> {
|
||||||
|
let source =
|
||||||
|
CalendarSource::new(&lanes.interactive, Arc::new(SystemClock), &self.calendars)
|
||||||
|
.map_err(ClawError::Backend)?;
|
||||||
|
Ok(vec![Box::new(source)])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,11 +40,12 @@
|
|||||||
//! the rest report interrupted.
|
//! the rest report interrupted.
|
||||||
//!
|
//!
|
||||||
//! Deterministic setup first (spec §1, §8): the allowlist — permissive,
|
//! Deterministic setup first (spec §1, §8): the allowlist — permissive,
|
||||||
//! v1's starting point — and the resolver chain (the shell toolset over
|
//! v1's starting point — and the resolver chain are assembled before
|
||||||
//! the interactive workspace, the memory tools, `bootstrap_save`, the
|
//! the first model call; the model never decides its own permissions.
|
||||||
//! notify tool, the task tools, and the watch tools) are assembled
|
//! The chain is the core toolsets (the shell over the interactive
|
||||||
//! before the first model call; the model never decides its own
|
//! workspace, memory, `bootstrap_save`) plus every
|
||||||
//! permissions. The system prompt is rendered per dispatch from the
|
//! [`crate::extension::ClawExtension`]'s tools in the standard set's
|
||||||
|
//! order. The system prompt is rendered per dispatch from the
|
||||||
//! bootstrap template over the live SOUL/USER files.
|
//! bootstrap template over the live SOUL/USER files.
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -67,7 +68,6 @@ use crate::backends::BackendRegistry;
|
|||||||
use crate::bootstrap::{Bootstrap, DEFAULT_SYSTEM_MD, render_system};
|
use crate::bootstrap::{Bootstrap, DEFAULT_SYSTEM_MD, render_system};
|
||||||
use crate::hub::{EventHub, TurnEvent};
|
use crate::hub::{EventHub, TurnEvent};
|
||||||
use crate::notifications::NotificationStore;
|
use crate::notifications::NotificationStore;
|
||||||
use crate::watch_store::ClawStore;
|
|
||||||
|
|
||||||
/// The key prefixes of the resident context (see the module docs):
|
/// The key prefixes of the resident context (see the module docs):
|
||||||
/// `interactive:`, `task:`, `phone:`, and `calendar:`. Forge keys —
|
/// `interactive:`, `task:`, `phone:`, and `calendar:`. Forge keys —
|
||||||
@@ -158,16 +158,6 @@ pub struct InteractiveHandler {
|
|||||||
backends: Arc<BackendRegistry>,
|
backends: Arc<BackendRegistry>,
|
||||||
workspace: PathBuf,
|
workspace: PathBuf,
|
||||||
tools: Arc<ShellTools>,
|
tools: Arc<ShellTools>,
|
||||||
watch_resolver: Arc<dyn rhema_core::ToolResolver>,
|
|
||||||
/// The identity tools: interactive-only by construction — the forge
|
|
||||||
/// handler never resolves them, so a forge comment cannot reach the
|
|
||||||
/// accounts.
|
|
||||||
accounts_resolver: Arc<dyn rhema_core::ToolResolver>,
|
|
||||||
/// The one tool that acts with a stored credential: the secret is
|
|
||||||
/// injected into a command's environment and never returns.
|
|
||||||
identity_exec: Arc<dyn rhema_core::ToolResolver>,
|
|
||||||
notify_resolver: Arc<dyn rhema_core::ToolResolver>,
|
|
||||||
task_resolver: Arc<dyn rhema_core::ToolResolver>,
|
|
||||||
/// The durable cross-session store the memory tools act on; the
|
/// The durable cross-session store the memory tools act on; the
|
||||||
/// resolver is built per dispatch so its scope tracks the workspace.
|
/// resolver is built per dispatch so its scope tracks the workspace.
|
||||||
memory: Arc<dyn MemoryStore>,
|
memory: Arc<dyn MemoryStore>,
|
||||||
@@ -179,98 +169,52 @@ pub struct InteractiveHandler {
|
|||||||
/// The notification table: the `notify` tool writes it, and
|
/// The notification table: the `notify` tool writes it, and
|
||||||
/// non-interactive replies auto-notify through it.
|
/// non-interactive replies auto-notify through it.
|
||||||
notifications: Arc<NotificationStore>,
|
notifications: Arc<NotificationStore>,
|
||||||
/// The consent table the `ask_secret` tool asks through and the
|
|
||||||
/// consent routes answer into.
|
|
||||||
consents: Arc<crate::consents::ConsentStore>,
|
|
||||||
/// The wake bridge between those routes and the tools waiting on
|
|
||||||
/// their rows.
|
|
||||||
bridge: Arc<crate::consents::WaitBridge>,
|
|
||||||
/// The presentation-identity tool: the same store the settings
|
|
||||||
/// pages write, offered to the model as `identity_set`.
|
|
||||||
identity_resolver: Arc<dyn rhema_core::ToolResolver>,
|
|
||||||
/// The attachment store the `attach_file` tool copies into and the
|
|
||||||
/// fetch route serves from.
|
|
||||||
attachments: Arc<crate::attachments::AttachmentStore>,
|
|
||||||
/// The phone state and command queue the `phone` tool reads and
|
|
||||||
/// queues through.
|
|
||||||
phone: Arc<crate::phone::PhoneStore>,
|
|
||||||
/// The coding-agent tool: instances and async jobs the resident
|
|
||||||
/// drives a full rhema-coding-agent session through.
|
|
||||||
job_resolver: Arc<dyn rhema_core::ToolResolver>,
|
|
||||||
/// How many tool rounds one turn may run before it is cut off — the
|
/// How many tool rounds one turn may run before it is cut off — the
|
||||||
/// config's runaway guard for an agent meant to work unattended; a
|
/// config's runaway guard for an agent meant to work unattended; a
|
||||||
/// follow-up message keeps going.
|
/// follow-up message keeps going.
|
||||||
max_turn_steps: u32,
|
max_turn_steps: u32,
|
||||||
|
/// The extension set the tool half of the chain is cut from, per
|
||||||
|
/// dispatch, in the standard set's order — the same set whose
|
||||||
|
/// sources the daemon's tick drives and whose boot pass runs first.
|
||||||
|
extensions: Arc<crate::extension::Extensions>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InteractiveHandler {
|
impl InteractiveHandler {
|
||||||
/// A handler whose chat turns run in `workspace` (the interactive
|
/// A handler whose chat turns run in `workspace` (the interactive
|
||||||
/// scratch space under the daemon's workspace root), whose watch
|
/// scratch space under the daemon's workspace root) and whose tool
|
||||||
/// tools mutate `watches`, whose notifications land in
|
/// half of the resolver chain comes from `extensions` — the same
|
||||||
/// `notifications`, whose task tools manage `tasks`, and whose
|
/// set whose sources the daemon's tick drives and whose boot pass
|
||||||
/// `coding_agent` tool enqueues jobs into `jobs`. Approval is
|
/// runs at assembly. The core toolsets are the handler's own: the
|
||||||
/// v1-permissive — the shell is the point of a resident coding
|
/// shell (approval v1-permissive — the shell is the point of a
|
||||||
/// agent — installed once, since resolution is synchronous within a
|
/// resident coding agent — installed once, since resolution is
|
||||||
/// turn.
|
/// synchronous within a turn), memory, and `bootstrap_save`.
|
||||||
#[allow(clippy::too_many_arguments)] // sixteen parts, each load-bearing
|
#[allow(clippy::too_many_arguments)] // eight parts, each load-bearing
|
||||||
pub fn new(
|
pub fn new(
|
||||||
backends: Arc<BackendRegistry>,
|
backends: Arc<BackendRegistry>,
|
||||||
workspace: impl Into<PathBuf>,
|
workspace: impl Into<PathBuf>,
|
||||||
watches: Arc<ClawStore>,
|
|
||||||
memory: Arc<dyn MemoryStore>,
|
memory: Arc<dyn MemoryStore>,
|
||||||
bootstrap: Arc<Bootstrap>,
|
bootstrap: Arc<Bootstrap>,
|
||||||
hub: Arc<EventHub>,
|
hub: Arc<EventHub>,
|
||||||
notifications: Arc<NotificationStore>,
|
notifications: Arc<NotificationStore>,
|
||||||
tasks: Arc<crate::task_store::TaskStore>,
|
|
||||||
jobs: Arc<crate::job_store::CodingJobStore>,
|
|
||||||
accounts: Arc<dyn rhema_identity::IdentityStore>,
|
|
||||||
max_turn_steps: u32,
|
max_turn_steps: u32,
|
||||||
consents: Arc<crate::consents::ConsentStore>,
|
extensions: Arc<crate::extension::Extensions>,
|
||||||
bridge: Arc<crate::consents::WaitBridge>,
|
|
||||||
attachments: Arc<crate::attachments::AttachmentStore>,
|
|
||||||
phone: Arc<crate::phone::PhoneStore>,
|
|
||||||
identity: Arc<crate::identity::IdentityStore>,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let workspace = workspace.into();
|
let workspace = workspace.into();
|
||||||
let tools = Arc::new(ShellTools::new(&workspace));
|
let tools = Arc::new(ShellTools::new(&workspace));
|
||||||
tools.install_approval(Arc::new(AllowAll));
|
tools.install_approval(Arc::new(AllowAll));
|
||||||
let watch_resolver =
|
|
||||||
crate::watch_tools::registry(watches.clone()).resolver_for(&Allowlist::permissive());
|
|
||||||
let accounts_resolver = crate::accounts_tools::registry(accounts.clone(), watches)
|
|
||||||
.resolver_for(&Allowlist::permissive());
|
|
||||||
let identity_exec: Arc<dyn rhema_core::ToolResolver> = Arc::new(
|
|
||||||
crate::accounts_tools::IdentityExec::new(accounts, workspace.clone()),
|
|
||||||
);
|
|
||||||
let notify_resolver = crate::notify_tools::registry(notifications.clone())
|
|
||||||
.resolver_for(&Allowlist::permissive());
|
|
||||||
let task_resolver =
|
|
||||||
crate::task_tools::registry(tasks).resolver_for(&Allowlist::permissive());
|
|
||||||
let job_resolver = crate::job_tools::registry(jobs).resolver_for(&Allowlist::permissive());
|
|
||||||
let bootstrap_resolver =
|
let bootstrap_resolver =
|
||||||
crate::bootstrap::registry(bootstrap.clone()).resolver_for(&Allowlist::permissive());
|
crate::bootstrap::registry(bootstrap.clone()).resolver_for(&Allowlist::permissive());
|
||||||
let identity_resolver = crate::identity::registry(identity.clone(), workspace.clone())
|
|
||||||
.resolver_for(&Allowlist::permissive());
|
|
||||||
Self {
|
Self {
|
||||||
backends,
|
backends,
|
||||||
workspace,
|
workspace,
|
||||||
tools,
|
tools,
|
||||||
job_resolver,
|
|
||||||
watch_resolver,
|
|
||||||
accounts_resolver,
|
|
||||||
identity_exec,
|
|
||||||
notify_resolver,
|
|
||||||
task_resolver,
|
|
||||||
memory,
|
memory,
|
||||||
bootstrap,
|
bootstrap,
|
||||||
bootstrap_resolver,
|
bootstrap_resolver,
|
||||||
identity_resolver,
|
|
||||||
hub,
|
hub,
|
||||||
notifications,
|
notifications,
|
||||||
consents,
|
|
||||||
bridge,
|
|
||||||
attachments,
|
|
||||||
phone,
|
|
||||||
max_turn_steps,
|
max_turn_steps,
|
||||||
|
extensions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,49 +251,24 @@ impl InteractiveHandler {
|
|||||||
.map_err(|err| format!("cannot build the interactive agent: {err}"))
|
.map_err(|err| format!("cannot build the interactive agent: {err}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The resolver chain one dispatch runs under: the shell toolset
|
/// The resolver chain one dispatch runs under: the core toolsets —
|
||||||
/// over the interactive workspace, then the resident toolsets. The
|
/// the shell over the interactive workspace, memory,
|
||||||
/// consent and attachment tools build per dispatch — their asks and
|
/// `bootstrap_save` — then every extension's tools in the standard
|
||||||
/// cards must land on the session that asked, and the handler
|
/// set's order. The whole chain is cut per dispatch: the
|
||||||
|
/// key-scoped toolsets (consents, attachments, phone) must bind
|
||||||
|
/// their asks and cards to the session that asked, and the handler
|
||||||
/// serves every session.
|
/// serves every session.
|
||||||
fn resolver(&self, key: &ResourceKey) -> Arc<ResolverChain> {
|
fn resolver(&self, key: &ResourceKey) -> Arc<ResolverChain> {
|
||||||
Arc::new(ResolverChain::new(vec![
|
let mut resolvers: Vec<Arc<dyn rhema_core::ToolResolver>> = vec![
|
||||||
self.tools.clone(),
|
self.tools.clone(),
|
||||||
Arc::new(MemoryResolver::new(
|
Arc::new(MemoryResolver::new(
|
||||||
self.memory.clone(),
|
self.memory.clone(),
|
||||||
Scope::Workspace(self.workspace.clone()),
|
Scope::Workspace(self.workspace.clone()),
|
||||||
)),
|
)),
|
||||||
self.bootstrap_resolver.clone(),
|
self.bootstrap_resolver.clone(),
|
||||||
self.notify_resolver.clone(),
|
];
|
||||||
self.task_resolver.clone(),
|
resolvers.extend(self.extensions.tools(key));
|
||||||
self.job_resolver.clone(),
|
Arc::new(ResolverChain::new(resolvers))
|
||||||
self.watch_resolver.clone(),
|
|
||||||
self.accounts_resolver.clone(),
|
|
||||||
self.identity_exec.clone(),
|
|
||||||
self.identity_resolver.clone(),
|
|
||||||
crate::consents::registry(
|
|
||||||
self.consents.clone(),
|
|
||||||
self.bridge.clone(),
|
|
||||||
self.hub.clone(),
|
|
||||||
key.clone(),
|
|
||||||
)
|
|
||||||
.resolver_for(&Allowlist::permissive()),
|
|
||||||
crate::attachments::registry(
|
|
||||||
self.attachments.clone(),
|
|
||||||
self.workspace.clone(),
|
|
||||||
self.hub.clone(),
|
|
||||||
key.clone(),
|
|
||||||
)
|
|
||||||
.resolver_for(&Allowlist::permissive()),
|
|
||||||
crate::phone::registry(
|
|
||||||
self.phone.clone(),
|
|
||||||
self.consents.clone(),
|
|
||||||
self.bridge.clone(),
|
|
||||||
self.hub.clone(),
|
|
||||||
key.clone(),
|
|
||||||
)
|
|
||||||
.resolver_for(&Allowlist::permissive()),
|
|
||||||
]))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One dispatch: drain, restore, turn, stash, reply.
|
/// One dispatch: drain, restore, turn, stash, reply.
|
||||||
@@ -634,6 +553,7 @@ mod tests {
|
|||||||
|
|
||||||
use crate::notifications::NotificationStore;
|
use crate::notifications::NotificationStore;
|
||||||
use crate::task_store::TaskStore;
|
use crate::task_store::TaskStore;
|
||||||
|
use crate::watch_store::ClawStore;
|
||||||
|
|
||||||
use crate::backends::{Backend, BackendRegistry};
|
use crate::backends::{Backend, BackendRegistry};
|
||||||
use rhema_core::{
|
use rhema_core::{
|
||||||
@@ -713,6 +633,43 @@ mod tests {
|
|||||||
Arc::new(crate::attachments::AttachmentStore::open(root).expect("store opens"))
|
Arc::new(crate::attachments::AttachmentStore::open(root).expect("store opens"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The standard extension set over the tests' throwaway stores —
|
||||||
|
/// the same chain the daemon assembles, built per test so each
|
||||||
|
/// assertion runs against its own tables.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn test_extensions(
|
||||||
|
hub: Arc<EventHub>,
|
||||||
|
watches: Arc<ClawStore>,
|
||||||
|
notifications: Arc<NotificationStore>,
|
||||||
|
tasks: Arc<TaskStore>,
|
||||||
|
jobs: Arc<crate::job_store::CodingJobStore>,
|
||||||
|
accounts: Arc<dyn rhema_identity::IdentityStore>,
|
||||||
|
consents: Arc<crate::consents::ConsentStore>,
|
||||||
|
bridge: Arc<crate::consents::WaitBridge>,
|
||||||
|
attachments: Arc<crate::attachments::AttachmentStore>,
|
||||||
|
phone: Arc<crate::phone::PhoneStore>,
|
||||||
|
identity: Arc<crate::identity::IdentityStore>,
|
||||||
|
workspace: &Path,
|
||||||
|
) -> Arc<crate::extension::Extensions> {
|
||||||
|
Arc::new(crate::extension::Extensions::standard(
|
||||||
|
crate::extension::ExtensionDeps {
|
||||||
|
watches,
|
||||||
|
accounts,
|
||||||
|
notifications,
|
||||||
|
tasks,
|
||||||
|
jobs,
|
||||||
|
consents,
|
||||||
|
bridge,
|
||||||
|
attachments,
|
||||||
|
phone,
|
||||||
|
identity,
|
||||||
|
hub,
|
||||||
|
workspace: workspace.to_path_buf(),
|
||||||
|
calendars: Vec::new(),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
fn test_registry(provider: Arc<dyn rhema_core::ModelProvider>) -> Arc<BackendRegistry> {
|
fn test_registry(provider: Arc<dyn rhema_core::ModelProvider>) -> Arc<BackendRegistry> {
|
||||||
Arc::new(
|
Arc::new(
|
||||||
BackendRegistry::new(vec![Backend::headless("mock", provider)], None)
|
BackendRegistry::new(vec![Backend::headless("mock", provider)], None)
|
||||||
@@ -778,23 +735,30 @@ mod tests {
|
|||||||
let store = Arc::new(ClawStore::in_memory().expect("store opens"));
|
let store = Arc::new(ClawStore::in_memory().expect("store opens"));
|
||||||
let hub = Arc::new(EventHub::default());
|
let hub = Arc::new(EventHub::default());
|
||||||
let workspace = tempfile::tempdir().expect("tempdir");
|
let workspace = tempfile::tempdir().expect("tempdir");
|
||||||
let handler = InteractiveHandler::new(
|
let notifications = notifications();
|
||||||
test_registry(recorder.clone()),
|
let extensions = test_extensions(
|
||||||
workspace.path().join("interactive"),
|
|
||||||
store,
|
|
||||||
Arc::new(rhema_memory::VolatileStore::new()),
|
|
||||||
Arc::new(Bootstrap::new(workspace.path().join("bootstrap")).expect("store opens")),
|
|
||||||
hub.clone(),
|
hub.clone(),
|
||||||
notifications(),
|
store,
|
||||||
|
notifications.clone(),
|
||||||
tasks(),
|
tasks(),
|
||||||
jobs(),
|
jobs(),
|
||||||
accounts(),
|
accounts(),
|
||||||
crate::config::DEFAULT_MAX_TURN_STEPS,
|
|
||||||
consents(),
|
consents(),
|
||||||
bridge(),
|
bridge(),
|
||||||
attachments(),
|
attachments(),
|
||||||
phone(),
|
phone(),
|
||||||
identity(),
|
identity(),
|
||||||
|
&workspace.path().join("interactive"),
|
||||||
|
);
|
||||||
|
let handler = InteractiveHandler::new(
|
||||||
|
test_registry(recorder.clone()),
|
||||||
|
workspace.path().join("interactive"),
|
||||||
|
Arc::new(rhema_memory::VolatileStore::new()),
|
||||||
|
Arc::new(Bootstrap::new(workspace.path().join("bootstrap")).expect("store opens")),
|
||||||
|
hub.clone(),
|
||||||
|
notifications.clone(),
|
||||||
|
crate::config::DEFAULT_MAX_TURN_STEPS,
|
||||||
|
extensions,
|
||||||
);
|
);
|
||||||
let dispatcher = Arc::new(Dispatcher::new(
|
let dispatcher = Arc::new(Dispatcher::new(
|
||||||
DispatcherConfig {
|
DispatcherConfig {
|
||||||
@@ -857,23 +821,31 @@ mod tests {
|
|||||||
// must name every tool the chain advertises, with its schema —
|
// must name every tool the chain advertises, with its schema —
|
||||||
// what the prompt lists is what the request carries.
|
// what the prompt lists is what the request carries.
|
||||||
let workspace = tempfile::tempdir().expect("tempdir");
|
let workspace = tempfile::tempdir().expect("tempdir");
|
||||||
let handler = InteractiveHandler::new(
|
let hub = Arc::new(EventHub::default());
|
||||||
test_registry(Arc::new(MockProvider::new([reply("hi back")]))),
|
let notifications = notifications();
|
||||||
workspace.path().join("interactive"),
|
let extensions = test_extensions(
|
||||||
|
hub.clone(),
|
||||||
Arc::new(ClawStore::in_memory().expect("store opens")),
|
Arc::new(ClawStore::in_memory().expect("store opens")),
|
||||||
Arc::new(rhema_memory::VolatileStore::new()),
|
notifications.clone(),
|
||||||
Arc::new(Bootstrap::new(workspace.path().join("bootstrap")).expect("store opens")),
|
|
||||||
Arc::new(EventHub::default()),
|
|
||||||
notifications(),
|
|
||||||
tasks(),
|
tasks(),
|
||||||
jobs(),
|
jobs(),
|
||||||
accounts(),
|
accounts(),
|
||||||
crate::config::DEFAULT_MAX_TURN_STEPS,
|
|
||||||
consents(),
|
consents(),
|
||||||
bridge(),
|
bridge(),
|
||||||
attachments(),
|
attachments(),
|
||||||
phone(),
|
phone(),
|
||||||
identity(),
|
identity(),
|
||||||
|
&workspace.path().join("interactive"),
|
||||||
|
);
|
||||||
|
let handler = InteractiveHandler::new(
|
||||||
|
test_registry(Arc::new(MockProvider::new([reply("hi back")]))),
|
||||||
|
workspace.path().join("interactive"),
|
||||||
|
Arc::new(rhema_memory::VolatileStore::new()),
|
||||||
|
Arc::new(Bootstrap::new(workspace.path().join("bootstrap")).expect("store opens")),
|
||||||
|
hub.clone(),
|
||||||
|
notifications.clone(),
|
||||||
|
crate::config::DEFAULT_MAX_TURN_STEPS,
|
||||||
|
extensions,
|
||||||
);
|
);
|
||||||
let specs = handler.resolver(&Key::from("interactive:web")).specs();
|
let specs = handler.resolver(&Key::from("interactive:web")).specs();
|
||||||
assert!(
|
assert!(
|
||||||
@@ -1120,23 +1092,30 @@ mod tests {
|
|||||||
let store = Arc::new(ClawStore::in_memory().expect("store opens"));
|
let store = Arc::new(ClawStore::in_memory().expect("store opens"));
|
||||||
let hub = Arc::new(EventHub::default());
|
let hub = Arc::new(EventHub::default());
|
||||||
let workspace = tempfile::tempdir().expect("tempdir");
|
let workspace = tempfile::tempdir().expect("tempdir");
|
||||||
let handler = InteractiveHandler::new(
|
let notifications = notifications();
|
||||||
test_registry(Arc::new(MockProvider::new([reply("hi back")]))),
|
let extensions = test_extensions(
|
||||||
workspace.path().join("interactive"),
|
|
||||||
store,
|
|
||||||
Arc::new(rhema_memory::VolatileStore::new()),
|
|
||||||
Arc::new(Bootstrap::new(workspace.path().join("bootstrap")).expect("store opens")),
|
|
||||||
hub.clone(),
|
hub.clone(),
|
||||||
notifications(),
|
store,
|
||||||
|
notifications.clone(),
|
||||||
tasks(),
|
tasks(),
|
||||||
jobs(),
|
jobs(),
|
||||||
accounts(),
|
accounts(),
|
||||||
crate::config::DEFAULT_MAX_TURN_STEPS,
|
|
||||||
consents(),
|
consents(),
|
||||||
bridge(),
|
bridge(),
|
||||||
attachments(),
|
attachments(),
|
||||||
phone(),
|
phone(),
|
||||||
identity(),
|
identity(),
|
||||||
|
&workspace.path().join("interactive"),
|
||||||
|
);
|
||||||
|
let handler = InteractiveHandler::new(
|
||||||
|
test_registry(Arc::new(MockProvider::new([reply("hi back")]))),
|
||||||
|
workspace.path().join("interactive"),
|
||||||
|
Arc::new(rhema_memory::VolatileStore::new()),
|
||||||
|
Arc::new(Bootstrap::new(workspace.path().join("bootstrap")).expect("store opens")),
|
||||||
|
hub.clone(),
|
||||||
|
notifications.clone(),
|
||||||
|
crate::config::DEFAULT_MAX_TURN_STEPS,
|
||||||
|
extensions,
|
||||||
);
|
);
|
||||||
|
|
||||||
let dispatcher = Arc::new(Dispatcher::new(
|
let dispatcher = Arc::new(Dispatcher::new(
|
||||||
@@ -1199,6 +1178,21 @@ mod tests {
|
|||||||
let hub = Arc::new(EventHub::default());
|
let hub = Arc::new(EventHub::default());
|
||||||
let workspace = tempfile::tempdir().expect("tempdir");
|
let workspace = tempfile::tempdir().expect("tempdir");
|
||||||
let workspace_path = workspace.path().join("interactive");
|
let workspace_path = workspace.path().join("interactive");
|
||||||
|
let notifications = notifications();
|
||||||
|
let extensions = test_extensions(
|
||||||
|
hub.clone(),
|
||||||
|
store,
|
||||||
|
notifications.clone(),
|
||||||
|
tasks(),
|
||||||
|
jobs(),
|
||||||
|
accounts(),
|
||||||
|
consents(),
|
||||||
|
bridge(),
|
||||||
|
attachments(),
|
||||||
|
phone(),
|
||||||
|
identity(),
|
||||||
|
&workspace_path,
|
||||||
|
);
|
||||||
let handler = InteractiveHandler::new(
|
let handler = InteractiveHandler::new(
|
||||||
test_registry(Arc::new(MockProvider::new([
|
test_registry(Arc::new(MockProvider::new([
|
||||||
scripted_call(
|
scripted_call(
|
||||||
@@ -1211,20 +1205,12 @@ mod tests {
|
|||||||
reply("the release channel is #deploys."),
|
reply("the release channel is #deploys."),
|
||||||
]))),
|
]))),
|
||||||
workspace_path.clone(),
|
workspace_path.clone(),
|
||||||
store,
|
|
||||||
memory.clone(),
|
memory.clone(),
|
||||||
Arc::new(Bootstrap::new(workspace.path().join("bootstrap")).expect("store opens")),
|
Arc::new(Bootstrap::new(workspace.path().join("bootstrap")).expect("store opens")),
|
||||||
hub,
|
hub.clone(),
|
||||||
notifications(),
|
notifications.clone(),
|
||||||
tasks(),
|
|
||||||
jobs(),
|
|
||||||
accounts(),
|
|
||||||
crate::config::DEFAULT_MAX_TURN_STEPS,
|
crate::config::DEFAULT_MAX_TURN_STEPS,
|
||||||
consents(),
|
extensions,
|
||||||
bridge(),
|
|
||||||
attachments(),
|
|
||||||
phone(),
|
|
||||||
identity(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let dispatcher = Arc::new(Dispatcher::new(
|
let dispatcher = Arc::new(Dispatcher::new(
|
||||||
@@ -1292,6 +1278,21 @@ mod tests {
|
|||||||
let store = Arc::new(ClawStore::in_memory().expect("store opens"));
|
let store = Arc::new(ClawStore::in_memory().expect("store opens"));
|
||||||
let hub = Arc::new(EventHub::default());
|
let hub = Arc::new(EventHub::default());
|
||||||
let workspace = tempfile::tempdir().expect("tempdir");
|
let workspace = tempfile::tempdir().expect("tempdir");
|
||||||
|
let notifications = notifications();
|
||||||
|
let extensions = test_extensions(
|
||||||
|
hub.clone(),
|
||||||
|
store,
|
||||||
|
notifications.clone(),
|
||||||
|
tasks(),
|
||||||
|
jobs(),
|
||||||
|
accounts(),
|
||||||
|
consents(),
|
||||||
|
bridge(),
|
||||||
|
attachments(),
|
||||||
|
phone(),
|
||||||
|
identity(),
|
||||||
|
&workspace.path().join("interactive"),
|
||||||
|
);
|
||||||
let handler = InteractiveHandler::new(
|
let handler = InteractiveHandler::new(
|
||||||
test_registry(Arc::new(MockProvider::new([
|
test_registry(Arc::new(MockProvider::new([
|
||||||
scripted_call(
|
scripted_call(
|
||||||
@@ -1308,20 +1309,12 @@ mod tests {
|
|||||||
reply("setup is done."),
|
reply("setup is done."),
|
||||||
]))),
|
]))),
|
||||||
workspace.path().join("interactive"),
|
workspace.path().join("interactive"),
|
||||||
store,
|
|
||||||
Arc::new(rhema_memory::VolatileStore::new()),
|
Arc::new(rhema_memory::VolatileStore::new()),
|
||||||
bootstrap.clone(),
|
bootstrap.clone(),
|
||||||
hub,
|
hub.clone(),
|
||||||
notifications(),
|
notifications.clone(),
|
||||||
tasks(),
|
|
||||||
jobs(),
|
|
||||||
accounts(),
|
|
||||||
crate::config::DEFAULT_MAX_TURN_STEPS,
|
crate::config::DEFAULT_MAX_TURN_STEPS,
|
||||||
consents(),
|
extensions,
|
||||||
bridge(),
|
|
||||||
attachments(),
|
|
||||||
phone(),
|
|
||||||
identity(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let dispatcher = Arc::new(Dispatcher::new(
|
let dispatcher = Arc::new(Dispatcher::new(
|
||||||
@@ -1378,23 +1371,30 @@ mod tests {
|
|||||||
let store = Arc::new(ClawStore::in_memory().expect("store opens"));
|
let store = Arc::new(ClawStore::in_memory().expect("store opens"));
|
||||||
let hub = Arc::new(EventHub::default());
|
let hub = Arc::new(EventHub::default());
|
||||||
let workspace = tempfile::tempdir().expect("tempdir");
|
let workspace = tempfile::tempdir().expect("tempdir");
|
||||||
let handler = InteractiveHandler::new(
|
let notifications = notifications();
|
||||||
test_registry(Arc::new(MockProvider::new([reply("hi back")]))),
|
let extensions = test_extensions(
|
||||||
workspace.path().join("interactive"),
|
hub.clone(),
|
||||||
store,
|
store,
|
||||||
Arc::new(rhema_memory::VolatileStore::new()),
|
notifications.clone(),
|
||||||
Arc::new(Bootstrap::new(workspace.path().join("bootstrap")).expect("store opens")),
|
|
||||||
hub,
|
|
||||||
notifications(),
|
|
||||||
tasks(),
|
tasks(),
|
||||||
jobs(),
|
jobs(),
|
||||||
accounts(),
|
accounts(),
|
||||||
crate::config::DEFAULT_MAX_TURN_STEPS,
|
|
||||||
consents(),
|
consents(),
|
||||||
bridge(),
|
bridge(),
|
||||||
attachments(),
|
attachments(),
|
||||||
phone(),
|
phone(),
|
||||||
identity(),
|
identity(),
|
||||||
|
&workspace.path().join("interactive"),
|
||||||
|
);
|
||||||
|
let handler = InteractiveHandler::new(
|
||||||
|
test_registry(Arc::new(MockProvider::new([reply("hi back")]))),
|
||||||
|
workspace.path().join("interactive"),
|
||||||
|
Arc::new(rhema_memory::VolatileStore::new()),
|
||||||
|
Arc::new(Bootstrap::new(workspace.path().join("bootstrap")).expect("store opens")),
|
||||||
|
hub.clone(),
|
||||||
|
notifications.clone(),
|
||||||
|
crate::config::DEFAULT_MAX_TURN_STEPS,
|
||||||
|
extensions,
|
||||||
);
|
);
|
||||||
let backing = Arc::new(VolatileStore::new());
|
let backing = Arc::new(VolatileStore::new());
|
||||||
let dispatcher = Arc::new(Dispatcher::new(
|
let dispatcher = Arc::new(Dispatcher::new(
|
||||||
@@ -1450,26 +1450,32 @@ mod tests {
|
|||||||
let hub = Arc::new(EventHub::default());
|
let hub = Arc::new(EventHub::default());
|
||||||
let workspace = tempfile::tempdir().expect("tempdir");
|
let workspace = tempfile::tempdir().expect("tempdir");
|
||||||
let backing = Arc::new(VolatileStore::new());
|
let backing = Arc::new(VolatileStore::new());
|
||||||
|
let extensions = test_extensions(
|
||||||
|
hub.clone(),
|
||||||
|
store,
|
||||||
|
notifications.clone(),
|
||||||
|
tasks(),
|
||||||
|
jobs(),
|
||||||
|
accounts(),
|
||||||
|
consents(),
|
||||||
|
bridge(),
|
||||||
|
attachments(),
|
||||||
|
phone(),
|
||||||
|
identity(),
|
||||||
|
&workspace.path().join("interactive"),
|
||||||
|
);
|
||||||
let handler = Arc::new(InteractiveHandler::new(
|
let handler = Arc::new(InteractiveHandler::new(
|
||||||
test_registry(Arc::new(MockProvider::new([
|
test_registry(Arc::new(MockProvider::new([
|
||||||
reply("the bread is done."),
|
reply("the bread is done."),
|
||||||
reply("hi back"),
|
reply("hi back"),
|
||||||
]))),
|
]))),
|
||||||
workspace.path().join("interactive"),
|
workspace.path().join("interactive"),
|
||||||
store,
|
|
||||||
Arc::new(rhema_memory::VolatileStore::new()),
|
Arc::new(rhema_memory::VolatileStore::new()),
|
||||||
Arc::new(Bootstrap::new(workspace.path().join("bootstrap")).expect("store opens")),
|
Arc::new(Bootstrap::new(workspace.path().join("bootstrap")).expect("store opens")),
|
||||||
hub,
|
hub.clone(),
|
||||||
notifications.clone(),
|
notifications.clone(),
|
||||||
tasks(),
|
|
||||||
jobs(),
|
|
||||||
accounts(),
|
|
||||||
crate::config::DEFAULT_MAX_TURN_STEPS,
|
crate::config::DEFAULT_MAX_TURN_STEPS,
|
||||||
consents(),
|
extensions,
|
||||||
bridge(),
|
|
||||||
attachments(),
|
|
||||||
phone(),
|
|
||||||
identity(),
|
|
||||||
));
|
));
|
||||||
|
|
||||||
let dispatcher = Arc::new(Dispatcher::new(
|
let dispatcher = Arc::new(Dispatcher::new(
|
||||||
@@ -1566,26 +1572,33 @@ mod tests {
|
|||||||
let store = Arc::new(ClawStore::in_memory().expect("store opens"));
|
let store = Arc::new(ClawStore::in_memory().expect("store opens"));
|
||||||
let hub = Arc::new(EventHub::default());
|
let hub = Arc::new(EventHub::default());
|
||||||
let workspace = tempfile::tempdir().expect("tempdir");
|
let workspace = tempfile::tempdir().expect("tempdir");
|
||||||
|
let notifications = notifications();
|
||||||
|
let extensions = test_extensions(
|
||||||
|
hub.clone(),
|
||||||
|
store,
|
||||||
|
notifications.clone(),
|
||||||
|
tasks(),
|
||||||
|
jobs(),
|
||||||
|
accounts(),
|
||||||
|
consents(),
|
||||||
|
bridge(),
|
||||||
|
attachments(),
|
||||||
|
phone(),
|
||||||
|
identity(),
|
||||||
|
&workspace.path().join("interactive"),
|
||||||
|
);
|
||||||
let handler = InteractiveHandler::new(
|
let handler = InteractiveHandler::new(
|
||||||
test_registry(Arc::new(MockProvider::new([
|
test_registry(Arc::new(MockProvider::new([
|
||||||
reply("noted, leaving early."),
|
reply("noted, leaving early."),
|
||||||
reply("evening summary: shipped the claw."),
|
reply("evening summary: shipped the claw."),
|
||||||
]))),
|
]))),
|
||||||
workspace.path().join("interactive"),
|
workspace.path().join("interactive"),
|
||||||
store,
|
|
||||||
Arc::new(rhema_memory::VolatileStore::new()),
|
Arc::new(rhema_memory::VolatileStore::new()),
|
||||||
Arc::new(Bootstrap::new(workspace.path().join("bootstrap")).expect("store opens")),
|
Arc::new(Bootstrap::new(workspace.path().join("bootstrap")).expect("store opens")),
|
||||||
hub,
|
hub.clone(),
|
||||||
notifications(),
|
notifications.clone(),
|
||||||
tasks(),
|
|
||||||
jobs(),
|
|
||||||
accounts(),
|
|
||||||
crate::config::DEFAULT_MAX_TURN_STEPS,
|
crate::config::DEFAULT_MAX_TURN_STEPS,
|
||||||
consents(),
|
extensions,
|
||||||
bridge(),
|
|
||||||
attachments(),
|
|
||||||
phone(),
|
|
||||||
identity(),
|
|
||||||
);
|
);
|
||||||
let backing = Arc::new(VolatileStore::new());
|
let backing = Arc::new(VolatileStore::new());
|
||||||
let dispatcher = Arc::new(Dispatcher::new(
|
let dispatcher = Arc::new(Dispatcher::new(
|
||||||
|
|||||||
@@ -45,6 +45,9 @@
|
|||||||
//! - [`watch_tools`] — the agent's watch-management tool set
|
//! - [`watch_tools`] — the agent's watch-management tool set
|
||||||
//! - [`notify_tools`] — the `notify` tool: the queue a device polls
|
//! - [`notify_tools`] — the `notify` tool: the queue a device polls
|
||||||
//! - [`task_tools`] — `schedule_task`, `list_tasks`, `cancel_task`
|
//! - [`task_tools`] — `schedule_task`, `list_tasks`, `cancel_task`
|
||||||
|
//! - [`extension`] — the extension seam: the `ClawExtension` trait and
|
||||||
|
//! the standard set — the tool chain, the detect passes, and the
|
||||||
|
//! boot pass all iterate it
|
||||||
//! - [`forge`] — the sources: one detectable stream per forge kind
|
//! - [`forge`] — the sources: one detectable stream per forge kind
|
||||||
//! - [`calendar`] — the [`CalendarSource`]: due calendar reminders in,
|
//! - [`calendar`] — the [`CalendarSource`]: due calendar reminders in,
|
||||||
//! events out
|
//! events out
|
||||||
@@ -71,6 +74,7 @@ pub mod config;
|
|||||||
pub mod consents;
|
pub mod consents;
|
||||||
pub mod daemon;
|
pub mod daemon;
|
||||||
pub mod environment;
|
pub mod environment;
|
||||||
|
pub mod extension;
|
||||||
pub mod forge;
|
pub mod forge;
|
||||||
pub mod handler;
|
pub mod handler;
|
||||||
pub mod http;
|
pub mod http;
|
||||||
@@ -104,6 +108,7 @@ pub use calendar::CalendarSource;
|
|||||||
pub use config::{ClawConfig, ConfigError, ForgeKind, Watch};
|
pub use config::{ClawConfig, ConfigError, ForgeKind, Watch};
|
||||||
pub use daemon::{ClawError, Daemon, TickReport, run_once};
|
pub use daemon::{ClawError, Daemon, TickReport, run_once};
|
||||||
pub use environment::write_report;
|
pub use environment::write_report;
|
||||||
|
pub use extension::{ClawExtension, ExtensionDeps, Extensions, Lanes};
|
||||||
pub use forge::ForgeSource;
|
pub use forge::ForgeSource;
|
||||||
pub use handler::ClawHandler;
|
pub use handler::ClawHandler;
|
||||||
pub use hub::{EventHub, TurnEvent};
|
pub use hub::{EventHub, TurnEvent};
|
||||||
|
|||||||
Reference in new issue
Block a user