use crate::emit::emit_ops_event;
use crate::ops_event::OpsEvent;
use crate::run_context::{scope_run_context, RunContext};
use crate::tenant::{current_tenant, scope_tenant, TenantResolver};
use crate::types::AgentId;
use async_trait::async_trait;
use klieo_core::{Agent, AgentContext, ToolDef};
use std::sync::Arc;
#[cfg(feature = "gates")]
use std::time::Duration;
#[cfg(feature = "gates")]
use crate::runtime::gated_invoker::GatedToolInvoker;
#[cfg(feature = "governor")]
use crate::runtime::governed_llm::GovernedLlmClient;
pub struct SupervisedAgent<A: Agent> {
pub(crate) inner: A,
pub(crate) tenant_resolver: Arc<dyn TenantResolver>,
pub(crate) agent_id: AgentId,
#[cfg(feature = "governor")]
pub(crate) governor: Option<Arc<dyn crate::governor::Governor>>,
#[cfg(feature = "governor")]
pub(crate) governor_provider: crate::types::ProviderId,
#[cfg(feature = "gates")]
pub(crate) gates: Vec<Arc<dyn crate::gates::Gate>>,
#[cfg(feature = "gates")]
pub(crate) approval_timeout: Duration,
#[cfg(feature = "gates")]
pub(crate) approval_timeout_policy: crate::gates::ApprovalTimeoutPolicy,
}
#[async_trait]
impl<A> Agent for SupervisedAgent<A>
where
A: Agent + 'static,
A::Input: Send + 'static,
A::Output: Send + 'static,
A::Error: Send + Sync + 'static,
{
type Input = A::Input;
type Output = A::Output;
type Error = A::Error;
fn name(&self) -> &str {
self.inner.name()
}
fn system_prompt(&self) -> &str {
self.inner.system_prompt()
}
fn tools(&self) -> &[ToolDef] {
self.inner.tools()
}
async fn run(
&self,
ctx: AgentContext,
input: Self::Input,
) -> Result<Self::Output, Self::Error> {
let tenant = self.tenant_resolver.resolve().await;
let agent_id = self.agent_id.clone();
let episodic = ctx.episodic.clone();
let run_id = ctx.run_id;
let ctx = self.wrap_llm(ctx);
let ctx = self.wrap_tools(ctx);
let run_ctx = RunContext {
episodic: episodic.clone(),
run_id,
};
scope_tenant(
tenant,
scope_run_context(Some(run_ctx), async move {
if let Err(err) = emit_ops_event(
&*episodic,
run_id,
OpsEvent::SupervisorStateChange {
tenant: current_tenant(),
agent: agent_id.clone(),
state: "running".into(),
reason: None,
},
)
.await
{
tracing::warn!(
target: "klieo.ops.audit",
error = %err,
"audit emit failed; episode not recorded"
);
}
let result = self.inner.run(ctx, input).await;
let state = if result.is_ok() {
"completed"
} else {
"failed"
};
if let Err(err) = emit_ops_event(
&*episodic,
run_id,
OpsEvent::SupervisorStateChange {
tenant: current_tenant(),
agent: agent_id,
state: state.into(),
reason: None,
},
)
.await
{
tracing::warn!(
target: "klieo.ops.audit",
error = %err,
"audit emit failed; episode not recorded"
);
}
result
}),
)
.await
}
}
impl<A: Agent> SupervisedAgent<A> {
fn wrap_llm(&self, ctx: AgentContext) -> AgentContext {
#[cfg(feature = "governor")]
if let Some(g) = &self.governor {
let llm = ctx.llm.clone();
let episodic = ctx.episodic.clone();
let run_id = ctx.run_id;
return ctx.with_llm(Arc::new(GovernedLlmClient::new(
llm,
g.clone(),
episodic,
run_id,
self.governor_provider.clone(),
None,
)));
}
ctx
}
fn wrap_tools(&self, ctx: AgentContext) -> AgentContext {
#[cfg(feature = "gates")]
if !self.gates.is_empty() {
let tools = ctx.tools.clone();
let episodic = ctx.episodic.clone();
let run_id = ctx.run_id;
return ctx.with_tools(Arc::new(
GatedToolInvoker::new(tools, self.gates.clone(), episodic, run_id)
.with_approval_timeout(self.approval_timeout)
.with_approval_timeout_policy(self.approval_timeout_policy),
));
}
ctx
}
}