Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions include/copilot/client.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,19 @@ class Client
/// @return Future that resolves to ping response
std::future<PingResponse> ping(std::optional<std::string> message = std::nullopt);

/// Get CLI status including version and protocol information
/// @return Future that resolves to status response
std::future<GetStatusResponse> get_status();

/// Get current authentication status
/// @return Future that resolves to auth status response
std::future<GetAuthStatusResponse> get_auth_status();

/// List available models with their metadata
/// @return Future that resolves to list of model info
/// @throws Error if not authenticated
std::future<std::vector<ModelInfo>> list_models();

// =========================================================================
// Internal API (used by Session)
// =========================================================================
Expand Down
13 changes: 12 additions & 1 deletion include/copilot/session.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ class Session : public std::enable_shared_from_this<Session>
using PermissionHandler = std::function<PermissionRequestResult(const PermissionRequest&)>;

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

~Session();

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

/// Get the workspace path for infinite sessions.
///
/// Contains checkpoints/, plan.md, and files/ subdirectories.
/// Returns nullopt if infinite sessions are disabled.
const std::optional<std::string>& workspace_path() const
{
return workspace_path_;
}

// =========================================================================
// Messaging
// =========================================================================
Expand Down Expand Up @@ -191,6 +201,7 @@ class Session : public std::enable_shared_from_this<Session>
private:
std::string session_id_;
Client* client_;
std::optional<std::string> workspace_path_;

// Event handlers
mutable std::mutex handlers_mutex_;
Expand Down
177 changes: 177 additions & 0 deletions include/copilot/types.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,51 @@ struct Tool
ToolHandler handler;
};

// =============================================================================
// Infinite Session Configuration
// =============================================================================

/// Configuration for infinite sessions with automatic context compaction.
///
/// When enabled, sessions automatically manage context window limits through
/// background compaction and persist state to a workspace directory.
struct InfiniteSessionConfig
{
/// Whether infinite sessions are enabled (default: true when config is provided)
std::optional<bool> enabled;

/// Context utilization threshold (0.0-1.0) at which background compaction starts.
/// Compaction runs asynchronously, allowing the session to continue processing.
/// Default: 0.80
std::optional<double> background_compaction_threshold;

/// Context utilization threshold (0.0-1.0) at which the session blocks until
/// compaction completes. This prevents context overflow when compaction hasn't
/// finished in time. Default: 0.95
std::optional<double> buffer_exhaustion_threshold;
};

inline void to_json(json& j, const InfiniteSessionConfig& c)
{
j = json::object();
if (c.enabled)
j["enabled"] = *c.enabled;
if (c.background_compaction_threshold)
j["backgroundCompactionThreshold"] = *c.background_compaction_threshold;
if (c.buffer_exhaustion_threshold)
j["bufferExhaustionThreshold"] = *c.buffer_exhaustion_threshold;
}

inline void from_json(const json& j, InfiniteSessionConfig& c)
{
if (j.contains("enabled"))
c.enabled = j.at("enabled").get<bool>();
if (j.contains("backgroundCompactionThreshold"))
c.background_compaction_threshold = j.at("backgroundCompactionThreshold").get<double>();
if (j.contains("bufferExhaustionThreshold"))
c.buffer_exhaustion_threshold = j.at("bufferExhaustionThreshold").get<double>();
}

// =============================================================================
// Session Configuration
// =============================================================================
Expand All @@ -550,6 +595,16 @@ struct SessionConfig
std::optional<std::map<std::string, json>> mcp_servers;
std::optional<std::vector<CustomAgentConfig>> custom_agents;

/// Directories to load skills from.
std::optional<std::vector<std::string>> skill_directories;

/// List of skill names to disable.
std::optional<std::vector<std::string>> disabled_skills;

/// Infinite session configuration for persistent workspaces and automatic compaction.
/// When enabled (default), sessions automatically manage context limits and persist state.
std::optional<InfiniteSessionConfig> infinite_sessions;

/// If true and provider/model not explicitly set, load from COPILOT_SDK_BYOK_* env vars.
/// Default: false (explicit configuration preferred over environment variables)
bool auto_byok_from_env = false;
Expand All @@ -565,6 +620,12 @@ struct ResumeSessionConfig
std::optional<std::map<std::string, json>> mcp_servers;
std::optional<std::vector<CustomAgentConfig>> custom_agents;

/// Directories to load skills from.
std::optional<std::vector<std::string>> skill_directories;

/// List of skill names to disable.
std::optional<std::vector<std::string>> disabled_skills;

/// If true and provider not explicitly set, load from COPILOT_SDK_BYOK_* env vars.
/// Default: false (explicit configuration preferred over environment variables)
bool auto_byok_from_env = false;
Expand Down Expand Up @@ -784,4 +845,120 @@ inline void from_json(const json& j, PingResponse& r)
r.protocol_version = j.at("protocolVersion").get<int>();
}

/// Response from status.get request
struct GetStatusResponse
{
std::string version;
int protocol_version;
};

inline void from_json(const json& j, GetStatusResponse& r)
{
j.at("version").get_to(r.version);
j.at("protocolVersion").get_to(r.protocol_version);
}

/// Response from auth.getStatus request
struct GetAuthStatusResponse
{
bool is_authenticated;
std::optional<std::string> auth_type;
std::optional<std::string> host;
std::optional<std::string> login;
std::optional<std::string> status_message;
};

inline void from_json(const json& j, GetAuthStatusResponse& r)
{
j.at("isAuthenticated").get_to(r.is_authenticated);
if (j.contains("authType") && !j["authType"].is_null())
r.auth_type = j["authType"].get<std::string>();
if (j.contains("host") && !j["host"].is_null())
r.host = j["host"].get<std::string>();
if (j.contains("login") && !j["login"].is_null())
r.login = j["login"].get<std::string>();
if (j.contains("statusMessage") && !j["statusMessage"].is_null())
r.status_message = j["statusMessage"].get<std::string>();
}

/// Model capabilities - what the model supports
struct ModelCapabilities
{
struct Supports
{
bool vision = false;
};
struct Limits
{
std::optional<int> max_prompt_tokens;
int max_context_window_tokens = 0;
};
Supports supports;
Limits limits;
};

inline void from_json(const json& j, ModelCapabilities& c)
{
if (j.contains("supports"))
{
if (j["supports"].contains("vision"))
j["supports"]["vision"].get_to(c.supports.vision);
}
if (j.contains("limits"))
{
if (j["limits"].contains("max_prompt_tokens") && !j["limits"]["max_prompt_tokens"].is_null())
c.limits.max_prompt_tokens = j["limits"]["max_prompt_tokens"].get<int>();
if (j["limits"].contains("max_context_window_tokens"))
j["limits"]["max_context_window_tokens"].get_to(c.limits.max_context_window_tokens);
}
}

/// Model policy state
struct ModelPolicy
{
std::string state;
std::string terms;
};

inline void from_json(const json& j, ModelPolicy& p)
{
j.at("state").get_to(p.state);
if (j.contains("terms"))
j.at("terms").get_to(p.terms);
}

/// Model billing information
struct ModelBilling
{
double multiplier = 1.0;
};

inline void from_json(const json& j, ModelBilling& b)
{
if (j.contains("multiplier"))
j.at("multiplier").get_to(b.multiplier);
}

/// Information about an available model
struct ModelInfo
{
std::string id;
std::string name;
ModelCapabilities capabilities;
std::optional<ModelPolicy> policy;
std::optional<ModelBilling> billing;
};

inline void from_json(const json& j, ModelInfo& m)
{
j.at("id").get_to(m.id);
j.at("name").get_to(m.name);
if (j.contains("capabilities"))
j.at("capabilities").get_to(m.capabilities);
if (j.contains("policy") && !j["policy"].is_null())
m.policy = j["policy"].get<ModelPolicy>();
if (j.contains("billing") && !j["billing"].is_null())
m.billing = j["billing"].get<ModelBilling>();
}

} // namespace copilot
90 changes: 88 additions & 2 deletions src/client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@ json build_session_create_request(const SessionConfig& config)
agents.push_back(agent);
request["customAgents"] = agents;
}
if (config.skill_directories.has_value())
request["skillDirectories"] = *config.skill_directories;
if (config.disabled_skills.has_value())
request["disabledSkills"] = *config.disabled_skills;
if (config.infinite_sessions.has_value())
request["infiniteSessions"] = *config.infinite_sessions;

return request;
}
Expand Down Expand Up @@ -136,6 +142,10 @@ json build_session_resume_request(const std::string& session_id, const ResumeSes
agents.push_back(agent);
request["customAgents"] = agents;
}
if (config.skill_directories.has_value())
request["skillDirectories"] = *config.skill_directories;
if (config.disabled_skills.has_value())
request["disabledSkills"] = *config.disabled_skills;

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

auto session = std::make_shared<Session>(session_id, this);
// Capture workspace path for infinite sessions
std::optional<std::string> workspace_path;
if (response.contains("workspacePath") && response["workspacePath"].is_string())
workspace_path = response["workspacePath"].get<std::string>();

auto session = std::make_shared<Session>(session_id, this, workspace_path);

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

auto session = std::make_shared<Session>(returned_session_id, this);
// Capture workspace_path if present (for infinite sessions)
std::optional<std::string> workspace_path;
if (response.contains("workspacePath") && response["workspacePath"].is_string())
workspace_path = response["workspacePath"].get<std::string>();

auto session = std::make_shared<Session>(returned_session_id, this, workspace_path);

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

std::future<GetStatusResponse> Client::get_status()
{
return std::async(
std::launch::async,
[this]()
{
if (state_ != ConnectionState::Connected)
{
if (options_.auto_start)
start().get();
else
throw std::runtime_error("Client not connected. Call start() first.");
}

auto response = rpc_->invoke("status.get", json::object()).get();
return response.get<GetStatusResponse>();
}
);
}

std::future<GetAuthStatusResponse> Client::get_auth_status()
{
return std::async(
std::launch::async,
[this]()
{
if (state_ != ConnectionState::Connected)
{
if (options_.auto_start)
start().get();
else
throw std::runtime_error("Client not connected. Call start() first.");
}

auto response = rpc_->invoke("auth.getStatus", json::object()).get();
return response.get<GetAuthStatusResponse>();
}
);
}

std::future<std::vector<ModelInfo>> Client::list_models()
{
return std::async(
std::launch::async,
[this]()
{
if (state_ != ConnectionState::Connected)
{
if (options_.auto_start)
start().get();
else
throw std::runtime_error("Client not connected. Call start() first.");
}

auto response = rpc_->invoke("models.list", json::object()).get();
std::vector<ModelInfo> models;
if (response.contains("models") && response["models"].is_array())
{
for (const auto& m : response["models"])
models.push_back(m.get<ModelInfo>());
}
return models;
}
);
}

std::shared_ptr<Session> Client::get_session(const std::string& session_id)
{
std::lock_guard<std::mutex> lock(mutex_);
Expand Down
5 changes: 3 additions & 2 deletions src/session.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ namespace copilot
// Constructor / Destructor
// =============================================================================

Session::Session(const std::string& session_id, Client* client)
: session_id_(session_id), client_(client)
Session::Session(const std::string& session_id, Client* client,
const std::optional<std::string>& workspace_path)
: session_id_(session_id), client_(client), workspace_path_(workspace_path)
{
}

Expand Down
Loading