1use super::reactor::Runtime;
26use crate::engine::template::Data;
27use serde_json::{Value, json};
28
29const CAP_LIST: usize = 16;
34const CAP_PEERS: usize = 24;
35
36pub const DEFAULT_TEMPLATE: &str = r#"You are {{instance}}, an autonomous, durable agent (agentd). You act by calling tools and reply when done.
40{{#if tools.internal_text}}Internal tools ({{tools.internal_text}}) are executed by your runtime and are durable; other tools come from connected MCP servers.
41{{/if}}Be concise and factual; never invent tool results.
42{{#if instruction}}
43## Instruction
44{{instruction}}
45{{/if}}{{#if extra}}
46{{extra}}
47{{/if}}{{#if workflows}}
48## Workflows
49{{#each workflows}}- {{this.name}}{{#if this.description}}: {{this.description}}{{/if}}
50{{/each}}{{/if}}{{#if services}}
51## Services (the external services this deployment may use)
52{{#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}}
53{{/each}}{{#if egress_closed}}Egress is CLOSED: only these services are reachable.
54{{/if}}{{/if}}{{#if streams}}
55## Streams (durable events; publish with an emit step, consume with a stream start)
56{{#each streams}}- {{this}}
57{{/each}}{{/if}}{{#if templates}}
58## Subagent templates (spawn with subagent.run {template, params})
59{{#each templates}}- {{this.name}} ({{this.tier}}){{#if this.params_text}} — params: {{this.params_text}}{{/if}}
60{{/each}}{{/if}}{{#if skills}}
61{{skills}}
62{{/if}}{{#if peers}}
63## Peers (agents reachable with a2a.send / a2a.delegate)
64{{#each peers}}- {{this.name}}{{#if this.note}} ({{this.note}}){{/if}}
65{{/each}}{{/if}}{{#if signals.any}}
66## Signals (durable coordination; deliver with workflow.signal)
67{{#each signals.waiting}}- waiting: {{this.name}} (run {{this.run}}, step {{this.step}})
68{{/each}}{{#each signals.recent}}- fired recently: {{this}}
69{{/each}}{{/if}}{{#if memory.keys_text}}
70## Memory
71Keys you can read with memory.get: {{memory.keys_text}}
72{{/if}}"#;
73
74impl Runtime {
75 pub(crate) fn prompt_data(
78 &self,
79 ctx: Option<&crate::context::ContextState>,
80 extra: Option<&str>,
81 ) -> Data {
82 let mut d = Data::new();
83 d.insert("instance".into(), json!(self.instance));
84 d.insert(
85 "instruction".into(),
86 json!(self.instruction.text.trim().to_string()),
87 );
88 d.insert("extra".into(), json!(extra.unwrap_or("")));
89 let internal = self.granted_internal_tools();
90 d.insert(
91 "tools".into(),
92 json!({"internal": internal, "internal_text": internal.join(", ")}),
93 );
94
95 d.insert(
97 "workflows".into(),
98 Value::Array(
99 self.workflows
100 .values()
101 .map(|w| json!({"name": w.name, "description": w.description}))
102 .collect(),
103 ),
104 );
105 d.insert(
106 "services".into(),
107 Value::Array(
108 self.settings
109 .services
110 .iter()
111 .take(CAP_LIST)
112 .map(|(name, e)| {
113 let tags: Vec<&str> = e
114 .tags
115 .values()
116 .flatten()
117 .map(String::as_str)
118 .collect::<std::collections::BTreeSet<_>>()
119 .into_iter()
120 .collect();
121 let tools: Vec<String> = e.allow.clone().unwrap_or_default();
122 json!({"name": name, "kind": e.kind.as_str(),
123 "tags": tags, "tags_text": tags.join(", "),
124 "tools": tools, "tools_text": tools.join(", "),
125 "rate": e.rate})
126 })
127 .collect(),
128 ),
129 );
130 d.insert(
131 "egress_closed".into(),
132 json!(self.settings.security.egress == crate::config::v2::Egress::Closed),
133 );
134 d.insert(
135 "streams".into(),
136 Value::Array(
137 self.settings
138 .streams
139 .keys()
140 .take(CAP_LIST)
141 .map(|k| json!(k))
142 .collect(),
143 ),
144 );
145 d.insert(
146 "templates".into(),
147 Value::Array(
148 self.settings
149 .subagents
150 .templates
151 .iter()
152 .take(CAP_LIST)
153 .map(|(name, t)| {
154 let tier = if t
155 .instruction
156 .lines()
157 .any(|l| l.trim_start().starts_with(":::"))
158 {
159 "instance"
160 } else {
161 "flat"
162 };
163 let params: Vec<String> = t
164 .params
165 .iter()
166 .map(|(p, spec)| {
167 if spec.required {
168 format!("{p} (required)")
169 } else {
170 p.clone()
171 }
172 })
173 .collect();
174 json!({"name": name, "tier": tier,
175 "params": params, "params_text": params.join(", ")})
176 })
177 .collect(),
178 ),
179 );
180 let mut skills = String::new();
183 if let Some(cat) = self.skills.render_catalogue() {
184 skills.push_str(&cat);
185 }
186 if let Some(c) = ctx {
187 let bodies: Vec<&crate::context::skills::SkillBody> = c
188 .skills
189 .iter()
190 .filter_map(|r| self.skills.body(&r.hash))
191 .collect();
192 if let Some(b) = crate::context::skills::render_bodies(&bodies) {
193 if !skills.is_empty() {
194 skills.push('\n');
195 }
196 skills.push_str(&b);
197 }
198 }
199 d.insert("skills".into(), json!(skills.trim_end()));
200
201 let mut peers: Vec<Value> = self
203 .settings
204 .a2a
205 .peers
206 .iter()
207 .map(|p| json!({"name": p.name, "note": Value::Null}))
208 .collect();
209 for rec in self.subagents.values() {
210 if rec.tier.as_deref() == Some("instance")
211 && !super::reactor::is_terminal_status(&rec.status)
212 {
213 peers.push(json!({"name": rec.handle, "note": format!(
214 "instance child of template '{}', {}",
215 rec.template.as_deref().unwrap_or("?"), rec.status)}));
216 }
217 }
218 peers.truncate(CAP_PEERS);
219 d.insert("peers".into(), Value::Array(peers));
220
221 let mut waiting: Vec<Value> = Vec::new();
222 for (rid, run) in &self.runs {
223 for (sid, st) in &run.steps {
224 if st.status == crate::engine::run::StepStatus::Suspended
225 && let Some(w) = &st.wait
226 && w["kind"] == "signal"
227 && let Some(name) = w["signal"].as_str()
228 {
229 waiting.push(json!({"name": name, "run": rid, "step": sid}));
230 }
231 }
232 }
233 waiting.truncate(CAP_LIST);
234 let recent: Vec<Value> = self
235 .recent_signals
236 .keys()
237 .rev()
238 .take(8)
239 .map(|k| json!(k))
240 .collect();
241 let any = !waiting.is_empty() || !recent.is_empty();
242 d.insert(
243 "signals".into(),
244 json!({"waiting": waiting, "recent": recent, "any": any}),
245 );
246
247 let keys = self.memory_keys_hint().unwrap_or_default();
248 d.insert(
249 "memory".into(),
250 json!({"keys": keys, "keys_text": keys.join(", ")}),
251 );
252 d
253 }
254
255 fn granted_internal_tools(&self) -> Vec<String> {
261 let mut families: Vec<String> = Vec::new();
262 for t in self.registry.iter() {
263 if t.class != crate::registry::ToolClass::Internal
264 || t.disabled
265 || !self
266 .registry
267 .allowed(&crate::registry::Caller::Root, &t.name)
268 {
269 continue;
270 }
271 let fam = match t.name.split_once('.') {
272 Some((head, _)) => format!("{head}.*"),
273 None => t.name.clone(),
274 };
275 if !families.contains(&fam) {
276 families.push(fam);
277 }
278 }
279 families.sort();
280 families
281 }
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 #[test]
289 fn the_default_template_needs_no_cel() {
290 let t = crate::context::prompt::Template::parse(DEFAULT_TEMPLATE)
294 .expect("the built-in template parses");
295 assert!(
296 !t.needs_cel,
297 "the default template must use bare paths only — it renders on every build"
298 );
299 assert!(
300 t.reads("instruction"),
301 "the default carries standing policy"
302 );
303 }
304
305 #[test]
306 fn the_default_renders_stable_before_volatile() {
307 let pos = |needle: &str| {
310 DEFAULT_TEMPLATE
311 .find(needle)
312 .unwrap_or_else(|| panic!("default template lost {needle}"))
313 };
314 assert!(pos("## Instruction") < pos("## Services"));
315 assert!(pos("## Services") < pos("## Peers"));
316 assert!(pos("## Peers") < pos("## Signals"));
317 assert!(pos("## Signals") < pos("## Memory"));
318 }
319}