Skip to content

Commit b02ccdd

Browse files
0xebCopilot
andcommitted
feat: add elicitation, exit-plan-mode, auto-mode-switch handlers and conformance tests
Add callback handler types (ElicitationContext/Result, ExitPlanModeRequest/Result, AutoModeSwitchRequest/Response) with registration, dispatch, and default behaviors. Wire up new handlers in Client request dispatch and SessionConfig. Add comprehensive conformance tests for all new handler types and parity types. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent e1a8df0 commit b02ccdd

6 files changed

Lines changed: 946 additions & 3 deletions

File tree

‎include/copilot/client.hpp‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,10 @@ class Client
258258
/// Handle incoming user input requests
259259
json handle_user_input_request(const json& params);
260260

261+
json handle_elicitation_request(const json& params);
262+
json handle_exit_plan_mode_request(const json& params);
263+
json handle_auto_mode_switch_request(const json& params);
264+
261265
/// Handle incoming hook invocations
262266
json handle_hooks_invoke(const json& params);
263267

‎include/copilot/session.hpp‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,9 @@ class Session : public std::enable_shared_from_this<Session>
169169
/// @return Subscription handle (unsubscribes on destruction)
170170
Subscription on(EventHandler handler);
171171

172+
/// Register an event handler that remains active for the session lifetime.
173+
void register_persistent_event_handler(EventHandler handler);
174+
172175
/// Dispatch an event to all subscribers (called by Client)
173176
void dispatch_event(const SessionEvent& event);
174177

@@ -210,6 +213,36 @@ class Session : public std::enable_shared_from_this<Session>
210213
/// Handle a user input request (called by Client)
211214
UserInputResponse handle_user_input_request(const UserInputRequest& request);
212215

216+
// =========================================================================
217+
// Elicitation Handling
218+
// =========================================================================
219+
220+
/// Register a handler for elicitation requests
221+
void register_elicitation_handler(ElicitationHandler handler);
222+
223+
/// Handle an elicitation request (called by Client)
224+
ElicitationResult handle_elicitation_request(const ElicitationContext& context);
225+
226+
// =========================================================================
227+
// Exit Plan Mode Handling
228+
// =========================================================================
229+
230+
/// Register a handler for exit-plan-mode requests
231+
void register_exit_plan_mode_handler(ExitPlanModeHandler handler);
232+
233+
/// Handle an exit-plan-mode request (called by Client)
234+
ExitPlanModeResult handle_exit_plan_mode_request(const ExitPlanModeRequest& request);
235+
236+
// =========================================================================
237+
// Auto Mode Switch Handling
238+
// =========================================================================
239+
240+
/// Register a handler for auto-mode-switch requests
241+
void register_auto_mode_switch_handler(AutoModeSwitchHandler handler);
242+
243+
/// Handle an auto-mode-switch request (called by Client)
244+
AutoModeSwitchResponse handle_auto_mode_switch_request(const AutoModeSwitchRequest& request);
245+
213246
// =========================================================================
214247
// Hooks
215248
// =========================================================================
@@ -282,6 +315,8 @@ class Session : public std::enable_shared_from_this<Session>
282315
mutable std::mutex handlers_mutex_;
283316
std::vector<std::pair<int, EventHandler>> event_handlers_;
284317
int next_handler_id_ = 0;
318+
std::mutex owned_event_subscriptions_mutex_;
319+
std::vector<Subscription> owned_event_subscriptions_;
285320

286321
// Tools
287322
mutable std::mutex tools_mutex_;
@@ -294,6 +329,18 @@ class Session : public std::enable_shared_from_this<Session>
294329
std::mutex user_input_mutex_;
295330
UserInputHandler user_input_handler_;
296331

332+
// Elicitation handler
333+
std::mutex elicitation_mutex_;
334+
ElicitationHandler elicitation_handler_;
335+
336+
// Exit plan mode handler
337+
std::mutex exit_plan_mode_mutex_;
338+
ExitPlanModeHandler exit_plan_mode_handler_;
339+
340+
// Auto mode switch handler
341+
std::mutex auto_mode_switch_mutex_;
342+
AutoModeSwitchHandler auto_mode_switch_handler_;
343+
297344
// Hooks
298345
std::mutex hooks_mutex_;
299346
std::optional<SessionHooks> hooks_;

‎include/copilot/types.hpp‎

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ using json = nlohmann::json;
2727
// Forward declarations
2828
class Session;
2929
struct SessionEvent;
30+
using EventHandler = std::function<void(const SessionEvent&)>;
3031

3132
// =============================================================================
3233
// Protocol Version
@@ -380,6 +381,258 @@ struct UserInputInvocation
380381
/// Handler for user input requests from the agent
381382
using UserInputHandler = std::function<UserInputResponse(const UserInputRequest&, const UserInputInvocation&)>;
382383

384+
// =============================================================================
385+
// Elicitation Types
386+
// =============================================================================
387+
388+
/// Elicitation display mode
389+
enum class ElicitationRequestedMode
390+
{
391+
Form,
392+
Url
393+
};
394+
395+
NLOHMANN_JSON_SERIALIZE_ENUM(
396+
ElicitationRequestedMode,
397+
{
398+
{ElicitationRequestedMode::Form, "form"},
399+
{ElicitationRequestedMode::Url, "url"},
400+
}
401+
)
402+
403+
/// JSON Schema for elicitation form fields
404+
struct ElicitationSchema
405+
{
406+
std::string type = "object";
407+
std::optional<std::map<std::string, json>> properties;
408+
std::optional<std::vector<std::string>> required;
409+
};
410+
411+
inline void to_json(json& j, const ElicitationSchema& s)
412+
{
413+
j = json{{"type", s.type}};
414+
if (s.properties)
415+
j["properties"] = *s.properties;
416+
if (s.required)
417+
j["required"] = *s.required;
418+
}
419+
420+
inline void from_json(const json& j, ElicitationSchema& s)
421+
{
422+
if (j.contains("type"))
423+
j.at("type").get_to(s.type);
424+
if (j.contains("properties"))
425+
s.properties = j.at("properties").get<std::map<std::string, json>>();
426+
if (j.contains("required"))
427+
s.required = j.at("required").get<std::vector<std::string>>();
428+
}
429+
430+
/// User action for elicitation response
431+
enum class ElicitationAction
432+
{
433+
Accept,
434+
Decline,
435+
Cancel
436+
};
437+
438+
NLOHMANN_JSON_SERIALIZE_ENUM(
439+
ElicitationAction,
440+
{
441+
{ElicitationAction::Accept, "accept"},
442+
{ElicitationAction::Decline, "decline"},
443+
{ElicitationAction::Cancel, "cancel"},
444+
}
445+
)
446+
447+
/// Context for an elicitation request from the server
448+
struct ElicitationContext
449+
{
450+
std::string session_id;
451+
std::string message;
452+
std::optional<ElicitationSchema> requested_schema;
453+
std::optional<ElicitationRequestedMode> mode;
454+
std::optional<std::string> elicitation_source;
455+
std::optional<std::string> url;
456+
};
457+
458+
inline void from_json(const json& j, ElicitationContext& c)
459+
{
460+
if (j.contains("sessionId"))
461+
j.at("sessionId").get_to(c.session_id);
462+
j.at("message").get_to(c.message);
463+
if (j.contains("requestedSchema") && !j["requestedSchema"].is_null())
464+
c.requested_schema = j.at("requestedSchema").get<ElicitationSchema>();
465+
if (j.contains("mode") && !j["mode"].is_null())
466+
c.mode = j.at("mode").get<ElicitationRequestedMode>();
467+
if (j.contains("elicitationSource") && !j["elicitationSource"].is_null())
468+
c.elicitation_source = j.at("elicitationSource").get<std::string>();
469+
if (j.contains("url") && !j["url"].is_null())
470+
c.url = j.at("url").get<std::string>();
471+
}
472+
473+
inline void to_json(json& j, const ElicitationContext& c)
474+
{
475+
j = json{{"message", c.message}};
476+
if (!c.session_id.empty())
477+
j["sessionId"] = c.session_id;
478+
if (c.requested_schema)
479+
j["requestedSchema"] = *c.requested_schema;
480+
if (c.mode)
481+
j["mode"] = *c.mode;
482+
if (c.elicitation_source)
483+
j["elicitationSource"] = *c.elicitation_source;
484+
if (c.url)
485+
j["url"] = *c.url;
486+
}
487+
488+
/// Result returned from an elicitation dialog
489+
struct ElicitationResult
490+
{
491+
ElicitationAction action = ElicitationAction::Cancel;
492+
std::optional<std::map<std::string, json>> content;
493+
};
494+
495+
inline void to_json(json& j, const ElicitationResult& r)
496+
{
497+
j = json{{"action", r.action}};
498+
if (r.content)
499+
j["content"] = *r.content;
500+
}
501+
502+
inline void from_json(const json& j, ElicitationResult& r)
503+
{
504+
j.at("action").get_to(r.action);
505+
if (j.contains("content") && !j["content"].is_null())
506+
r.content = j.at("content").get<std::map<std::string, json>>();
507+
}
508+
509+
/// Elicitation handler function type
510+
using ElicitationHandler = std::function<ElicitationResult(const ElicitationContext&)>;
511+
512+
// =============================================================================
513+
// Exit Plan Mode Types
514+
// =============================================================================
515+
516+
/// Request to exit plan mode
517+
struct ExitPlanModeRequest
518+
{
519+
std::string summary;
520+
std::optional<std::string> plan_content;
521+
std::vector<std::string> actions;
522+
std::string recommended_action = "autopilot";
523+
};
524+
525+
inline void from_json(const json& j, ExitPlanModeRequest& r)
526+
{
527+
j.at("summary").get_to(r.summary);
528+
if (j.contains("planContent") && !j["planContent"].is_null())
529+
r.plan_content = j.at("planContent").get<std::string>();
530+
if (j.contains("actions"))
531+
r.actions = j.at("actions").get<std::vector<std::string>>();
532+
if (j.contains("recommendedAction"))
533+
j.at("recommendedAction").get_to(r.recommended_action);
534+
}
535+
536+
inline void to_json(json& j, const ExitPlanModeRequest& r)
537+
{
538+
j = json{{"summary", r.summary}, {"recommendedAction", r.recommended_action}};
539+
if (r.plan_content)
540+
j["planContent"] = *r.plan_content;
541+
if (!r.actions.empty())
542+
j["actions"] = r.actions;
543+
}
544+
545+
/// Response to an exit-plan-mode request
546+
struct ExitPlanModeResult
547+
{
548+
bool approved = true;
549+
std::optional<std::string> selected_action;
550+
std::optional<std::string> feedback;
551+
};
552+
553+
inline void to_json(json& j, const ExitPlanModeResult& r)
554+
{
555+
j = json{{"approved", r.approved}};
556+
if (r.selected_action)
557+
j["selectedAction"] = *r.selected_action;
558+
if (r.feedback)
559+
j["feedback"] = *r.feedback;
560+
}
561+
562+
inline void from_json(const json& j, ExitPlanModeResult& r)
563+
{
564+
j.at("approved").get_to(r.approved);
565+
if (j.contains("selectedAction") && !j["selectedAction"].is_null())
566+
r.selected_action = j.at("selectedAction").get<std::string>();
567+
if (j.contains("feedback") && !j["feedback"].is_null())
568+
r.feedback = j.at("feedback").get<std::string>();
569+
}
570+
571+
/// Context for exit-plan-mode invocation
572+
struct ExitPlanModeInvocation
573+
{
574+
std::string session_id;
575+
};
576+
577+
/// Exit plan mode handler function type
578+
using ExitPlanModeHandler =
579+
std::function<ExitPlanModeResult(const ExitPlanModeRequest&, const ExitPlanModeInvocation&)>;
580+
581+
// =============================================================================
582+
// Auto Mode Switch Types
583+
// =============================================================================
584+
585+
/// Request to switch to auto mode after a rate limit
586+
struct AutoModeSwitchRequest
587+
{
588+
std::optional<std::string> error_code;
589+
std::optional<double> retry_after_seconds;
590+
};
591+
592+
inline void from_json(const json& j, AutoModeSwitchRequest& r)
593+
{
594+
if (j.contains("errorCode") && !j["errorCode"].is_null())
595+
r.error_code = j.at("errorCode").get<std::string>();
596+
if (j.contains("retryAfterSeconds") && !j["retryAfterSeconds"].is_null())
597+
r.retry_after_seconds = j.at("retryAfterSeconds").get<double>();
598+
}
599+
600+
inline void to_json(json& j, const AutoModeSwitchRequest& r)
601+
{
602+
j = json::object();
603+
if (r.error_code)
604+
j["errorCode"] = *r.error_code;
605+
if (r.retry_after_seconds)
606+
j["retryAfterSeconds"] = *r.retry_after_seconds;
607+
}
608+
609+
/// Response to auto-mode-switch request
610+
enum class AutoModeSwitchResponse
611+
{
612+
Yes,
613+
YesAlways,
614+
No
615+
};
616+
617+
NLOHMANN_JSON_SERIALIZE_ENUM(
618+
AutoModeSwitchResponse,
619+
{
620+
{AutoModeSwitchResponse::Yes, "yes"},
621+
{AutoModeSwitchResponse::YesAlways, "yes_always"},
622+
{AutoModeSwitchResponse::No, "no"},
623+
}
624+
)
625+
626+
/// Context for auto-mode-switch invocation
627+
struct AutoModeSwitchInvocation
628+
{
629+
std::string session_id;
630+
};
631+
632+
/// Auto mode switch handler function type
633+
using AutoModeSwitchHandler =
634+
std::function<AutoModeSwitchResponse(const AutoModeSwitchRequest&, const AutoModeSwitchInvocation&)>;
635+
383636
// =============================================================================
384637
// Hook Handler Types
385638
// =============================================================================
@@ -1150,6 +1403,18 @@ struct SessionConfig
11501403
/// Handler for user input requests from the agent (enables ask_user tool).
11511404
std::optional<UserInputHandler> on_user_input_request;
11521405

1406+
/// Handler for elicitation requests from the server.
1407+
std::optional<ElicitationHandler> on_elicitation_request;
1408+
1409+
/// Handler for exit-plan-mode requests from the agent.
1410+
std::optional<ExitPlanModeHandler> on_exit_plan_mode;
1411+
1412+
/// Handler for auto-mode-switch requests.
1413+
std::optional<AutoModeSwitchHandler> on_auto_mode_switch;
1414+
1415+
/// Pre-registered event handler - wired up when session is created.
1416+
std::optional<EventHandler> on_event;
1417+
11531418
/// Hook handlers for session lifecycle events.
11541419
std::optional<SessionHooks> hooks;
11551420

@@ -1237,6 +1502,18 @@ struct ResumeSessionConfig
12371502
/// Handler for user input requests from the agent (enables ask_user tool).
12381503
std::optional<UserInputHandler> on_user_input_request;
12391504

1505+
/// Handler for elicitation requests from the server.
1506+
std::optional<ElicitationHandler> on_elicitation_request;
1507+
1508+
/// Handler for exit-plan-mode requests from the agent.
1509+
std::optional<ExitPlanModeHandler> on_exit_plan_mode;
1510+
1511+
/// Handler for auto-mode-switch requests.
1512+
std::optional<AutoModeSwitchHandler> on_auto_mode_switch;
1513+
1514+
/// Pre-registered event handler - wired up when session is created.
1515+
std::optional<EventHandler> on_event;
1516+
12401517
/// Hook handlers for session lifecycle events.
12411518
std::optional<SessionHooks> hooks;
12421519

0 commit comments

Comments
 (0)