Skip to content

Commit fdf5bed

Browse files
authored
feat: SDK v0.1.8 parity - infinite sessions and client utilities (#3)
Add InfiniteSessionConfig for automatic context window management with configurable truncation strategy and summarization options. Add client utility methods: - get_status(): Returns CLI version and protocol info - get_auth_status(): Returns authentication state - list_models(): Returns available models with capabilities Add skills configuration (skill_directories, disabled_skills) to SessionConfig and ResumeSessionConfig. Includes comprehensive E2E test coverage for all new features. Tested with BYOK provider configuration.
1 parent 8414c9d commit fdf5bed

6 files changed

Lines changed: 473 additions & 5 deletions

File tree

include/copilot/client.hpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,19 @@ class Client
143143
/// @return Future that resolves to ping response
144144
std::future<PingResponse> ping(std::optional<std::string> message = std::nullopt);
145145

146+
/// Get CLI status including version and protocol information
147+
/// @return Future that resolves to status response
148+
std::future<GetStatusResponse> get_status();
149+
150+
/// Get current authentication status
151+
/// @return Future that resolves to auth status response
152+
std::future<GetAuthStatusResponse> get_auth_status();
153+
154+
/// List available models with their metadata
155+
/// @return Future that resolves to list of model info
156+
/// @throws Error if not authenticated
157+
std::future<std::vector<ModelInfo>> list_models();
158+
146159
// =========================================================================
147160
// Internal API (used by Session)
148161
// =========================================================================

include/copilot/session.hpp

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,8 @@ class Session : public std::enable_shared_from_this<Session>
106106
using PermissionHandler = std::function<PermissionRequestResult(const PermissionRequest&)>;
107107

108108
/// Create a session (called by Client)
109-
Session(const std::string& session_id, Client* client);
109+
Session(const std::string& session_id, Client* client,
110+
const std::optional<std::string>& workspace_path = std::nullopt);
110111

111112
~Session();
112113

@@ -124,6 +125,15 @@ class Session : public std::enable_shared_from_this<Session>
124125
return session_id_;
125126
}
126127

128+
/// Get the workspace path for infinite sessions.
129+
///
130+
/// Contains checkpoints/, plan.md, and files/ subdirectories.
131+
/// Returns nullopt if infinite sessions are disabled.
132+
const std::optional<std::string>& workspace_path() const
133+
{
134+
return workspace_path_;
135+
}
136+
127137
// =========================================================================
128138
// Messaging
129139
// =========================================================================
@@ -191,6 +201,7 @@ class Session : public std::enable_shared_from_this<Session>
191201
private:
192202
std::string session_id_;
193203
Client* client_;
204+
std::optional<std::string> workspace_path_;
194205

195206
// Event handlers
196207
mutable std::mutex handlers_mutex_;

include/copilot/types.hpp

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,51 @@ struct Tool
531531
ToolHandler handler;
532532
};
533533

534+
// =============================================================================
535+
// Infinite Session Configuration
536+
// =============================================================================
537+
538+
/// Configuration for infinite sessions with automatic context compaction.
539+
///
540+
/// When enabled, sessions automatically manage context window limits through
541+
/// background compaction and persist state to a workspace directory.
542+
struct InfiniteSessionConfig
543+
{
544+
/// Whether infinite sessions are enabled (default: true when config is provided)
545+
std::optional<bool> enabled;
546+
547+
/// Context utilization threshold (0.0-1.0) at which background compaction starts.
548+
/// Compaction runs asynchronously, allowing the session to continue processing.
549+
/// Default: 0.80
550+
std::optional<double> background_compaction_threshold;
551+
552+
/// Context utilization threshold (0.0-1.0) at which the session blocks until
553+
/// compaction completes. This prevents context overflow when compaction hasn't
554+
/// finished in time. Default: 0.95
555+
std::optional<double> buffer_exhaustion_threshold;
556+
};
557+
558+
inline void to_json(json& j, const InfiniteSessionConfig& c)
559+
{
560+
j = json::object();
561+
if (c.enabled)
562+
j["enabled"] = *c.enabled;
563+
if (c.background_compaction_threshold)
564+
j["backgroundCompactionThreshold"] = *c.background_compaction_threshold;
565+
if (c.buffer_exhaustion_threshold)
566+
j["bufferExhaustionThreshold"] = *c.buffer_exhaustion_threshold;
567+
}
568+
569+
inline void from_json(const json& j, InfiniteSessionConfig& c)
570+
{
571+
if (j.contains("enabled"))
572+
c.enabled = j.at("enabled").get<bool>();
573+
if (j.contains("backgroundCompactionThreshold"))
574+
c.background_compaction_threshold = j.at("backgroundCompactionThreshold").get<double>();
575+
if (j.contains("bufferExhaustionThreshold"))
576+
c.buffer_exhaustion_threshold = j.at("bufferExhaustionThreshold").get<double>();
577+
}
578+
534579
// =============================================================================
535580
// Session Configuration
536581
// =============================================================================
@@ -550,6 +595,16 @@ struct SessionConfig
550595
std::optional<std::map<std::string, json>> mcp_servers;
551596
std::optional<std::vector<CustomAgentConfig>> custom_agents;
552597

598+
/// Directories to load skills from.
599+
std::optional<std::vector<std::string>> skill_directories;
600+
601+
/// List of skill names to disable.
602+
std::optional<std::vector<std::string>> disabled_skills;
603+
604+
/// Infinite session configuration for persistent workspaces and automatic compaction.
605+
/// When enabled (default), sessions automatically manage context limits and persist state.
606+
std::optional<InfiniteSessionConfig> infinite_sessions;
607+
553608
/// If true and provider/model not explicitly set, load from COPILOT_SDK_BYOK_* env vars.
554609
/// Default: false (explicit configuration preferred over environment variables)
555610
bool auto_byok_from_env = false;
@@ -565,6 +620,12 @@ struct ResumeSessionConfig
565620
std::optional<std::map<std::string, json>> mcp_servers;
566621
std::optional<std::vector<CustomAgentConfig>> custom_agents;
567622

623+
/// Directories to load skills from.
624+
std::optional<std::vector<std::string>> skill_directories;
625+
626+
/// List of skill names to disable.
627+
std::optional<std::vector<std::string>> disabled_skills;
628+
568629
/// If true and provider not explicitly set, load from COPILOT_SDK_BYOK_* env vars.
569630
/// Default: false (explicit configuration preferred over environment variables)
570631
bool auto_byok_from_env = false;
@@ -784,4 +845,120 @@ inline void from_json(const json& j, PingResponse& r)
784845
r.protocol_version = j.at("protocolVersion").get<int>();
785846
}
786847

848+
/// Response from status.get request
849+
struct GetStatusResponse
850+
{
851+
std::string version;
852+
int protocol_version;
853+
};
854+
855+
inline void from_json(const json& j, GetStatusResponse& r)
856+
{
857+
j.at("version").get_to(r.version);
858+
j.at("protocolVersion").get_to(r.protocol_version);
859+
}
860+
861+
/// Response from auth.getStatus request
862+
struct GetAuthStatusResponse
863+
{
864+
bool is_authenticated;
865+
std::optional<std::string> auth_type;
866+
std::optional<std::string> host;
867+
std::optional<std::string> login;
868+
std::optional<std::string> status_message;
869+
};
870+
871+
inline void from_json(const json& j, GetAuthStatusResponse& r)
872+
{
873+
j.at("isAuthenticated").get_to(r.is_authenticated);
874+
if (j.contains("authType") && !j["authType"].is_null())
875+
r.auth_type = j["authType"].get<std::string>();
876+
if (j.contains("host") && !j["host"].is_null())
877+
r.host = j["host"].get<std::string>();
878+
if (j.contains("login") && !j["login"].is_null())
879+
r.login = j["login"].get<std::string>();
880+
if (j.contains("statusMessage") && !j["statusMessage"].is_null())
881+
r.status_message = j["statusMessage"].get<std::string>();
882+
}
883+
884+
/// Model capabilities - what the model supports
885+
struct ModelCapabilities
886+
{
887+
struct Supports
888+
{
889+
bool vision = false;
890+
};
891+
struct Limits
892+
{
893+
std::optional<int> max_prompt_tokens;
894+
int max_context_window_tokens = 0;
895+
};
896+
Supports supports;
897+
Limits limits;
898+
};
899+
900+
inline void from_json(const json& j, ModelCapabilities& c)
901+
{
902+
if (j.contains("supports"))
903+
{
904+
if (j["supports"].contains("vision"))
905+
j["supports"]["vision"].get_to(c.supports.vision);
906+
}
907+
if (j.contains("limits"))
908+
{
909+
if (j["limits"].contains("max_prompt_tokens") && !j["limits"]["max_prompt_tokens"].is_null())
910+
c.limits.max_prompt_tokens = j["limits"]["max_prompt_tokens"].get<int>();
911+
if (j["limits"].contains("max_context_window_tokens"))
912+
j["limits"]["max_context_window_tokens"].get_to(c.limits.max_context_window_tokens);
913+
}
914+
}
915+
916+
/// Model policy state
917+
struct ModelPolicy
918+
{
919+
std::string state;
920+
std::string terms;
921+
};
922+
923+
inline void from_json(const json& j, ModelPolicy& p)
924+
{
925+
j.at("state").get_to(p.state);
926+
if (j.contains("terms"))
927+
j.at("terms").get_to(p.terms);
928+
}
929+
930+
/// Model billing information
931+
struct ModelBilling
932+
{
933+
double multiplier = 1.0;
934+
};
935+
936+
inline void from_json(const json& j, ModelBilling& b)
937+
{
938+
if (j.contains("multiplier"))
939+
j.at("multiplier").get_to(b.multiplier);
940+
}
941+
942+
/// Information about an available model
943+
struct ModelInfo
944+
{
945+
std::string id;
946+
std::string name;
947+
ModelCapabilities capabilities;
948+
std::optional<ModelPolicy> policy;
949+
std::optional<ModelBilling> billing;
950+
};
951+
952+
inline void from_json(const json& j, ModelInfo& m)
953+
{
954+
j.at("id").get_to(m.id);
955+
j.at("name").get_to(m.name);
956+
if (j.contains("capabilities"))
957+
j.at("capabilities").get_to(m.capabilities);
958+
if (j.contains("policy") && !j["policy"].is_null())
959+
m.policy = j["policy"].get<ModelPolicy>();
960+
if (j.contains("billing") && !j["billing"].is_null())
961+
m.billing = j["billing"].get<ModelBilling>();
962+
}
963+
787964
} // namespace copilot

src/client.cpp

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,12 @@ json build_session_create_request(const SessionConfig& config)
8787
agents.push_back(agent);
8888
request["customAgents"] = agents;
8989
}
90+
if (config.skill_directories.has_value())
91+
request["skillDirectories"] = *config.skill_directories;
92+
if (config.disabled_skills.has_value())
93+
request["disabledSkills"] = *config.disabled_skills;
94+
if (config.infinite_sessions.has_value())
95+
request["infiniteSessions"] = *config.infinite_sessions;
9096

9197
return request;
9298
}
@@ -136,6 +142,10 @@ json build_session_resume_request(const std::string& session_id, const ResumeSes
136142
agents.push_back(agent);
137143
request["customAgents"] = agents;
138144
}
145+
if (config.skill_directories.has_value())
146+
request["skillDirectories"] = *config.skill_directories;
147+
if (config.disabled_skills.has_value())
148+
request["disabledSkills"] = *config.disabled_skills;
139149

140150
return request;
141151
}
@@ -543,7 +553,12 @@ std::future<std::shared_ptr<Session>> Client::create_session(SessionConfig confi
543553
auto response = rpc_->invoke("session.create", request).get();
544554
std::string session_id = response["sessionId"].get<std::string>();
545555

546-
auto session = std::make_shared<Session>(session_id, this);
556+
// Capture workspace path for infinite sessions
557+
std::optional<std::string> workspace_path;
558+
if (response.contains("workspacePath") && response["workspacePath"].is_string())
559+
workspace_path = response["workspacePath"].get<std::string>();
560+
561+
auto session = std::make_shared<Session>(session_id, this, workspace_path);
547562

548563
// Register tools locally for handling callbacks from the server
549564
for (const auto& tool : config.tools)
@@ -582,7 +597,12 @@ Client::resume_session(const std::string& session_id, ResumeSessionConfig config
582597
auto response = rpc_->invoke("session.resume", request).get();
583598
std::string returned_session_id = response["sessionId"].get<std::string>();
584599

585-
auto session = std::make_shared<Session>(returned_session_id, this);
600+
// Capture workspace_path if present (for infinite sessions)
601+
std::optional<std::string> workspace_path;
602+
if (response.contains("workspacePath") && response["workspacePath"].is_string())
603+
workspace_path = response["workspacePath"].get<std::string>();
604+
605+
auto session = std::make_shared<Session>(returned_session_id, this, workspace_path);
586606

587607
// Register tools locally for handling callbacks from the server
588608
for (const auto& tool : config.tools)
@@ -714,6 +734,72 @@ std::future<PingResponse> Client::ping(std::optional<std::string> message)
714734
);
715735
}
716736

737+
std::future<GetStatusResponse> Client::get_status()
738+
{
739+
return std::async(
740+
std::launch::async,
741+
[this]()
742+
{
743+
if (state_ != ConnectionState::Connected)
744+
{
745+
if (options_.auto_start)
746+
start().get();
747+
else
748+
throw std::runtime_error("Client not connected. Call start() first.");
749+
}
750+
751+
auto response = rpc_->invoke("status.get", json::object()).get();
752+
return response.get<GetStatusResponse>();
753+
}
754+
);
755+
}
756+
757+
std::future<GetAuthStatusResponse> Client::get_auth_status()
758+
{
759+
return std::async(
760+
std::launch::async,
761+
[this]()
762+
{
763+
if (state_ != ConnectionState::Connected)
764+
{
765+
if (options_.auto_start)
766+
start().get();
767+
else
768+
throw std::runtime_error("Client not connected. Call start() first.");
769+
}
770+
771+
auto response = rpc_->invoke("auth.getStatus", json::object()).get();
772+
return response.get<GetAuthStatusResponse>();
773+
}
774+
);
775+
}
776+
777+
std::future<std::vector<ModelInfo>> Client::list_models()
778+
{
779+
return std::async(
780+
std::launch::async,
781+
[this]()
782+
{
783+
if (state_ != ConnectionState::Connected)
784+
{
785+
if (options_.auto_start)
786+
start().get();
787+
else
788+
throw std::runtime_error("Client not connected. Call start() first.");
789+
}
790+
791+
auto response = rpc_->invoke("models.list", json::object()).get();
792+
std::vector<ModelInfo> models;
793+
if (response.contains("models") && response["models"].is_array())
794+
{
795+
for (const auto& m : response["models"])
796+
models.push_back(m.get<ModelInfo>());
797+
}
798+
return models;
799+
}
800+
);
801+
}
802+
717803
std::shared_ptr<Session> Client::get_session(const std::string& session_id)
718804
{
719805
std::lock_guard<std::mutex> lock(mutex_);

src/session.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ namespace copilot
1111
// Constructor / Destructor
1212
// =============================================================================
1313

14-
Session::Session(const std::string& session_id, Client* client)
15-
: session_id_(session_id), client_(client)
14+
Session::Session(const std::string& session_id, Client* client,
15+
const std::optional<std::string>& workspace_path)
16+
: session_id_(session_id), client_(client), workspace_path_(workspace_path)
1617
{
1718
}
1819

0 commit comments

Comments
 (0)