Frontend - Large World: collapse org/company/team tiers into one expandable React Flow hierarchy (WorldFlow) with per-click expand, persisted node positions, a compact tree sidebar, wrench multi-select delete across levels, and a sized right slide-out (phone/tablet/full) showing an agent summary + drill button. - Agent page: GitHub-style animated contribution grid (VitalsCard), collapsible System Prompt + Personality cards, restructured anatomy cards, bigger avatar with name/title header row, Markdown/JSON-aware rendering, brain registry + history, avatar generate/upload. - User-icon menu (Infrastructure/Brains/Tools/Profile/Credits) + ToolPanel; Master Planner deploy wizard (Specialists/Swarm/Scheduled/Triggered); Team Runs view; reap-progress modal; dashboard is the single live interface. Backend - cm-brain crate (.brain as the agent definition) + brain apply/history. - Hard-purge reap (FK-ordered) + sandbox release + SSE batch-delete. - Swarm self-verifying loop, mode-aware planner, web.search tool, webhooks (migration 0013), org/company/team delete endpoints, scheduler sweeps. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
160 lines
4.5 KiB
Rust
160 lines
4.5 KiB
Rust
use futures::stream::BoxStream;
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_json::Value;
|
|
|
|
/// Provider-neutral conversation role.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum ChatRole {
|
|
User,
|
|
Assistant,
|
|
}
|
|
|
|
/// Provider-neutral message content. Tool results travel as user-role parts,
|
|
/// matching both wire formats' conventions.
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
#[serde(tag = "type", rename_all = "snake_case")]
|
|
pub enum ContentPart {
|
|
// Struct variant (not newtype): internally-tagged enums cannot
|
|
// serialize newtype primitives, and this type round-trips through
|
|
// `agent_runs.checkpoint`.
|
|
Text {
|
|
text: String,
|
|
},
|
|
ToolUse {
|
|
id: String,
|
|
name: String,
|
|
input: Value,
|
|
},
|
|
ToolResult {
|
|
tool_use_id: String,
|
|
content: Value,
|
|
},
|
|
}
|
|
|
|
/// Anthropic and OpenAI both restrict tool names to
|
|
/// `^[a-zA-Z0-9_-]{1,128}$`; our registry uses dotted names
|
|
/// (`clock.now`, `email.send`). The HTTP providers encode dots as `__`
|
|
/// on the wire and decode on receipt — internal naming (DB step rows,
|
|
/// scenarios, UI traces) never changes. No registry tool may contain
|
|
/// a literal `__`.
|
|
pub fn wire_tool_name(internal: &str) -> String {
|
|
internal.replace('.', "__")
|
|
}
|
|
|
|
pub fn internal_tool_name(wire: &str) -> String {
|
|
wire.replace("__", ".")
|
|
}
|
|
|
|
impl ContentPart {
|
|
/// Convenience constructor for plain text parts.
|
|
pub fn text(value: impl Into<String>) -> ContentPart {
|
|
ContentPart::Text { text: value.into() }
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct ChatMessage {
|
|
pub role: ChatRole,
|
|
pub parts: Vec<ContentPart>,
|
|
}
|
|
|
|
/// A tool offered to the model. `input_schema` is JSON Schema.
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct ToolDescriptor {
|
|
pub name: String,
|
|
pub description: String,
|
|
pub input_schema: Value,
|
|
}
|
|
|
|
/// The checkpointable, provider-neutral request. This struct is what gets
|
|
/// serialized into `agent_runs.checkpoint`, so resume works identically
|
|
/// across providers.
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct ChatRequest {
|
|
pub system: String,
|
|
pub messages: Vec<ChatMessage>,
|
|
pub tools: Vec<ToolDescriptor>,
|
|
pub model: String,
|
|
pub max_tokens: u32,
|
|
/// When true, providers that support a server-side web-search tool (Anthropic)
|
|
/// attach it so the model can ground answers in live web data.
|
|
#[serde(default)]
|
|
pub web_search: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum StopReason {
|
|
EndTurn,
|
|
ToolUse,
|
|
MaxTokens,
|
|
}
|
|
|
|
/// Streaming events every provider normalizes to.
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
#[serde(tag = "type", rename_all = "snake_case")]
|
|
pub enum LlmEvent {
|
|
TextDelta(String),
|
|
ToolUse {
|
|
id: String,
|
|
name: String,
|
|
input: Value,
|
|
},
|
|
/// Token accounting for this provider call (drives credit metering).
|
|
Usage {
|
|
input_tokens: u32,
|
|
output_tokens: u32,
|
|
},
|
|
Stop(StopReason),
|
|
}
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum LlmError {
|
|
#[error("scenario error: {0}")]
|
|
Scenario(String),
|
|
#[error("transport error: {0}")]
|
|
Transport(String),
|
|
#[error("provider returned an error: {0}")]
|
|
Api(String),
|
|
#[error("malformed provider response: {0}")]
|
|
Wire(String),
|
|
}
|
|
|
|
pub type EventStream = BoxStream<'static, Result<LlmEvent, LlmError>>;
|
|
|
|
#[async_trait::async_trait]
|
|
pub trait LlmProvider: Send + Sync {
|
|
async fn stream(&self, request: ChatRequest) -> Result<EventStream, LlmError>;
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod wire_name_tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn dotted_registry_names_round_trip_the_wire_codec() {
|
|
for name in [
|
|
"clock.now",
|
|
"email.send",
|
|
"files.write",
|
|
"files.list",
|
|
"files.delete",
|
|
"routine.schedule",
|
|
"chat.send",
|
|
"chat.inbox",
|
|
"browser.goto",
|
|
"shell.exec",
|
|
"slack.post",
|
|
] {
|
|
let wire = wire_tool_name(name);
|
|
assert!(
|
|
wire.chars()
|
|
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-'),
|
|
"{wire} must satisfy the Anthropic/OpenAI name pattern"
|
|
);
|
|
assert_eq!(internal_tool_name(&wire), name, "lossless round trip");
|
|
}
|
|
}
|
|
}
|