AGENT ARCHITECTURE · IMPLEMENTATION GUIDE
Beyond Dots and Duet: build your own portable agent
Keep tasks, memory, tools, and permissions in your application. Make the model provider replaceable—and test that it really is.
Two products, one architectural question
OpenAI Dots and Claude Duet make a similar promise: hand an agent a responsibility and let it keep working between conversations. Dots documents background delegation, cloud computers, and coordination with Work and Codex. Duet documents proactive briefings, connected apps, and work on its own cloud computer. These are product capabilities described by their vendors, not a performance comparison. OpenAI’s Dots overview · Anthropic’s Duet guide.
For a builder, the useful question is how to offer that kind of continuity while retaining the freedom to change models. The answer starts with ownership: implement the persistent service around the model, including the information and execution records that let work continue.
This guide proposes an architecture for that service. It does not describe the private internals of either product or claim feature parity with them. The workflow and acceptance tests are our design recommendations.
Define what must stay yours
A provider-independent agent should retain its task history, connected accounts, approval rules, and artifacts when its inference provider changes. Users should not have to reconnect their tools or restate an ongoing responsibility just because the next model call goes elsewhere.
That does not make all models interchangeable. A replacement still needs to handle the required inputs and tools, meet quality requirements, and comply with your data-routing policy. Independence means you control that transition.
Start with application-owned records for users, tasks, schedules, tool operations, approvals, and artifacts. Provider session IDs can be useful execution details; the durable record of the user’s work should remain understandable without them.
Design the runtime around durable tasks
The runtime receives a message, timer, or application event. It loads a task, constructs the context for a model call, validates proposed actions, and records what happens. It can then continue, wait for input, or save a future wake-up.
User messages
Webhooks and timers
Keep the service available continuously, but give each run a bounded amount of work. Wake a task when something relevant changes or a saved timer expires. A model does not need to generate tokens while waiting for a person to approve a draft.
Each task needs a status, a deadline or cancellation policy, and limits on time, model usage, and tool calls. A paused task should say what it needs. A completed task should point to the resulting artifact or verified outcome. For the underlying request loop, see how a turn works in an agent harness.
Keep memory separate from execution state
Remembering that a user prefers concise updates is different from knowing that a draft pull request already exists. Store preferences and useful facts with their sources and timestamps. Store execution progress as explicit records: which step ran, what it returned, and what remains unresolved.
Also distinguish a recorded intention from an external effect. If a request times out while creating a pull request, the remote service may still have created it. Recover by checking the remote result, using an idempotency key where supported, or reconciling an operation identifier before trying again. A local database transaction cannot make an unrelated remote API call atomic.
Temporal’s agent memory and state example explains this distinction through a restart and an external action. Its execution history records progress; the external service remains authoritative about whether the action happened.
Put model differences behind an adapter
Define the information your runtime sends and receives: messages, tool definitions, validated tool requests, final output, usage, and errors. An adapter translates that contract to the provider’s API. Keep application code dependent on the contract rather than on one vendor’s response object.
For TypeScript, AI SDK Core supplies a common model interface and provider integrations. OpenAI’s Agents SDK also documents provider and adapter surfaces for mixed-provider applications. Either can be part of a portable design; inspect the features your application actually uses. AI SDK provider architecture · OpenAI model and provider guidance.
Keep a capability check at selection time. Does this model accept the images the task needs? Can it request your tools? How will you validate structured output? What are its context and budget constraints? A shared method name does not answer those questions.
Save a portable checkpoint containing the objective, relevant facts, completed operations, artifact references, and next decision. Keep provider-specific reasoning blocks and continuation tokens within their adapter’s supported flow. At a provider change, rebuild the next request from the portable checkpoint; do not assume another provider can consume an unfinished native session.
Keep tools and authority independent of inference
Give the agent small, explicit operations such as read_issue, run_tests, and create_draft_pr. Keep schemas, credentials, and implementation in your tool layer. Anthropic’s client-tool documentation describes this separation: the model returns a tool request, and application code executes it and sends back the result. Claude tool-use documentation.
Use MCP where it helps reuse integrations. Your service can act as an MCP host and expose available tools to the selected model through its adapter. MCP standardizes context and tool exchange; it does not choose your runtime, persistent memory, or model. MCP architecture and scope.
Check authorization in executable application logic before dispatch. Bind approval to the actual action: account, target, arguments, and relevant version. If the proposed recipients or repository change, evaluate the changed operation again. Treat content from issues, documents, and websites as evidence, never as permission to expand access.
For a service used by multiple people, carry the user or tenant identity through storage queries and tool execution. Keep tokens out of prompts, isolate workspaces, and enforce access with the connected account. A model switch must preserve those boundaries.
Follow one responsibility through the system
Consider this illustrative request: “Watch the checkout project for new bugs. Investigate reports, prepare fixes with test evidence, and ask me before merging.”
- Save the responsibility. Record the repository, allowed actions, notification destination, budget, and merge restriction. Resolve missing scope before monitoring begins.
- Accept an event once. A new-issue webhook creates or updates the corresponding task. Use the source event’s identity to avoid starting duplicate investigations when delivery is retried.
- Investigate in a bounded workspace. Load the issue and relevant code. Ask the selected model for a next step, then run permitted tools in an isolated checkout. Save findings and results.
- Prepare a reviewable change. Record the tested revision, patch, validation output, and draft pull request. If the remote request times out, reconcile whether the draft exists before creating another.
- Pause at the approval boundary. Present the change and evidence. A later approval resumes the task after checking that the revision and authorization still match. A changed diff requires renewed review.
The user can close the chat throughout this process. The backend owns the task’s lifecycle and sends an update when a result or decision is ready. If the backend restarts, its durable records determine where work resumes.
Choose a small starting stack
For a TypeScript service with responsibilities that span hours or days, we would start with the following arrangement. It is a proposed stack, not a requirement to adopt every component for a short-lived prototype.
On smaller screens, scroll the table horizontally.
| Component | Responsibility |
|---|---|
| TypeScript service + AI SDK Core | Application API, context construction, and model adapters. Pin and test the provider integrations you use. |
| Temporal | Durable workflow progress, timers, and waits. Execute model and network calls as activities with explicit retry policies. |
| PostgreSQL + object storage | User and task records, memory, approvals, operation identifiers, and artifact references; larger files live in object storage. |
| Direct APIs + MCP client | Tool discovery and execution through your permission checks, using the correct connected account. |
| Isolated execution worker | Code or browser work when needed, with scoped credentials, resource limits, and retained results. |
Temporal provides documented execution patterns for retries, parallel activities, and long-running work. Define the source of truth for each record: workflow history tracks execution, application storage supplies user-facing records and artifacts, and remote systems own their effects.
Start with one model-driven loop and one useful integration. Add another agent only when a task has a clear delegation boundary. See when to use subagents for a decision framework.
Build in stages with observable outcomes
- Complete a bounded task. Read an issue and produce a sourced investigation. Support two providers against the same fixtures and inspect the actual result.
- Make it resumable. Persist progress, add a saved schedule, and restart a worker during execution. Confirm that completed steps remain recorded.
- Add one controlled write. Introduce draft creation, exact-action approvals where needed, and recovery from uncertain API outcomes.
- Prove portability. Pause after a saved step, change providers, and resume with the same task scope and evidence. Keep the old adapter available until the candidate meets your acceptance criteria.
Browser automation, voice, additional channels, and multiple workers can follow a demonstrated need. The first release should already let a user inspect activity, cancel work, and understand why a task is waiting.
Test the boundaries, not just the final answer
A fluent response cannot establish that the correct action happened. Use controlled fixtures and failure injection to inspect artifacts, external state, and permission decisions. The following are acceptance tests to implement, not reported results.
| Test | Required evidence |
|---|---|
| Duplicate webhook | One logical task for the same event, with no repeated external write. |
| Worker crash after a write | The resumed task reconciles the remote outcome before retrying. |
| Provider switch at a checkpoint | Task facts, artifacts, and approvals remain available; completed operations stay completed. |
| Changed action after approval | An approval for the old target or arguments does not authorize the new operation. |
| Misleading instructions in tool output | The runtime rejects unauthorized operations even if the model proposes them. |
| Budget exhaustion or cancellation | The task stops dispatching new work, records its partial result, and reports any action already in flight. |
Compare accepted task outcomes, human corrections, recovery failures, elapsed time, and total usage across providers. Include unsuccessful attempts in the cost. For a repeatable grading process, see building an evaluation loop.
The strongest demonstration is a task that survives both a worker restart and a model change while producing the correct result. That tests whether the application truly owns the work.
Join the discussion
Share a question, correction, or something you learned. Sign in with GitHub to comment.
Comments powered by Giscus. You can also browse the discussions on GitHub.