use std::sync::Arc;
use async_trait::async_trait;
use serde_json::{Value, json};
use crate::core::{Outcome, Skill, SkillDescriptor, SkillError, Tainted};
use crate::manifest::{ExecutionKind, Identity, Manifest};
use crate::model::{ModelCall, ModelId, ModelProvider};
use super::StepCtx;
#[derive(Debug)]
pub(super) struct Declarative {
kind: ExecutionKind,
capability: String,
name: String,
provider: Arc<dyn ModelProvider>,
tools: Option<(
Arc<crate::tools::ToolCatalog>,
Arc<dyn crate::tools::ToolClient>,
)>,
max_turns: u32,
}
impl Declarative {
pub(super) fn new(
kind: ExecutionKind,
capability: String,
name: String,
provider: Arc<dyn ModelProvider>,
tools: Option<(
Arc<crate::tools::ToolCatalog>,
Arc<dyn crate::tools::ToolClient>,
)>,
max_turns: u32,
) -> Self {
Self {
kind,
capability,
name,
provider,
tools,
max_turns,
}
}
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
async fn tool_loop(
&self,
cx: &mut StepCtx<'_>,
input: Tainted<Value>,
system: String,
model_role: (ModelId, Option<u32>, Option<crate::model::ReasoningEffort>),
egress: Option<crate::core::Sensitivity>,
granted: Vec<crate::manifest::ToolGrant>,
oversight: Option<Proposal>,
formation: Option<crate::manifest::MemoryFormation>,
) -> Result<Outcome, SkillError> {
let (model, max_output_tokens, reasoning_effort) = model_role;
let (catalog, client) = self.tools.clone().ok_or_else(|| {
SkillError::Other(
"this agent declares `tool-calling` but the plane has no tool \
catalogue — `RuntimeBuilder::tools` is what lets a declarative \
agent reach one"
.into(),
)
})?;
let (offered, declared) = offered_tools(&catalog, &granted);
let prompt = Tainted::object([
("system".to_owned(), Tainted::trusted(json!(system))),
("input".to_owned(), input),
]);
let mut exchanges: Vec<crate::model::ToolExchange> = Vec::new();
let mut continuation: Option<crate::model::ProviderContinuation> = None;
let mut conversation_label = prompt.label().clone();
for _turn in 0..self.max_turns {
let mut call = ModelCall::new(
Arc::clone(&self.provider),
model.clone(),
prompt.peek().clone(),
)
.with_tools(declared.clone())
.continuing(exchanges.clone())
.with_output_sensitivity(conversation_label.sensitivity);
if let Some(state) = continuation.take() {
call = call.with_continuation(state);
}
if let Some(max_output_tokens) = max_output_tokens {
call = call.with_max_output_tokens(max_output_tokens);
}
if let Some(effort) = reasoning_effort {
call = call.with_reasoning_effort(effort);
}
if let Some(ceiling) = egress {
call = call.with_max_sensitivity(ceiling);
}
let outbound = prompt.with_joined_label(&conversation_label);
let completion = cx.sink(call, &outbound).await?;
let label = completion.label().join(&conversation_label);
let completion = Tainted::with_label(completion.into_unlabelled(), label);
if completion.peek().tool_calls.is_empty() {
let formed_source = Tainted::with_label(
completion
.peek()
.structured
.clone()
.unwrap_or_else(|| json!({ "text": completion.peek().text.clone() })),
completion.label().clone(),
);
let answer =
completion.map(|c| c.structured.unwrap_or_else(|| json!({ "text": c.text })));
return self
.settle(
cx,
answer,
formed_source,
oversight,
formation.as_ref(),
&model,
)
.await;
}
continuation.clone_from(&completion.peek().continuation);
exchanges.clear();
for asked in completion.peek().tool_calls.clone() {
let Some((id, grant)) = catalog.resolve(&asked.name).and_then(|id| {
offered
.iter()
.find(|(offered, _)| *offered == id)
.map(|(_, grant)| (id, *grant))
}) else {
exchanges.push(crate::model::ToolExchange::failed(
asked,
"no tool of that name is granted to this agent",
));
continue;
};
let Some(declaration) = declared.iter().find(|tool| tool.name == asked.name) else {
exchanges.push(crate::model::ToolExchange::failed(
asked,
"the selected tool has no model-facing declaration",
));
continue;
};
if let Err(detail) =
crate::model::validate_schema(&declaration.parameters, &asked.arguments)
{
exchanges.push(crate::model::ToolExchange::failed(asked, detail));
continue;
}
let reference = id.reference();
let args = crate::core::Tainted::with_label(
asked.arguments.clone(),
completion.label().clone(),
);
if grant.requires_approval {
let Some(spec) = oversight.as_ref() else {
return Err(SkillError::Other(
"a tool grant requires approval but the agent declares no oversight policy — there is nobody to ask"
.into(),
));
};
cx.deadline(spec.deadline.name.clone(), &spec.deadline.spec(), None)
.await?;
let decision = cx
.task(&spec.approve_call(&reference, &asked.arguments))
.await?;
if !decision.approved {
exchanges.push(crate::model::ToolExchange::failed(
asked,
"a reviewer did not approve this call",
));
continue;
}
}
if id.server == crate::tools::AGENT_SERVER {
match cx.commission(&id.tool, args).await {
Ok(answer) => {
conversation_label = conversation_label.join(answer.label());
exchanges
.push(crate::model::ToolExchange::ok(asked, answer.peek().clone()));
}
Err(e) => match model_facing(&e) {
Some(detail) => {
exchanges.push(crate::model::ToolExchange::failed(asked, detail));
}
None => return Err(e.into()),
},
}
continue;
}
let prepared = crate::tools::ToolCall::prepare(
&catalog,
Arc::clone(&client),
id,
asked.arguments.clone(),
)
.map_err(|e| SkillError::Other(e.to_string()))?;
match cx.sink(prepared, &args).await {
Ok(result) => {
conversation_label = conversation_label.join(result.label());
exchanges
.push(crate::model::ToolExchange::ok(asked, result.peek().clone()));
}
Err(e) => match model_facing(&e) {
Some(detail) => {
exchanges.push(crate::model::ToolExchange::failed(asked, detail));
}
None => return Err(e.into()),
},
}
}
}
Ok(Outcome::fail(format!(
"'{}' did not finish within {} model turns — it was still asking for tools",
self.name, self.max_turns
)))
}
async fn settle(
&self,
cx: &mut StepCtx<'_>,
answer: Tainted<Value>,
formed_source: Tainted<Value>,
oversight: Option<Proposal>,
formation: Option<&crate::manifest::MemoryFormation>,
model: &ModelId,
) -> Result<Outcome, SkillError> {
if let Some(spec) = oversight.filter(Proposal::gates_the_answer) {
cx.deadline(spec.deadline.name.clone(), &spec.deadline.spec(), None)
.await?;
let decision = cx.task(&spec.approve_answer(answer.peek().clone())).await?;
if !decision.approved {
return Ok(Outcome::fail(format!(
"{} refused this answer: {}",
decision.actor, decision.reason
)));
}
}
self.form_answer(cx, formation, formed_source, model)
.await?;
Ok(Outcome::done(answer))
}
async fn form_answer(
&self,
cx: &mut StepCtx<'_>,
declaration: Option<&crate::manifest::MemoryFormation>,
answer: Tainted<Value>,
model: &ModelId,
) -> Result<(), SkillError> {
let Some(declaration) = declaration else {
return Ok(());
};
let model = match cx.manifest() {
Some(m) => untrusted_contact_model(m, model),
None => model.clone(),
};
let expires_at = if let Some(seconds) = declaration.retention_seconds {
let now = cx.now().await?;
Some(now + time::Duration::seconds(i64::try_from(seconds).unwrap_or(i64::MAX)))
} else {
None
};
cx.form_memories(
crate::memory::Formation {
subject: declaration.subject.clone(),
purpose: declaration.purpose.clone(),
instruction: declaration.instruction.clone(),
max_items: declaration.max_items,
expires_at,
access_retention_seconds: declaration.access_retention_seconds,
max_sensitivity: declaration.max_sensitivity,
},
answer,
Arc::clone(&self.provider),
model,
)
.await?;
Ok(())
}
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
async fn planned(
&self,
cx: &mut StepCtx<'_>,
input: Tainted<Value>,
system: String,
model_role: (ModelId, Option<u32>, Option<crate::model::ReasoningEffort>),
egress: Option<crate::core::Sensitivity>,
granted: Vec<crate::manifest::ToolGrant>,
oversight: Option<Proposal>,
formation: Option<crate::manifest::MemoryFormation>,
output_schema: Option<Value>,
) -> Result<Outcome, SkillError> {
let (model, max_output_tokens, reasoning_effort) = model_role;
if input.label().trust != crate::core::Trust::Trusted {
return Err(SkillError::Other(
"a `planned` agent refuses untrusted input: the plan is compiled from \
what the planner reads, and untrusted input authoring a plan is the \
attacker choosing the control flow. Hand hostile content to this \
agent through a tool or a parse step, or use `tool-calling`"
.into(),
));
}
let tools = match (granted.is_empty(), self.tools.clone()) {
(true, _) => None,
(false, Some(wired)) => Some(wired),
(false, None) => {
return Err(SkillError::Other(
"this agent declares `planned` with tool grants but the plane has \
no tool catalogue — `RuntimeBuilder::tools` is what lets a \
declarative agent reach one"
.into(),
));
}
};
let (offered, declared) = tools
.as_ref()
.map(|(catalog, _)| offered_tools(catalog, &granted))
.unwrap_or_default();
let surface: Vec<Value> = declared
.iter()
.map(|t| {
json!({
"tool": t.name,
"description": t.description,
"parameters": t.parameters,
})
})
.collect();
let prompt = Tainted::object([
("system".to_owned(), Tainted::trusted(json!(system))),
("input".to_owned(), input.clone()),
("tools".to_owned(), Tainted::trusted(json!(surface))),
]);
let mut call = ModelCall::new(
Arc::clone(&self.provider),
model.clone(),
prompt.peek().clone(),
)
.with_output_sensitivity(prompt.label().sensitivity)
.expecting(plan_schema(self.max_turns));
if let Some(max_output_tokens) = max_output_tokens {
call = call.with_max_output_tokens(max_output_tokens);
}
if let Some(effort) = reasoning_effort {
call = call.with_reasoning_effort(effort);
}
if let Some(ceiling) = egress {
call = call.with_max_sensitivity(ceiling);
}
let completion = cx.sink(call, &prompt).await?;
let plan_label = completion.label().join(prompt.label()).clone();
let Some(plan_value) = completion.peek().structured.clone() else {
return Ok(Outcome::fail("the planner returned no structured plan"));
};
let plan: PlanDoc = match serde_json::from_value(plan_value) {
Ok(plan) => plan,
Err(e) => {
return Ok(Outcome::fail(format!(
"the planner's output is not a plan: {e}"
)));
}
};
if plan.steps.is_empty() || plan.steps.len() > self.max_turns as usize {
return Ok(Outcome::fail(format!(
"the plan has {} steps and this agent is bounded to {}",
plan.steps.len(),
self.max_turns
)));
}
let mut outputs: Vec<Tainted<Value>> = Vec::new();
for (index, step) in plan.steps.iter().enumerate() {
match (&step.tool, &step.parse) {
(Some(name), None) => {
let Some((catalog, client)) = tools.as_ref() else {
return Ok(Outcome::fail(format!(
"plan step {index} calls '{name}' but this agent grants no tools"
)));
};
let Some((id, grant)) = catalog.resolve(name).and_then(|id| {
offered
.iter()
.find(|(offered, _)| *offered == id)
.map(|(_, grant)| (id, *grant))
}) else {
return Ok(Outcome::fail(format!(
"plan step {index} calls '{name}', which is not granted to \
this agent"
)));
};
let Some(declaration) = declared.iter().find(|tool| &tool.name == name) else {
return Ok(Outcome::fail(format!(
"plan step {index}: '{name}' has no model-facing declaration"
)));
};
let args = step.args.clone().unwrap_or_default();
let assembled = match assemble_arguments(
&Value::Object(args),
&plan_label,
&input,
&outputs,
) {
Ok(assembled) => assembled,
Err(why) => {
return Ok(Outcome::fail(format!("plan step {index}: {why}")));
}
};
if let Err(detail) =
crate::model::validate_schema(&declaration.parameters, assembled.peek())
{
return Ok(Outcome::fail(format!("plan step {index}: {detail}")));
}
let reference = id.reference();
if grant.requires_approval {
let Some(spec) = oversight.as_ref() else {
return Err(SkillError::Other(
"a tool grant requires approval but the agent declares no \
oversight policy — there is nobody to ask"
.into(),
));
};
cx.deadline(spec.deadline.name.clone(), &spec.deadline.spec(), None)
.await?;
let decision = cx
.task(&spec.approve_call(&reference, assembled.peek()))
.await?;
if !decision.approved {
return Ok(Outcome::fail(format!(
"{} refused the call to {reference}: {}",
decision.actor, decision.reason
)));
}
}
let out = if id.server == crate::tools::AGENT_SERVER {
cx.commission(&id.tool, assembled).await?
} else {
let prepared = crate::tools::ToolCall::prepare(
catalog,
Arc::clone(client),
id,
assembled.peek().clone(),
)
.map_err(|e| SkillError::Other(e.to_string()))?;
cx.sink(prepared, &assembled).await?
};
outputs.push(out);
}
(None, Some(parse)) => {
let source = match resolve_reference(&parse.from, &input, &outputs) {
Ok(source) => source,
Err(why) => {
return Ok(Outcome::fail(format!("plan step {index}: {why}")));
}
};
let Some(schema) = bounded_parse_schema(&parse.schema) else {
return Ok(Outcome::fail(format!(
"plan step {index}: a parse schema must be an object schema"
)));
};
let parse_model = match cx.manifest() {
Some(m) => untrusted_contact_model(m, &model),
None => model.clone(),
};
let prompt = Tainted::object([
(
"system".to_owned(),
Tainted::trusted(json!(PARSE_INSTRUCTION)),
),
("source".to_owned(), source.clone()),
]);
let mut call = ModelCall::new(
Arc::clone(&self.provider),
parse_model,
prompt.peek().clone(),
)
.with_output_sensitivity(prompt.label().sensitivity)
.expecting(schema);
if let Some(ceiling) = egress {
call = call.with_max_sensitivity(ceiling);
}
let completion = cx.sink(call, &prompt).await?;
let label = completion.label().join(prompt.label()).clone();
let Some(mut value) = completion.peek().structured.clone() else {
return Ok(Outcome::fail(format!(
"plan step {index}: the parse returned nothing structured"
)));
};
let enough = value
.get("have_enough_information")
.and_then(Value::as_bool)
.unwrap_or(false);
if !enough {
return Ok(Outcome::fail(format!(
"plan step {index}: the parse declared the source does not \
contain enough information — the plan must hand it more of \
the source, not let a guess stand"
)));
}
if let Some(map) = value.as_object_mut() {
map.remove("have_enough_information");
}
outputs.push(Tainted::with_label(value, label));
}
_ => {
return Ok(Outcome::fail(format!(
"plan step {index} must name exactly one of `tool` or `parse`"
)));
}
}
}
let answer = match plan.answer.as_deref() {
Some(reference) => match resolve_reference(reference, &input, &outputs) {
Ok(answer) => answer,
Err(why) => return Ok(Outcome::fail(format!("plan answer: {why}"))),
},
None => match outputs.last() {
Some(last) => last.clone(),
None => return Ok(Outcome::fail("the plan produced nothing to answer with")),
},
};
if let Some(schema) = output_schema
&& let Err(detail) = crate::model::validate_schema(&schema, answer.peek())
{
return Ok(Outcome::fail(format!(
"the answer does not satisfy the declared output shape: {detail}"
)));
}
let formed_source = answer.clone();
self.settle(
cx,
answer,
formed_source,
oversight,
formation.as_ref(),
&model,
)
.await
}
}
#[allow(clippy::too_many_lines)]
#[async_trait]
impl Skill for Declarative {
fn descriptor(&self) -> SkillDescriptor {
SkillDescriptor::new(self.name.clone())
.provides(crate::core::Capability::new(self.capability.clone()))
}
async fn invoke(
&self,
cx: &mut StepCtx<'_>,
input: Tainted<Value>,
) -> Result<Outcome, SkillError> {
let (
system,
model,
max_output_tokens,
reasoning_effort,
schema,
egress,
oversight,
granted,
formation,
) = {
let m = cx.manifest().ok_or_else(|| {
SkillError::Other(
"a declarative agent ran without a manifest — it has nothing to be".into(),
)
})?;
let (model, max_output_tokens, reasoning_effort) = privileged(m).ok_or_else(|| {
SkillError::Other(format!(
"manifest '{}' declares execution but no privileged model — a \
declarative agent has nothing to call",
m.metadata.name
))
})?;
(
m.spec
.identity
.as_ref()
.map(Identity::system_prompt)
.unwrap_or_default(),
model,
max_output_tokens,
reasoning_effort,
m.output_schema().cloned(),
m.spec.security.max_sensitivity_egress,
m.spec.oversight.as_ref().map(Proposal::from_manifest),
m.spec.tools.clone(),
m.spec.memory_formation.clone(),
)
};
match self.kind {
ExecutionKind::Completion => {
let prompt = Tainted::object([
("system".to_owned(), Tainted::trusted(json!(system))),
("input".to_owned(), input),
]);
let mut call = ModelCall::new(
Arc::clone(&self.provider),
model.clone(),
prompt.peek().clone(),
)
.with_output_sensitivity(prompt.label().sensitivity);
if let Some(max_output_tokens) = max_output_tokens {
call = call.with_max_output_tokens(max_output_tokens);
}
if let Some(effort) = reasoning_effort {
call = call.with_reasoning_effort(effort);
}
if let Some(schema) = schema {
call = call.expecting(schema);
}
if let Some(ceiling) = egress {
call = call.with_max_sensitivity(ceiling);
}
let completion = cx.sink(call, &prompt).await?;
let label = completion.label().join(prompt.label());
let completion = Tainted::with_label(completion.into_unlabelled(), label);
let formed_source = Tainted::with_label(
completion
.peek()
.structured
.clone()
.unwrap_or_else(|| json!({ "text": completion.peek().text.clone() })),
completion.label().clone(),
);
let answer =
completion.map(|c| c.structured.unwrap_or_else(|| json!({ "text": c.text })));
self.settle(
cx,
answer,
formed_source,
oversight,
formation.as_ref(),
&model,
)
.await
}
ExecutionKind::ToolCalling => {
self.tool_loop(
cx,
input,
system,
(model, max_output_tokens, reasoning_effort),
egress,
granted,
oversight,
formation,
)
.await
}
ExecutionKind::Planned => {
self.planned(
cx,
input,
system,
(model, max_output_tokens, reasoning_effort),
egress,
granted,
oversight,
formation,
schema,
)
.await
}
}
}
}
#[derive(Debug, Clone)]
struct Proposal {
approval: crate::manifest::Approval,
approvers: Vec<String>,
deadline: crate::manifest::OversightDeadline,
on_expiry: crate::core::OnExpiry,
allow_unattended: bool,
}
impl Proposal {
fn from_manifest(o: &crate::manifest::Oversight) -> Self {
use crate::manifest::Expiry;
Self {
approval: o.approval,
approvers: o.approvers.clone(),
deadline: o.deadline.clone(),
on_expiry: match o.on_expiry {
Expiry::Deny => crate::core::OnExpiry::Deny,
Expiry::Escalate => crate::core::OnExpiry::Escalate,
Expiry::Proceed => crate::core::OnExpiry::Proceed,
},
allow_unattended: o.allow_unattended,
}
}
const fn gates_the_answer(&self) -> bool {
matches!(self.approval, crate::manifest::Approval::Required)
}
fn approve_answer(&self, answer: Value) -> crate::core::TaskSpec {
self.task("agent.approve", "approve this agent's answer", answer)
}
fn approve_call(&self, reference: &str, arguments: &Value) -> crate::core::TaskSpec {
self.task(
"agent.approve_call",
format!("approve this agent's call to {reference}"),
json!({ "tool": reference, "arguments": arguments }),
)
}
fn task(&self, kind: &str, summary: impl Into<String>, action: Value) -> crate::core::TaskSpec {
let mut spec = crate::core::TaskSpec::new(
kind,
crate::core::Justification::new(summary, action),
self.deadline.name.clone(),
);
spec.candidate_roles.clone_from(&self.approvers);
spec.on_expiry = self.on_expiry;
spec.allow_unattended = self.allow_unattended;
spec
}
}
const PARSE_INSTRUCTION: &str = "Extract the requested fields from the source. Record only \
what the source literally states: do not infer or invent email addresses, dates, \
identifiers, names or amounts that are not present. If the source does not contain \
enough information, set `have_enough_information` to false and every other field to \
an empty or zero value.";
fn plan_schema(max_steps: u32) -> Value {
json!({
"type": "object",
"additionalProperties": false,
"required": ["steps"],
"properties": {
"steps": {
"type": "array",
"minItems": 1,
"maxItems": max_steps,
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"tool": {
"type": "string",
"description": "a granted tool to call, named exactly as offered"
},
"args": {
"type": "object",
"description": "the tool's arguments. A string beginning with '$' \
is a reference to earlier data, not a literal: '$input' is \
the run's input and '$step0' is the first step's output, \
and a JSON Pointer may follow the head, e.g. \
'$step1/customer/email'. Escape a literal leading '$' as \
'$$'. Prefer references over copying values: a reference \
carries the data's provenance, a copy does not"
},
"parse": {
"type": "object",
"additionalProperties": false,
"required": ["from", "schema"],
"properties": {
"from": {
"type": "string",
"description": "reference to the value to extract from, \
e.g. '$step0/body'"
},
"schema": {
"type": "object",
"description": "a JSON Schema with type 'object' naming \
the fields to extract"
}
},
"description": "extract structured fields from a prior output \
instead of calling a tool"
}
}
}
},
"answer": {
"type": "string",
"description": "reference selecting the run's answer, e.g. '$step1/summary'; \
omitted means the last step's output"
}
}
})
}
#[derive(Debug, serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct PlanDoc {
steps: Vec<PlanStep>,
#[serde(default)]
answer: Option<String>,
}
#[derive(Debug, serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct PlanStep {
#[serde(default)]
tool: Option<String>,
#[serde(default)]
args: Option<serde_json::Map<String, Value>>,
#[serde(default)]
parse: Option<ParseStep>,
}
#[derive(Debug, serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct ParseStep {
from: String,
schema: Value,
}
fn resolve_reference(
reference: &str,
input: &Tainted<Value>,
outputs: &[Tainted<Value>],
) -> Result<Tainted<Value>, String> {
let (head, pointer) = match reference.find('/') {
Some(split) => (&reference[..split], &reference[split..]),
None => (reference, ""),
};
let base = if head == "$input" {
input
} else {
let step = head
.strip_prefix("$step")
.and_then(|n| n.parse::<usize>().ok())
.ok_or_else(|| {
format!(
"'{reference}' is not a reference this plan can hold — use $input \
or $step<N>"
)
})?;
outputs
.get(step)
.ok_or_else(|| format!("'{reference}' points at a step that has not run yet"))?
};
base.project_pointer(pointer)
.ok_or_else(|| format!("'{reference}' selects nothing in the value it points at"))
}
fn assemble_arguments(
value: &Value,
plan_label: &crate::core::Label,
input: &Tainted<Value>,
outputs: &[Tainted<Value>],
) -> Result<Tainted<Value>, String> {
match value {
Value::String(s) if s.starts_with("$$") => Ok(Tainted::with_label(
Value::String(s[1..].to_owned()),
plan_label.clone(),
)),
Value::String(s) if s.starts_with('$') => resolve_reference(s, input, outputs),
Value::Object(map) => {
let mut fields = Vec::with_capacity(map.len());
for (name, nested) in map {
fields.push((
name.clone(),
assemble_arguments(nested, plan_label, input, outputs)?,
));
}
Ok(Tainted::object(fields))
}
Value::Array(items) => {
let mut elements = Vec::with_capacity(items.len());
for nested in items {
elements.push(assemble_arguments(nested, plan_label, input, outputs)?);
}
Ok(Tainted::array(elements))
}
other => Ok(Tainted::with_label(other.clone(), plan_label.clone())),
}
}
fn bounded_parse_schema(declared: &Value) -> Option<Value> {
if declared.get("type") != Some(&json!("object")) {
return None;
}
let mut schema = declared.clone();
let map = schema.as_object_mut()?;
map.insert("additionalProperties".to_owned(), json!(false));
let properties = map
.entry("properties")
.or_insert_with(|| json!({}))
.as_object_mut()?;
properties.insert(
"have_enough_information".to_owned(),
json!({
"type": "boolean",
"description": "Whether the source provided enough information. Set false \
rather than inventing any value."
}),
);
let required = map.entry("required").or_insert_with(|| json!([]));
let required = required.as_array_mut()?;
if !required.contains(&json!("have_enough_information")) {
required.push(json!("have_enough_information"));
}
Some(schema)
}
fn untrusted_contact_model(m: &Manifest, fallback: &ModelId) -> ModelId {
m.spec
.models
.as_ref()
.and_then(|models| models.quarantined.as_ref())
.map_or_else(|| fallback.clone(), |r| ModelId::new(&r.provider, &r.model))
}
fn offered_tools<'g>(
catalog: &crate::tools::ToolCatalog,
granted: &'g [crate::manifest::ToolGrant],
) -> (
Vec<(crate::tools::ToolId, &'g crate::manifest::ToolGrant)>,
Vec<crate::model::ToolDeclaration>,
) {
let offered: Vec<(crate::tools::ToolId, &crate::manifest::ToolGrant)> = granted
.iter()
.filter_map(|g| catalog.resolve_reference(&g.reference).map(|id| (id, g)))
.collect();
let declared: Vec<crate::model::ToolDeclaration> = offered
.iter()
.map(|(id, grant)| {
let (description, arguments) = catalog.declaration(id).map_or_else(
|| {
(
grant.description.clone().unwrap_or_default(),
grant
.arguments
.clone()
.unwrap_or_else(|| json!({ "type": "object" })),
)
},
|(description, arguments)| (description.to_owned(), arguments.clone()),
);
crate::model::ToolDeclaration::new(id.wire_name(), description, arguments)
})
.collect();
(offered, declared)
}
fn privileged(
m: &Manifest,
) -> Option<(ModelId, Option<u32>, Option<crate::model::ReasoningEffort>)> {
let r = m.spec.models.as_ref()?.privileged.as_ref()?;
Some((
ModelId::new(&r.provider, &r.model),
r.max_tokens,
r.reasoning_effort,
))
}
fn model_facing(e: &crate::core::StepError) -> Option<String> {
match e {
crate::core::StepError::Policy(p) => Some(p.for_model().to_owned()),
crate::core::StepError::Effect(inner)
if inner.disposition() != crate::core::Disposition::InDoubt =>
{
Some(inner.to_string())
}
_ => None,
}
}