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.

One Claude request through the platformA user request moves through the product shell into the Claude runtime, while durable run state supports admission and finalization.PRODUCT SHELLPROVIDER RUNTIMEUserquestionChat APIauthenticate · admitShared workflowprepare product contextDurable run stateownership · queue · leaseFinalizationhistory · live UI · releaseClaude SDK runneragent loop · session · resultControlled tool bridgeapproved capabilities onlyThe platform owns durable truth; the SDK owns the agent loop.

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.

The eight responsibilities in one requestA request moves from authentication and durable admission through context preparation, Claude execution, tools and events, then returns through a provider-neutral result and finalization.1Authenticateintent arrives2Admit + persistdurable ownership3Prepare contextproduct policy4Start SDKsupervise loop5Bridge toolsapproved MCP only6Translate eventsstable semantics7Return resultprovider-neutral8Finalize truthhistory + continuity

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.

Record the obligation before starting ClaudeThe API claims ownership and commits a durable run before a worker starts the Claude SDK.request turnclaim + insertCOMMITTEDschedulestart runChat APIAdmissionPostgresWorkerClaude SDKA crash after COMMITTED loses a process, not the obligation.

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.

Product context becomes a supervised SDK sessionThe shared workflow turns product-owned context and policy into the system instructions, session, tools, events and control signals required by the Claude SDK runner.authorizetranslateProduct contextquestion + conversation historydocuments + instructionspermission-filtered toolscitations + live transportinteraction modeShared workflowone authorizedexecution planClaude SDK sessionsystem instructionsmodel + effortnew or resumable sessionapproved MCP toolsevent hooks + control signalsProduct policy stays outside; provider mechanics stay inside.

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:

  1. Tool authorization is decided before the run.
  2. Existing product tools remain the implementation source of truth.
  3. Provider-specific wire formats stop at the bridge.
Claude reaches existing tools through one controlled bridgeAn approved tool catalogue feeds an in-process MCP bridge, which translates Claude calls into the platform's typed dispatcher and existing business tools.PRODUCT CONTROL PLANEMCP calladaptinvokeClaude Agent SDKprovider formatIn-process MCP bridgestable names + schemasApproved cataloguechosen before the runTyped dispatcherproduct contractsExisting toolsdocuments · webinvestment datacollaborationClaude chooses a tool. The platform chooses which tools exist.

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.

SDK messages become stable product eventsThe runner translates raw Claude SDK messages into identified and ordered product events that can feed both the live interface and durable activity history.consumenormalizeRaw SDK messagesassistant outputtool activityresultssystem eventsRunner translatorprovider-specific inproduct semantics outProduct eventsthinking · texttool started · finisheduser input requestedusage · terminal stateidentity · sequencerevisionLive UIresponsiveHistoryreplayableOne semantic event model serves both now and later.

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.

The provider-neutral result is the narrow waistClaude-specific messages and session semantics enter one provider-neutral result, allowing the rest of the platform to finalize the turn using stable product states.translateconsumeClaude-specific runtimeSDK message classestool calls + resultsprovider terminal statesession semanticsstops at this boundaryProvider-neutral resultanswer · end state · usagemodel · continuityordered activity eventsProduct lifecyclecompleted · cancelledrate-limited · failedfinalizationhistory + deliveryOne stable contract decouples the runtime from the product.

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.

Finalization reconciles two historiesThe finalizer compares the provider transcript with product history; matching histories allow safe resume, while a mismatch remains a recoverable obligation.YESNOProvider transcriptClaude session continuityProduct historywhat the user can trustFinalizerpersist answer + eventsrelease ownershippublish terminal statehistoriesagree?Safe to resumemark resumableNot completerecover unfinishedobligationA session ID is evidence of identity, not evidence of safe continuity.

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:

ChannelWhat it representsWhat it must not pretend to prove
Durable run stateAdmission, ownership, queue and lifecycle obligationsThat the user saw an answer
SDK session storeClaude's resumable transcriptThat product history was persisted
Product historyUser-visible messages and completed activityThat a worker still owns the run
Live event streamImmediate progress in the interfaceDurable 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 ownsOur platform owns
The agent reasoning and tool-use loopAdmission and one-writer ownership
Streaming SDK messagesProduct activity events and replay
Claude session semanticsDurable transcript storage policy
MCP tool invocationTool authorization and implementation
Provider terminal resultsProduct completion and history
Interrupting the live agentCancellation, 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?