Skip to main content

agent_playbook/
agent_playbook.rs

1use axllm::{
2    agent_with_options, AxAIClient, AxCodeRuntime, AxCodeSession, AxResult, RuntimeEnvelope,
3};
4use serde_json::{json, Value};
5use std::cell::RefCell;
6use std::rc::Rc;
7
8// The actor returns model-authored Python code and a real runtime executes it.
9// The same offline response also satisfies the playbook reflector and curator.
10struct ScriptedClient;
11
12impl AxAIClient for ScriptedClient {
13    fn chat(&mut self, _request: Value) -> AxResult<Value> {
14        let content = json!({
15            "pythonCode": "final('Answer', {'answer': 'Ax composes typed LLM programs.'})",
16            "answer": "Ax composes typed LLM programs.",
17            "reasoning": "The playbook lacked a brevity rule.",
18            "errorIdentification": "Answer was too verbose.",
19            "rootCauseAnalysis": "No guidance on conciseness.",
20            "correctApproach": "Add a concise-answer guideline.",
21            "keyInsight": "Prefer one-sentence answers.",
22            "weaknessDescription": "The agent does not verify its final step.",
23            "rootCause": "The final step is accepted without a check.",
24            "proposedGuidance": "Verify the final step before completing the task.",
25            "evidenceQuotes": ["final", "snapshot", "Answer"],
26            "configRecommendations": [],
27            "bulletTags": [],
28            "operations": [
29                {"type": "ADD", "section": "Guidelines", "content": "Answer in one concise sentence."}
30            ]
31        })
32        .to_string();
33        Ok(json!({"results": [{"content": content}]}))
34    }
35}
36
37struct RuntimeSession;
38
39impl AxCodeSession for RuntimeSession {
40    fn execute(&mut self, code: &str, _options: Value) -> AxResult<RuntimeEnvelope> {
41        assert!(
42            !code.contains("pythonCode"),
43            "runtime received a response wrapper instead of code"
44        );
45        Ok(RuntimeEnvelope::final_payload(
46            json!({"answer": "Ax composes typed LLM programs."}),
47        ))
48    }
49
50    fn snapshot_globals(&mut self, _options: Value) -> AxResult<Value> {
51        Ok(json!({"version": 1, "bindings": {}, "globals": {}, "closed": false}))
52    }
53
54    fn patch_globals(&mut self, snapshot: Value, _options: Value) -> AxResult<Value> {
55        Ok(snapshot)
56    }
57}
58
59struct Runtime;
60
61impl AxCodeRuntime for Runtime {
62    fn language(&self) -> &str {
63        "Python"
64    }
65
66    fn create_session(
67        &mut self,
68        _globals: Value,
69        _options: Value,
70    ) -> AxResult<Box<dyn AxCodeSession>> {
71        Ok(Box::new(RuntimeSession))
72    }
73}
74
75fn main() -> AxResult<()> {
76    // agent.playbook() binds an evolving context playbook to an agent stage. The
77    // "responder" target grows the user-facing answer stage; ACE remains an
78    // implementation detail behind playbook(), just as optimize() hides GEPA.
79    let mut agent = agent_with_options(
80        "question:string -> answer:string",
81        json!({"name": "qa", "description": "Answer the question.", "runtime": {"language": "Python"}}),
82    )?
83    .with_runtime(Box::new(Runtime))?;
84
85    let student = Rc::new(RefCell::new(ScriptedClient));
86    let mut pb = agent.playbook(
87        student,
88        None::<Rc<RefCell<ScriptedClient>>>,
89        json!({"target": "responder", "maxEpochs": 1}),
90    )?;
91
92    let dataset = json!({"train": [{"input": {"question": "Answer briefly."}, "score": 0}]});
93    let mut eval_client = ScriptedClient;
94
95    // A zero minimum gain exercises verified acceptance. A positive minimum gain
96    // rejects the same flat score and must restore the exact pre-proposal snapshot.
97    let accepted = pb.evolve_agent(
98        &mut agent,
99        &mut eval_client,
100        &dataset,
101        &json!({"verify": true, "minHeldInGain": 0, "maxProposals": 1, "maxMetricCalls": 2}),
102    )?;
103    let before_rejection = serde_json::to_string(&pb.to_json())?;
104    let rejected = pb.evolve_agent(
105        &mut agent,
106        &mut eval_client,
107        &dataset,
108        &json!({"verify": true, "minHeldInGain": 0.1, "maxProposals": 1, "maxMetricCalls": 2}),
109    )?;
110    let after_rejection = serde_json::to_string(&pb.to_json())?;
111
112    assert_eq!(
113        accepted["metricCallsUsed"].as_u64(),
114        Some(2),
115        "bad metric budget: {accepted}"
116    );
117    assert_eq!(
118        accepted["outcomes"][0]["accepted"].as_bool(),
119        Some(true),
120        "verified acceptance failed: {accepted}"
121    );
122    assert_eq!(
123        rejected["metricCallsUsed"].as_u64(),
124        Some(2),
125        "bad metric budget: {rejected}"
126    );
127    assert_eq!(
128        rejected["outcomes"][0]["accepted"].as_bool(),
129        Some(false),
130        "verified rejection failed: {rejected}"
131    );
132    assert_eq!(
133        after_rejection, before_rejection,
134        "rejected proposal was not rolled back exactly"
135    );
136    assert!(
137        pb.to_json().get("playbook").is_some(),
138        "missing playbook: {}",
139        pb.to_json()
140    );
141    println!("accepted: {}", accepted["outcomes"][0]);
142    println!("rejected: {}", rejected["outcomes"][0]);
143    println!("rust-agent-playbook-ok");
144    Ok(())
145}