Skip to content

SDK lacks agent session pattern, is stateless per call, and touches the filesystem for conversation data #232

@shellicar

Description

@shellicar

The problems

Three problems, all distinct. Each would be worth fixing on its own. They touch the same code, which is why they are tracked together.

Problem 1: the SDK does not mutate the Conversation

The SDK has no way to run an ongoing conversation with the model across queries. Every call rebuilds the full context from the caller's hands. The caller supplies the full message list on every call, takes the assistant response and any tool_result messages back, and appends them to its own message list before the next call.

This is the wrong division of labour. The SDK is the thing that knows how to talk to the model. It should maintain the message list on the caller's behalf: push the user message, the assembled assistant message, and the tool results into the Conversation as turns happen. The caller should supply only what genuinely changes per turn (the new user message, the optional system reminder, the abort controller).

Problem 2: the SDK is stateless

Every call to runAgent takes a full options object with roughly fifteen fields: model, tools, betas, systemPrompts, maxTokens, thinking, cacheTtl, cachedReminders, requireToolApproval, pauseAfterCompact, compactInputTokens, transformToolResult, messages, systemReminder. Almost none of these change between calls.

The caller has to re-supply every field on every call because the SDK has no place to hold any of these values between calls. Every call also reconstructs a runner object, a control channel, an approval coordinator, and the turn-loop state, even though none of those need to be reconstructed per call. Per-call reconstruction is wasteful at runtime and a source of drift: it invites per-call state accumulation, which invites per-call bugs.

The fix is to make the SDK stateful. The Client, Tool registry, Control channel, and Approval coordinator should be constructed once and reused across queries. Only the abort controller and the per-query input should be per-query.

Problem 3: the SDK reads and writes files for conversation data

ConversationStore knows about history file paths. historyFile is an AnthropicAgentOptions field that forces every consumer to opt in to SDK-managed persistence. The feature/usage-auditing branch was adding RawEventWriter and AuditWriter calling mkdirSync and appendFileSync from inside the SDK.

The SDK is a library for talking to the Anthropic API. It should not touch the filesystem for conversation data. Every file path, every fs call, every persistence decision belongs to the consumer. The SDK exposes the Conversation as an in-memory block. What the consumer does with those messages (saves them, streams them to a database, throws them away) is the consumer's concern.

The one pragmatic exception is credential storage for the Anthropic API (the OAuth token file inside the Client block). That is access to the API, not conversation data.

Design

The authoritative design document is .claude/plans/sdk-shape.md. It contains the principles, the block responsibilities, the lifetimes, and the glossary. The issue body should not duplicate the plan; read the plan file.

Prior work

PR #231 fixed the compaction-truncation symptom (Conversation now retains full history; cloneForRequest() returns the post-compaction slice for API requests). That was the narrow first fix before the full refactor.

Metadata

Metadata

Assignees

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions