#![forbid(unsafe_code)]
#![warn(missing_docs)]
pub mod edge;
pub use edge::{EdgeAdapter, build_attribution};
use std::sync::Arc;
use connectrpc::client::{ClientConfig, HttpClient};
use futures::Stream;
use polyc_agent::text_message;
use polyc_proto::proto::polychrome::agent::v1::{
AgentEnd, AgentRequest, AgentServiceClient, AgentStart, ClassifyRequest,
CompactionReason as WireCompactionReason, ContextCompacted, InterruptRequest, Message,
ParticipantMessage, Verdict, agent_response, content, tool_call_content,
};
use polyc_proto::proto::polychrome::approval::v1::{
ApprovalResponseRequest, ApprovalServiceClient, ListPendingRequest,
};
use polyc_proto::proto::polychrome::ops::v1::{
AckRequest, DecideRequest, NotificationServiceClient, OperatorMailboxServiceClient,
PollPendingRequest, decide_reply, ops_action_view,
};
use polyc_proto::proto::polychrome::persona::v1::{
AdminInviteRequest, AutoLinkOutcome, AutoLinkRequest, CompleteLinkRequest, DescribeRequest,
LinkOutcome, PersonaServiceClient, StartDeepLinkRequest, StartLinkRequest,
};
#[derive(Debug, thiserror::Error)]
pub enum DialError {
#[error("invalid agent address {addr:?}: {source}")]
InvalidAddress {
addr: String,
#[source]
source: http::uri::InvalidUri,
},
#[error("tls setup failed for agent address: {0}")]
Tls(String),
#[error(transparent)]
Connect(#[from] connectrpc::ConnectError),
}
impl DialError {
#[must_use]
pub const fn code(&self) -> Option<connectrpc::ErrorCode> {
match self {
Self::Connect(e) => Some(e.code),
Self::InvalidAddress { .. } | Self::Tls(_) => None,
}
}
#[must_use]
pub const fn is_retryable(&self) -> bool {
matches!(
self.code(),
Some(
connectrpc::ErrorCode::Unavailable
| connectrpc::ErrorCode::DeadlineExceeded
| connectrpc::ErrorCode::ResourceExhausted
| connectrpc::ErrorCode::Aborted
)
)
}
}
const AGENT_DIAL_TIMEOUT: std::time::Duration = std::time::Duration::from_mins(3);
const CONTROL_DIAL_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
const CONNECT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
fn http_client_for(uri: &http::Uri) -> Result<HttpClient, DialError> {
if uri.scheme_str() == Some("https") {
use rustls_platform_verifier::ConfigVerifierExt;
let tls = rustls::ClientConfig::with_platform_verifier()
.map_err(|e| DialError::Tls(e.to_string()))?;
Ok(HttpClient::builder()
.connect_timeout(CONNECT_TIMEOUT)
.with_tls(std::sync::Arc::new(tls)))
} else {
Ok(HttpClient::builder()
.connect_timeout(CONNECT_TIMEOUT)
.plaintext())
}
}
fn traced_options() -> connectrpc::client::CallOptions {
let mut headers = http::HeaderMap::new();
polyc_runtime::propagation::inject_current_span_into(&mut headers);
connectrpc::client::CallOptions::default()
.with_headers(headers.into_iter().filter_map(|(n, v)| n.map(|n| (n, v))))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompactionReason {
Summarized,
Truncated,
}
impl CompactionReason {
#[must_use]
pub fn notice_headline(self, summarized_messages: u32) -> String {
match self {
Self::Summarized => {
let plural = if summarized_messages == 1 { "" } else { "s" };
format!(
"🧠 Summarized {summarized_messages} earlier message{plural} to keep the \
conversation manageable"
)
}
Self::Truncated => {
"✂️ Trimmed earlier tool output to keep the conversation manageable".to_owned()
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TurnEvent {
ContextCompacted {
reason: CompactionReason,
summarized_messages: u32,
summary_preview: String,
},
TextDelta(String),
ToolStarted {
name: String,
},
ApprovalPending {
request_id: String,
tool_name: String,
title: String,
args_json: String,
reason: String,
},
HandoffStarted {
child_agent_id: String,
reason: String,
},
Done,
}
#[derive(Clone)]
pub struct AgentDialer {
client: Arc<AgentServiceClient<HttpClient>>,
}
impl AgentDialer {
pub fn new(addr: &str) -> Result<Self, DialError> {
let uri = addr
.parse::<http::Uri>()
.map_err(|source| DialError::InvalidAddress {
addr: addr.to_owned(),
source,
})?;
let http = http_client_for(&uri)?;
let config = ClientConfig::new(uri).with_default_timeout(AGENT_DIAL_TIMEOUT);
let client = AgentServiceClient::new(http, config);
Ok(Self {
client: Arc::new(client),
})
}
pub fn approval_dialer(addr: &str) -> Result<ApprovalDialer, DialError> {
ApprovalDialer::new(addr)
}
pub async fn run_turn(
&self,
conversation_id: &str,
exec_id: &str,
user_text: &str,
) -> Result<String, DialError> {
self.run_turn_with(conversation_id, exec_id, user_text, Attribution::default())
.await
}
pub async fn run_turn_with(
&self,
conversation_id: &str,
exec_id: &str,
user_text: &str,
attribution: Attribution,
) -> Result<String, DialError> {
let request = build_request(
conversation_id,
exec_id,
vec![text_message("user", user_text)],
None,
attribution,
);
let mut stream = self
.client
.connect_with_options(request, traced_options())
.await?;
let mut text_parts: Vec<String> = Vec::new();
let mut scaffolding: Vec<String> = Vec::new();
while let Some(view) = stream.message().await? {
let response = view.to_owned_message();
match response.r#type {
Some(agent_response::Type::Outputs(outputs)) => {
for msg in outputs.messages {
let is_assistant = matches!(msg.role.as_str(), "model" | "assistant");
let Some(content_block) = msg.content.into_option() else {
continue;
};
match content_block.r#type {
Some(content::Type::Text(t)) if is_assistant && !t.text.is_empty() => {
text_parts.push(t.text);
}
Some(content::Type::Text(_)) => {}
other => {
if let Some(rendered) = render_content(other) {
scaffolding.push(rendered);
}
}
}
}
}
Some(agent_response::Type::End(_)) => break,
Some(agent_response::Type::Compacted(_)) | None => {}
}
}
let reply = if text_parts.is_empty() {
scaffolding.join("\n")
} else {
text_parts.join("\n")
};
Ok(reply)
}
pub async fn run_turn_streaming(
&self,
conversation_id: &str,
exec_id: &str,
user_text: &str,
) -> Result<impl Stream<Item = Result<TurnEvent, DialError>>, DialError> {
self.run_turn_streaming_messages(
conversation_id,
exec_id,
vec![text_message("user", user_text)],
)
.await
}
pub async fn run_turn_streaming_messages(
&self,
conversation_id: &str,
exec_id: &str,
messages: Vec<Message>,
) -> Result<impl Stream<Item = Result<TurnEvent, DialError>>, DialError> {
self.run_turn_streaming_messages_with(
conversation_id,
exec_id,
messages,
None,
Attribution::default(),
)
.await
}
pub async fn run_turn_streaming_messages_with(
&self,
conversation_id: &str,
exec_id: &str,
messages: Vec<Message>,
payment_receipt: Option<PaymentReceipt>,
attribution: Attribution,
) -> Result<impl Stream<Item = Result<TurnEvent, DialError>>, DialError> {
let request = build_request(
conversation_id,
exec_id,
messages,
payment_receipt,
attribution,
);
self.run_turn_streaming_request(request).await
}
async fn run_turn_streaming_request(
&self,
request: AgentRequest,
) -> Result<impl Stream<Item = Result<TurnEvent, DialError>>, DialError> {
let mut stream = self
.client
.connect_with_options(request, traced_options())
.await?;
Ok(async_stream::try_stream! {
while let Some(view) = stream.message().await? {
let response = view.to_owned_message();
match response.r#type {
Some(agent_response::Type::Compacted(c)) => {
yield event_from_compacted(*c);
}
Some(agent_response::Type::Outputs(outputs)) => {
for msg in outputs.messages {
if let Some(event) = message_to_event(msg) {
yield event;
}
}
}
Some(agent_response::Type::End(end)) => {
for event in events_from_end(*end) {
yield event;
}
return;
}
None => {}
}
}
yield TurnEvent::Done;
})
}
pub async fn should_respond(
&self,
conversation_id: &str,
bot_name: &str,
transcript: Vec<ParticipantMessage>,
) -> Result<bool, DialError> {
let request = ClassifyRequest {
conversation_id: conversation_id.to_owned(),
bot_name: bot_name.to_owned(),
transcript,
..Default::default()
};
let resp = self
.client
.classify_with_options(request, traced_options())
.await?
.into_owned();
Ok(resp.verdict.to_i32() == Verdict::VERDICT_RESPOND as i32)
}
pub async fn interrupt(&self, conversation_id: &str) -> Result<bool, DialError> {
let request = InterruptRequest {
conversation_id: conversation_id.to_owned(),
..Default::default()
};
let resp = self
.client
.interrupt_with_options(request, traced_options())
.await?
.into_owned();
Ok(resp.interrupted)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ApprovalChoice {
Approve,
ApproveForSession,
Deny,
Abort,
Defer,
}
impl ApprovalChoice {
#[must_use]
pub const fn approved(self) -> bool {
matches!(self, Self::Approve | Self::ApproveForSession)
}
#[must_use]
pub const fn approved_for_session(self) -> bool {
matches!(self, Self::ApproveForSession)
}
#[must_use]
pub const fn is_abort(self) -> bool {
matches!(self, Self::Abort)
}
#[must_use]
pub const fn is_defer(self) -> bool {
matches!(self, Self::Defer)
}
}
#[must_use]
pub fn approval_decided_text(label: &str, choice: ApprovalChoice, decider: &str) -> String {
match choice {
ApprovalChoice::ApproveForSession => format!(
"✅ Approved by {decider} — running \"{label}\"… (won't ask again this session)"
),
ApprovalChoice::Approve => format!("✅ Approved by {decider} — running \"{label}\"…"),
ApprovalChoice::Deny => format!("🚫 Denied by {decider} — \"{label}\" was not run."),
ApprovalChoice::Abort => {
format!("🛑 Aborted by {decider} — \"{label}\" was not run; the turn was stopped.")
}
ApprovalChoice::Defer => {
format!("↩️ Sent back by {decider} — \"{label}\" is still waiting for a decision.")
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ApprovalOutcome {
pub persisted: bool,
pub signature_hex: String,
pub signed_by_hex: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PendingApproval {
pub request_id: String,
pub tool_name: String,
pub args_json: String,
pub reason: String,
}
#[derive(Clone)]
pub struct ApprovalDialer {
client: Arc<ApprovalServiceClient<HttpClient>>,
}
impl ApprovalDialer {
pub fn new(addr: &str) -> Result<Self, DialError> {
let uri = addr
.parse::<http::Uri>()
.map_err(|source| DialError::InvalidAddress {
addr: addr.to_owned(),
source,
})?;
let http = http_client_for(&uri)?;
let client = ApprovalServiceClient::new(
http,
ClientConfig::new(uri).with_default_timeout(CONTROL_DIAL_TIMEOUT),
);
Ok(Self {
client: Arc::new(client),
})
}
pub async fn respond(
&self,
request_id: &str,
choice: ApprovalChoice,
reason: &str,
conversation_id: &str,
modified_args_json: &str,
injected_context: &str,
) -> Result<ApprovalOutcome, DialError> {
use polyc_proto::proto::polychrome::approval::v1::{
Approve, Defer, Deny, approval_response_request::Decision,
};
let decision = if choice.is_defer() {
Decision::Defer(Box::new(Defer {
reason: reason.to_owned(),
..Default::default()
}))
} else if choice.approved() {
Decision::Approve(Box::new(Approve {
modified_args_json: modified_args_json.to_owned(),
injected_context: injected_context.to_owned(),
reason: reason.to_owned(),
approved_for_session: choice.approved_for_session(),
..Default::default()
}))
} else {
Decision::Deny(Box::new(Deny {
reason: reason.to_owned(),
abort: choice.is_abort(),
..Default::default()
}))
};
let request = ApprovalResponseRequest {
request_id: request_id.to_owned(),
conversation_id: conversation_id.to_owned(),
decision: Some(decision),
..Default::default()
};
let reply = self
.client
.respond_with_options(request, traced_options())
.await?
.into_owned();
Ok(ApprovalOutcome {
persisted: reply.persisted,
signature_hex: reply.signature_hex,
signed_by_hex: reply.signed_by_hex,
})
}
pub async fn list_pending(
&self,
conversation_id: &str,
) -> Result<Vec<PendingApproval>, DialError> {
let request = ListPendingRequest {
conversation_id: conversation_id.to_owned(),
..Default::default()
};
let reply = self
.client
.list_pending_with_options(request, traced_options())
.await?
.into_owned();
Ok(reply
.pending
.into_iter()
.map(|p| PendingApproval {
request_id: p.request_id,
tool_name: p.tool_name,
args_json: p.args_json,
reason: p.reason,
})
.collect())
}
pub async fn excise_taint(
&self,
conversation_id: &str,
positions: &[u64],
all_quarantined: bool,
source_only: bool,
reason: &str,
actor: polyc_proto::proto::polychrome::persona::v1::ExternalIdentity,
) -> Result<ExcisionOutcome, DialError> {
use polyc_proto::proto::polychrome::approval::v1::ExciseTaintRequest;
let request = ExciseTaintRequest {
conversation_id: conversation_id.to_owned(),
positions: positions.to_vec(),
all_quarantined,
source_only,
reason: reason.to_owned(),
actor: buffa::MessageField::some(actor),
..Default::default()
};
let reply = self
.client
.excise_taint_with_options(request, traced_options())
.await?
.into_owned();
Ok(ExcisionOutcome {
persisted: reply.persisted,
excised_positions: reply.excised_positions,
signature_hex: reply.signature_hex,
signed_by_hex: reply.signed_by_hex,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExcisionOutcome {
pub persisted: bool,
pub excised_positions: Vec<u64>,
pub signature_hex: String,
pub signed_by_hex: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StartedLink {
pub code: String,
pub expires_at_ms: u64,
pub persona_id: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StartedDeepLink {
pub token: String,
pub expires_at_ms: u64,
pub persona_id: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LinkCeremony {
Linked {
persona_id: String,
},
AlreadyLinked {
persona_id: String,
},
InvalidOrExpired,
Throttled,
Failed,
}
impl LinkCeremony {
#[must_use]
pub const fn user_message(&self) -> &'static str {
match self {
Self::Linked { .. } => {
"✅ Linked — this account now shares one Polychrome persona with your other channels."
}
Self::AlreadyLinked { .. } => {
"✅ Already linked — this account was already on that persona."
}
Self::InvalidOrExpired => {
"That link is invalid or expired. Start a fresh one from your other channel and try again."
}
Self::Throttled => "Too many attempts — wait a few minutes, then try again.",
Self::Failed => "That link failed to complete. Start a fresh one and try again.",
}
}
}
#[must_use]
pub const fn invite_error_message(err: &DialError) -> &'static str {
match err.code() {
Some(ErrorCode::PermissionDenied) => ADMIN_ONLY_INVITE,
Some(ErrorCode::ResourceExhausted) => {
"That's a lot of invites in a row — wait a couple of minutes, then try again."
}
_ => "I couldn't create the invite right now — try again in a moment.",
}
}
pub const ADMIN_ONLY_INVITE: &str = "Only admins can send invites.";
#[derive(Clone)]
pub struct PersonaDialer {
client: Arc<PersonaServiceClient<HttpClient>>,
}
impl PersonaDialer {
pub fn new(addr: &str) -> Result<Self, DialError> {
let uri = addr
.parse::<http::Uri>()
.map_err(|source| DialError::InvalidAddress {
addr: addr.to_owned(),
source,
})?;
let http = http_client_for(&uri)?;
let client = PersonaServiceClient::new(
http,
ClientConfig::new(uri).with_default_timeout(CONTROL_DIAL_TIMEOUT),
);
Ok(Self {
client: Arc::new(client),
})
}
pub async fn start_link(&self, identity: ExternalIdentity) -> Result<StartedLink, DialError> {
let request = StartLinkRequest {
identity: buffa::MessageField::some(identity),
..Default::default()
};
let reply = self
.client
.start_link_with_options(request, traced_options())
.await?
.into_owned();
Ok(StartedLink {
code: reply.code,
expires_at_ms: reply.expires_at_ms,
persona_id: reply.persona_id,
})
}
pub async fn start_deeplink(
&self,
identity: ExternalIdentity,
) -> Result<StartedDeepLink, DialError> {
let request = StartDeepLinkRequest {
identity: buffa::MessageField::some(identity),
..Default::default()
};
let reply = self
.client
.start_deep_link_with_options(request, traced_options())
.await?
.into_owned();
Ok(StartedDeepLink {
token: reply.token,
expires_at_ms: reply.expires_at_ms,
persona_id: reply.persona_id,
})
}
pub async fn admin_invite(
&self,
actor: ExternalIdentity,
target: ExternalIdentity,
) -> Result<StartedLink, DialError> {
let request = AdminInviteRequest {
actor: buffa::MessageField::some(actor),
target: buffa::MessageField::some(target),
..Default::default()
};
let reply = self
.client
.admin_invite_with_options(request, traced_options())
.await?
.into_owned();
Ok(StartedLink {
code: reply.code,
expires_at_ms: reply.expires_at_ms,
persona_id: reply.persona_id,
})
}
pub async fn auto_link(
&self,
identity: ExternalIdentity,
asserted_email: &str,
basis: &str,
) -> Result<Option<String>, DialError> {
let request = AutoLinkRequest {
identity: buffa::MessageField::some(identity),
asserted_email: asserted_email.to_owned(),
basis: basis.to_owned(),
..Default::default()
};
let reply = self
.client
.auto_link_with_options(request, traced_options())
.await?
.into_owned();
Ok(match reply.outcome.as_known() {
Some(AutoLinkOutcome::Linked) => Some(reply.persona_id),
_ => None,
})
}
pub async fn complete_link(
&self,
code: &str,
identity: ExternalIdentity,
) -> Result<LinkCeremony, DialError> {
let request = CompleteLinkRequest {
code: code.to_owned(),
identity: buffa::MessageField::some(identity),
..Default::default()
};
let reply = self
.client
.complete_link_with_options(request, traced_options())
.await?
.into_owned();
Ok(match reply.outcome.as_known() {
Some(LinkOutcome::Linked) => LinkCeremony::Linked {
persona_id: reply.persona_id,
},
Some(LinkOutcome::AlreadyLinked) => LinkCeremony::AlreadyLinked {
persona_id: reply.persona_id,
},
Some(LinkOutcome::InvalidCode | LinkOutcome::Expired) => LinkCeremony::InvalidOrExpired,
Some(LinkOutcome::Throttled) => LinkCeremony::Throttled,
Some(LinkOutcome::Unspecified) | None => LinkCeremony::Failed,
})
}
pub async fn describe(&self, identity: ExternalIdentity) -> Result<PersonaView, DialError> {
let request = DescribeRequest {
identity: buffa::MessageField::some(identity),
..Default::default()
};
let reply = self
.client
.describe_with_options(request, traced_options())
.await?
.into_owned();
let Some(profile) = reply.profile.into_option() else {
return Ok(PersonaView {
persona_id: reply.persona_id,
..Default::default()
});
};
Ok(PersonaView {
persona_id: reply.persona_id,
status: profile.status,
identities: profile
.identities
.into_iter()
.map(|id| LinkedIdentity {
provider: id.provider,
external_id: id.external_id,
display_name: id.display_name,
})
.collect(),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LinkedIdentity {
pub provider: String,
pub external_id: String,
pub display_name: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct PersonaView {
pub persona_id: String,
pub status: String,
pub identities: Vec<LinkedIdentity>,
}
pub use polyc_proto::proto::polychrome::agent::v1::Message as TurnMessage;
pub use polyc_proto::proto::polychrome::agent::v1::ParticipantMessage as GateMessage;
pub use polyc_proto::proto::polychrome::agent::v1::PaymentReceipt;
pub use polyc_proto::proto::polychrome::persona::v1::ExternalIdentity;
pub use connectrpc::ErrorCode;
#[derive(Debug, Clone, Default)]
pub struct Attribution {
pub caller: Option<ExternalIdentity>,
pub participants: Vec<ExternalIdentity>,
}
#[must_use]
pub fn attributed_message(speaker: &str, text: &str) -> TurnMessage {
text_message("user", &format!("{speaker}: {text}"))
}
#[must_use]
pub fn user_message(text: &str) -> TurnMessage {
text_message("user", text)
}
#[must_use]
pub fn namespaced_id(namespace: &str, native_id: &str) -> String {
format!("{namespace}:{native_id}")
}
#[must_use]
pub fn hashed_conversation_id(namespace: uuid::Uuid, parts: &[&str]) -> String {
let joined = parts.join(":");
uuid::Uuid::new_v5(&namespace, joined.as_bytes())
.hyphenated()
.to_string()
}
#[must_use]
pub fn framed_conversation_id(namespace: uuid::Uuid, parts: &[&str]) -> String {
let mut framed = String::new();
for p in parts {
framed.push_str(&p.len().to_string());
framed.push(':');
framed.push_str(p);
}
uuid::Uuid::new_v5(&namespace, framed.as_bytes())
.hyphenated()
.to_string()
}
fn build_request(
conversation_id: &str,
exec_id: &str,
messages: Vec<Message>,
payment_receipt: Option<PaymentReceipt>,
attribution: Attribution,
) -> AgentRequest {
AgentRequest {
conversation_id: conversation_id.to_owned(),
exec_id: exec_id.to_owned(),
start: buffa::MessageField::some(AgentStart {
agent_id: String::new(),
agent_config: Vec::new(),
messages,
payment_receipt: payment_receipt
.map_or_else(buffa::MessageField::none, buffa::MessageField::some),
caller: attribution
.caller
.map_or_else(buffa::MessageField::none, buffa::MessageField::some),
participants: attribution.participants,
..Default::default()
}),
..Default::default()
}
}
fn events_from_end(end: AgentEnd) -> Vec<TurnEvent> {
let mut events: Vec<TurnEvent> = end
.pending_approvals
.into_iter()
.map(|pa| TurnEvent::ApprovalPending {
request_id: pa.request_id,
tool_name: pa.tool_name,
title: pa.title,
args_json: pa.args_json,
reason: pa.reason,
})
.collect();
if let Some(h) = end.handoff.into_option() {
events.push(TurnEvent::HandoffStarted {
child_agent_id: h.child_agent_id,
reason: h.reason,
});
}
events.push(TurnEvent::Done);
events
}
fn event_from_compacted(c: ContextCompacted) -> TurnEvent {
let reason = if c.reason.to_i32() == WireCompactionReason::COMPACTION_REASON_SUMMARIZED as i32 {
CompactionReason::Summarized
} else {
CompactionReason::Truncated
};
TurnEvent::ContextCompacted {
reason,
summarized_messages: c.summarized_messages,
summary_preview: c.summary_preview,
}
}
fn message_to_event(msg: Message) -> Option<TurnEvent> {
let content_block = msg.content.into_option()?;
match content_block.r#type? {
content::Type::ToolCall(tc) => {
let name = match tc.r#type.as_ref() {
Some(tool_call_content::Type::FunctionCall(fc)) if !fc.name.is_empty() => {
fc.name.clone()
}
_ => tc.id.clone(),
};
Some(TurnEvent::ToolStarted { name })
}
content::Type::Text(t) => {
if is_assistant_role(&msg.role) && !t.text.is_empty() {
Some(TurnEvent::TextDelta(t.text))
} else {
None
}
}
_ => None,
}
}
fn is_assistant_role(role: &str) -> bool {
role == "model" || role == "assistant"
}
fn render_content(ty: Option<content::Type>) -> Option<String> {
match ty? {
content::Type::Text(t) => {
if t.text.is_empty() {
None
} else {
Some(t.text)
}
}
content::Type::ToolCall(tc) => {
let name = match tc.r#type.as_ref() {
Some(tool_call_content::Type::FunctionCall(fc)) => fc.name.as_str(),
None => "",
};
if name.is_empty() {
Some(format!("[tool_call:{}]", tc.id))
} else {
Some(format!("[tool_call:{name} {}]", tc.id))
}
}
content::Type::ToolResult(tr) => Some(format!("[tool_result:{}]", tr.call_id)),
_ => None,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OpsAction {
UpgradeTo {
version: String,
},
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PendingNotice {
pub action_id: String,
pub target: String,
pub action: OpsAction,
pub expires_unix: u64,
pub payload_hash: String,
pub delivered: bool,
}
#[derive(Clone)]
pub struct NotificationDialer {
client: Arc<NotificationServiceClient<HttpClient>>,
}
impl NotificationDialer {
pub fn new(addr: &str) -> Result<Self, DialError> {
let uri = addr
.parse::<http::Uri>()
.map_err(|source| DialError::InvalidAddress {
addr: addr.to_owned(),
source,
})?;
let http = http_client_for(&uri)?;
let client = NotificationServiceClient::new(
http,
ClientConfig::new(uri).with_default_timeout(CONTROL_DIAL_TIMEOUT),
);
Ok(Self {
client: Arc::new(client),
})
}
pub async fn poll_pending(&self, provider: &str) -> Result<Vec<PendingNotice>, DialError> {
let request = PollPendingRequest {
provider: provider.to_owned(),
..Default::default()
};
let reply = self
.client
.poll_pending_with_options(request, traced_options())
.await?
.into_owned();
Ok(reply.pending.into_iter().map(pending_notice).collect())
}
pub async fn ack(
&self,
provider: &str,
action_id: &str,
target: &str,
) -> Result<bool, DialError> {
let request = AckRequest {
provider: provider.to_owned(),
action_id: action_id.to_owned(),
target: target.to_owned(),
..Default::default()
};
let reply = self
.client
.ack_with_options(request, traced_options())
.await?
.into_owned();
Ok(reply.persisted)
}
}
fn pending_notice(
p: polyc_proto::proto::polychrome::ops::v1::PendingNotification,
) -> PendingNotice {
let action = p.action.into_option().and_then(|view| view.action).map_or(
OpsAction::Unknown,
|a| match a {
ops_action_view::Action::UpgradeTo(u) => OpsAction::UpgradeTo { version: u.version },
},
);
PendingNotice {
action_id: p.action_id,
target: p.target,
action,
expires_unix: p.expires_unix,
payload_hash: p.payload_hash,
delivered: p.delivered,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DecideOutcome {
Applied,
Denied,
Rejected,
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DecideResult {
pub outcome: DecideOutcome,
pub detail: String,
}
pub const OPS_PROMPT_HEADING: &str = "Approval needed";
impl OpsAction {
#[must_use]
pub fn summary(&self) -> String {
match self {
Self::UpgradeTo { version } => {
let copy = polyc_runtime::update_copy::update_copy(
&polyc_runtime::compat::Compatibility::Warm,
version,
);
format!("{} — {}", copy.headline, copy.detail)
}
Self::Unknown => "Approve a pending action".to_owned(),
}
}
#[must_use]
pub fn approve_verb(&self) -> Option<&'static str> {
match self {
Self::UpgradeTo { version } => {
polyc_runtime::update_copy::update_copy(
&polyc_runtime::compat::Compatibility::Warm,
version,
)
.action
}
Self::Unknown => None,
}
}
}
impl DecideOutcome {
#[must_use]
pub const fn metric_label(&self) -> &'static str {
match self {
Self::Applied => "applied",
Self::Denied => "denied",
Self::Rejected => "rejected",
Self::Unknown => "unknown",
}
}
}
impl DecideResult {
#[must_use]
pub fn decided_line(&self, decider: &str) -> String {
match self.outcome {
DecideOutcome::Applied => format!("✅ Approved by {decider} — applying."),
DecideOutcome::Denied => format!("🚫 Denied by {decider}."),
DecideOutcome::Rejected => {
let why = if self.detail.is_empty() {
"not authorized or no longer valid"
} else {
self.detail.as_str()
};
format!("⛔ Rejected — {why}.")
}
DecideOutcome::Unknown => {
"⚠️ Something went wrong recording that decision — try again.".to_owned()
}
}
}
}
#[derive(Clone)]
pub struct OperatorMailboxDialer {
client: Arc<OperatorMailboxServiceClient<HttpClient>>,
}
impl OperatorMailboxDialer {
pub fn new(addr: &str) -> Result<Self, DialError> {
let uri = addr
.parse::<http::Uri>()
.map_err(|source| DialError::InvalidAddress {
addr: addr.to_owned(),
source,
})?;
let http = http_client_for(&uri)?;
let client = OperatorMailboxServiceClient::new(
http,
ClientConfig::new(uri).with_default_timeout(CONTROL_DIAL_TIMEOUT),
);
Ok(Self {
client: Arc::new(client),
})
}
pub async fn decide(
&self,
action_id: &str,
approved: bool,
provider: &str,
external_user_id: &str,
reason: &str,
payload_hash: &str,
) -> Result<DecideResult, DialError> {
let request = DecideRequest {
action_id: action_id.to_owned(),
approved,
provider: provider.to_owned(),
external_user_id: external_user_id.to_owned(),
reason: reason.to_owned(),
payload_hash: payload_hash.to_owned(),
..Default::default()
};
let reply = self
.client
.decide_with_options(request, traced_options())
.await?
.into_owned();
let outcome = match reply.outcome.as_known() {
Some(decide_reply::Outcome::APPLIED) => DecideOutcome::Applied,
Some(decide_reply::Outcome::DENIED) => DecideOutcome::Denied,
Some(decide_reply::Outcome::REJECTED) => DecideOutcome::Rejected,
Some(decide_reply::Outcome::OUTCOME_UNSPECIFIED) | None => DecideOutcome::Unknown,
};
Ok(DecideResult {
outcome,
detail: reply.detail,
})
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::pedantic, clippy::nursery, missing_docs)]
use super::*;
use polyc_proto::proto::polychrome::agent::v1::{
Content, FunctionCallContent, TextContent, ThoughtContent, ThoughtSummaryContent,
ToolCallContent, ToolResultContent, thought_summary_content,
};
#[test]
fn ops_copy_is_shared_and_jargon_free() {
let up = OpsAction::UpgradeTo {
version: "1.2.3".to_owned(),
};
let summary = up.summary();
assert!(
summary.contains("1.2.3"),
"summary names the version: {summary}"
);
assert!(
summary.contains("restarts the service"),
"warm upgrade summary is honest about the restart: {summary}"
);
assert_eq!(
up.approve_verb(),
Some(polyc_runtime::update_copy::APPLY_NOW),
"an in-place upgrade carries the shared Apply now verb"
);
assert!(
!summary.to_lowercase().contains("operator"),
"no banned jargon in the upgrade ask: {summary}"
);
assert_eq!(OpsAction::Unknown.approve_verb(), None);
let unknown = OpsAction::Unknown.summary();
assert!(!unknown.to_lowercase().contains("control-plane"));
assert!(!unknown.is_empty());
assert!(!OPS_PROMPT_HEADING.to_lowercase().contains("operator"));
assert_eq!(DecideOutcome::Applied.metric_label(), "applied");
assert_eq!(DecideOutcome::Denied.metric_label(), "denied");
assert_eq!(DecideOutcome::Rejected.metric_label(), "rejected");
assert_eq!(DecideOutcome::Unknown.metric_label(), "unknown");
let applied = DecideResult {
outcome: DecideOutcome::Applied,
detail: String::new(),
}
.decided_line("Chris");
assert!(applied.contains("Approved") && applied.contains("Chris"));
let rejected = DecideResult {
outcome: DecideOutcome::Rejected,
detail: "not an operator".to_owned(),
}
.decided_line("Chris");
assert!(rejected.contains("Rejected") && rejected.contains("not an operator"));
let unknown = DecideResult {
outcome: DecideOutcome::Unknown,
detail: String::new(),
}
.decided_line("Chris");
assert!(!unknown.to_lowercase().contains("please"));
}
#[test]
fn approval_choice_flags() {
assert!(ApprovalChoice::Approve.approved());
assert!(!ApprovalChoice::Approve.approved_for_session());
assert!(!ApprovalChoice::Approve.is_abort());
assert!(ApprovalChoice::ApproveForSession.approved());
assert!(ApprovalChoice::ApproveForSession.approved_for_session());
assert!(!ApprovalChoice::Deny.approved());
assert!(!ApprovalChoice::Deny.is_abort());
assert!(!ApprovalChoice::Abort.approved());
assert!(ApprovalChoice::Abort.is_abort());
}
#[test]
fn compacted_summarized_maps_with_preview() {
let c = ContextCompacted {
reason: buffa::EnumValue::from(
WireCompactionReason::COMPACTION_REASON_SUMMARIZED as i32,
),
summarized_messages: 12,
summary_preview: "earlier work: fixed bug X".to_owned(),
..Default::default()
};
assert_eq!(
event_from_compacted(c),
TurnEvent::ContextCompacted {
reason: CompactionReason::Summarized,
summarized_messages: 12,
summary_preview: "earlier work: fixed bug X".to_owned(),
}
);
}
#[test]
fn compacted_truncated_maps_without_preview() {
let c = ContextCompacted {
reason: buffa::EnumValue::from(
WireCompactionReason::COMPACTION_REASON_TRUNCATED as i32,
),
..Default::default()
};
assert_eq!(
event_from_compacted(c),
TurnEvent::ContextCompacted {
reason: CompactionReason::Truncated,
summarized_messages: 0,
summary_preview: String::new(),
}
);
}
#[test]
fn compacted_unknown_reason_falls_back_to_truncated() {
let c = ContextCompacted {
reason: buffa::EnumValue::from(
WireCompactionReason::COMPACTION_REASON_UNSPECIFIED as i32,
),
..Default::default()
};
assert!(matches!(
event_from_compacted(c),
TurnEvent::ContextCompacted {
reason: CompactionReason::Truncated,
..
}
));
}
#[test]
fn dial_error_retryable_classification() {
use connectrpc::ErrorCode;
for code in [
ErrorCode::Unavailable,
ErrorCode::DeadlineExceeded,
ErrorCode::ResourceExhausted,
ErrorCode::Aborted,
] {
let err = DialError::Connect(connectrpc::ConnectError::new(code, "x"));
assert_eq!(err.code(), Some(code));
assert!(err.is_retryable(), "{code:?} should be retryable");
}
for code in [
ErrorCode::InvalidArgument,
ErrorCode::Unauthenticated,
ErrorCode::NotFound,
ErrorCode::PermissionDenied,
ErrorCode::Internal,
] {
let err = DialError::Connect(connectrpc::ConnectError::new(code, "x"));
assert_eq!(err.code(), Some(code));
assert!(!err.is_retryable(), "{code:?} should not be retryable");
}
let bad_addr = DialError::InvalidAddress {
addr: "http://a b".to_owned(),
source: "http://a b".parse::<http::Uri>().unwrap_err(),
};
assert_eq!(bad_addr.code(), None);
assert!(!bad_addr.is_retryable());
let tls = DialError::Tls("no provider".to_owned());
assert_eq!(tls.code(), None);
assert!(!tls.is_retryable());
}
fn text(s: &str) -> Option<content::Type> {
Some(content::Type::Text(Box::new(TextContent {
text: s.to_owned(),
..Default::default()
})))
}
fn tool_call(id: &str, name: &str) -> Option<content::Type> {
Some(content::Type::ToolCall(Box::new(ToolCallContent {
id: id.to_owned(),
r#type: Some(tool_call_content::Type::FunctionCall(Box::new(
FunctionCallContent {
name: name.to_owned(),
..Default::default()
},
))),
..Default::default()
})))
}
fn tool_result(call_id: &str) -> Option<content::Type> {
Some(content::Type::ToolResult(Box::new(ToolResultContent {
call_id: call_id.to_owned(),
..Default::default()
})))
}
fn thought(summary: &str) -> Option<content::Type> {
Some(content::Type::Thought(Box::new(ThoughtContent {
summary: vec![ThoughtSummaryContent {
r#type: Some(thought_summary_content::Type::Text(Box::new(TextContent {
text: summary.to_owned(),
..Default::default()
}))),
..Default::default()
}],
..Default::default()
})))
}
#[test]
fn renders_text_verbatim() {
assert_eq!(render_content(text("hi")), Some("hi".to_owned()));
}
#[test]
fn renders_tool_call_with_name_and_id() {
assert_eq!(
render_content(tool_call("call_42", "search")),
Some("[tool_call:search call_42]".to_owned())
);
}
#[test]
fn renders_tool_call_without_function_name() {
assert_eq!(
render_content(Some(content::Type::ToolCall(Box::new(ToolCallContent {
id: "call_bare".to_owned(),
r#type: None,
..Default::default()
})))),
Some("[tool_call:call_bare]".to_owned())
);
}
#[test]
fn renders_tool_result_by_call_id() {
assert_eq!(
render_content(tool_result("call_42")),
Some("[tool_result:call_42]".to_owned())
);
}
#[test]
fn reasoning_is_never_the_reply() {
assert_eq!(render_content(thought("considering options")), None);
}
#[test]
fn thought_only_turn_yields_empty_reply() {
assert_eq!(aggregate(vec![thought("secret reasoning")]), "");
}
#[test]
fn empty_text_skipped() {
assert_eq!(render_content(text("")), None);
}
#[test]
fn unknown_variant_skipped() {
assert_eq!(render_content(None), None);
}
fn aggregate(blocks: Vec<Option<content::Type>>) -> String {
let mut parts: Vec<String> = Vec::new();
for b in blocks {
if let Some(s) = render_content(b) {
parts.push(s);
}
}
parts.join("\n")
}
#[test]
fn aggregate_pure_text_turn() {
assert_eq!(
aggregate(vec![text("hello"), text("world")]),
"hello\nworld"
);
}
#[test]
fn aggregate_tool_call_only_turn() {
assert_eq!(
aggregate(vec![tool_call("call_1", "lookup")]),
"[tool_call:lookup call_1]"
);
}
#[test]
fn aggregate_mixed_text_and_tool_call() {
assert_eq!(
aggregate(vec![text("thinking..."), tool_call("call_1", "search")]),
"thinking...\n[tool_call:search call_1]"
);
}
fn message(role: &str, ty: Option<content::Type>) -> Message {
Message {
role: role.to_owned(),
content: buffa::MessageField::some(Content {
r#type: ty,
..Default::default()
}),
..Default::default()
}
}
#[test]
fn assistant_text_becomes_text_delta() {
assert_eq!(
message_to_event(message("assistant", text("hello"))),
Some(TurnEvent::TextDelta("hello".to_owned()))
);
}
#[test]
fn model_role_also_counts_as_assistant() {
assert_eq!(
message_to_event(message("model", text("hi"))),
Some(TurnEvent::TextDelta("hi".to_owned()))
);
}
#[test]
fn tool_role_text_is_skipped() {
assert_eq!(message_to_event(message("tool", text("result blob"))), None);
}
#[test]
fn empty_assistant_text_is_skipped() {
assert_eq!(message_to_event(message("assistant", text(""))), None);
}
#[test]
fn tool_call_becomes_tool_started_with_name() {
assert_eq!(
message_to_event(message("model", tool_call("call_7", "search"))),
Some(TurnEvent::ToolStarted {
name: "search".to_owned()
})
);
}
#[test]
fn tool_call_falls_back_to_call_id() {
assert_eq!(
message_to_event(message(
"tool",
Some(content::Type::ToolCall(Box::new(ToolCallContent {
id: "call_bare".to_owned(),
r#type: None,
..Default::default()
})))
)),
Some(TurnEvent::ToolStarted {
name: "call_bare".to_owned()
})
);
}
#[test]
fn tool_result_produces_no_event() {
assert_eq!(
message_to_event(message("tool", tool_result("call_7"))),
None
);
}
fn map_turn(msgs: Vec<Message>) -> Vec<TurnEvent> {
let mut events: Vec<TurnEvent> = msgs.into_iter().filter_map(message_to_event).collect();
events.push(TurnEvent::Done);
events
}
#[test]
fn synthetic_turn_yields_expected_event_sequence() {
let turn = vec![
message("model", text("Let me look that up.")),
message("tool", tool_call("call_1", "search")),
message("model", text("Found it.")),
];
assert_eq!(
map_turn(turn),
vec![
TurnEvent::TextDelta("Let me look that up.".to_owned()),
TurnEvent::ToolStarted {
name: "search".to_owned()
},
TurnEvent::TextDelta("Found it.".to_owned()),
TurnEvent::Done,
]
);
}
use polyc_proto::proto::polychrome::agent::v1::{
Handoff as WireHandoff, PendingApproval as WirePendingApproval,
};
#[test]
fn end_with_nothing_yields_only_done() {
assert_eq!(events_from_end(AgentEnd::default()), vec![TurnEvent::Done]);
}
#[test]
fn end_with_handoff_yields_handoff_then_done() {
let end = AgentEnd {
handoff: buffa::MessageField::some(WireHandoff {
call_id: "call_1".to_owned(),
child_agent_id: "researcher".to_owned(),
reason: "needs deep dive".to_owned(),
..Default::default()
}),
..Default::default()
};
assert_eq!(
events_from_end(end),
vec![
TurnEvent::HandoffStarted {
child_agent_id: "researcher".to_owned(),
reason: "needs deep dive".to_owned(),
},
TurnEvent::Done,
]
);
}
#[test]
fn end_orders_approvals_before_handoff_before_done() {
let end = AgentEnd {
pending_approvals: vec![WirePendingApproval {
request_id: "r1".to_owned(),
tool_name: "delete_file".to_owned(),
args_json: "{}".to_owned(),
title: "Delete a file".to_owned(),
..Default::default()
}],
handoff: buffa::MessageField::some(WireHandoff {
child_agent_id: "child".to_owned(),
..Default::default()
}),
..Default::default()
};
assert_eq!(
events_from_end(end),
vec![
TurnEvent::ApprovalPending {
request_id: "r1".to_owned(),
tool_name: "delete_file".to_owned(),
title: "Delete a file".to_owned(),
args_json: "{}".to_owned(),
reason: String::new(),
},
TurnEvent::HandoffStarted {
child_agent_id: "child".to_owned(),
reason: String::new(),
},
TurnEvent::Done,
]
);
}
#[test]
fn namespaced_id_is_prefix_colon_native() {
assert_eq!(
namespaced_id("github", "owner/repo#42"),
"github:owner/repo#42"
);
assert_eq!(namespaced_id("web", "abc-123"), "web:abc-123");
}
#[test]
fn hashed_conversation_id_is_deterministic_v5() {
let ns = uuid::Uuid::from_u128(0x1234_5678_9abc_4def_8123_4567_89ab_cdef);
let a = hashed_conversation_id(ns, &["T1", "C1", "169.000"]);
let b = hashed_conversation_id(ns, &["T1", "C1", "169.000"]);
assert_eq!(a, b);
assert_ne!(a, hashed_conversation_id(ns, &["T1", "C2", "169.000"]));
let parsed: uuid::Uuid = a.parse().unwrap();
assert_eq!(parsed.get_version_num(), 5);
}
#[test]
fn framed_conversation_id_resists_separator_collisions() {
let ns = uuid::Uuid::from_u128(0x99);
assert_ne!(
framed_conversation_id(ns, &["a:b", "c"]),
framed_conversation_id(ns, &["a", "b:c"]),
);
assert_eq!(
hashed_conversation_id(ns, &["a:b", "c"]),
hashed_conversation_id(ns, &["a", "b:c"]),
);
let id = framed_conversation_id(ns, &["mail", "<abc@x>"]);
assert_eq!(id, framed_conversation_id(ns, &["mail", "<abc@x>"]));
assert_eq!(id.parse::<uuid::Uuid>().unwrap().get_version_num(), 5);
}
#[test]
fn hashed_conversation_id_matches_slacks_inline_algorithm() {
let ns = uuid::Uuid::from_u128(0xa1b2_c3d4_e5f6_4789_abcd_ef01_2345_6789);
let parts = ["T01234ABCD", "C0B5V0JA401", "1700000000.000100"];
let inline = uuid::Uuid::new_v5(&ns, parts.join(":").as_bytes())
.hyphenated()
.to_string();
assert_eq!(hashed_conversation_id(ns, &parts), inline);
}
#[test]
fn link_outcome_messages_honor_presentation_rules() {
assert!(
LinkCeremony::InvalidOrExpired
.user_message()
.contains("invalid or expired")
);
assert!(LinkCeremony::Throttled.user_message().contains("wait"));
assert!(
LinkCeremony::Linked {
persona_id: "p".to_owned()
}
.user_message()
.contains("Linked")
);
}
}