Skip to content

Commit 8aef91c

Browse files
committed
docs(cli): document crate root, client adapters, and output layer
1 parent 7fd4b36 commit 8aef91c

49 files changed

Lines changed: 1338 additions & 103 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

crates/palyra-cli/src/acp_bridge.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
//! ACP (Agent Client Protocol) stdio bridge: exposes the Palyra gateway as an
2+
//! ACP agent so external editors and IDE clients can drive sessions, prompts,
3+
//! tool approvals, and session listings over stdin/stdout JSON-RPC. Bindings
4+
//! map ACP session ids onto gateway sessions and optional daemon-side state.
5+
16
use std::{
27
collections::HashMap,
38
path::{Path, PathBuf},
@@ -18,16 +23,21 @@ use crate::{
1823
AgentApprovalMode, AgentConnection, AgentRunInputArgs, SessionResolveInput,
1924
};
2025

26+
// `_meta` extension keys accepted on session/new, session/load, and
27+
// session/prompt to override the CLI-provided session defaults per request.
2128
const META_SESSION_KEY: &str = "sessionKey";
2229
const META_SESSION_LABEL: &str = "sessionLabel";
2330
const META_RESET_SESSION: &str = "resetSession";
2431
const META_REQUIRE_EXISTING: &str = "requireExisting";
2532

33+
// Permission option ids surfaced to the ACP client; the `-always` variants map
34+
// to a session-scoped `ApprovalDecisionScope` when forwarded to the gateway.
2635
const PERMISSION_ALLOW_ONCE: &str = "allow-once";
2736
const PERMISSION_ALLOW_ALWAYS: &str = "allow-always";
2837
const PERMISSION_REJECT_ONCE: &str = "reject-once";
2938
const PERMISSION_REJECT_ALWAYS: &str = "reject-always";
3039

40+
/// Resolved link between an ACP session id and its gateway session identity.
3141
#[derive(Debug, Clone)]
3242
struct SessionBinding {
3343
gateway_session_id_ulid: String,
@@ -36,6 +46,8 @@ struct SessionBinding {
3646
cwd: PathBuf,
3747
}
3848

49+
/// Optional daemon control-plane channel used to persist session bindings and
50+
/// session config beyond the lifetime of this bridge process.
3951
#[derive(Clone)]
4052
struct AcpDaemonControl {
4153
client: Arc<TokioMutex<ControlPlaneClient>>,
@@ -45,7 +57,11 @@ struct AcpDaemonControl {
4557
}
4658

4759
impl AcpDaemonControl {
60+
/// Sends one `console/v1/acp/command` request and unwraps its envelope.
4861
async fn command(&self, command: &str, params: Value) -> acp::Result<Value> {
62+
// The `client` block restates the full handshake (scopes plus
63+
// capabilities) on every command because the console ACP endpoint is
64+
// stateless across requests.
4965
let payload = json!({
5066
"client": {
5167
"protocol_version": 1,
@@ -106,6 +122,11 @@ impl AcpDaemonControl {
106122
}
107123
}
108124

125+
/// In-memory binding and active-run registry shared across ACP handlers.
126+
///
127+
/// Bindings are indexed three ways because clients may echo any of the three
128+
/// identifiers (ACP session id, gateway session key, gateway session ulid) as
129+
/// the ACP `SessionId` on follow-up requests.
109130
#[derive(Debug, Default)]
110131
struct BridgeState {
111132
bindings_by_acp_session_id: HashMap<String, SessionBinding>,
@@ -146,6 +167,8 @@ impl BridgeState {
146167
}
147168
}
148169

170+
/// Client-directed calls funneled through one dispatch task so agent handlers
171+
/// can stay `Clone` while `AgentSideConnection` is owned by a single task.
149172
enum ClientBridgeRequest {
150173
SessionUpdate {
151174
notification: acp::SessionNotification,
@@ -157,6 +180,7 @@ enum ClientBridgeRequest {
157180
},
158181
}
159182

183+
/// ACP `Agent` implementation backed by the gateway gRPC runtime client.
160184
#[derive(Clone)]
161185
struct PalyraAcpAgent {
162186
connection: AgentConnection,
@@ -168,6 +192,7 @@ struct PalyraAcpAgent {
168192
default_cwd: PathBuf,
169193
}
170194

195+
/// Per-request `_meta` overrides parsed from an ACP request, if any.
171196
#[derive(Debug, Default, Clone)]
172197
struct SessionMetaOverrides {
173198
session_key: Option<String>,
@@ -176,6 +201,8 @@ struct SessionMetaOverrides {
176201
require_existing: Option<bool>,
177202
}
178203

204+
/// Session defaults supplied via CLI flags, applied whenever an ACP request
205+
/// does not carry the corresponding `_meta` override.
179206
#[derive(Debug, Default, Clone)]
180207
pub(crate) struct AcpSessionDefaults {
181208
pub(crate) session_key: Option<String>,
@@ -295,6 +322,9 @@ impl PalyraAcpAgent {
295322
}
296323
}
297324

325+
// Gateway runs accept plain text, so only text-bearing blocks are kept:
326+
// text, embedded text resources, and resource links (serialized inline).
327+
// Image/audio blocks are intentionally dropped.
298328
fn prompt_text(prompt: &[acp::ContentBlock]) -> String {
299329
let mut chunks = Vec::new();
300330
for block in prompt {
@@ -543,6 +573,10 @@ impl PalyraAcpAgent {
543573
let mut run_stream =
544574
client.open_run_stream(initial_request).await.map_err(acp_internal_error)?;
545575

576+
// AIDEV-NOTE: if streaming below fails mid-run (any `?` until the end
577+
// of this function), the active_runs entry is not removed, so a later
578+
// session/cancel would target the dead run id. Cleaning this up needs
579+
// a behavior change (e.g. a scope guard around the stream loop).
546580
{
547581
let mut state = self.lock_state()?;
548582
state
@@ -640,6 +674,9 @@ impl PalyraAcpAgent {
640674
}
641675
Some(common_v1::run_stream_event::Body::Status(status)) => {
642676
if status.kind == common_v1::stream_status::StatusKind::Failed as i32 {
677+
// The stream contract has no structured cancelled
678+
// status; cancellations arrive as Failed with a
679+
// cancel-flavored message, hence the text heuristic.
643680
stop_reason = if status.message.to_ascii_lowercase().contains("cancel") {
644681
acp::StopReason::Cancelled
645682
} else {
@@ -801,6 +838,8 @@ impl acp::Agent for PalyraAcpAgent {
801838
&self,
802839
arguments: acp::ListSessionsRequest,
803840
) -> acp::Result<acp::ListSessionsResponse> {
841+
// Prefer the daemon listing (it includes persisted bindings); fall
842+
// back to the gateway list when no daemon control-plane is attached.
804843
if let Some(response) = self.list_daemon_sessions(arguments.cursor.clone()).await? {
805844
return Ok(response);
806845
}
@@ -810,6 +849,14 @@ impl acp::Agent for PalyraAcpAgent {
810849
}
811850
}
812851

852+
/// Runs the ACP stdio bridge until the client closes the stream.
853+
///
854+
/// Blocks the calling thread on a dedicated Tokio runtime; the connection
855+
/// futures are `!Send`, so everything is driven on a single-thread `LocalSet`.
856+
///
857+
/// # Errors
858+
/// Returns an error when the runtime cannot be built, the daemon ACP
859+
/// control-plane connection fails, or the stdio I/O loop terminates abnormally.
813860
pub fn run_agent_acp_bridge(
814861
connection: AgentConnection,
815862
control_plane_overrides: app::ConnectionOverrides,
@@ -821,6 +868,9 @@ pub fn run_agent_acp_bridge(
821868
let local_set = tokio::task::LocalSet::new();
822869
local_set
823870
.run_until(async move {
871+
// Unbounded is safe here: every sender awaits its oneshot
872+
// response before sending again, so the queue depth is bounded
873+
// by the number of in-flight ACP requests.
824874
let (client_request_tx, mut client_request_rx) =
825875
mpsc::unbounded_channel::<ClientBridgeRequest>();
826876
let state = Arc::new(Mutex::new(BridgeState::default()));
@@ -846,6 +896,9 @@ pub fn run_agent_acp_bridge(
846896
default_cwd,
847897
);
848898

899+
// Blocking stdio wrapped as async is acceptable: this runtime
900+
// exists solely for the bridge, so a stalled read cannot
901+
// starve unrelated tasks.
849902
let outgoing = AllowStdIo::new(std::io::stdout());
850903
let incoming = AllowStdIo::new(std::io::stdin());
851904
let (conn, handle_io) =
@@ -886,6 +939,10 @@ fn parse_json_bytes(raw: &[u8]) -> Option<Value> {
886939
serde_json::from_slice::<Value>(raw).ok()
887940
}
888941

942+
/// Maps a gateway tool-approval request onto an ACP permission request.
943+
///
944+
/// Returns `Ok(None)` when the approval carries no proposal id: without it the
945+
/// client's decision could not be correlated back to the gateway proposal.
889946
fn build_tool_permission_request(
890947
session_id: &acp::SessionId,
891948
approval: &common_v1::ToolApprovalRequest,
@@ -948,6 +1005,11 @@ fn build_tool_permission_request(
9481005
Ok(Some(acp::RequestPermissionRequest::new(session_id.clone(), tool_call, options)))
9491006
}
9501007

1008+
/// Translates an ACP permission outcome into the gateway approval fields
1009+
/// `(approved, reason, decision_scope, decision_scope_ttl_ms)`.
1010+
///
1011+
/// Unknown option ids and non-selection outcomes fail closed as a once-scoped
1012+
/// denial; the TTL is never set so the gateway default applies.
9511013
fn map_permission_outcome(
9521014
response: acp::RequestPermissionResponse,
9531015
) -> (bool, String, i32, Option<i64>) {
@@ -999,6 +1061,8 @@ fn map_permission_outcome(
9991061
}
10001062
}
10011063

1064+
/// Maps gateway session summaries to ACP session infos, preferring locally
1065+
/// remembered bindings for cwd/label and falling back to gateway fields.
10021066
fn map_list_sessions_response(
10031067
response: gateway_v1::ListSessionsResponse,
10041068
state: &BridgeState,
@@ -1034,6 +1098,8 @@ fn map_list_sessions_response(
10341098
.next_cursor(non_empty(Some(response.next_after_session_key)))
10351099
}
10361100

1101+
/// Maps a daemon `session.list` JSON response to ACP session infos; unnamed
1102+
/// sessions fall back to their id and the bridge's default cwd.
10371103
fn map_daemon_sessions_response(response: Value, default_cwd: &Path) -> acp::ListSessionsResponse {
10381104
let sessions = response
10391105
.get("sessions")

crates/palyra-cli/src/acp_bridge/tests.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
//! Unit tests for ACP bridge protocol mapping: prompt assembly, permission
2+
//! option semantics, session listing fallbacks, and binding lookups.
3+
14
use super::{
25
acp, build_tool_permission_request, map_list_sessions_response, map_permission_outcome,
36
AcpSessionDefaults, AgentConnection, BridgeState, ClientBridgeRequest, PalyraAcpAgent,

0 commit comments

Comments
 (0)