use super::reactor::Runtime;
use crate::engine::template::Data;
use serde_json::{Value, json};
const CAP_LIST: usize = 16;
const CAP_PEERS: usize = 24;
pub const DEFAULT_TEMPLATE: &str = r#"You are {{instance}}, an autonomous, durable agent (agentd). You act by calling tools and reply when done.
{{#if tools.internal_text}}Internal tools ({{tools.internal_text}}) are executed by your runtime and are durable; other tools come from connected MCP servers.
{{/if}}Be concise and factual; never invent tool results.
{{#if instruction}}
## Instruction
{{instruction}}
{{/if}}{{#if extra}}
{{extra}}
{{/if}}{{#if workflows}}
## Workflows
{{#each workflows}}- {{this.name}}{{#if this.description}}: {{this.description}}{{/if}}
{{/each}}{{/if}}{{#if services}}
## Services (the external services this deployment may use)
{{#each services}}- {{this.name}}{{#if this.tags_text}} [{{this.tags_text}}]{{/if}}{{#if this.tools_text}} — tools: {{this.tools_text}}{{/if}}{{#if this.rate}} (rate {{this.rate}}){{/if}}
{{/each}}{{#if egress_closed}}Egress is CLOSED: only these services are reachable.
{{/if}}{{/if}}{{#if streams}}
## Streams (durable events; publish with an emit step, consume with a stream start)
{{#each streams}}- {{this}}
{{/each}}{{/if}}{{#if templates}}
## Subagent templates (spawn with subagent.run {template, params})
{{#each templates}}- {{this.name}} ({{this.tier}}){{#if this.params_text}} — params: {{this.params_text}}{{/if}}
{{/each}}{{/if}}{{#if skills}}
{{skills}}
{{/if}}{{#if peers}}
## Peers (agents reachable with a2a.send / a2a.delegate)
{{#each peers}}- {{this.name}}{{#if this.note}} ({{this.note}}){{/if}}
{{/each}}{{/if}}{{#if signals.any}}
## Signals (durable coordination; deliver with workflow.signal)
{{#each signals.waiting}}- waiting: {{this.name}} (run {{this.run}}, step {{this.step}})
{{/each}}{{#each signals.recent}}- fired recently: {{this}}
{{/each}}{{/if}}{{#if memory.keys_text}}
## Memory
Keys you can read with memory.get: {{memory.keys_text}}
{{/if}}"#;
impl Runtime {
pub(crate) fn prompt_data(
&self,
ctx: Option<&crate::context::ContextState>,
extra: Option<&str>,
) -> Data {
let mut d = Data::new();
d.insert("instance".into(), json!(self.instance));
d.insert(
"instruction".into(),
json!(self.instruction.text.trim().to_string()),
);
d.insert("extra".into(), json!(extra.unwrap_or("")));
let internal = self.granted_internal_tools();
d.insert(
"tools".into(),
json!({"internal": internal, "internal_text": internal.join(", ")}),
);
d.insert(
"workflows".into(),
Value::Array(
self.workflows
.values()
.map(|w| json!({"name": w.name, "description": w.description}))
.collect(),
),
);
d.insert(
"services".into(),
Value::Array(
self.settings
.services
.iter()
.take(CAP_LIST)
.map(|(name, e)| {
let tags: Vec<&str> = e
.tags
.values()
.flatten()
.map(String::as_str)
.collect::<std::collections::BTreeSet<_>>()
.into_iter()
.collect();
let tools: Vec<String> = e.allow.clone().unwrap_or_default();
json!({"name": name, "kind": e.kind.as_str(),
"tags": tags, "tags_text": tags.join(", "),
"tools": tools, "tools_text": tools.join(", "),
"rate": e.rate})
})
.collect(),
),
);
d.insert(
"egress_closed".into(),
json!(self.settings.security.egress == crate::config::v2::Egress::Closed),
);
d.insert(
"streams".into(),
Value::Array(
self.settings
.streams
.keys()
.take(CAP_LIST)
.map(|k| json!(k))
.collect(),
),
);
d.insert(
"templates".into(),
Value::Array(
self.settings
.subagents
.templates
.iter()
.take(CAP_LIST)
.map(|(name, t)| {
let tier = if t
.instruction
.lines()
.any(|l| l.trim_start().starts_with(":::"))
{
"instance"
} else {
"flat"
};
let params: Vec<String> = t
.params
.iter()
.map(|(p, spec)| {
if spec.required {
format!("{p} (required)")
} else {
p.clone()
}
})
.collect();
json!({"name": name, "tier": tier,
"params": params, "params_text": params.join(", ")})
})
.collect(),
),
);
let mut skills = String::new();
if let Some(cat) = self.skills.render_catalogue() {
skills.push_str(&cat);
}
if let Some(c) = ctx {
let bodies: Vec<&crate::context::skills::SkillBody> = c
.skills
.iter()
.filter_map(|r| self.skills.body(&r.hash))
.collect();
if let Some(b) = crate::context::skills::render_bodies(&bodies) {
if !skills.is_empty() {
skills.push('\n');
}
skills.push_str(&b);
}
}
d.insert("skills".into(), json!(skills.trim_end()));
let mut peers: Vec<Value> = self
.settings
.a2a
.peers
.iter()
.map(|p| json!({"name": p.name, "note": Value::Null}))
.collect();
for rec in self.subagents.values() {
if rec.tier.as_deref() == Some("instance")
&& !super::reactor::is_terminal_status(&rec.status)
{
peers.push(json!({"name": rec.handle, "note": format!(
"instance child of template '{}', {}",
rec.template.as_deref().unwrap_or("?"), rec.status)}));
}
}
peers.truncate(CAP_PEERS);
d.insert("peers".into(), Value::Array(peers));
let mut waiting: Vec<Value> = Vec::new();
for (rid, run) in &self.runs {
for (sid, st) in &run.steps {
if st.status == crate::engine::run::StepStatus::Suspended
&& let Some(w) = &st.wait
&& w["kind"] == "signal"
&& let Some(name) = w["signal"].as_str()
{
waiting.push(json!({"name": name, "run": rid, "step": sid}));
}
}
}
waiting.truncate(CAP_LIST);
let recent: Vec<Value> = self
.recent_signals
.keys()
.rev()
.take(8)
.map(|k| json!(k))
.collect();
let any = !waiting.is_empty() || !recent.is_empty();
d.insert(
"signals".into(),
json!({"waiting": waiting, "recent": recent, "any": any}),
);
let keys = self.memory_keys_hint().unwrap_or_default();
d.insert(
"memory".into(),
json!({"keys": keys, "keys_text": keys.join(", ")}),
);
d
}
fn granted_internal_tools(&self) -> Vec<String> {
let mut families: Vec<String> = Vec::new();
for t in self.registry.iter() {
if t.class != crate::registry::ToolClass::Internal
|| t.disabled
|| !self
.registry
.allowed(&crate::registry::Caller::Root, &t.name)
{
continue;
}
let fam = match t.name.split_once('.') {
Some((head, _)) => format!("{head}.*"),
None => t.name.clone(),
};
if !families.contains(&fam) {
families.push(fam);
}
}
families.sort();
families
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_default_template_needs_no_cel() {
let t = crate::context::prompt::Template::parse(DEFAULT_TEMPLATE)
.expect("the built-in template parses");
assert!(
!t.needs_cel,
"the default template must use bare paths only — it renders on every build"
);
assert!(
t.reads("instruction"),
"the default carries standing policy"
);
}
#[test]
fn the_default_renders_stable_before_volatile() {
let pos = |needle: &str| {
DEFAULT_TEMPLATE
.find(needle)
.unwrap_or_else(|| panic!("default template lost {needle}"))
};
assert!(pos("## Instruction") < pos("## Services"));
assert!(pos("## Services") < pos("## Peers"));
assert!(pos("## Peers") < pos("## Signals"));
assert!(pos("## Signals") < pos("## Memory"));
}
}