diff --git a/crates/soft-agent/examples/lex/grid_budget.spec b/crates/soft-agent/examples/lex/grid_budget.spec new file mode 100644 index 0000000..e75c574 --- /dev/null +++ b/crates/soft-agent/examples/lex/grid_budget.spec @@ -0,0 +1,4 @@ +spec grid_budget { + forall current_kw :: Float, delta_kw :: Float, grid_kw :: Float, pv_kw :: Float: + under_budget(current_kw, delta_kw, grid_kw, pv_kw) +} diff --git a/crates/soft-agent/examples/lex/multi_depot_depot.lex b/crates/soft-agent/examples/lex/multi_depot_depot.lex new file mode 100644 index 0000000..da6a6aa --- /dev/null +++ b/crates/soft-agent/examples/lex/multi_depot_depot.lex @@ -0,0 +1,32 @@ +fn config() -> AgentConfig { + agent_new("depot") + |> agent_peers(["vehicle"]) + |> agent_effects(["a2a"]) + |> agent_specs(["specs/grid_budget.spec"]) + |> agent_handles([ + { topic: "RequestSession", fn_name: "on_request_session" }, + ]) +} + +fn within(current :: Float, delta :: Float, grid :: Float, pv :: Float) -> Bool { + current + delta <= grid + pv +} + +fn on_request_session( + state :: { current_kw :: Float, budget_kw :: Float, pv_kw :: Float, requested_kw :: Float }, + msg :: { from :: Str, topic :: Str, payload_json :: Str }, +) -> List[ActionRecord] { + if within(state.current_kw, state.requested_kw, state.budget_kw, state.pv_kw) { + [{ + kind: "send_a2a", server: "", tool: "", args_json: "", + peer: msg.from, a2a_topic: "GrantSession", + payload_json: "{\"charger_id\":\"c-1\"}", prompt: "", + }] + } else { + [{ + kind: "send_a2a", server: "", tool: "", args_json: "", + peer: msg.from, a2a_topic: "DenySession", + payload_json: "{\"reason\":\"grid_budget\"}", prompt: "", + }] + } +} diff --git a/crates/soft-agent/examples/lex/multi_depot_host.lex b/crates/soft-agent/examples/lex/multi_depot_host.lex new file mode 100644 index 0000000..3929316 --- /dev/null +++ b/crates/soft-agent/examples/lex/multi_depot_host.lex @@ -0,0 +1,19 @@ +fn projected_load(current :: Float, delta :: Float) -> Float { + current + delta +} + +fn budget_total(grid :: Float, pv :: Float) -> Float { + grid + pv +} + +fn under_budget(current :: Float, delta :: Float, grid :: Float, pv :: Float) -> Bool { + projected_load(current, delta) <= budget_total(grid, pv) +} + +fn soc_after(current :: Float, used :: Float) -> Float { + current - used +} + +fn above_reserve(current :: Float, used :: Float, reserve :: Float) -> Bool { + soc_after(current, used) >= reserve +} diff --git a/crates/soft-agent/examples/lex/multi_depot_tms.lex b/crates/soft-agent/examples/lex/multi_depot_tms.lex new file mode 100644 index 0000000..64c4ec2 --- /dev/null +++ b/crates/soft-agent/examples/lex/multi_depot_tms.lex @@ -0,0 +1,17 @@ +fn config() -> AgentConfig { + agent_new("tms") + |> agent_peers(["vehicle"]) + |> agent_effects(["a2a"]) + |> agent_handles([ + { topic: "Acknowledge", fn_name: "terminate" }, + { topic: "Complete", fn_name: "terminate" }, + { topic: "Failed", fn_name: "terminate" }, + ]) +} + +fn terminate( + state :: { running :: Bool }, + msg :: { from :: Str, topic :: Str, payload_json :: Str }, +) -> List[ActionRecord] { + [] +} diff --git a/crates/soft-agent/examples/lex/multi_depot_vehicle.lex b/crates/soft-agent/examples/lex/multi_depot_vehicle.lex new file mode 100644 index 0000000..02eb59c --- /dev/null +++ b/crates/soft-agent/examples/lex/multi_depot_vehicle.lex @@ -0,0 +1,56 @@ +fn config() -> AgentConfig { + agent_new("vehicle") + |> agent_peers(["depot", "tms"]) + |> agent_effects(["a2a"]) + |> agent_specs(["specs/soc_reserve.spec"]) + |> agent_handles([ + { topic: "Dispatch", fn_name: "on_dispatch" }, + { topic: "GrantSession", fn_name: "on_grant" }, + { topic: "DenySession", fn_name: "on_deny" }, + ]) +} + +fn enough_soc(soc :: Float, energy :: Float, reserve :: Float) -> Bool { + soc - energy >= reserve +} + +fn on_dispatch( + state :: { soc :: Float, reserve :: Float, energy_needed :: Float }, + msg :: { from :: Str, topic :: Str, payload_json :: Str }, +) -> List[ActionRecord] { + if enough_soc(state.soc, state.energy_needed, state.reserve) { + [{ + kind: "send_a2a", server: "", tool: "", args_json: "", + peer: msg.from, a2a_topic: "Acknowledge", + payload_json: "{\"delivery_id\":\"d-1\"}", prompt: "", + }] + } else { + [{ + kind: "send_a2a", server: "", tool: "", args_json: "", + peer: "depot", a2a_topic: "RequestSession", + payload_json: "{\"vehicle_id\":\"v-1\",\"power_kw\":50}", prompt: "", + }] + } +} + +fn on_grant( + state :: { soc :: Float, reserve :: Float, energy_needed :: Float }, + msg :: { from :: Str, topic :: Str, payload_json :: Str }, +) -> List[ActionRecord] { + [{ + kind: "send_a2a", server: "", tool: "", args_json: "", + peer: "tms", a2a_topic: "Complete", + payload_json: "{\"delivery_id\":\"d-1\"}", prompt: "", + }] +} + +fn on_deny( + state :: { soc :: Float, reserve :: Float, energy_needed :: Float }, + msg :: { from :: Str, topic :: Str, payload_json :: Str }, +) -> List[ActionRecord] { + [{ + kind: "send_a2a", server: "", tool: "", args_json: "", + peer: "tms", a2a_topic: "Failed", + payload_json: "{\"reason\":\"depot_denied\"}", prompt: "", + }] +} diff --git a/crates/soft-agent/examples/lex/single_vehicle.lex b/crates/soft-agent/examples/lex/single_vehicle.lex new file mode 100644 index 0000000..25e4e8a --- /dev/null +++ b/crates/soft-agent/examples/lex/single_vehicle.lex @@ -0,0 +1,58 @@ +fn config() -> AgentConfig { + agent_new("vehicle") + |> agent_peers(["depot", "tms"]) + |> agent_mcp_servers(["telemetry", "local_planner"]) + |> agent_effects(["llm_local", "mcp", "a2a"]) + |> agent_specs(["specs/soc_reserve.spec"]) + |> agent_system_prompt("You are an autonomous truck.") + |> agent_handles([ + { topic: "Dispatch", fn_name: "on_dispatch" }, + { topic: "GrantSession", fn_name: "on_grant" }, + { topic: "DenySession", fn_name: "on_deny" }, + ]) +} + +fn enough_soc(soc :: Float, energy :: Float, reserve :: Float) -> Bool { + soc - energy >= reserve +} + +fn on_dispatch( + state :: { soc :: Float, reserve :: Float, energy_needed :: Float }, + msg :: { from :: Str, topic :: Str, payload_json :: Str }, +) -> List[ActionRecord] { + if enough_soc(state.soc, state.energy_needed, state.reserve) { + [{ + kind: "send_a2a", server: "", tool: "", args_json: "", + peer: msg.from, a2a_topic: "Acknowledge", + payload_json: "{\"delivery_id\":\"d-1\"}", prompt: "", + }] + } else { + [{ + kind: "send_a2a", server: "", tool: "", args_json: "", + peer: "depot", a2a_topic: "RequestSession", + payload_json: "{\"vehicle_id\":\"v-1\",\"power_kw\":50}", prompt: "", + }] + } +} + +fn on_grant( + state :: { soc :: Float, reserve :: Float, energy_needed :: Float }, + msg :: { from :: Str, topic :: Str, payload_json :: Str }, +) -> List[ActionRecord] { + [{ + kind: "send_a2a", server: "", tool: "", args_json: "", + peer: "tms", a2a_topic: "Complete", + payload_json: "{\"delivery_id\":\"d-1\"}", prompt: "", + }] +} + +fn on_deny( + state :: { soc :: Float, reserve :: Float, energy_needed :: Float }, + msg :: { from :: Str, topic :: Str, payload_json :: Str }, +) -> List[ActionRecord] { + [{ + kind: "send_a2a", server: "", tool: "", args_json: "", + peer: "tms", a2a_topic: "Failed", + payload_json: "{\"reason\":\"depot_denied\"}", prompt: "", + }] +} diff --git a/crates/soft-agent/examples/lex/soc_reserve.spec b/crates/soft-agent/examples/lex/soc_reserve.spec new file mode 100644 index 0000000..41960ba --- /dev/null +++ b/crates/soft-agent/examples/lex/soc_reserve.spec @@ -0,0 +1,4 @@ +spec soc_reserve { + forall current_soc :: Float, energy_used :: Float, reserve :: Float: + above_reserve(current_soc, energy_used, reserve) +} diff --git a/crates/soft-agent/examples/multi_depot.rs b/crates/soft-agent/examples/multi_depot.rs index b9286f2..432c586 100644 --- a/crates/soft-agent/examples/multi_depot.rs +++ b/crates/soft-agent/examples/multi_depot.rs @@ -16,155 +16,17 @@ use serde_json::{json, Value}; use soft_agent::{A2aMessage, Action, Gate, InProcessRouter, Mailbox, MailboxSender, Runner}; use tempfile::tempdir; -const HOST: &str = r#" -fn projected_load(current :: Float, delta :: Float) -> Float { - current + delta -} - -fn budget_total(grid :: Float, pv :: Float) -> Float { - grid + pv -} - -fn under_budget(current :: Float, delta :: Float, grid :: Float, pv :: Float) -> Bool { - projected_load(current, delta) <= budget_total(grid, pv) -} - -fn soc_after(current :: Float, used :: Float) -> Float { - current - used -} - -fn above_reserve(current :: Float, used :: Float, reserve :: Float) -> Bool { - soc_after(current, used) >= reserve -} -"#; - -const GRID_BUDGET_SPEC: &str = r#" -spec grid_budget { - forall current_kw :: Float, delta_kw :: Float, grid_kw :: Float, pv_kw :: Float: - under_budget(current_kw, delta_kw, grid_kw, pv_kw) -} -"#; - -const SOC_RESERVE_SPEC: &str = r#" -spec soc_reserve { - forall current_soc :: Float, energy_used :: Float, reserve :: Float: - above_reserve(current_soc, energy_used, reserve) -} -"#; - -const VEHICLE_LEX: &str = r#" -fn config() -> AgentConfig { - agent_new("vehicle") - |> agent_peers(["depot", "tms"]) - |> agent_effects(["a2a"]) - |> agent_specs(["specs/soc_reserve.spec"]) - |> agent_handles([ - { topic: "Dispatch", fn_name: "on_dispatch" }, - { topic: "GrantSession", fn_name: "on_grant" }, - { topic: "DenySession", fn_name: "on_deny" }, - ]) -} - -fn enough_soc(soc :: Float, energy :: Float, reserve :: Float) -> Bool { - soc - energy >= reserve -} - -fn on_dispatch( - state :: { soc :: Float, reserve :: Float, energy_needed :: Float }, - msg :: { from :: Str, topic :: Str, payload_json :: Str }, -) -> List[ActionRecord] { - if enough_soc(state.soc, state.energy_needed, state.reserve) { - [{ - kind: "send_a2a", server: "", tool: "", args_json: "", - peer: msg.from, a2a_topic: "Acknowledge", - payload_json: "{\"delivery_id\":\"d-1\"}", prompt: "", - }] - } else { - [{ - kind: "send_a2a", server: "", tool: "", args_json: "", - peer: "depot", a2a_topic: "RequestSession", - payload_json: "{\"vehicle_id\":\"v-1\",\"power_kw\":50}", prompt: "", - }] - } -} - -fn on_grant( - state :: { soc :: Float, reserve :: Float, energy_needed :: Float }, - msg :: { from :: Str, topic :: Str, payload_json :: Str }, -) -> List[ActionRecord] { - [{ - kind: "send_a2a", server: "", tool: "", args_json: "", - peer: "tms", a2a_topic: "Complete", - payload_json: "{\"delivery_id\":\"d-1\"}", prompt: "", - }] -} +const HOST: &str = include_str!("lex/multi_depot_host.lex"); -fn on_deny( - state :: { soc :: Float, reserve :: Float, energy_needed :: Float }, - msg :: { from :: Str, topic :: Str, payload_json :: Str }, -) -> List[ActionRecord] { - [{ - kind: "send_a2a", server: "", tool: "", args_json: "", - peer: "tms", a2a_topic: "Failed", - payload_json: "{\"reason\":\"depot_denied\"}", prompt: "", - }] -} -"#; +const GRID_BUDGET_SPEC: &str = include_str!("lex/grid_budget.spec"); -const DEPOT_LEX: &str = r#" -fn config() -> AgentConfig { - agent_new("depot") - |> agent_peers(["vehicle"]) - |> agent_effects(["a2a"]) - |> agent_specs(["specs/grid_budget.spec"]) - |> agent_handles([ - { topic: "RequestSession", fn_name: "on_request_session" }, - ]) -} +const SOC_RESERVE_SPEC: &str = include_str!("lex/soc_reserve.spec"); -fn within(current :: Float, delta :: Float, grid :: Float, pv :: Float) -> Bool { - current + delta <= grid + pv -} +const VEHICLE_LEX: &str = include_str!("lex/multi_depot_vehicle.lex"); -fn on_request_session( - state :: { current_kw :: Float, budget_kw :: Float, pv_kw :: Float, requested_kw :: Float }, - msg :: { from :: Str, topic :: Str, payload_json :: Str }, -) -> List[ActionRecord] { - if within(state.current_kw, state.requested_kw, state.budget_kw, state.pv_kw) { - [{ - kind: "send_a2a", server: "", tool: "", args_json: "", - peer: msg.from, a2a_topic: "GrantSession", - payload_json: "{\"charger_id\":\"c-1\"}", prompt: "", - }] - } else { - [{ - kind: "send_a2a", server: "", tool: "", args_json: "", - peer: msg.from, a2a_topic: "DenySession", - payload_json: "{\"reason\":\"grid_budget\"}", prompt: "", - }] - } -} -"#; +const DEPOT_LEX: &str = include_str!("lex/multi_depot_depot.lex"); -const TMS_LEX: &str = r#" -fn config() -> AgentConfig { - agent_new("tms") - |> agent_peers(["vehicle"]) - |> agent_effects(["a2a"]) - |> agent_handles([ - { topic: "Acknowledge", fn_name: "terminate" }, - { topic: "Complete", fn_name: "terminate" }, - { topic: "Failed", fn_name: "terminate" }, - ]) -} - -fn terminate( - state :: { running :: Bool }, - msg :: { from :: Str, topic :: Str, payload_json :: Str }, -) -> List[ActionRecord] { - [] -} -"#; +const TMS_LEX: &str = include_str!("lex/multi_depot_tms.lex"); #[derive(Clone)] struct Scenario { diff --git a/crates/soft-agent/examples/single_lex.rs b/crates/soft-agent/examples/single_lex.rs index 72601db..8da5115 100644 --- a/crates/soft-agent/examples/single_lex.rs +++ b/crates/soft-agent/examples/single_lex.rs @@ -9,66 +9,7 @@ use serde_json::json; use soft_agent::{A2aMessage, Mailbox, Runner}; use tempfile::tempdir; -const VEHICLE_LEX: &str = r#" -fn config() -> AgentConfig { - agent_new("vehicle") - |> agent_peers(["depot", "tms"]) - |> agent_mcp_servers(["telemetry", "local_planner"]) - |> agent_effects(["llm_local", "mcp", "a2a"]) - |> agent_specs(["specs/soc_reserve.spec"]) - |> agent_system_prompt("You are an autonomous truck.") - |> agent_handles([ - { topic: "Dispatch", fn_name: "on_dispatch" }, - { topic: "GrantSession", fn_name: "on_grant" }, - { topic: "DenySession", fn_name: "on_deny" }, - ]) -} - -fn enough_soc(soc :: Float, energy :: Float, reserve :: Float) -> Bool { - soc - energy >= reserve -} - -fn on_dispatch( - state :: { soc :: Float, reserve :: Float, energy_needed :: Float }, - msg :: { from :: Str, topic :: Str, payload_json :: Str }, -) -> List[ActionRecord] { - if enough_soc(state.soc, state.energy_needed, state.reserve) { - [{ - kind: "send_a2a", server: "", tool: "", args_json: "", - peer: msg.from, a2a_topic: "Acknowledge", - payload_json: "{\"delivery_id\":\"d-1\"}", prompt: "", - }] - } else { - [{ - kind: "send_a2a", server: "", tool: "", args_json: "", - peer: "depot", a2a_topic: "RequestSession", - payload_json: "{\"vehicle_id\":\"v-1\",\"power_kw\":50}", prompt: "", - }] - } -} - -fn on_grant( - state :: { soc :: Float, reserve :: Float, energy_needed :: Float }, - msg :: { from :: Str, topic :: Str, payload_json :: Str }, -) -> List[ActionRecord] { - [{ - kind: "send_a2a", server: "", tool: "", args_json: "", - peer: "tms", a2a_topic: "Complete", - payload_json: "{\"delivery_id\":\"d-1\"}", prompt: "", - }] -} - -fn on_deny( - state :: { soc :: Float, reserve :: Float, energy_needed :: Float }, - msg :: { from :: Str, topic :: Str, payload_json :: Str }, -) -> List[ActionRecord] { - [{ - kind: "send_a2a", server: "", tool: "", args_json: "", - peer: "tms", a2a_topic: "Failed", - payload_json: "{\"reason\":\"depot_denied\"}", prompt: "", - }] -} -"#; +const VEHICLE_LEX: &str = include_str!("lex/single_vehicle.lex"); fn main() -> Result<(), Box> { println!("=== single-agent demo (config + handlers entirely in Lex) ===\n"); diff --git a/crates/soft-agent/src/dsl_preamble.lex b/crates/soft-agent/src/dsl_preamble.lex new file mode 100644 index 0000000..04660b6 --- /dev/null +++ b/crates/soft-agent/src/dsl_preamble.lex @@ -0,0 +1,57 @@ +type ActionRecord = { + kind :: Str, server :: Str, tool :: Str, args_json :: Str, + peer :: Str, a2a_topic :: Str, payload_json :: Str, prompt :: Str, +} + +type HandlerEntry = { topic :: Str, fn_name :: Str } + +type AgentConfig = { + name :: Str, + peers :: List[Str], + mcp_servers :: List[Str], + effects :: List[Str], + handlers :: List[HandlerEntry], + spec_paths :: List[Str], + system_prompt :: Str, +} + +fn agent_new(name :: Str) -> AgentConfig { + { name: name, peers: [], mcp_servers: [], effects: [], + handlers: [], spec_paths: [], system_prompt: "" } +} + +fn agent_peers(c :: AgentConfig, peers :: List[Str]) -> AgentConfig { + { name: c.name, peers: peers, mcp_servers: c.mcp_servers, + effects: c.effects, handlers: c.handlers, + spec_paths: c.spec_paths, system_prompt: c.system_prompt } +} + +fn agent_mcp_servers(c :: AgentConfig, servers :: List[Str]) -> AgentConfig { + { name: c.name, peers: c.peers, mcp_servers: servers, + effects: c.effects, handlers: c.handlers, + spec_paths: c.spec_paths, system_prompt: c.system_prompt } +} + +fn agent_effects(c :: AgentConfig, effects :: List[Str]) -> AgentConfig { + { name: c.name, peers: c.peers, mcp_servers: c.mcp_servers, + effects: effects, handlers: c.handlers, + spec_paths: c.spec_paths, system_prompt: c.system_prompt } +} + +fn agent_handles(c :: AgentConfig, handlers :: List[HandlerEntry]) -> AgentConfig { + { name: c.name, peers: c.peers, mcp_servers: c.mcp_servers, + effects: c.effects, handlers: handlers, + spec_paths: c.spec_paths, system_prompt: c.system_prompt } +} + +fn agent_specs(c :: AgentConfig, paths :: List[Str]) -> AgentConfig { + { name: c.name, peers: c.peers, mcp_servers: c.mcp_servers, + effects: c.effects, handlers: c.handlers, + spec_paths: paths, system_prompt: c.system_prompt } +} + +fn agent_system_prompt(c :: AgentConfig, prompt :: Str) -> AgentConfig { + { name: c.name, peers: c.peers, mcp_servers: c.mcp_servers, + effects: c.effects, handlers: c.handlers, + spec_paths: c.spec_paths, system_prompt: prompt } +} diff --git a/crates/soft-agent/src/lex_dsl.rs b/crates/soft-agent/src/lex_dsl.rs index b6ab92b..d8499fa 100644 --- a/crates/soft-agent/src/lex_dsl.rs +++ b/crates/soft-agent/src/lex_dsl.rs @@ -36,65 +36,11 @@ use crate::{AgentConfig, Effect, Error}; /// builders. Each builder copies the whole record because lex-syntax 0.2 /// has no record-update sugar yet (probably worth filing as a small /// papercut alongside the underscore one). -pub const DSL_PREAMBLE: &str = r#" -type ActionRecord = { - kind :: Str, server :: Str, tool :: Str, args_json :: Str, - peer :: Str, a2a_topic :: Str, payload_json :: Str, prompt :: Str, -} - -type HandlerEntry = { topic :: Str, fn_name :: Str } - -type AgentConfig = { - name :: Str, - peers :: List[Str], - mcp_servers :: List[Str], - effects :: List[Str], - handlers :: List[HandlerEntry], - spec_paths :: List[Str], - system_prompt :: Str, -} - -fn agent_new(name :: Str) -> AgentConfig { - { name: name, peers: [], mcp_servers: [], effects: [], - handlers: [], spec_paths: [], system_prompt: "" } -} - -fn agent_peers(c :: AgentConfig, peers :: List[Str]) -> AgentConfig { - { name: c.name, peers: peers, mcp_servers: c.mcp_servers, - effects: c.effects, handlers: c.handlers, - spec_paths: c.spec_paths, system_prompt: c.system_prompt } -} - -fn agent_mcp_servers(c :: AgentConfig, servers :: List[Str]) -> AgentConfig { - { name: c.name, peers: c.peers, mcp_servers: servers, - effects: c.effects, handlers: c.handlers, - spec_paths: c.spec_paths, system_prompt: c.system_prompt } -} - -fn agent_effects(c :: AgentConfig, effects :: List[Str]) -> AgentConfig { - { name: c.name, peers: c.peers, mcp_servers: c.mcp_servers, - effects: effects, handlers: c.handlers, - spec_paths: c.spec_paths, system_prompt: c.system_prompt } -} - -fn agent_handles(c :: AgentConfig, handlers :: List[HandlerEntry]) -> AgentConfig { - { name: c.name, peers: c.peers, mcp_servers: c.mcp_servers, - effects: c.effects, handlers: handlers, - spec_paths: c.spec_paths, system_prompt: c.system_prompt } -} - -fn agent_specs(c :: AgentConfig, paths :: List[Str]) -> AgentConfig { - { name: c.name, peers: c.peers, mcp_servers: c.mcp_servers, - effects: c.effects, handlers: c.handlers, - spec_paths: paths, system_prompt: c.system_prompt } -} - -fn agent_system_prompt(c :: AgentConfig, prompt :: Str) -> AgentConfig { - { name: c.name, peers: c.peers, mcp_servers: c.mcp_servers, - effects: c.effects, handlers: c.handlers, - spec_paths: c.spec_paths, system_prompt: prompt } -} -"#; +/// +/// Sourced from the sibling `dsl_preamble.lex` so the file is browsable +/// as Lex (LSP/syntax highlighting works) and the line count attributes +/// to Lex rather than a Rust raw string. +pub const DSL_PREAMBLE: &str = include_str!("dsl_preamble.lex"); /// What [`Runner::from_lex_source`](crate::Runner::from_lex_source) /// extracts from a user's `config()` Lex value: the structured config diff --git a/crates/soft-agent/tests/cross_depot.rs b/crates/soft-agent/tests/cross_depot.rs index 971eab4..7d64ac7 100644 --- a/crates/soft-agent/tests/cross_depot.rs +++ b/crates/soft-agent/tests/cross_depot.rs @@ -6,127 +6,13 @@ use serde_json::json; use soft_agent::{A2aMessage, InProcessRouter, Mailbox, Runner}; -const VEHICLE_LEX: &str = r#" -fn config() -> AgentConfig { - agent_new("vehicle") - |> agent_peers(["depot1", "depot2", "tms"]) - |> agent_effects(["a2a"]) - |> agent_handles([ - { topic: "Dispatch", fn_name: "on_dispatch" }, - { topic: "DenySession", fn_name: "on_deny" }, - { topic: "GrantSession", fn_name: "on_grant" }, - ]) -} - -fn on_dispatch( - state :: { tried :: Int }, - msg :: { from :: Str, topic :: Str, payload_json :: Str }, -) -> { state :: { tried :: Int }, actions :: List[ActionRecord] } { - { state: { tried: 1 }, - actions: [{ - kind: "send_a2a", server: "", tool: "", args_json: "", - peer: "depot1", a2a_topic: "RequestSession", - payload_json: "{}", prompt: "", - }] } -} - -fn on_deny( - state :: { tried :: Int }, - msg :: { from :: Str, topic :: Str, payload_json :: Str }, -) -> { state :: { tried :: Int }, actions :: List[ActionRecord] } { - if state.tried >= 2 { - # Already tried both depots — give up, tell tms. - { state: { tried: state.tried }, - actions: [{ - kind: "send_a2a", server: "", tool: "", args_json: "", - peer: "tms", a2a_topic: "Failed", - payload_json: "{}", prompt: "", - }] } - } else { - # First denial — try depot2. - { state: { tried: state.tried + 1 }, - actions: [{ - kind: "send_a2a", server: "", tool: "", args_json: "", - peer: "depot2", a2a_topic: "RequestSession", - payload_json: "{}", prompt: "", - }] } - } -} - -fn on_grant( - state :: { tried :: Int }, - msg :: { from :: Str, topic :: Str, payload_json :: Str }, -) -> List[ActionRecord] { - [{ - kind: "send_a2a", server: "", tool: "", args_json: "", - peer: "tms", a2a_topic: "Complete", - payload_json: "{}", prompt: "", - }] -} -"#; - -const DEPOT_DENIES_LEX: &str = r#" -fn config() -> AgentConfig { - agent_new("depot1") - |> agent_peers(["vehicle"]) - |> agent_effects(["a2a"]) - |> agent_handles([ - { topic: "RequestSession", fn_name: "on_request" }, - ]) -} - -fn on_request( - state :: { ok :: Bool }, - msg :: { from :: Str, topic :: Str, payload_json :: Str }, -) -> List[ActionRecord] { - [{ - kind: "send_a2a", server: "", tool: "", args_json: "", - peer: msg.from, a2a_topic: "DenySession", - payload_json: "{}", prompt: "", - }] -} -"#; - -const DEPOT_GRANTS_LEX: &str = r#" -fn config() -> AgentConfig { - agent_new("depot2") - |> agent_peers(["vehicle"]) - |> agent_effects(["a2a"]) - |> agent_handles([ - { topic: "RequestSession", fn_name: "on_request" }, - ]) -} +const VEHICLE_LEX: &str = include_str!("fixtures/cross_depot_vehicle.lex"); -fn on_request( - state :: { ok :: Bool }, - msg :: { from :: Str, topic :: Str, payload_json :: Str }, -) -> List[ActionRecord] { - [{ - kind: "send_a2a", server: "", tool: "", args_json: "", - peer: msg.from, a2a_topic: "GrantSession", - payload_json: "{}", prompt: "", - }] -} -"#; +const DEPOT_DENIES_LEX: &str = include_str!("fixtures/cross_depot_depot_denies.lex"); -const TMS_LEX: &str = r#" -fn config() -> AgentConfig { - agent_new("tms") - |> agent_peers(["vehicle"]) - |> agent_effects(["a2a"]) - |> agent_handles([ - { topic: "Complete", fn_name: "terminate" }, - { topic: "Failed", fn_name: "terminate" }, - ]) -} +const DEPOT_GRANTS_LEX: &str = include_str!("fixtures/cross_depot_depot_grants.lex"); -fn terminate( - state :: { running :: Bool }, - msg :: { from :: Str, topic :: Str, payload_json :: Str }, -) -> List[ActionRecord] { - [] -} -"#; +const TMS_LEX: &str = include_str!("fixtures/cross_depot_tms.lex"); #[test] fn vehicle_falls_over_from_depot1_to_depot2() { diff --git a/crates/soft-agent/tests/fixtures/cross_depot_depot_denies.lex b/crates/soft-agent/tests/fixtures/cross_depot_depot_denies.lex new file mode 100644 index 0000000..526744e --- /dev/null +++ b/crates/soft-agent/tests/fixtures/cross_depot_depot_denies.lex @@ -0,0 +1,19 @@ +fn config() -> AgentConfig { + agent_new("depot1") + |> agent_peers(["vehicle"]) + |> agent_effects(["a2a"]) + |> agent_handles([ + { topic: "RequestSession", fn_name: "on_request" }, + ]) +} + +fn on_request( + state :: { ok :: Bool }, + msg :: { from :: Str, topic :: Str, payload_json :: Str }, +) -> List[ActionRecord] { + [{ + kind: "send_a2a", server: "", tool: "", args_json: "", + peer: msg.from, a2a_topic: "DenySession", + payload_json: "{}", prompt: "", + }] +} diff --git a/crates/soft-agent/tests/fixtures/cross_depot_depot_grants.lex b/crates/soft-agent/tests/fixtures/cross_depot_depot_grants.lex new file mode 100644 index 0000000..1888946 --- /dev/null +++ b/crates/soft-agent/tests/fixtures/cross_depot_depot_grants.lex @@ -0,0 +1,19 @@ +fn config() -> AgentConfig { + agent_new("depot2") + |> agent_peers(["vehicle"]) + |> agent_effects(["a2a"]) + |> agent_handles([ + { topic: "RequestSession", fn_name: "on_request" }, + ]) +} + +fn on_request( + state :: { ok :: Bool }, + msg :: { from :: Str, topic :: Str, payload_json :: Str }, +) -> List[ActionRecord] { + [{ + kind: "send_a2a", server: "", tool: "", args_json: "", + peer: msg.from, a2a_topic: "GrantSession", + payload_json: "{}", prompt: "", + }] +} diff --git a/crates/soft-agent/tests/fixtures/cross_depot_tms.lex b/crates/soft-agent/tests/fixtures/cross_depot_tms.lex new file mode 100644 index 0000000..81079a7 --- /dev/null +++ b/crates/soft-agent/tests/fixtures/cross_depot_tms.lex @@ -0,0 +1,16 @@ +fn config() -> AgentConfig { + agent_new("tms") + |> agent_peers(["vehicle"]) + |> agent_effects(["a2a"]) + |> agent_handles([ + { topic: "Complete", fn_name: "terminate" }, + { topic: "Failed", fn_name: "terminate" }, + ]) +} + +fn terminate( + state :: { running :: Bool }, + msg :: { from :: Str, topic :: Str, payload_json :: Str }, +) -> List[ActionRecord] { + [] +} diff --git a/crates/soft-agent/tests/fixtures/cross_depot_vehicle.lex b/crates/soft-agent/tests/fixtures/cross_depot_vehicle.lex new file mode 100644 index 0000000..729789b --- /dev/null +++ b/crates/soft-agent/tests/fixtures/cross_depot_vehicle.lex @@ -0,0 +1,56 @@ +fn config() -> AgentConfig { + agent_new("vehicle") + |> agent_peers(["depot1", "depot2", "tms"]) + |> agent_effects(["a2a"]) + |> agent_handles([ + { topic: "Dispatch", fn_name: "on_dispatch" }, + { topic: "DenySession", fn_name: "on_deny" }, + { topic: "GrantSession", fn_name: "on_grant" }, + ]) +} + +fn on_dispatch( + state :: { tried :: Int }, + msg :: { from :: Str, topic :: Str, payload_json :: Str }, +) -> { state :: { tried :: Int }, actions :: List[ActionRecord] } { + { state: { tried: 1 }, + actions: [{ + kind: "send_a2a", server: "", tool: "", args_json: "", + peer: "depot1", a2a_topic: "RequestSession", + payload_json: "{}", prompt: "", + }] } +} + +fn on_deny( + state :: { tried :: Int }, + msg :: { from :: Str, topic :: Str, payload_json :: Str }, +) -> { state :: { tried :: Int }, actions :: List[ActionRecord] } { + if state.tried >= 2 { + # Already tried both depots — give up, tell tms. + { state: { tried: state.tried }, + actions: [{ + kind: "send_a2a", server: "", tool: "", args_json: "", + peer: "tms", a2a_topic: "Failed", + payload_json: "{}", prompt: "", + }] } + } else { + # First denial — try depot2. + { state: { tried: state.tried + 1 }, + actions: [{ + kind: "send_a2a", server: "", tool: "", args_json: "", + peer: "depot2", a2a_topic: "RequestSession", + payload_json: "{}", prompt: "", + }] } + } +} + +fn on_grant( + state :: { tried :: Int }, + msg :: { from :: Str, topic :: Str, payload_json :: Str }, +) -> List[ActionRecord] { + [{ + kind: "send_a2a", server: "", tool: "", args_json: "", + peer: "tms", a2a_topic: "Complete", + payload_json: "{}", prompt: "", + }] +} diff --git a/crates/soft-agent/tests/fixtures/multi_agent_depot.lex b/crates/soft-agent/tests/fixtures/multi_agent_depot.lex new file mode 100644 index 0000000..0bc5129 --- /dev/null +++ b/crates/soft-agent/tests/fixtures/multi_agent_depot.lex @@ -0,0 +1,26 @@ +type ActionRecord = { + kind :: Str, + server :: Str, + tool :: Str, + args_json :: Str, + peer :: Str, + a2a_topic :: Str, + payload_json :: Str, + prompt :: Str, +} + +fn on_request_session( + state :: { ok :: Bool }, + msg :: { from :: Str, topic :: Str, payload_json :: Str }, +) -> List[ActionRecord] { + [{ + kind: "send_a2a", + server: "", + tool: "", + args_json: "", + peer: msg.from, + a2a_topic: "GrantSession", + payload_json: "{\"charger_id\":\"charger-1\",\"power_kw\":30}", + prompt: "", + }] +} diff --git a/crates/soft-agent/tests/fixtures/multi_agent_vehicle.lex b/crates/soft-agent/tests/fixtures/multi_agent_vehicle.lex new file mode 100644 index 0000000..29d9814 --- /dev/null +++ b/crates/soft-agent/tests/fixtures/multi_agent_vehicle.lex @@ -0,0 +1,33 @@ +type ActionRecord = { + kind :: Str, + server :: Str, + tool :: Str, + args_json :: Str, + peer :: Str, + a2a_topic :: Str, + payload_json :: Str, + prompt :: Str, +} + +fn on_dispatch( + state :: { ready :: Bool }, + msg :: { from :: Str, topic :: Str, payload_json :: Str }, +) -> List[ActionRecord] { + [{ + kind: "send_a2a", + server: "", + tool: "", + args_json: "", + peer: "depot", + a2a_topic: "RequestSession", + payload_json: "{\"vehicle_id\":\"v-1\",\"power_kw\":30}", + prompt: "", + }] +} + +fn on_grant( + state :: { ready :: Bool }, + msg :: { from :: Str, topic :: Str, payload_json :: Str }, +) -> List[ActionRecord] { + [] +} diff --git a/crates/soft-agent/tests/multi_agent.rs b/crates/soft-agent/tests/multi_agent.rs index bff9112..258b5d8 100644 --- a/crates/soft-agent/tests/multi_agent.rs +++ b/crates/soft-agent/tests/multi_agent.rs @@ -9,72 +9,11 @@ use soft_agent::{A2aMessage, AgentConfig, Effect, InProcessRouter, LexHost, Mail /// - `on_dispatch`: TMS sends a Dispatch → vehicle requests charging from depot. /// - `on_grant`: depot replies with GrantSession → vehicle does nothing /// (terminal in this test). -const VEHICLE_LEX: &str = r#" -type ActionRecord = { - kind :: Str, - server :: Str, - tool :: Str, - args_json :: Str, - peer :: Str, - a2a_topic :: Str, - payload_json :: Str, - prompt :: Str, -} - -fn on_dispatch( - state :: { ready :: Bool }, - msg :: { from :: Str, topic :: Str, payload_json :: Str }, -) -> List[ActionRecord] { - [{ - kind: "send_a2a", - server: "", - tool: "", - args_json: "", - peer: "depot", - a2a_topic: "RequestSession", - payload_json: "{\"vehicle_id\":\"v-1\",\"power_kw\":30}", - prompt: "", - }] -} - -fn on_grant( - state :: { ready :: Bool }, - msg :: { from :: Str, topic :: Str, payload_json :: Str }, -) -> List[ActionRecord] { - [] -} -"#; +const VEHICLE_LEX: &str = include_str!("fixtures/multi_agent_vehicle.lex"); /// Depot has one handler: receive RequestSession → reply GrantSession to /// the requesting vehicle (echoed via `msg.from`). -const DEPOT_LEX: &str = r#" -type ActionRecord = { - kind :: Str, - server :: Str, - tool :: Str, - args_json :: Str, - peer :: Str, - a2a_topic :: Str, - payload_json :: Str, - prompt :: Str, -} - -fn on_request_session( - state :: { ok :: Bool }, - msg :: { from :: Str, topic :: Str, payload_json :: Str }, -) -> List[ActionRecord] { - [{ - kind: "send_a2a", - server: "", - tool: "", - args_json: "", - peer: msg.from, - a2a_topic: "GrantSession", - payload_json: "{\"charger_id\":\"charger-1\",\"power_kw\":30}", - prompt: "", - }] -} -"#; +const DEPOT_LEX: &str = include_str!("fixtures/multi_agent_depot.lex"); #[test] fn router_delivers_send_a2a_between_two_agents() {