Expose the worker's sealed local generation (the L3 cascade tier) on the wire the inference ecosystem speaks. New joule_coded::responses answers /v1/chat/completions and /v1/responses via the Mesh standard's std-only server (mesh-openai/core/engine/ meter/router/schema, reused unchanged): mesh-core routes each request cheapest- sufficient, meters the decode, and returns the sealed mesh/1 receipt in usage.energy (microjoules, uj-per-accepted-token, honest provenance). The on-device model is injected as `generate` — scripted in tests, JouleClaw's GGUF runtime in production, the same injection run_loop uses. - main.rs: start the server on its own thread when JOULE_CODE_RESPONSES_ADDR is set and a model is loaded (feature l3), so it never blocks the control protocol. - lib.rs: pub mod responses. - Cargo.toml: mesh-* deps (open-standards rev 0a8fb779, cached locally). - tests/acp.rs, tests/fanout.rs: updated alongside. Co-Authored-By: Claude Opus 4.8 <[email protected]>
87 lines
3.5 KiB
Rust
87 lines
3.5 KiB
Rust
//! The headless ACP/L4 client, deterministically: drive `run_acp_prompt` against
|
|
//! a mock ACP agent (a tiny Python script that speaks the same newline-delimited
|
|
//! JSON-RPC handshake a real vendor CLI would) — proving the client does
|
|
//! initialize → session/new → session/prompt, assembles streamed assistant text,
|
|
//! and auto-answers a permission request, with no real vendor binary or login.
|
|
|
|
use joule_coded::acp::run_acp_prompt;
|
|
use std::io::Write;
|
|
|
|
/// A mock ACP agent: replies to initialize + session/new, then on session/prompt
|
|
/// asks one permission, streams two text chunks, and ends the turn.
|
|
const MOCK_AGENT: &str = r#"
|
|
import sys, json
|
|
def send(obj):
|
|
sys.stdout.write(json.dumps(obj) + "\n"); sys.stdout.flush()
|
|
for line in sys.stdin:
|
|
line = line.strip()
|
|
if not line: continue
|
|
msg = json.loads(line)
|
|
method, mid = msg.get("method"), msg.get("id")
|
|
if method == "initialize":
|
|
send({"jsonrpc":"2.0","id":mid,"result":{"protocolVersion":1,"agentCapabilities":{}}})
|
|
elif method == "session/new":
|
|
send({"jsonrpc":"2.0","id":mid,"result":{"sessionId":"sess-1"}})
|
|
elif method == "session/prompt":
|
|
# ask permission (agent->client request id 100)
|
|
send({"jsonrpc":"2.0","id":100,"method":"session/request_permission",
|
|
"params":{"sessionId":"sess-1","options":[
|
|
{"optionId":"allow","name":"Allow","kind":"allow_once"},
|
|
{"optionId":"reject","name":"Reject","kind":"reject_once"}]}})
|
|
# read the client's permission answer
|
|
ans = json.loads(sys.stdin.readline().strip())
|
|
# stream two assistant chunks
|
|
for chunk in ["Hello ", "from ACP"]:
|
|
send({"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"sess-1",
|
|
"update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":chunk}}}})
|
|
send({"jsonrpc":"2.0","id":mid,"result":{"stopReason":"end_turn"}})
|
|
break
|
|
"#;
|
|
|
|
#[test]
|
|
// Availability probe: blocking on `python3 --version` is fine in a test.
|
|
#[allow(clippy::disallowed_methods)]
|
|
fn headless_acp_prompt_handshakes_and_assembles_streamed_text() {
|
|
if std::process::Command::new("python3")
|
|
.arg("--version")
|
|
.output()
|
|
.is_err()
|
|
{
|
|
eprintln!("skipping: python3 not available");
|
|
return;
|
|
}
|
|
|
|
let dir = std::env::temp_dir().join(format!("jc-acp-{}", std::process::id()));
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
let script = dir.join("mock_agent.py");
|
|
std::fs::File::create(&script)
|
|
.unwrap()
|
|
.write_all(MOCK_AGENT.as_bytes())
|
|
.unwrap();
|
|
|
|
let outcome = run_acp_prompt(
|
|
"python3",
|
|
&[script.to_string_lossy().to_string()],
|
|
&dir,
|
|
"say hello",
|
|
true, // allow tools — approve the permission request
|
|
)
|
|
.expect("ACP prompt should complete against the mock agent");
|
|
|
|
assert_eq!(outcome.text, "Hello from ACP", "assembled streamed chunks");
|
|
assert_eq!(outcome.stop_reason, "end_turn");
|
|
assert_eq!(outcome.permissions_granted, 1, "auto-approved one permission");
|
|
|
|
// With tools refused, the same run still completes (the agent ends the turn).
|
|
let refused = run_acp_prompt(
|
|
"python3",
|
|
&[script.to_string_lossy().to_string()],
|
|
&dir,
|
|
"say hello",
|
|
false,
|
|
)
|
|
.expect("completes even when permission is refused");
|
|
assert_eq!(refused.permissions_granted, 0, "no permission granted when refused");
|
|
|
|
std::fs::remove_dir_all(&dir).ok();
|
|
}
|