clients/java: v0.2 multi-turn Session API
- Session implements AutoCloseable; try-with-resources is the canonical form
- AtomicBoolean compareAndSet idempotency on close()
- ForgeClient.createSession / .listSessions / .getSession
- TurnResult.text() helper, records throughout for shapes
- Session.toString redacts embedded client (no bearer leak)
- SessionTest.java: 13 tests covering try-with-resources/idempotency/exception/list/state/redaction/regression
- README "Multi-turn / Sessions (v0.2)" section
v0.1 surface unchanged.
Spec: memory/spec-clawdforge-v0.2.md
Server core: 940861f
This commit is contained in:
parent
5737903217
commit
33b9ed5e22
9 changed files with 1241 additions and 0 deletions
|
|
@ -62,6 +62,124 @@ try (ForgeClient client = ForgeClient.builder()
|
|||
long-lived multi-tenant deployments, construct once and reuse, rather
|
||||
than relying on `close()`.
|
||||
|
||||
## Multi-turn / Sessions (v0.2)
|
||||
|
||||
For conversations that need context across turns — debugging back and
|
||||
forth, step-by-step builds, long-running agent tasks — use the
|
||||
`/sessions` API. The canonical Java form is **try-with-resources**: the
|
||||
session is auto-closed on the way out of the block, even if the body
|
||||
throws.
|
||||
|
||||
```java
|
||||
import com.clawdforge.ForgeClient;
|
||||
import com.clawdforge.Session;
|
||||
import com.clawdforge.SessionOptions;
|
||||
import com.clawdforge.TurnResult;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
try (ForgeClient forge = ForgeClient.builder()
|
||||
.baseUrl("http://192.168.0.5:8800")
|
||||
.token(System.getenv("CLAWDFORGE_TOKEN"))
|
||||
.build()) {
|
||||
|
||||
try (Session s = forge.createSession(SessionOptions.builder()
|
||||
.agent("claude")
|
||||
.build())) {
|
||||
|
||||
TurnResult r1 = s.turn("Read README.md and summarize");
|
||||
System.out.println(r1.text());
|
||||
|
||||
TurnResult r2 = s.turnWithFiles(
|
||||
"Now look at the auth flow",
|
||||
List.of("ff_uploaded_token"));
|
||||
System.out.println(r2.text());
|
||||
}
|
||||
// session is closed (DELETE /sessions/<id>) here
|
||||
}
|
||||
```
|
||||
|
||||
For the all-defaults case the no-arg overload is enough:
|
||||
|
||||
```java
|
||||
try (Session s = forge.createSession()) {
|
||||
TurnResult r = s.turn("hello");
|
||||
}
|
||||
```
|
||||
|
||||
### `Session`
|
||||
|
||||
| Method | Endpoint | Returns |
|
||||
|---|---|---|
|
||||
| `id()` | — | `String` (server-assigned id) |
|
||||
| `agent()` | — | `String` (e.g. `"claude"`) |
|
||||
| `createdAt()` | — | `long` (unix seconds) |
|
||||
| `isClosed()` | — | `boolean` (client's view) |
|
||||
| `turn(String prompt)` | `POST /sessions/<id>/turn` | `TurnResult` |
|
||||
| `turnWithFiles(String, List<String>)` | `POST /sessions/<id>/turn` | `TurnResult` |
|
||||
| `turn(String, TurnOptions)` | `POST /sessions/<id>/turn` | `TurnResult` |
|
||||
| `state()` | `GET /sessions/<id>` | `SessionState` |
|
||||
| `close()` | `DELETE /sessions/<id>` | `void` |
|
||||
|
||||
`Session` implements `AutoCloseable` so try-with-resources just works.
|
||||
|
||||
### Idempotent close
|
||||
|
||||
`Session.close()` is idempotent on the client: a second call short-
|
||||
circuits without an HTTP round-trip via an `AtomicBoolean`
|
||||
`compareAndSet`. The server's `DELETE /sessions/<id>` is itself
|
||||
idempotent (returns `already_closed: true` on a re-close), so even a
|
||||
mix of explicit `close()` calls and try-with-resources auto-close is
|
||||
safe — there's at most one DELETE on the wire, and it's cheap if
|
||||
duplicated. If the DELETE fails on the first attempt the closed flag
|
||||
is cleared, so a subsequent `close()` retries.
|
||||
|
||||
### Listing & inspecting sessions
|
||||
|
||||
```java
|
||||
List<SessionState> sessions = forge.listSessions();
|
||||
SessionState st = forge.getSession(sessions.get(0).sessionId());
|
||||
System.out.println(st.turnCount() + " turns, last at " + st.lastTurnAt());
|
||||
```
|
||||
|
||||
A `404` from `getSession()` surfaces as `ApiException` with status
|
||||
`404` — note the server intentionally returns 404 (not 403) for a
|
||||
session belonging to a different token, to avoid leaking cross-token
|
||||
session existence.
|
||||
|
||||
### `Session.toString()` redaction
|
||||
|
||||
`Session.toString()` deliberately omits the embedded `ForgeClient`
|
||||
reference (which holds the bearer token), mirroring the
|
||||
`AppToken.toString()` redaction. A typical render:
|
||||
|
||||
```
|
||||
Session{id=sess_abc123, agent=claude, closed=false}
|
||||
```
|
||||
|
||||
You can stuff a `Session` into a log line without leaking the bearer.
|
||||
|
||||
### Records you'll see
|
||||
|
||||
```java
|
||||
record TurnEvent(String type, String content, String name,
|
||||
Map<String, Object> args, Object result) {}
|
||||
|
||||
record TurnResult(boolean ok, String sessionId, int turnIndex,
|
||||
List<TurnEvent> events, String stopReason,
|
||||
long durationMs) {
|
||||
public String text() { /* concatenates all "text" events */ }
|
||||
}
|
||||
|
||||
record SessionState(String sessionId, String agent, String appName,
|
||||
long createdAt, Long lastTurnAt, int turnCount,
|
||||
Long closedAt) {}
|
||||
```
|
||||
|
||||
`TurnResult.text()` is sugar for the common case — concatenates every
|
||||
`"text"` event's content in order. Callers who need the structured
|
||||
event stream (thinking, tool_call, etc.) iterate `events()` directly.
|
||||
|
||||
## Naming & wire format
|
||||
|
||||
The clawdforge HTTP API uses **`snake_case`** on the wire
|
||||
|
|
@ -106,6 +224,9 @@ Methods (all may throw subclasses of `ForgeException`):
|
|||
| `createToken(String name, List<String> ipCidrs)` | `POST /admin/tokens` | `AppToken` |
|
||||
| `listTokens()` | `GET /admin/tokens` | `List<AppToken>` |
|
||||
| `revokeToken(String name)` | `DELETE /admin/tokens/<name>` | `void` |
|
||||
| `createSession()` / `createSession(SessionOptions)` | `POST /sessions` | `Session` |
|
||||
| `listSessions()` | `GET /sessions` | `List<SessionState>` |
|
||||
| `getSession(String id)` | `GET /sessions/<id>` | `SessionState` |
|
||||
|
||||
### `RunRequest`
|
||||
|
||||
|
|
@ -130,6 +251,15 @@ Public state is exposed via Java 17 records:
|
|||
- `FileToken(String fileToken, int ttlSecs, long size)`
|
||||
- `AppToken(String name, String token, List<String> ipCidrs, long createdAt)`
|
||||
|
||||
v0.2 session types:
|
||||
|
||||
- `TurnEvent(String type, String content, String name, Map<String, Object> args, Object result)`
|
||||
- `TurnResult(boolean ok, String sessionId, int turnIndex, List<TurnEvent> events, String stopReason, long durationMs)`
|
||||
- `SessionState(String sessionId, String agent, String appName, long createdAt, Long lastTurnAt, int turnCount, Long closedAt)`
|
||||
|
||||
`Session` is a final class (not a record) so its `toString()` can omit
|
||||
the embedded `ForgeClient` reference and avoid leaking the bearer token.
|
||||
|
||||
`RunResult.result()` is a Jackson `JsonNode` because the server may return
|
||||
either a structured JSON value (when the prompt asked for JSON) or a plain
|
||||
JSON string. Narrow with `isObject()` / `isTextual()` / etc.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue