feat: add configuration for model, mode, and thought level (#205)

* feat: add configuration for model, mode, and thought level

* docs: document Claude effort-level filesystem config

* fix: prevent panic on empty modes/thoughtLevels in parse_agent_config

Use `.first()` with safe fallback instead of direct `[0]` index access,
which would panic if the Vec is empty and no default is set.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: harden session lifecycle and align cli.mdx example with claude.json

- destroySession: wrap session/cancel RPC in try/catch so local cleanup
  always succeeds even when the agent is unreachable
- createSession/resumeOrCreateSession: clean up the remote session if
  post-creation config calls (setMode/setModel/setThoughtLevel) fail,
  preventing leaked orphan sessions
- cli.mdx: fix example output to match current claude.json (model name,
  model order, and populated modes)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: harden session lifecycle and align config persistence logic

- resumeOrCreateSession: Remove destroy-on-error for the resume path. Config
  errors now propagate without destroying a pre-existing session. The destroy
  pattern remains in createSession (where the session is newly created and has
  no prior state to preserve).

- setSessionMode fallback: When session/set_mode returns -32601 and the
  fallback uses session/set_config_option, now keep modes.currentModeId
  in sync with the updated currentValue. Prevents stale cached state in
  getModes() when the fallback path is used.

- persistSessionStateFromMethod: Re-read the record from persistence instead
  of using a stale pre-await snapshot. Prevents race conditions where
  concurrent session/update events (processed by persistSessionStateFromEvent)
  are silently overwritten by optimistic updates.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

* fix: correct doc examples with valid Codex modes and update stable API list

- Replace invalid Codex mode values ("plan", "build") with valid ones
  ("auto", "full-access") in agent-sessions.mdx and sdk-overview.mdx
- Update CLAUDE.md stable method enumerations to include new session
  config methods (setSessionMode, setSessionModel, etc.)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add OpenAPI annotations for process endpoints and fix config persistence race

Add summary/description to all process management endpoint specs and the
not_found error type. Fix hydrateSessionConfigOptions to re-read from
persistence after the network call, and sync mode-category configOptions
on session/update current_mode_update events.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Nathan Flurry 2026-03-06 00:24:32 -08:00 committed by GitHub
parent e7343e14bd
commit c91791f88d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 1675 additions and 70 deletions

View file

@ -520,6 +520,127 @@ describe("Integration: TypeScript SDK flat session API", () => {
await sdk.dispose();
});
it("blocks manual session/cancel and requires destroySession", async () => {
const sdk = await SandboxAgent.connect({
baseUrl,
token,
});
const session = await sdk.createSession({ agent: "mock" });
await expect(session.send("session/cancel")).rejects.toThrow(
"Use destroySession(sessionId) instead.",
);
await expect(sdk.sendSessionMethod(session.id, "session/cancel", {})).rejects.toThrow(
"Use destroySession(sessionId) instead.",
);
const destroyed = await sdk.destroySession(session.id);
expect(destroyed.destroyedAt).toBeDefined();
const reloaded = await sdk.getSession(session.id);
expect(reloaded?.destroyedAt).toBeDefined();
await sdk.dispose();
});
it("supports typed config helpers and createSession preconfiguration", async () => {
const sdk = await SandboxAgent.connect({
baseUrl,
token,
});
const session = await sdk.createSession({
agent: "mock",
model: "mock",
});
const options = await session.getConfigOptions();
expect(options.some((option) => option.category === "model")).toBe(true);
await expect(session.setModel("unknown-model")).rejects.toThrow("does not support value");
await sdk.dispose();
});
it("setModel happy path switches to a valid model", async () => {
const sdk = await SandboxAgent.connect({
baseUrl,
token,
});
const session = await sdk.createSession({ agent: "mock" });
await session.setModel("mock-fast");
const options = await session.getConfigOptions();
const modelOption = options.find((o) => o.category === "model");
expect(modelOption?.currentValue).toBe("mock-fast");
await sdk.dispose();
});
it("setMode happy path switches to a valid mode", async () => {
const sdk = await SandboxAgent.connect({
baseUrl,
token,
});
const session = await sdk.createSession({ agent: "mock" });
await session.setMode("plan");
const modes = await session.getModes();
expect(modes?.currentModeId).toBe("plan");
await sdk.dispose();
});
it("setThoughtLevel happy path switches to a valid thought level", async () => {
const sdk = await SandboxAgent.connect({
baseUrl,
token,
});
const session = await sdk.createSession({ agent: "mock" });
await session.setThoughtLevel("high");
const options = await session.getConfigOptions();
const thoughtOption = options.find((o) => o.category === "thought_level");
expect(thoughtOption?.currentValue).toBe("high");
await sdk.dispose();
});
it("setModel/setMode/setThoughtLevel can be changed multiple times", async () => {
const sdk = await SandboxAgent.connect({
baseUrl,
token,
});
const session = await sdk.createSession({ agent: "mock" });
// Model: mock → mock-fast → mock
await session.setModel("mock-fast");
expect((await session.getConfigOptions()).find((o) => o.category === "model")?.currentValue).toBe("mock-fast");
await session.setModel("mock");
expect((await session.getConfigOptions()).find((o) => o.category === "model")?.currentValue).toBe("mock");
// Mode: normal → plan → normal
await session.setMode("plan");
expect((await session.getModes())?.currentModeId).toBe("plan");
await session.setMode("normal");
expect((await session.getModes())?.currentModeId).toBe("normal");
// Thought level: low → high → medium → low
await session.setThoughtLevel("high");
expect((await session.getConfigOptions()).find((o) => o.category === "thought_level")?.currentValue).toBe("high");
await session.setThoughtLevel("medium");
expect((await session.getConfigOptions()).find((o) => o.category === "thought_level")?.currentValue).toBe("medium");
await session.setThoughtLevel("low");
expect((await session.getConfigOptions()).find((o) => o.category === "thought_level")?.currentValue).toBe("low");
await sdk.dispose();
});
it("supports MCP and skills config HTTP helpers", async () => {
const sdk = await SandboxAgent.connect({
baseUrl,