use std::collections::HashMap;
use std::sync::Arc;
use oxicode_agent::AgentTool;
use super::installer::BehaviorSessionServices;
use super::ledger::CompatibilityContract;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct BehaviorPackId(pub String);
impl BehaviorPackId {
pub fn coding_omp_v1() -> Self {
BehaviorPackId("coding-omp-v1".to_string())
}
}
impl std::fmt::Display for BehaviorPackId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ToolImplementationId(pub String);
impl std::fmt::Display for ToolImplementationId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CapabilityClass {
FsRead,
FsWrite,
Search,
Process,
Network,
Lsp,
Memory,
Delegation,
Ui,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SideEffectClass {
ReadOnly,
Mutating,
Networked,
ProcessSpawning,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToolStateScope {
Stateless,
HashlineSession,
ShellSession,
EvalKernel,
DebugTarget,
Workspace,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PortRequirementKind {
HashlineSnapshotStore,
LspProvider,
TtsrEngine,
UrlResolver,
SubagentRunner,
MemoryBackend,
TodoStateProvider,
ShellSession,
EvalKernel,
DebugService,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PortRequirement {
pub kind: PortRequirementKind,
pub required: bool,
}
#[derive(Debug, Clone)]
pub struct PromptLayerSpec {
pub id: String,
pub body: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ExtensionKind {
HashlineState,
LspHost,
ShellSession,
EvalKernel,
DebugService,
TtsrEngine,
Delegation,
}
impl ExtensionKind {
pub fn slug(&self) -> &'static str {
match self {
ExtensionKind::HashlineState => "hashline-state",
ExtensionKind::LspHost => "lsp-host",
ExtensionKind::ShellSession => "shell-session",
ExtensionKind::EvalKernel => "eval-kernel",
ExtensionKind::DebugService => "debug-service",
ExtensionKind::TtsrEngine => "ttsr-engine",
ExtensionKind::Delegation => "delegation",
}
}
pub fn port(&self) -> PortRequirementKind {
match self {
ExtensionKind::HashlineState => PortRequirementKind::HashlineSnapshotStore,
ExtensionKind::LspHost => PortRequirementKind::LspProvider,
ExtensionKind::TtsrEngine => PortRequirementKind::TtsrEngine,
ExtensionKind::Delegation => PortRequirementKind::SubagentRunner,
ExtensionKind::ShellSession => PortRequirementKind::ShellSession,
ExtensionKind::EvalKernel => PortRequirementKind::EvalKernel,
ExtensionKind::DebugService => PortRequirementKind::DebugService,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExtensionScope {
SessionWorkspace,
Workspace,
SessionLanguage,
WorkspaceDebugTarget,
Turn,
ChildAgentLifecycle,
}
#[derive(Debug, Clone)]
pub struct RuntimeExtensionSpec {
pub kind: ExtensionKind,
pub scope: ExtensionScope,
pub required: bool,
}
#[derive(Debug, Clone)]
pub struct BehaviorToolDescriptor {
pub id: ToolImplementationId,
pub exposed_name: String,
pub capability: CapabilityClass,
pub side_effect: SideEffectClass,
pub required_ports: Vec<PortRequirement>,
pub state_scope: ToolStateScope,
pub essential: bool,
pub replaces: Option<ToolImplementationId>,
}
impl BehaviorToolDescriptor {
pub fn new(id: &str, exposed_name: &str) -> Self {
BehaviorToolDescriptor {
id: ToolImplementationId(id.to_string()),
exposed_name: exposed_name.to_string(),
capability: CapabilityClass::FsRead,
side_effect: SideEffectClass::ReadOnly,
required_ports: Vec::new(),
state_scope: ToolStateScope::Stateless,
essential: false,
replaces: None,
}
}
pub fn capability(mut self, capability: CapabilityClass) -> Self {
self.capability = capability;
self
}
pub fn side_effect(mut self, side_effect: SideEffectClass) -> Self {
self.side_effect = side_effect;
self
}
pub fn state_scope(mut self, scope: ToolStateScope) -> Self {
self.state_scope = scope;
self
}
pub fn port(mut self, kind: PortRequirementKind, required: bool) -> Self {
self.required_ports.push(PortRequirement { kind, required });
self
}
pub fn essential(mut self) -> Self {
self.essential = true;
self
}
pub fn replaces(mut self, id: &str) -> Self {
self.replaces = Some(ToolImplementationId(id.to_string()));
self
}
}
pub type ToolFactory = Arc<
dyn Fn(&BehaviorSessionServices) -> Result<Arc<dyn AgentTool>, BehaviorInstallError>
+ Send
+ Sync,
>;
#[derive(Clone)]
pub struct BehaviorPack {
pub id: BehaviorPackId,
pub schema_version: u32,
pub prompt_layers: Vec<PromptLayerSpec>,
pub extensions: Vec<RuntimeExtensionSpec>,
pub tools: Vec<BehaviorToolDescriptor>,
pub compatibility: CompatibilityContract,
pub(crate) factories: HashMap<ToolImplementationId, ToolFactory>,
}
#[derive(Debug, Clone)]
pub enum BehaviorInstallError {
UnknownPack(BehaviorPackId),
DuplicatePackId(BehaviorPackId),
UnsupportedSchemaVersion {
pack: BehaviorPackId,
got: u32,
},
DuplicateToolImplementation {
pack: BehaviorPackId,
id: ToolImplementationId,
},
DuplicateExposedName {
exposed_name: String,
existing: ToolImplementationId,
incoming: ToolImplementationId,
},
RequiredExtensionMissing {
pack: BehaviorPackId,
kind: ExtensionKind,
},
RequiredServiceMissing {
descriptor: ToolImplementationId,
kind: PortRequirementKind,
},
FactoryFailed {
descriptor: ToolImplementationId,
reason: String,
},
HostRejected {
descriptor: ToolImplementationId,
exposed_name: String,
reason: String,
},
}
impl std::fmt::Display for BehaviorInstallError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BehaviorInstallError::UnknownPack(id) => write!(f, "unknown behavior pack: {id}"),
BehaviorInstallError::DuplicatePackId(id) => {
write!(f, "behavior pack registered twice: {id}")
}
BehaviorInstallError::UnsupportedSchemaVersion { pack, got } => {
write!(f, "pack {pack} declares unsupported schema version {got}")
}
BehaviorInstallError::DuplicateToolImplementation { pack, id } => {
write!(f, "pack {pack} registers tool implementation twice: {id}")
}
BehaviorInstallError::DuplicateExposedName {
exposed_name,
existing,
incoming,
} => write!(
f,
"duplicate exposed tool name '{exposed_name}': {existing} vs {incoming} (declare `replaces` for a compatible replacement)"
),
BehaviorInstallError::RequiredExtensionMissing { pack, kind } => {
write!(
f,
"pack {pack} requires unavailable extension: {}",
kind.slug()
)
}
BehaviorInstallError::RequiredServiceMissing { descriptor, kind } => {
write!(
f,
"tool {descriptor} requires unavailable service: {kind:?}"
)
}
BehaviorInstallError::FactoryFailed { descriptor, reason } => {
write!(f, "tool factory failed for {descriptor}: {reason}")
}
BehaviorInstallError::HostRejected {
exposed_name,
reason,
..
} => write!(f, "host rejected tool '{exposed_name}': {reason}"),
}
}
}
impl std::error::Error for BehaviorInstallError {}
impl BehaviorPack {
pub fn new(id: BehaviorPackId, target: String) -> Self {
BehaviorPack {
id,
schema_version: 1,
prompt_layers: Vec::new(),
extensions: Vec::new(),
tools: Vec::new(),
compatibility: CompatibilityContract {
target,
entries: Vec::new(),
},
factories: HashMap::new(),
}
}
pub fn with_prompt_layer(mut self, spec: PromptLayerSpec) -> Self {
self.prompt_layers.push(spec);
self
}
pub fn with_extension(mut self, spec: RuntimeExtensionSpec) -> Self {
self.extensions.push(spec);
self
}
pub fn with_compatibility(mut self, compatibility: CompatibilityContract) -> Self {
self.compatibility = compatibility;
self
}
pub fn with_tool(
mut self,
descriptor: BehaviorToolDescriptor,
factory: ToolFactory,
) -> Result<Self, BehaviorInstallError> {
if self.factories.contains_key(&descriptor.id) {
return Err(BehaviorInstallError::DuplicateToolImplementation {
pack: self.id.clone(),
id: descriptor.id.clone(),
});
}
self.factories.insert(descriptor.id.clone(), factory);
self.tools.push(descriptor);
Ok(self)
}
pub(crate) fn factory_for(&self, id: &ToolImplementationId) -> Option<ToolFactory> {
self.factories.get(id).cloned()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn descriptor_builder_sets_fields() {
let d = BehaviorToolDescriptor::new("edit.hashline.v1", "edit")
.capability(CapabilityClass::FsWrite)
.side_effect(SideEffectClass::Mutating)
.state_scope(ToolStateScope::HashlineSession)
.port(PortRequirementKind::HashlineSnapshotStore, true)
.essential();
assert_eq!(d.id.0, "edit.hashline.v1");
assert!(d.essential);
assert_eq!(d.required_ports.len(), 1);
assert!(d.required_ports[0].required);
assert!(d.replaces.is_none());
}
#[test]
fn with_tool_rejects_duplicate_ids() {
let pack = BehaviorPack::new(BehaviorPackId("p".to_string()), "omp@test".to_string());
let mk = || BehaviorToolDescriptor::new("t.v1", "tool");
let factory: ToolFactory = Arc::new(|_| {
Err(BehaviorInstallError::FactoryFailed {
descriptor: ToolImplementationId("t.v1".to_string()),
reason: "unused".to_string(),
})
});
let pack = pack.with_tool(mk(), factory.clone()).unwrap();
assert!(pack.with_tool(mk(), factory).is_err());
}
#[test]
fn extension_slug_and_port_agree() {
assert_eq!(ExtensionKind::ShellSession.slug(), "shell-session");
assert_eq!(
ExtensionKind::ShellSession.port(),
PortRequirementKind::ShellSession
);
assert_eq!(
ExtensionKind::HashlineState.port(),
PortRequirementKind::HashlineSnapshotStore
);
}
#[test]
fn error_display_is_informative() {
let e = BehaviorInstallError::DuplicateExposedName {
exposed_name: "read".to_string(),
existing: ToolImplementationId("read.file.v1".to_string()),
incoming: ToolImplementationId("read.custom.v1".to_string()),
};
let text = e.to_string();
assert!(text.contains("read"));
assert!(text.contains("replaces"));
}
}