Skip to content

Commit 03eef9d

Browse files
Phase H redo: migrate tests, examples, and scenarios to per-trait handler API
Mechanical migration of all consumers to the new optional handler-trait API introduced in 455a17b: - rust/tests/session_test.rs: ~30 SessionHandler impls split into the corresponding PermissionHandler / ElicitationHandler / UserInputHandler / ExitPlanModeHandler / AutoModeSwitchHandler / ToolHandler impls. Tests that called handler.on_event(...) directly now call handler.handle(...) on the appropriate trait. The legacy tool.call direct-RPC test was dropped; equivalent coverage is already provided by external_tool_requested_dispatches_to_handler_and_responds. - rust/tests/e2e/*: per-trait migration; with_handler(...) -> the matching with_*_handler builder. - rust/examples/*: same migration. - test/scenarios/**/rust/src/main.rs: same migration. Also fixes a bug in Client::create_session / Client::resume_session where to_wire() was called after .take()-ing all handler fields, so the derived wire flags (requestPermission, requestElicitation, hooks, etc.) were always false. Computing the wire payload now happens before the takes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 455a17b commit 03eef9d

58 files changed

Lines changed: 891 additions & 4918 deletions

File tree

Some content is hidden

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

rust/README.md

Lines changed: 119 additions & 131 deletions
Large diffs are not rendered by default.

rust/examples/chat.rs

Lines changed: 31 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -13,39 +13,29 @@ use std::sync::Arc;
1313
use std::time::Duration;
1414

1515
use async_trait::async_trait;
16-
use github_copilot_sdk::handler::{
17-
HandlerEvent, HandlerResponse, PermissionResult, SessionHandler, UserInputResponse,
18-
};
19-
use github_copilot_sdk::types::{MessageOptions, SessionConfig, SessionEvent};
16+
use github_copilot_sdk::handler::{ApproveAllHandler, UserInputHandler, UserInputResponse};
17+
use github_copilot_sdk::types::{MessageOptions, SessionConfig, SessionEvent, SessionId};
2018
use github_copilot_sdk::{Client, ClientOptions};
2119

22-
/// Handler that prints assistant message deltas as they stream in
23-
/// and auto-approves permissions.
24-
struct ChatHandler;
20+
/// User input handler that prompts on stdin.
21+
struct StdinUserInputHandler;
2522

2623
#[async_trait]
27-
impl SessionHandler for ChatHandler {
28-
async fn on_event(&self, event: HandlerEvent) -> HandlerResponse {
29-
match event {
30-
HandlerEvent::SessionEvent { event, .. } => {
31-
print_event(&event);
32-
HandlerResponse::Ok
33-
}
34-
HandlerEvent::PermissionRequest { .. } => {
35-
HandlerResponse::Permission(PermissionResult::Approved)
36-
}
37-
HandlerEvent::UserInput { question, .. } => {
38-
// Prompt the user on behalf of the agent.
39-
print!("\n[agent asks] {question}\n> ");
40-
io::stdout().flush().ok();
41-
let answer = read_line().unwrap_or_default();
42-
HandlerResponse::UserInput(Some(UserInputResponse {
43-
answer,
44-
was_freeform: true,
45-
}))
46-
}
47-
_ => HandlerResponse::Ok,
48-
}
24+
impl UserInputHandler for StdinUserInputHandler {
25+
async fn handle(
26+
&self,
27+
_session_id: SessionId,
28+
question: String,
29+
_choices: Option<Vec<String>>,
30+
_allow_freeform: Option<bool>,
31+
) -> Option<UserInputResponse> {
32+
print!("\n[agent asks] {question}\n> ");
33+
io::stdout().flush().ok();
34+
let answer = read_line()?;
35+
Some(UserInputResponse {
36+
answer,
37+
was_freeform: true,
38+
})
4939
}
5040
}
5141

@@ -91,9 +81,11 @@ async fn main() -> Result<(), github_copilot_sdk::Error> {
9181
let client = Client::start(ClientOptions::default()).await?;
9282

9383
let config = {
94-
let mut cfg = SessionConfig::default();
84+
let mut cfg = SessionConfig::default()
85+
.with_permission_handler(Arc::new(ApproveAllHandler))
86+
.with_user_input_handler(Arc::new(StdinUserInputHandler));
9587
cfg.streaming = Some(true);
96-
cfg.with_handler(Arc::new(ChatHandler))
88+
cfg
9789
};
9890
let session = client.create_session(config).await?;
9991

@@ -102,6 +94,14 @@ async fn main() -> Result<(), github_copilot_sdk::Error> {
10294
session.id()
10395
);
10496

97+
// Spawn a task to print streamed assistant deltas as session events arrive.
98+
let mut events = session.subscribe();
99+
tokio::spawn(async move {
100+
while let Ok(event) = events.recv().await {
101+
print_event(&event);
102+
}
103+
});
104+
105105
loop {
106106
print!("> ");
107107
io::stdout().flush().ok();

rust/examples/hooks.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ async fn main() -> Result<(), github_copilot_sdk::Error> {
103103

104104
// hooks: true is set automatically when a hooks handler is provided.
105105
let config = SessionConfig::default()
106-
.with_handler(Arc::new(ApproveAllHandler))
106+
.with_permission_handler(Arc::new(ApproveAllHandler))
107107
.with_hooks(Arc::new(AuditHooks));
108108
let session = client.create_session(config).await?;
109109

rust/examples/lifecycle_observer.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ async fn main() -> Result<(), github_copilot_sdk::Error> {
6363
}
6464
});
6565

66-
let config = SessionConfig::default().with_handler(Arc::new(ApproveAllHandler));
66+
let config = SessionConfig::default().with_permission_handler(Arc::new(ApproveAllHandler));
6767
let session = client.create_session(config).await?;
6868
println!("[client] session created: {}", session.id());
6969

rust/examples/session_fs.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
125125
let session = client
126126
.create_session(
127127
SessionConfig::default()
128-
.with_handler(Arc::new(ApproveAllHandler))
128+
.with_permission_handler(Arc::new(ApproveAllHandler))
129129
.with_session_fs_provider(provider),
130130
)
131131
.await?;

rust/examples/tool_server.rs

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,7 @@ use async_trait::async_trait;
3030
#[cfg(feature = "derive")]
3131
use github_copilot_sdk::handler::ApproveAllHandler;
3232
#[cfg(feature = "derive")]
33-
use github_copilot_sdk::tool::{
34-
JsonSchema, ToolHandler, ToolHandlerRouter, schema_for, tool_parameters,
35-
};
33+
use github_copilot_sdk::tool::{JsonSchema, ToolHandler, schema_for, tool_parameters};
3634
#[cfg(feature = "derive")]
3735
use github_copilot_sdk::types::{MessageOptions, SessionConfig, Tool, ToolInvocation, ToolResult};
3836
#[cfg(feature = "derive")]
@@ -145,19 +143,18 @@ impl ToolHandler for RollDiceTool {
145143
#[cfg(feature = "derive")]
146144
#[tokio::main]
147145
async fn main() -> Result<(), github_copilot_sdk::Error> {
148-
let router = ToolHandlerRouter::new(
149-
vec![Box::new(GetWeatherTool), Box::new(RollDiceTool)],
150-
Arc::new(ApproveAllHandler),
151-
);
152-
let tools = router.tools();
153-
let handler = Arc::new(router);
146+
let tool_handlers: Vec<Arc<dyn ToolHandler>> =
147+
vec![Arc::new(GetWeatherTool), Arc::new(RollDiceTool)];
148+
let tools: Vec<Tool> = tool_handlers.iter().map(|h| h.tool()).collect();
154149

155150
let client = Client::start(ClientOptions::default()).await?;
156151

157152
let config = {
158-
let mut cfg = SessionConfig::default();
153+
let mut cfg = SessionConfig::default()
154+
.with_permission_handler(Arc::new(ApproveAllHandler))
155+
.with_tool_handlers(tool_handlers);
159156
cfg.tools = Some(tools);
160-
cfg.with_handler(handler)
157+
cfg
161158
};
162159
let session = client.create_session(config).await?;
163160

rust/src/handler.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,4 +229,4 @@ mod tests {
229229
.await;
230230
assert!(matches!(result, PermissionResult::Denied));
231231
}
232-
}
232+
}

rust/src/permission.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,8 @@ mod tests {
138138
async fn approve_all_approves() {
139139
let h = approve_all();
140140
assert!(matches!(
141-
h.handle(SessionId::from("s"), RequestId::new("1"), data()).await,
141+
h.handle(SessionId::from("s"), RequestId::new("1"), data())
142+
.await,
142143
PermissionResult::Approved
143144
));
144145
}
@@ -147,7 +148,8 @@ mod tests {
147148
async fn deny_all_denies() {
148149
let h = deny_all();
149150
assert!(matches!(
150-
h.handle(SessionId::from("s"), RequestId::new("1"), data()).await,
151+
h.handle(SessionId::from("s"), RequestId::new("1"), data())
152+
.await,
151153
PermissionResult::Denied
152154
));
153155
}
@@ -156,7 +158,8 @@ mod tests {
156158
async fn approve_if_consults_predicate() {
157159
let h = approve_if(|d| d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell"));
158160
assert!(matches!(
159-
h.handle(SessionId::from("s"), RequestId::new("1"), data()).await,
161+
h.handle(SessionId::from("s"), RequestId::new("1"), data())
162+
.await,
160163
PermissionResult::Denied
161164
));
162165
}
@@ -213,4 +216,4 @@ mod tests {
213216
fn resolve_handler_with_neither_returns_none() {
214217
assert!(resolve_handler(None, None).is_none());
215218
}
216-
}
219+
}

rust/src/session.rs

Lines changed: 28 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,7 @@ use crate::types::{
3131
ElicitationResult, ExitPlanModeData, GetMessagesResponse, MessageOptions,
3232
PermissionRequestData, RequestId, ResumeSessionConfig, SectionOverride, SessionCapabilities,
3333
SessionConfig, SessionEvent, SessionId, SetModelOptions, SystemMessageConfig, ToolInvocation,
34-
ToolResult, ToolResultExpanded, TraceContext, UiInputOptions,
35-
ensure_attachment_display_names,
34+
ToolResult, ToolResultExpanded, TraceContext, UiInputOptions, ensure_attachment_display_names,
3635
};
3736
use crate::{Client, Error, JsonRpcResponse, SessionError, SessionEventNotification, error_codes};
3837

@@ -53,19 +52,6 @@ pub(crate) struct SessionHandlers {
5352
pub tools: Arc<HashMap<String, Arc<dyn crate::tool::ToolHandler>>>,
5453
}
5554

56-
impl SessionHandlers {
57-
pub(crate) fn empty() -> Self {
58-
Self {
59-
permission: None,
60-
elicitation: None,
61-
user_input: None,
62-
exit_plan_mode: None,
63-
auto_mode_switch: None,
64-
tools: Arc::new(HashMap::new()),
65-
}
66-
}
67-
}
68-
6955
/// Shared state between a [`Session`] and its event loop, used by [`Session::send_and_wait`].
7056
struct IdleWaiter {
7157
tx: oneshot::Sender<Result<Option<SessionEvent>, Error>>,
@@ -794,6 +780,19 @@ impl Client {
794780
/// broadcast (and silently skips dispatch if one arrives anyway).
795781
pub async fn create_session(&self, mut config: SessionConfig) -> Result<Session, Error> {
796782
let total_start = Instant::now();
783+
let session_id = config
784+
.session_id
785+
.clone()
786+
.unwrap_or_else(|| SessionId::from(uuid::Uuid::new_v4().to_string()));
787+
config.session_id = Some(session_id.clone());
788+
if config.hooks_handler.is_some() && config.hooks.is_none() {
789+
config.hooks = Some(true);
790+
}
791+
if let Some(transforms) = config.transform.clone() {
792+
inject_transform_sections(&mut config, transforms.as_ref());
793+
}
794+
let wire = config.to_wire(session_id.clone());
795+
797796
let permission_handler = crate::permission::resolve_handler(
798797
config.permission_handler.take(),
799798
config.permission_policy.take(),
@@ -841,18 +840,6 @@ impl Client {
841840
));
842841
}
843842

844-
if hooks.is_some() && config.hooks.is_none() {
845-
config.hooks = Some(true);
846-
}
847-
if let Some(ref transforms) = transforms {
848-
inject_transform_sections(&mut config, transforms.as_ref());
849-
}
850-
let session_id = config
851-
.session_id
852-
.clone()
853-
.unwrap_or_else(|| SessionId::from(uuid::Uuid::new_v4().to_string()));
854-
config.session_id = Some(session_id.clone());
855-
let wire = config.to_wire(session_id.clone());
856843
let mut params = serde_json::to_value(&wire)?;
857844
let trace_ctx = self.resolve_trace_context().await;
858845
inject_trace_context(&mut params, &trace_ctx);
@@ -948,6 +935,15 @@ impl Client {
948935
/// fields are unset.
949936
pub async fn resume_session(&self, mut config: ResumeSessionConfig) -> Result<Session, Error> {
950937
let total_start = Instant::now();
938+
let session_id = config.session_id.clone();
939+
if config.hooks_handler.is_some() && config.hooks.is_none() {
940+
config.hooks = Some(true);
941+
}
942+
if let Some(transforms) = config.transform.clone() {
943+
inject_transform_sections_resume(&mut config, transforms.as_ref());
944+
}
945+
let wire = config.to_wire();
946+
951947
let permission_handler = crate::permission::resolve_handler(
952948
config.permission_handler.take(),
953949
config.permission_policy.take(),
@@ -995,14 +991,6 @@ impl Client {
995991
));
996992
}
997993

998-
if hooks.is_some() && config.hooks.is_none() {
999-
config.hooks = Some(true);
1000-
}
1001-
if let Some(ref transforms) = transforms {
1002-
inject_transform_sections_resume(&mut config, transforms.as_ref());
1003-
}
1004-
let session_id = config.session_id.clone();
1005-
let wire = config.to_wire();
1006994
let mut params = serde_json::to_value(&wire)?;
1007995
let trace_ctx = self.resolve_trace_context().await;
1008996
inject_trace_context(&mut params, &trace_ctx);
@@ -1568,8 +1556,7 @@ async fn handle_notification(
15681556
tool_name = %tool_name,
15691557
"ToolHandler::call dispatch"
15701558
);
1571-
let result_value =
1572-
serde_json::to_value(tool_result).unwrap_or(Value::Null);
1559+
let result_value = serde_json::to_value(tool_result).unwrap_or(Value::Null);
15731560
let rpc_start = Instant::now();
15741561
let _ = client
15751562
.call(
@@ -2179,8 +2166,10 @@ mod tests {
21792166
json!({ "kind": "reject" })
21802167
);
21812168
assert_eq!(
2182-
serde_json::to_value(permission_request_response(&PermissionResult::UserNotAvailable))
2183-
.expect("serializing fallback permission response should succeed"),
2169+
serde_json::to_value(permission_request_response(
2170+
&PermissionResult::UserNotAvailable
2171+
))
2172+
.expect("serializing fallback permission response should succeed"),
21842173
json!({ "kind": "reject" })
21852174
);
21862175
}

rust/src/tool.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ where
335335
#[cfg(test)]
336336
mod tests {
337337
use super::*;
338-
use crate::types::{PermissionRequestData, RequestId, SessionId};
338+
use crate::types::SessionId;
339339

340340
struct EchoTool;
341341

0 commit comments

Comments
 (0)