- Published on
How We Use the Claude Agent SDK in Our Investment Research Platform — Part 1
It's Not a Model Swap
Our investment research platform helps analysts turn company filings, market data, internal documents and web evidence into cited research. A single request may analyse several sources, call specialist tools and stream its progress for minutes before producing an answer.
That makes an agent runtime only one part of the product. Around it, the platform still has to enforce permissions, coordinate concurrent work, preserve history and recover from failure.
Consider a user asking:
Analyse this company's recent performance, explain what changed, and support the answer with evidence.
The tempting architecture is wonderfully small:
Question -> Claude Agent SDK -> Answer
That is enough for a demo. It is not enough for a product.
A production system has to answer a much larger set of questions. What if two requests arrive for the same conversation? What happens if the process disappears after Claude finishes but before the answer is saved? Can a user stop a long-running analysis without corrupting the next turn? Which tools may Claude call, and how does the UI explain what those tools did? Can the next request safely resume the previous Claude session?
Our central design decision was simple:
The Claude Agent SDK owns the agent loop. Our platform owns the conversation lifecycle.
That boundary shaped everything else.
The architecture at a glance
The SDK runner sits inside a larger product workflow. It does not replace admission, durable state, tool authorization, live events, or finalization.
Figure 1. One Claude request moving through the platform.
There are two important boundaries in this picture.
The first separates the product shell from the provider runtime. The product shell knows about users, conversations, queues, permissions, history and delivery. The provider runtime knows how to configure Claude, send input, receive SDK messages, execute approved tools and return a result.
The second separates live delivery from durable truth. A streaming event may make the interface feel immediate, but it is not proof that a run is owned, an answer is saved, or a session is safe to resume.
Walking through one request
The request passes through eight responsibilities. Each one turns a runtime event into something the product can safely understand and operate.
1. The chat API receives intent, not permission to run
Purpose: establish who is asking and whether work may begin.
The request carries the question, selected model, project context and enabled capabilities. That expresses intent; it does not authorize execution. The API authenticates the caller, then asks admission whether this conversation may begin another run.
Admission either grants one-writer ownership or records the message as queued work. Without that gate, two workers could produce competing answers for the same conversation.
The key lesson is that accepting an HTTP request and starting an agent are different operations.
2. Admission creates a durable run
Purpose: make the obligation survive the process that accepted it.
Once admitted, the platform records the run before scheduling background work. The record identifies the conversation, selected provider and model, current generation owner, and live-update stream.
If the process disappears one millisecond later, another process can inspect durable state and see that the work was admitted. It never has to infer truth from a missing in-memory task.
This follows a rule we use throughout the architecture:
Record the obligation first. Perform the external side effect second. Confirm completion last.
Starting Claude is the external side effect. The durable record comes first.
3. The shared workflow prepares product context
Purpose: apply product policy before provider-specific execution begins.
The shared workflow gathers the context owned by the product rather than any model provider:
- the user's question and conversation history;
- documents and project instructions;
- permission-filtered tools and data sources;
- citation state and live activity transport;
- the selected interaction mode.
This preparation stays outside the Claude runner, keeping product policy in one place. The provider-specific component receives an already-authorized execution plan, including one explicit tool catalogue for the turn; it never discovers arbitrary tools from its environment.
4. The Claude runner translates the plan into an SDK session
Purpose: convert an authorized product plan into a supervised Claude session.
The provider boundary maps the execution plan into Claude Agent SDK concepts:
- system instructions;
- model and effort settings;
- a new session or a resumable session;
- an in-process MCP server containing the approved tools;
- hooks for receiving ordered SDK messages;
- cancellation and lease-loss signals.
The SDK's bidirectional client lets the runner send input with query() and consume the complete stream with receive_messages(): assistant output, tool activity, results and system events. The ClaudeSDKClient reference describes this lifecycle.
The SDK decides when to use a tool, feeds results back to Claude, continues the reasoning loop and produces a terminal result. Our runner supervises that loop on behalf of the product.
5. Tools cross a controlled MCP bridge
Purpose: let Claude use existing capabilities without widening its permissions.
Our document, web, investment-data and collaboration tools already existed; Claude did not need a second implementation. We expose the approved subset through an in-process MCP bridge, with a stable name and input schema for each tool.
The bridge translates an SDK call into the platform's typed dispatcher, then converts the result back into the MCP response shape Claude expects.
The SDK supports this pattern through create_sdk_mcp_server, which lets applications register custom tools without running another MCP subprocess.
This boundary gives us three properties:
- Tool authorization is decided before the run.
- Existing product tools remain the implementation source of truth.
- Provider-specific wire formats stop at the bridge.
The agent chooses among its approved tools, but cannot widen its own permissions.
6. SDK messages become product events
Purpose: give live delivery and historical replay one stable event language.
Raw SDK messages contain excellent runtime information, but they are not automatically a good interface or durable product history. The runner translates them into a smaller semantic stream: thinking, text, tool started, tool finished, user input requested, context usage and terminal state.
Every event has an identity, sequence and revision, allowing live updates and historical replay to describe the same activity without duplication.
The live transport provides responsiveness. If it disappears, the authoritative run and completed history still exist elsewhere.
This is why we treat streaming infrastructure as a courier, not as the database of record.
7. The runner returns a provider-neutral result
Purpose: stop Claude-specific types at the provider boundary.
When the SDK loop ends, the runner returns one provider-neutral result instead of leaking SDK message classes into the application. It contains:
- the final answer;
- how the turn ended;
- usage information;
- model provenance;
- durable continuity information;
- the ordered activity events.
That contract is the architecture's narrow waist. Everything before it may be Claude-specific; everything after it reasons in product terms such as completed, cancelled, rate-limited or failed.
8. Finalization decides what is true
Purpose: reconcile runtime success with durable product truth.
Receiving a successful SDK result does not complete the product turn. The finalizer still persists the user-visible answer and activity history, verifies transcript durability, updates conversation continuity, releases ownership and publishes the terminal UI state.
Only after the product history and SDK transcript agree do we mark the Claude session as resumable. A session ID alone is not enough: it could point to a transcript containing work the user never received.
An agent runtime therefore creates two histories:
- the provider transcript needed to continue the agent;
- the product history the user can see and trust.
Safe resume means those histories agree.
Four channels, four jobs
One common source of complexity is trying to make one storage or messaging system represent every kind of truth. We keep the responsibilities separate:
| Channel | What it represents | What it must not pretend to prove |
|---|---|---|
| Durable run state | Admission, ownership, queue and lifecycle obligations | That the user saw an answer |
| SDK session store | Claude's resumable transcript | That product history was persisted |
| Product history | User-visible messages and completed activity | That a worker still owns the run |
| Live event stream | Immediate progress in the interface | Durable completion or ownership |
These records meet during finalization, but they are not interchangeable.
What the SDK owns and what the platform owns
The cleanest way to understand the architecture is to draw the responsibility line explicitly.
| Claude Agent SDK owns | Our platform owns |
|---|---|
| The agent reasoning and tool-use loop | Admission and one-writer ownership |
| Streaming SDK messages | Product activity events and replay |
| Claude session semantics | Durable transcript storage policy |
| MCP tool invocation | Tool authorization and implementation |
| Provider terminal results | Product completion and history |
| Interrupting the live agent | Cancellation, queue handoff and recovery |
Neither side is a thin wrapper around the other. They solve different problems.
Why this boundary matters
With this architecture, we can evolve the Claude runtime without rewriting the surrounding product workflow. We can change session options, tool exposure or event translation while keeping admission, history and user-facing completion stable.
The reverse is also true. We can strengthen queueing, recovery or persistence without teaching the SDK about our product database.
Most importantly, failures become explicit. If Claude succeeds but history persistence fails, that is not vaguely "an agent error." It is a known unfinished obligation with a recovery path.
That is the difference between calling an SDK and operating an agent system.
Next: one conversation, one owner
Part 1 deliberately skipped the machinery behind admission. The next article will explain why a conversation needs a single writer, how ownership generations fence stale workers, what a lease heartbeat actually guarantees, and how queued work takes over without getting stuck.
Because once an agent can run for minutes and call real tools, the most important question is no longer just:
Can it answer?
It is:
Who is allowed to answer, and what happens when that worker disappears?