clients/cpp: v0.2 multi-turn Session API
- Session move-only RAII; destructor best-effort close
- Client::create_session / list_sessions / get_session
- TurnResult.text() helper
- All post-parse json::get<T>() wrapped via with_protocol_guard (no nlohmann leak)
- tests/test_session.cpp: ~12 tests covering RAII/idempotency/move/list/state/404/protocol-error/regression
- ASan + UBSan clean
- README "Multi-turn / Sessions (v0.2)" section
v0.1 surface unchanged. C++17 preserved. cpp-httplib 0.20.1 preserved.
Spec: memory/spec-clawdforge-v0.2.md
Server core: 940861f
This commit is contained in:
parent
22e57e3dad
commit
1f6606d3b9
6 changed files with 1208 additions and 1 deletions
|
|
@ -78,6 +78,107 @@ int main() {
|
|||
}
|
||||
```
|
||||
|
||||
## Multi-turn / Sessions (v0.2)
|
||||
|
||||
`clawdforge::Session` is the multi-turn handle returned by
|
||||
`Client::create_session`. RAII — when the `Session` goes out of scope its
|
||||
destructor best-effort closes the session on the server.
|
||||
|
||||
```cpp
|
||||
#include <clawdforge/client.hpp>
|
||||
#include <iostream>
|
||||
|
||||
namespace cf = clawdforge;
|
||||
|
||||
int main() {
|
||||
cf::Client client{cf::ClientOptions{
|
||||
.base_url = "http://localhost:8800",
|
||||
.token = "cf_...",
|
||||
}};
|
||||
|
||||
{
|
||||
// Destructor closes if not already closed.
|
||||
auto s = client.create_session(cf::CreateSessionOptions{.agent = "claude"});
|
||||
|
||||
auto r1 = s.turn("Read README.md and summarize");
|
||||
std::cout << r1.text() << "\n";
|
||||
|
||||
auto r2 = s.turn("Now look at the auth flow",
|
||||
cf::TurnOptions{.files = {"ff_xyz"}});
|
||||
std::cout << r2.text() << "\n";
|
||||
} // ~Session sends DELETE /sessions/<id>
|
||||
|
||||
// List + state lookups:
|
||||
auto sessions = client.list_sessions();
|
||||
for (const auto& st : sessions) {
|
||||
std::cout << st.session_id << " turns=" << st.turn_count << "\n";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Explicit close as fallback
|
||||
|
||||
```cpp
|
||||
auto s = client.create_session();
|
||||
try {
|
||||
s.turn("...");
|
||||
} catch (...) {
|
||||
s.close(); // optional — destructor would do it anyway
|
||||
throw;
|
||||
}
|
||||
s.close(); // idempotent — second + subsequent calls short-circuit
|
||||
// without an HTTP round-trip
|
||||
```
|
||||
|
||||
`close()` is idempotent both client-side (an internal flag short-circuits
|
||||
without a network call) and server-side (the server returns
|
||||
`{"ok":true,"already_closed":true}` on a second hit). Safe to call from
|
||||
any cleanup path.
|
||||
|
||||
### Move-only — mirrors `Client`
|
||||
|
||||
`Session` follows the same shape as `Client`: move-only, copy-deleted. A
|
||||
moved-from `Session` is marked already-closed so its destructor doesn't
|
||||
double-call. Move-assign best-effort closes the LHS before taking over the
|
||||
RHS's state.
|
||||
|
||||
```cpp
|
||||
auto a = client.create_session();
|
||||
auto b = std::move(a); // a's session id transfers to b; ~a is a no-op
|
||||
```
|
||||
|
||||
### Methods
|
||||
|
||||
| Method | HTTP | Returns |
|
||||
|---|---|---|
|
||||
| `Client::create_session(opts={})` | `POST /sessions` | `Session` (move-only) |
|
||||
| `Client::list_sessions()` | `GET /sessions` | `std::vector<SessionState>` |
|
||||
| `Client::get_session(id)` | `GET /sessions/<id>` | `SessionState` |
|
||||
| `Session::turn(prompt, opts={})` | `POST /sessions/<id>/turn` | `TurnResult` |
|
||||
| `Session::state()` | `GET /sessions/<id>` | `SessionState` |
|
||||
| `Session::close()` | `DELETE /sessions/<id>` | `void` (idempotent) |
|
||||
|
||||
`TurnResult::text()` concatenates the `content` of every `type == "text"`
|
||||
event into a single string, dropping `thinking` / `tool_call` frames. Use
|
||||
when you only want the model's user-facing reply.
|
||||
|
||||
Tool-call events carry their `args` and `result` payloads as raw JSON
|
||||
strings (`args_json` / `result_json`) rather than parsed structures —
|
||||
re-parse via `nlohmann::json::parse(*event.args_json)` if you need the
|
||||
structured value.
|
||||
|
||||
Cross-token access — pulling a session id that exists under a different
|
||||
token — surfaces as `clawdforge::APIError` with `status_code() == 404`,
|
||||
matching the server's no-existence-leak design.
|
||||
|
||||
Calling `Session::turn` after `close()` (or on a moved-from `Session`)
|
||||
throws `clawdforge::ProtocolError("Session::turn called on closed session")`.
|
||||
|
||||
### v0.1 surface unchanged
|
||||
|
||||
The `/run` path stays byte-identical to v0.1. Single-turn callers don't
|
||||
need to migrate.
|
||||
|
||||
## API
|
||||
|
||||
`clawdforge::Client` (in `<clawdforge/client.hpp>`):
|
||||
|
|
@ -90,6 +191,9 @@ int main() {
|
|||
| `create_token(req)` | `POST /admin/tokens` | `AppToken` |
|
||||
| `list_tokens()` | `GET /admin/tokens` | `std::vector<AppTokenInfo>` |
|
||||
| `revoke_token(name)` | `DELETE /admin/tokens/<name>` | `void` |
|
||||
| `create_session(opts={})` | `POST /sessions` | `Session` |
|
||||
| `list_sessions()` | `GET /sessions` | `std::vector<SessionState>` |
|
||||
| `get_session(id)` | `GET /sessions/<id>` | `SessionState` |
|
||||
|
||||
All methods throw on failure. `ClientOptions` lets you set `base_url`, `token`,
|
||||
`admin_token`, `timeout`, `connect_timeout`, `user_agent`, and `insecure_tls`.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue