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 {
|
||||
/// 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.
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
|
||||
@@ -30,13 +30,11 @@ use rhema_rsd::{
|
||||
SystemClock,
|
||||
};
|
||||
|
||||
use crate::calendar::CalendarSource;
|
||||
use crate::config::{ClawConfig, ConfigError, ForgeKind};
|
||||
use crate::forge::ForgeSource;
|
||||
use crate::config::{ClawConfig, ConfigError};
|
||||
use crate::extension::{ExtensionDeps, Extensions, Lanes};
|
||||
use crate::handler::ClawHandler;
|
||||
use crate::http::ClawState;
|
||||
use crate::notifications::NotificationStore;
|
||||
use crate::task_source::TaskScheduler;
|
||||
use crate::task_store::TaskStore;
|
||||
use rhema_memory::FileMemoryStore;
|
||||
|
||||
@@ -125,15 +123,16 @@ fn interactive_dispatch() -> DispatcherConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// The resident daemon: one source per forge kind, the calendar
|
||||
/// reminders, the task scheduler, the coding-job source, the three
|
||||
/// drivers, and the HTTP surface.
|
||||
/// The resident daemon: the extension set's sources (the forge watches,
|
||||
/// the task scheduler, the coding jobs, the calendar reminders), the
|
||||
/// three drivers, and the HTTP surface.
|
||||
pub struct Daemon {
|
||||
sources: Vec<ForgeSource>,
|
||||
calendar_source: CalendarSource,
|
||||
scheduler: TaskScheduler,
|
||||
job_source: crate::jobs::JobsSource,
|
||||
jobs: Arc<crate::job_store::CodingJobStore>,
|
||||
/// The extension set: the boot pass runs its hooks, and the tick
|
||||
/// detects through the sources collected from it at assembly.
|
||||
extensions: Arc<Extensions>,
|
||||
/// The set's sources, in set order, each registered on the lane it
|
||||
/// feeds; built once, at assembly.
|
||||
sources: Vec<Box<dyn Source>>,
|
||||
forge_dispatcher: Arc<Dispatcher>,
|
||||
interactive_dispatcher: Arc<Dispatcher>,
|
||||
jobs_dispatcher: Arc<Dispatcher>,
|
||||
@@ -183,6 +182,11 @@ impl Daemon {
|
||||
let notifications = Arc::new(NotificationStore::open(&db)?);
|
||||
let tasks = Arc::new(TaskStore::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
|
||||
// drives, jobs the jobs dispatcher answers.
|
||||
let jobs = Arc::new(crate::job_store::CodingJobStore::open(&db)?);
|
||||
@@ -221,23 +225,6 @@ impl Daemon {
|
||||
.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
|
||||
// dispatch time, so a watch added mid-run is answerable without a
|
||||
// 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
|
||||
// asks through them, the consent routes answer through them.
|
||||
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,
|
||||
// and regenerated over whatever the last boot wrote.
|
||||
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(
|
||||
backends.clone(),
|
||||
interactive_workspace,
|
||||
watches.clone(),
|
||||
memory,
|
||||
bootstrap.clone(),
|
||||
hub.clone(),
|
||||
notifications.clone(),
|
||||
tasks.clone(),
|
||||
jobs.clone(),
|
||||
accounts.clone(),
|
||||
config.max_turn_steps,
|
||||
consents.clone(),
|
||||
bridge.clone(),
|
||||
attachments.clone(),
|
||||
phone.clone(),
|
||||
identity.clone(),
|
||||
extensions.clone(),
|
||||
);
|
||||
let interactive_driver = Arc::new(
|
||||
Driver::new(
|
||||
@@ -350,24 +353,7 @@ impl Daemon {
|
||||
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);
|
||||
// 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(
|
||||
interactive_dispatcher.clone(),
|
||||
@@ -389,11 +375,8 @@ impl Daemon {
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
extensions,
|
||||
sources,
|
||||
calendar_source,
|
||||
scheduler,
|
||||
job_source,
|
||||
jobs,
|
||||
forge_dispatcher,
|
||||
interactive_dispatcher,
|
||||
jobs_dispatcher,
|
||||
@@ -435,13 +418,10 @@ impl Daemon {
|
||||
/// top of the next dispatch (safe tools re-run, the rest report
|
||||
/// interrupted). Idempotent; called before the first pass.
|
||||
pub async fn bootstrap(&self) -> Result<(), ClawError> {
|
||||
// The job rows first: a turn the restart cut is failed
|
||||
// interrupted before anything re-runs, and a still-queued row
|
||||
// is re-armed with a fresh dedup id.
|
||||
let touched = self.jobs.recover()?;
|
||||
if touched > 0 {
|
||||
eprintln!("[claw] jobs: {touched} row(s) recovered at boot");
|
||||
}
|
||||
// The extension boot hooks first: a coding job the restart cut
|
||||
// is failed interrupted before anything re-runs, and a
|
||||
// still-queued row is re-armed with a fresh dedup id.
|
||||
self.extensions.boot()?;
|
||||
self.forge_dispatcher.recover_all()?;
|
||||
self.jobs_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
|
||||
/// the key for a pass like this one.
|
||||
pub async fn tick(&self) -> Result<TickReport, ClawError> {
|
||||
let mut detected = self.scheduler.detect().await?;
|
||||
detected += self.calendar_source.detect().await?;
|
||||
detected += self.job_source.detect().await?;
|
||||
// The tick only detects — every timer here drives
|
||||
// deterministic detection, and the set's order is the contract:
|
||||
// due tasks before the calendar reminders that fire for them.
|
||||
let mut detected = 0usize;
|
||||
for source in &self.sources {
|
||||
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.
|
||||
//!
|
||||
//! Deterministic setup first (spec §1, §8): the allowlist — permissive,
|
||||
//! v1's starting point — and the resolver chain (the shell toolset over
|
||||
//! the interactive workspace, the memory tools, `bootstrap_save`, the
|
||||
//! notify tool, the task tools, and the watch tools) are assembled
|
||||
//! before the first model call; the model never decides its own
|
||||
//! permissions. The system prompt is rendered per dispatch from the
|
||||
//! v1's starting point — and the resolver chain are assembled before
|
||||
//! the first model call; the model never decides its own permissions.
|
||||
//! The chain is the core toolsets (the shell over the interactive
|
||||
//! workspace, memory, `bootstrap_save`) plus every
|
||||
//! [`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.
|
||||
|
||||
use std::collections::HashMap;
|
||||
@@ -67,7 +68,6 @@ use crate::backends::BackendRegistry;
|
||||
use crate::bootstrap::{Bootstrap, DEFAULT_SYSTEM_MD, render_system};
|
||||
use crate::hub::{EventHub, TurnEvent};
|
||||
use crate::notifications::NotificationStore;
|
||||
use crate::watch_store::ClawStore;
|
||||
|
||||
/// The key prefixes of the resident context (see the module docs):
|
||||
/// `interactive:`, `task:`, `phone:`, and `calendar:`. Forge keys —
|
||||
@@ -158,16 +158,6 @@ pub struct InteractiveHandler {
|
||||
backends: Arc<BackendRegistry>,
|
||||
workspace: PathBuf,
|
||||
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
|
||||
/// resolver is built per dispatch so its scope tracks the workspace.
|
||||
memory: Arc<dyn MemoryStore>,
|
||||
@@ -179,98 +169,52 @@ pub struct InteractiveHandler {
|
||||
/// The notification table: the `notify` tool writes it, and
|
||||
/// non-interactive replies auto-notify through it.
|
||||
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
|
||||
/// config's runaway guard for an agent meant to work unattended; a
|
||||
/// follow-up message keeps going.
|
||||
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 {
|
||||
/// A handler whose chat turns run in `workspace` (the interactive
|
||||
/// scratch space under the daemon's workspace root), whose watch
|
||||
/// tools mutate `watches`, whose notifications land in
|
||||
/// `notifications`, whose task tools manage `tasks`, and whose
|
||||
/// `coding_agent` tool enqueues jobs into `jobs`. Approval is
|
||||
/// v1-permissive — the shell is the point of a resident coding
|
||||
/// agent — installed once, since resolution is synchronous within a
|
||||
/// turn.
|
||||
#[allow(clippy::too_many_arguments)] // sixteen parts, each load-bearing
|
||||
/// scratch space under the daemon's workspace root) and whose tool
|
||||
/// half of the resolver chain comes from `extensions` — the same
|
||||
/// set whose sources the daemon's tick drives and whose boot pass
|
||||
/// runs at assembly. The core toolsets are the handler's own: the
|
||||
/// shell (approval v1-permissive — the shell is the point of a
|
||||
/// resident coding agent — installed once, since resolution is
|
||||
/// synchronous within a turn), memory, and `bootstrap_save`.
|
||||
#[allow(clippy::too_many_arguments)] // eight parts, each load-bearing
|
||||
pub fn new(
|
||||
backends: Arc<BackendRegistry>,
|
||||
workspace: impl Into<PathBuf>,
|
||||
watches: Arc<ClawStore>,
|
||||
memory: Arc<dyn MemoryStore>,
|
||||
bootstrap: Arc<Bootstrap>,
|
||||
hub: Arc<EventHub>,
|
||||
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,
|
||||
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>,
|
||||
extensions: Arc<crate::extension::Extensions>,
|
||||
) -> Self {
|
||||
let workspace = workspace.into();
|
||||
let tools = Arc::new(ShellTools::new(&workspace));
|
||||
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 =
|
||||
crate::bootstrap::registry(bootstrap.clone()).resolver_for(&Allowlist::permissive());
|
||||
let identity_resolver = crate::identity::registry(identity.clone(), workspace.clone())
|
||||
.resolver_for(&Allowlist::permissive());
|
||||
Self {
|
||||
backends,
|
||||
workspace,
|
||||
tools,
|
||||
job_resolver,
|
||||
watch_resolver,
|
||||
accounts_resolver,
|
||||
identity_exec,
|
||||
notify_resolver,
|
||||
task_resolver,
|
||||
memory,
|
||||
bootstrap,
|
||||
bootstrap_resolver,
|
||||
identity_resolver,
|
||||
hub,
|
||||
notifications,
|
||||
consents,
|
||||
bridge,
|
||||
attachments,
|
||||
phone,
|
||||
max_turn_steps,
|
||||
extensions,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,49 +251,24 @@ impl InteractiveHandler {
|
||||
.map_err(|err| format!("cannot build the interactive agent: {err}"))
|
||||
}
|
||||
|
||||
/// The resolver chain one dispatch runs under: the shell toolset
|
||||
/// over the interactive workspace, then the resident toolsets. The
|
||||
/// consent and attachment tools build per dispatch — their asks and
|
||||
/// cards must land on the session that asked, and the handler
|
||||
/// The resolver chain one dispatch runs under: the core toolsets —
|
||||
/// the shell over the interactive workspace, memory,
|
||||
/// `bootstrap_save` — then every extension's tools in the standard
|
||||
/// 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.
|
||||
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(),
|
||||
Arc::new(MemoryResolver::new(
|
||||
self.memory.clone(),
|
||||
Scope::Workspace(self.workspace.clone()),
|
||||
)),
|
||||
self.bootstrap_resolver.clone(),
|
||||
self.notify_resolver.clone(),
|
||||
self.task_resolver.clone(),
|
||||
self.job_resolver.clone(),
|
||||
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()),
|
||||
]))
|
||||
];
|
||||
resolvers.extend(self.extensions.tools(key));
|
||||
Arc::new(ResolverChain::new(resolvers))
|
||||
}
|
||||
|
||||
/// One dispatch: drain, restore, turn, stash, reply.
|
||||
@@ -634,6 +553,7 @@ mod tests {
|
||||
|
||||
use crate::notifications::NotificationStore;
|
||||
use crate::task_store::TaskStore;
|
||||
use crate::watch_store::ClawStore;
|
||||
|
||||
use crate::backends::{Backend, BackendRegistry};
|
||||
use rhema_core::{
|
||||
@@ -713,6 +633,43 @@ mod tests {
|
||||
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> {
|
||||
Arc::new(
|
||||
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 hub = Arc::new(EventHub::default());
|
||||
let workspace = tempfile::tempdir().expect("tempdir");
|
||||
let handler = InteractiveHandler::new(
|
||||
test_registry(recorder.clone()),
|
||||
workspace.path().join("interactive"),
|
||||
store,
|
||||
Arc::new(rhema_memory::VolatileStore::new()),
|
||||
Arc::new(Bootstrap::new(workspace.path().join("bootstrap")).expect("store opens")),
|
||||
let notifications = notifications();
|
||||
let extensions = test_extensions(
|
||||
hub.clone(),
|
||||
notifications(),
|
||||
store,
|
||||
notifications.clone(),
|
||||
tasks(),
|
||||
jobs(),
|
||||
accounts(),
|
||||
crate::config::DEFAULT_MAX_TURN_STEPS,
|
||||
consents(),
|
||||
bridge(),
|
||||
attachments(),
|
||||
phone(),
|
||||
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(
|
||||
DispatcherConfig {
|
||||
@@ -857,23 +821,31 @@ mod tests {
|
||||
// must name every tool the chain advertises, with its schema —
|
||||
// what the prompt lists is what the request carries.
|
||||
let workspace = tempfile::tempdir().expect("tempdir");
|
||||
let handler = InteractiveHandler::new(
|
||||
test_registry(Arc::new(MockProvider::new([reply("hi back")]))),
|
||||
workspace.path().join("interactive"),
|
||||
let hub = Arc::new(EventHub::default());
|
||||
let notifications = notifications();
|
||||
let extensions = test_extensions(
|
||||
hub.clone(),
|
||||
Arc::new(ClawStore::in_memory().expect("store opens")),
|
||||
Arc::new(rhema_memory::VolatileStore::new()),
|
||||
Arc::new(Bootstrap::new(workspace.path().join("bootstrap")).expect("store opens")),
|
||||
Arc::new(EventHub::default()),
|
||||
notifications(),
|
||||
notifications.clone(),
|
||||
tasks(),
|
||||
jobs(),
|
||||
accounts(),
|
||||
crate::config::DEFAULT_MAX_TURN_STEPS,
|
||||
consents(),
|
||||
bridge(),
|
||||
attachments(),
|
||||
phone(),
|
||||
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();
|
||||
assert!(
|
||||
@@ -1120,23 +1092,30 @@ mod tests {
|
||||
let store = Arc::new(ClawStore::in_memory().expect("store opens"));
|
||||
let hub = Arc::new(EventHub::default());
|
||||
let workspace = tempfile::tempdir().expect("tempdir");
|
||||
let handler = InteractiveHandler::new(
|
||||
test_registry(Arc::new(MockProvider::new([reply("hi back")]))),
|
||||
workspace.path().join("interactive"),
|
||||
store,
|
||||
Arc::new(rhema_memory::VolatileStore::new()),
|
||||
Arc::new(Bootstrap::new(workspace.path().join("bootstrap")).expect("store opens")),
|
||||
let notifications = notifications();
|
||||
let extensions = test_extensions(
|
||||
hub.clone(),
|
||||
notifications(),
|
||||
store,
|
||||
notifications.clone(),
|
||||
tasks(),
|
||||
jobs(),
|
||||
accounts(),
|
||||
crate::config::DEFAULT_MAX_TURN_STEPS,
|
||||
consents(),
|
||||
bridge(),
|
||||
attachments(),
|
||||
phone(),
|
||||
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(
|
||||
@@ -1199,6 +1178,21 @@ mod tests {
|
||||
let hub = Arc::new(EventHub::default());
|
||||
let workspace = tempfile::tempdir().expect("tempdir");
|
||||
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(
|
||||
test_registry(Arc::new(MockProvider::new([
|
||||
scripted_call(
|
||||
@@ -1211,20 +1205,12 @@ mod tests {
|
||||
reply("the release channel is #deploys."),
|
||||
]))),
|
||||
workspace_path.clone(),
|
||||
store,
|
||||
memory.clone(),
|
||||
Arc::new(Bootstrap::new(workspace.path().join("bootstrap")).expect("store opens")),
|
||||
hub,
|
||||
notifications(),
|
||||
tasks(),
|
||||
jobs(),
|
||||
accounts(),
|
||||
hub.clone(),
|
||||
notifications.clone(),
|
||||
crate::config::DEFAULT_MAX_TURN_STEPS,
|
||||
consents(),
|
||||
bridge(),
|
||||
attachments(),
|
||||
phone(),
|
||||
identity(),
|
||||
extensions,
|
||||
);
|
||||
|
||||
let dispatcher = Arc::new(Dispatcher::new(
|
||||
@@ -1292,6 +1278,21 @@ mod tests {
|
||||
let store = Arc::new(ClawStore::in_memory().expect("store opens"));
|
||||
let hub = Arc::new(EventHub::default());
|
||||
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(
|
||||
test_registry(Arc::new(MockProvider::new([
|
||||
scripted_call(
|
||||
@@ -1308,20 +1309,12 @@ mod tests {
|
||||
reply("setup is done."),
|
||||
]))),
|
||||
workspace.path().join("interactive"),
|
||||
store,
|
||||
Arc::new(rhema_memory::VolatileStore::new()),
|
||||
bootstrap.clone(),
|
||||
hub,
|
||||
notifications(),
|
||||
tasks(),
|
||||
jobs(),
|
||||
accounts(),
|
||||
hub.clone(),
|
||||
notifications.clone(),
|
||||
crate::config::DEFAULT_MAX_TURN_STEPS,
|
||||
consents(),
|
||||
bridge(),
|
||||
attachments(),
|
||||
phone(),
|
||||
identity(),
|
||||
extensions,
|
||||
);
|
||||
|
||||
let dispatcher = Arc::new(Dispatcher::new(
|
||||
@@ -1378,23 +1371,30 @@ mod tests {
|
||||
let store = Arc::new(ClawStore::in_memory().expect("store opens"));
|
||||
let hub = Arc::new(EventHub::default());
|
||||
let workspace = tempfile::tempdir().expect("tempdir");
|
||||
let handler = InteractiveHandler::new(
|
||||
test_registry(Arc::new(MockProvider::new([reply("hi back")]))),
|
||||
workspace.path().join("interactive"),
|
||||
let notifications = notifications();
|
||||
let extensions = test_extensions(
|
||||
hub.clone(),
|
||||
store,
|
||||
Arc::new(rhema_memory::VolatileStore::new()),
|
||||
Arc::new(Bootstrap::new(workspace.path().join("bootstrap")).expect("store opens")),
|
||||
hub,
|
||||
notifications(),
|
||||
notifications.clone(),
|
||||
tasks(),
|
||||
jobs(),
|
||||
accounts(),
|
||||
crate::config::DEFAULT_MAX_TURN_STEPS,
|
||||
consents(),
|
||||
bridge(),
|
||||
attachments(),
|
||||
phone(),
|
||||
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 dispatcher = Arc::new(Dispatcher::new(
|
||||
@@ -1450,26 +1450,32 @@ mod tests {
|
||||
let hub = Arc::new(EventHub::default());
|
||||
let workspace = tempfile::tempdir().expect("tempdir");
|
||||
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(
|
||||
test_registry(Arc::new(MockProvider::new([
|
||||
reply("the bread is done."),
|
||||
reply("hi back"),
|
||||
]))),
|
||||
workspace.path().join("interactive"),
|
||||
store,
|
||||
Arc::new(rhema_memory::VolatileStore::new()),
|
||||
Arc::new(Bootstrap::new(workspace.path().join("bootstrap")).expect("store opens")),
|
||||
hub,
|
||||
hub.clone(),
|
||||
notifications.clone(),
|
||||
tasks(),
|
||||
jobs(),
|
||||
accounts(),
|
||||
crate::config::DEFAULT_MAX_TURN_STEPS,
|
||||
consents(),
|
||||
bridge(),
|
||||
attachments(),
|
||||
phone(),
|
||||
identity(),
|
||||
extensions,
|
||||
));
|
||||
|
||||
let dispatcher = Arc::new(Dispatcher::new(
|
||||
@@ -1566,26 +1572,33 @@ mod tests {
|
||||
let store = Arc::new(ClawStore::in_memory().expect("store opens"));
|
||||
let hub = Arc::new(EventHub::default());
|
||||
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(
|
||||
test_registry(Arc::new(MockProvider::new([
|
||||
reply("noted, leaving early."),
|
||||
reply("evening summary: shipped the claw."),
|
||||
]))),
|
||||
workspace.path().join("interactive"),
|
||||
store,
|
||||
Arc::new(rhema_memory::VolatileStore::new()),
|
||||
Arc::new(Bootstrap::new(workspace.path().join("bootstrap")).expect("store opens")),
|
||||
hub,
|
||||
notifications(),
|
||||
tasks(),
|
||||
jobs(),
|
||||
accounts(),
|
||||
hub.clone(),
|
||||
notifications.clone(),
|
||||
crate::config::DEFAULT_MAX_TURN_STEPS,
|
||||
consents(),
|
||||
bridge(),
|
||||
attachments(),
|
||||
phone(),
|
||||
identity(),
|
||||
extensions,
|
||||
);
|
||||
let backing = Arc::new(VolatileStore::new());
|
||||
let dispatcher = Arc::new(Dispatcher::new(
|
||||
|
||||
@@ -45,6 +45,9 @@
|
||||
//! - [`watch_tools`] — the agent's watch-management tool set
|
||||
//! - [`notify_tools`] — the `notify` tool: the queue a device polls
|
||||
//! - [`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
|
||||
//! - [`calendar`] — the [`CalendarSource`]: due calendar reminders in,
|
||||
//! events out
|
||||
@@ -71,6 +74,7 @@ pub mod config;
|
||||
pub mod consents;
|
||||
pub mod daemon;
|
||||
pub mod environment;
|
||||
pub mod extension;
|
||||
pub mod forge;
|
||||
pub mod handler;
|
||||
pub mod http;
|
||||
@@ -104,6 +108,7 @@ pub use calendar::CalendarSource;
|
||||
pub use config::{ClawConfig, ConfigError, ForgeKind, Watch};
|
||||
pub use daemon::{ClawError, Daemon, TickReport, run_once};
|
||||
pub use environment::write_report;
|
||||
pub use extension::{ClawExtension, ExtensionDeps, Extensions, Lanes};
|
||||
pub use forge::ForgeSource;
|
||||
pub use handler::ClawHandler;
|
||||
pub use hub::{EventHub, TurnEvent};
|
||||
|
||||
Reference in new issue
Block a user