loc bengaluru, ist | local --:-- srijanshukla18@gmail.com →
[post]/ai/durable-agent-sessions-openai-sdk-docker-temporal

running durable agent sessions with OpenAI SDK, Docker, and Temporal

/ 7 min read· ai

From a survey of the agent-platform field through strict sandbox requirements to a durable agent session: one OpenAI SDK run in Docker, hardened with a Temporal workflow.

Companion to three ways to put an LLM inside a Temporal workflow - this is how I actually wired up the third design.

Before building our agent runtime, I spent a bunch of time looking at all the off-the-shelf agent platforms out there right now. Everyone is promising roughly the same bundle: durable execution, sandboxes, tool approvals, memory, evals, chat channels.

Quick notes on what I looked at:

  • Cloudflare Agents - TypeScript runtime on Durable Objects with scheduling, sandboxes, approvals.
  • Mastra - really nice TypeScript DX, suspend/resume workflows, memory, evals.
  • LangGraph + LangSmith - the mature option for stateful graphs + human-in-the-loop + tracing.
  • Anthropic Managed Agents - great sandbox + session log + MCP setup, obviously tied to Claude.
  • OpenAI Agents SDK - lightweight loop + tools + SandboxAgent, doesn’t force a deployment platform on you.
  • Eve by Vercel - TypeScript durable workflows + sandboxes + OAuth/approvals on Vercel (still in beta).
  • Cloudflare OS - open-source workspace running code in V8 isolates instead of full Linux containers.
  • Google ADK and Microsoft Agent Framework + Durable Task - if you’re all-in on GCP or Azure.
  • CrewAI + AMP - Python multi-agent flows.
  • Plus composable stacks like PydanticAI + Temporal, Inngest AgentKit, and hosted sandboxes like E2B.

Instead of buying into an all-in-one vendor platform, I wanted a small setup we could completely own and swap pieces out of later.

What an “Agent Session” actually needs

Whether the agent is investigating an alert, reviewing a PR, or analyzing files, the session object needs the same basic fields:

Agent Session
├── objective
├── identity
├── context
├── capabilities
├── workspace
├── durable state
├── event history
├── artifacts
├── approvals
├── cost and limits
└── final result

My requirements for v1 were pretty strict:

  • One durable session per task (triggered via API, webhook, cron, or chat)
  • OpenAI Agents SDK (SandboxAgent) for the inner tool loop
  • Ephemeral Docker container for the sandbox
  • Skills loaded from plain markdown directories
  • Memory stored in a read-only simple.md file inside the container
  • Explicit allowlist for tools and network
  • No subagents in v1
  • If an activity crashes and retries under the hood, the user still sees one continuous session in the UI

That gives us an input payload like this,

type AgentSessionInput = {
  sessionId: string;
  objective: string;
  context?: {
    text?: string;
    files?: string[];
    references?: string[];
  };
  skills: string[];
  memoryFile?: string;
  permissions: {
    tools: string[];
    networkAllowlist: string[];
    writablePaths: string[];
    requiresApproval: string[];
  };
  limits: {
    timeoutSeconds: number;
    maxModelTurns: number;
    maxToolCalls: number;
    cpu: number;
    memoryMb: number;
  };
  model: {
    name: string;
    baseUrl?: string;
  };
};

Step 1: Single run inside Docker (before adding Temporal)

First I got a single stateless run working cleanly inside an ephemeral Docker container:

TASK ─► SandboxAgent (Docker) ─► result
          ├── tools: read, search, shell, tests
          ├── Skills + simple.md (read-only)
          └── /workspace: input ro · working rw · output rw

Inside the container, /workspace is mounted with strict read-only vs writable paths:

/workspace/
├── input/        read-only
├── working/      writable
├── output/       writable
├── task.md       read-only
├── simple.md     read-only
└── skills/       read-only

Notice simple.md is mounted read-only. The agent cannot directly overwrite its own long-term memory file during a run; it can only propose memory edits in its final output, which a human or controller process reviews afterward.

Same thing for credentials: zero API keys or GitHub tokens ever enter the Docker container. If the agent wants to call an external API or publish a PR comment, it requests the tool call, and the trusted controller outside the container checks policy, asks a human for approval if needed, uses a scoped token, and hands back only the sanitized response:

model requests tool ─► policy check ─► approval if needed
  ─► scoped credential ─► sanitized result back

For example, a PR review run looks like this:

User requests PR review
        │
        ▼
Trusted controller service
  - verifies GitHub webhook
  - fetches PR metadata
  - checks out PR
  - creates task.md
        │
        ▼
Ephemeral Docker sandbox
  - repository
  - task.md
  - review skill
  - no GitHub credentials
  - no network
        │
        ▼
OpenAI SandboxAgent
  - shell tool
  - skills
  - structured review output
        │
        ▼
Trusted controller
  - validates findings
  - displays draft
  - optionally publishes to GitHub

Step 2: Wrapping the session in Temporal

Running SandboxAgent directly in Docker works until the worker process restarts mid-run, or a task takes 2 hours, or the agent needs to wait 14 hours for a human on Slack to approve a destructive action.

So I wrapped the lifecycle in a Temporal workflow (AgentSessionWorkflow):

AgentSessionWorkflow

CREATED
   │
   ▼
prepareSession()
   │
   ▼
provisionSandbox()
   │
   ▼
runAgent()
   │
   ├── approval required ──► WAITING_FOR_APPROVAL
   │                            │
   │                            ▼
   │                         resumeAgent()
   │
   ▼
validateResult()
   │
   ▼
persistArtifacts()
   │
   ▼
destroySandbox()
   │
   ▼
COMPLETED

Each box is a separate Temporal Activity:

prepareSession
- Resolve input files and context
- Load selected skills
- Load simple.md
- Calculate effective permissions

provisionSandbox
- Create ephemeral Docker container
- Mount workspace
- Apply CPU, memory and network restrictions
- Return sandbox ID

runAgent
- Start SandboxAgent
- Stream normalized events
- Execute allowed tool calls
- Return structured result

executeExternalTool
- Perform privileged calls outside the sandbox
- Use narrowly scoped credentials
- Apply approval rules

persistArtifacts
- Save output files, logs and structured results

destroySandbox
- Remove the container and temporary workspace

Human approval is just a Temporal Signal. If the agent hits an action in requiresApproval, the workflow pauses at WAITING_FOR_APPROVAL. Even if that takes 3 days and the worker pods redeploy twice in the meantime, Temporal keeps the workflow state alive without holding a thread or polling a DB.

Two design choices that saved a lot of pain here:

  1. Don’t make every single token or bash command a Temporal Activity. Temporal owns the coarse lifecycle boundaries (prepare -> provision -> run -> validate -> persist -> destroy). Inside runAgent, the SDK runs its normal loop and streams events out with sequence numbers so the UI stays live.
  2. Hide the OpenAI SDK behind a tiny AgentRunner interface:
interface AgentRunner {
  run(
    session: AgentSession,
    environment: ExecutionEnvironment,
  ): AsyncIterable<AgentEvent>;
}

Right now AgentRunner wraps OpenAI’s SandboxAgent:

AgentRunner
  └── OpenAI SandboxAgent

Because the Temporal workflow, Docker sandbox, skill loader, and approval signals only talk to AgentRunner, we can swap the inner loop later without touching the rest of the system:

AgentRunner
  ├── Claude Agent SDK
  ├── OpenAI successor runtime
  ├── Cloudflare agent runtime
  ├── self-hosted model harness
  └── some future capable model that barely needs a framework

Takeaway

Models and agent SDKs churn every 3 months. The part of your stack that actually lasts is the control plane around the model:

  • Docker/VM sandbox boundaries where credentials never live inside the container
  • Scoped per-resource permissions
  • Temporal signals for human approvals and crash recovery
  • Workflow-level token/cost/turn caps

See the companion post (three ways to put an LLM inside a Temporal workflow) for how this compares to simpler single-call and fixed-pipeline workflows.