Deep research agent customization options
The agent can be customized by changing the prompt constants in the agent file to adjust the workflow, delegation strategy, or researcher behavior. The delegation limits can also be tuned to allow for more parallel sub-agents or delegation rounds.
Deep Agents harness profiles
Deep Agents supports harness profiles in beta, which are declarative bundles of system prompt, tool, middleware, and subagent tweaks that are registered per provider or specific model. This enables per-provider and per-model tuning. Claude Agent SDK requires configuration in code at each model call site.
Persist thread ID in sessionStorage for page reloads
Create a LangGraph thread when the page loads and persist its ID in sessionStorage (using a key like "sandbox-thread-id") so page reloads reconnect to the same sandbox. This avoids creating new threads and sandboxes on refresh. A "new thread" button should clear the stored ID so the next mount creates a fresh thread and sandbox.
Thread ID included in sandbox API URL
The thread ID must be included in the API URL when fetching files, such as GET /sandbox/{encodeURIComponent(threadId)}/tree and GET /sandbox/{encodeURIComponent(threadId)}/file, so requests always hit the correct sandbox for the current conversation.
Real-time file sync on tool calls
Update files as the agent works, not after it finishes. Watch the stream's messages for ToolMessage instances from file-mutating tools. When write_file or edit_file tool calls complete, refresh that specific file. When execute completes, refresh everything (since a shell command could modify any file). Track processed tool call IDs to avoid redundant refreshes.
Detecting changed files by snapshotting
Before each agent run, snapshot the current file contents. After files refresh, compare against the snapshot to identify which files changed. When a user selects a changed file, default to the diff view so they immediately see what the agent modified.
Diff display libraries by framework
Diff display libraries: React uses @pierre/diffs with FileDiff component and parseDiffFromFile. Vue uses @git-diff-view/vue with DiffView component and generateDiffFile from @git-diff-view/file. Svelte uses @git-diff-view/svelte with DiffView and generateDiffFile. Angular uses ngx-diff with ngx-unified-diff component using [before] and [after] bindings.
Best practice: sync files on every relevant tool call
Sync files on every relevant tool call, not just when the run finishes. Watch for write_file, edit_file, delete (Python only), and execute tool messages and refresh immediately. This provides real-time feedback as the agent modifies the sandbox.
Best practice: default to diff view for changed files
When a user clicks a file that was modified by the agent, show the diff first — that's what they care about. This provides immediate context about agent modifications.
Best practice: filter node_modules from file tree
Filter node_modules from the file tree when fetching. Nobody wants to browse thousands of dependency files. This keeps the UI clean and responsive.
Best practice: compact tool results for read-only operations
For read-only operations like read_file, show a compact one-liner in chat (e.g., "Read router.js L1-42") instead of dumping the full output. Reserve full output display for mutating tools.
Best practice: add guardrails before launch
For autonomous coding agents, configure rate limits, error handling, and data privacy middleware before launch. Keep secrets out of the sandbox and use the sandbox auth proxy instead of environment variables or file uploads for API keys.
useStream hook configuration for sandbox IDE
useStream is configured with apiUrl (e.g., http://localhost:2024), assistantId ("deep_agent_ide"), threadId (persisted or null), and onThreadId callback to update stored thread ID. For production, point apiUrl at your LangSmith Deployment and pass a stable thread_id on each run.
Agent Auth OAuth 2.0 flow for user credentials
Agent Auth provides a managed OAuth 2.0 flow for agents that need to call external APIs on behalf of users. Configure an OAuth provider, and the agent can request tokens scoped to each user. On first use, the agent interrupts execution and presents an OAuth consent URL. After the user authenticates, the agent resumes with a valid token. Tokens are stored and refreshed automatically. Example: `auth_result = await auth_client.authenticate(provider="github", scopes=["repo", "read:org"], user_id=runtime.server_info.user.identity)` then use `auth_result.token` for API calls.
Async programming best practices for production
When building deep agents for production: Create async tools (LangChain runs sync tools in a separate thread to avoid blocking, but native async avoids threading overhead entirely). Use async middleware methods; custom middleware should implement async hooks like `abefore_agent` instead of `before_agent`. Use async for external resource lifecycle; creating sandboxes or connecting to MCP servers involves network calls and should be awaited. Graph factories that provision these resources are async.
Memory scoping across user and assistant boundaries
Memory scoping determines who should see and modify data. User scope (recommended default, namespace `(user_id)`) is for per-user preferences and context with each user getting their own private memory. Assistant scope (namespace `(assistant_id)`) is for shared instructions for one assistant where memory is shared across all users of the same assistant. Global/Organization scope (namespace `(org_id)`) is for read-only policies for all users and assistants, typically for organization-wide policies that should be writable only through application code, not by the agent itself.
Shared memory as prompt injection vector
Shared memory (assistant, user, or organization scope) is a vector for prompt injection. If one user can write to memory that another user's conversation reads, a malicious user could inject instructions into that shared state. Enforce read-only access where appropriate, for example making organization-wide policies writable only through application code, not by the agent itself. Use permissions to declaratively deny writes to shared paths, or backend policy hooks for custom validation logic.
Permissions for controlling file access in agents
Permissions are declarative allow/deny rules that control which files and directories the agent can read or write. Use permissions to isolate the agent to a working directory, protect sensitive files, or enforce read-only memory. Rules are evaluated in declaration order and the first matching rule wins.
PIIMiddleware for data privacy in agents
Use PIIMiddleware to detect and handle PII before it reaches the model or gets stored in logs. Python: `PIIMiddleware("email", strategy="redact", apply_to_input=True)` or `PIIMiddleware("credit_card", strategy="mask", apply_to_input=True)`. TypeScript: `piiMiddleware("email", { strategy: "redact", applyToInput: true })`. Strategies include redact (replace with `[REDACTED_EMAIL]`), mask (partial masking like `****-****-****-1234`), hash (deterministic hash), and block (raise an error). Custom detectors can be written for domain-specific patterns.
useStream frontend hook for connecting UI to agent
Deep Agents use `useStream` to connect your UI to the agent backend. @useStream is a frontend hook available for React, Vue, Svelte, and Angular that streams messages, subagent progress, and custom state from your agent in real time. Locally, `useStream` points at `http://localhost:2024`. In production, point it at your LangSmith Deployment and configure reconnection so users don't lose progress if their connection drops. Example: `const stream = useStream<typeof agent>({ apiUrl: "https://your-deployment.langsmith.dev", assistantId: "agent" })`.
How to pass memory files to agents
Pass file paths to the `memory=` parameter when creating an agent with `create_deep_agent()`. You can also pass skills via `skills=` for procedural memory. A backend controls where files are stored and who can access them.
Agent memory reading strategies
The agent can load memory files into the system prompt at startup, or read them on demand during the conversation. For example, skills use on-demand loading: the agent reads only skill descriptions at startup, then reads the full skill file only when it matches a task. This keeps context lean until a capability is needed.
Agent memory update with edit_file tool
When the agent learns new information, it can use its built-in `edit_file` tool to update memory files. Updates can happen during the conversation (the default) or in the background between conversations via background consolidation. Changes are persisted and available in the next conversation.
Read-only vs writable memory
Not all memory is writable: developer-defined skills and organization policies are typically read-only. Use read-write permissions for user preferences, agent self-improvement, and learned skills. Use read-only permissions for organization policies, compliance rules, shared knowledge bases, and developer-defined skills to prevent prompt injection via shared memory.
Agent-scoped memory pattern with namespace
Agent-scoped memory is shared across all users, so the agent builds up its own persona, accumulated knowledge, and learned preferences through every conversation. Set the backend namespace to `(assistant_id,)` so every conversation for this agent reads and writes to the same memory file. Access the assistant ID via `rt.server_info.assistant_id` (Python, deepagents>=0.5.0) or `rt.serverInfo.assistantId` (JavaScript, deepagents>=1.9.0).
User-scoped memory pattern with namespace
Give each user their own memory file so the agent remembers preferences, context, and history per user while core agent instructions stay fixed. The namespace uses `(user_id,)` or `(rt.server_info.user.identity,)` so each user gets an isolated copy of the memory file and preferences never leak between users.
Episodic memory through checkpointers
Episodic memory stores records of past experiences: what happened, in what order, and what the outcome was. Deep Agents already use checkpointers which support episodic memory: every conversation is persisted as a checkpointed thread.
Search past conversations tool pattern
To make past conversations searchable, wrap thread search in a tool. Use `client.threads.search(metadata={"user_id": user_id}, limit=5)` to search conversations for a specific user. The `user_id` is pulled from the runtime context via `runtime.server_info.user.identity` rather than passed as a parameter.
Organization-level memory namespace
Organization-level memory follows the same pattern as user-scoped memory, but with an organization-wide namespace instead of a per-user one. Use it for policies or knowledge that should apply across all users and agents in an organization. Access the org ID via `rt.context.org_id` (Python) or `rt.context.orgId` (JavaScript).
Organization memory is typically read-only
Organization-level memory is typically read-only to prevent prompt injection via shared state. Use permissions to enforce read-only access or policy hooks for custom validation logic.
Advanced memory configuration dimensions
Memory can be configured across multiple dimensions: Duration (short-term or long-term), Information type (episodic, procedural, or semantic), Scope (user, agent, or organization), Update strategy (during conversation or between conversations), Retrieval (loaded into prompt or on demand), and Agent permissions (read-write or read-only).
Background consolidation for memory updates
Instead of updating memories during the conversation (hot path), memories can be processed between conversations as a background task called sleep time compute. A separate deep agent reviews recent conversations, extracts key facts, and merges them with existing memories. The hot path approach is sufficient for most applications and adds no user-facing latency, but background consolidation can improve memory quality across many conversations.
Consolidation agent pattern
The recommended pattern is to deploy a consolidation agent alongside your main agent — a deep agent that reads recent conversation history via `search_recent_conversations` tool, extracts key facts, and merges them into the memory store. Register it in `langgraph.json` and trigger it on a cron schedule. The tool uses `runtime.server_info.user.identity` to get the user ID and `client.threads.search(metadata={"user_id": user_id}, updated_after=since.isoformat(), limit=20)` to fetch recent conversations.
Cron schedule configuration for consolidation
Schedule the consolidation agent with a cron job using `client.crons.create(assistant_id="consolidation_agent", schedule="0 */6 * * *", input={"messages": [{"role": "user", "content": "Consolidate recent memories."}]})`. All cron schedules are interpreted in UTC. The cron interval must match the lookback window inside the consolidation agent: if the cron runs every 6 hours, the agent's search_recent_conversations tool should look back 6 hours.
Concurrent write conflicts in memory
Multiple threads can write to memory in parallel, but concurrent writes to the same file can cause last-write-wins conflicts. For user-scoped memory this is rare since users typically have one active conversation at a time. For agent-scoped or organization-scoped memory, consider using background consolidation to serialize writes, or structure memory as separate files per topic to reduce contention.
Multiple agents in same deployment with separate memory
To give each agent its own memory in a shared deployment, add `assistant_id` to the namespace. Use `StoreBackend(namespace=lambda rt: (rt.server_info.assistant_id, rt.server_info.user.identity))` for agent+user scoped memory or use `assistant_id` alone for per-agent isolation without per-user scoping.
LangSmith tracing for memory auditing
Use LangSmith tracing to audit what your agent writes to memory. Every file write appears as a tool call in the trace.
Storing persistent data in LangGraph
Deep Agents provides filesystem-backed persistence using backends. Store persistent data across multiple agent interactions by: (1) passing memory file paths to the `memory=` parameter when creating an agent, (2) configuring a backend that routes those paths to a store (like StoreBackend), (3) having the agent read files at startup or on demand, (4) having the agent update files with the `edit_file` tool. Data is stored in the configured backend and remains available across conversations because each thread is persisted as a checkpoint.
Agent-scoped memory full example with seeding
Create agent-scoped memory by using create_deep_agent with CompositeBackend routing /memories/ and /skills/ to StoreBackend with namespace=(assistant_id,). Seed the store with initial memories using store.put(("my-agent",), "/memories/AGENTS.md", create_file_data(...)). Invoke the agent across multiple threads with different thread_ids and it will remember and update what it learns across all threads sharing that agent.
User-scoped memory full example with isolation
Create user-scoped memory by using create_deep_agent with CompositeBackend routing /memories/ and /skills/ to StoreBackend with namespace=(rt.server_info.user.identity,). Seed per-user memories using store.put(("user-alice",), "/memories/preferences.md", create_file_data(...)) and store.put(("user-bob",), "/memories/preferences.md", create_file_data(...)) with different preferences for each user. When deployed, each authenticated request resolves rt.server_info.user.identity to the calling user so Alice and Bob automatically see only their own preferences.
Populate organization memory from application code
Use the Store API to populate organization memory: client.store.put_item((org_id,), "/compliance.md", create_file_data("""## Compliance policies
- Never disclose internal pricing
- Always include disclaimers on financial advice
""")). This allows your application to control what's in shared organizational memory files without the agent modifying them.
Consolidation agent example with search_recent_conversations
A consolidation agent reads recent conversations and merges facts into memory. It includes a search_recent_conversations tool that uses: user_id = runtime.server_info.user.identity; since = datetime.now(timezone.utc) - timedelta(hours=6); threads = await sdk_client.threads.search(metadata={"user_id": user_id}, updated_after=since.isoformat(), limit=20); then fetches history for each thread and returns them as a string.
Security considerations for shared memory
If one user can write to memory that another user reads, a malicious user could inject instructions into shared state. To mitigate: (1) Default to user scope (user_id) unless you have a specific reason to share, (2) Use read-only memory for shared policies (populate via application code, not the agent), (3) Add human-in-the-loop validation before the agent writes to shared memory using an interrupt to require human approval for writes to sensitive paths.
Custom tools with multimodal outputs
Custom tools can contain multimodal files, such as images. The return value is converted to a ToolMessage the model reads on the next turn. Access the normalized representation with content_blocks on the resulting message.
Multimodal-heavy workload strategies
For multimodal-heavy workloads: store images, screenshots, and charts in a filesystem backend or external object store then pass file paths or URLs through messages; prefer references over base64-encoded image blocks in long-running conversations; use subagents for image-heavy inspection so the main agent receives a compact text result; tune summarization thresholds or provide a custom token counter when your provider charges many tokens for images.
Hide filesystem tools from model via HarnessProfile
To hide the default filesystem tools (ls, read_file, write_file, edit_file, delete, glob, grep) from the model, register a HarnessProfile with excluded_tools. This removes tools from the model-visible surface while keeping FilesystemMiddleware scaffolding in place. Example: register_harness_profile('anthropic:claude-sonnet-4-6', HarnessProfile(excluded_tools=frozenset({'ls', 'read_file', 'write_file', 'edit_file', 'delete', 'glob', 'grep'})))
Restrict filesystem tools to subset with allowlist
To expose only a subset of filesystem tools instead of hiding all, pass a tools allowlist to FilesystemMiddleware (requires deepagents>=0.7). Any tool not in the list is removed from the model's tool list. read_file must always be included—omitting it raises ValueError. The execute and delete tools are also dropped whenever the configured backend doesn't support them. Pass the FilesystemMiddleware instance through middleware= in create_deep_agent. This replaces the default for the main agent; general-purpose subagents inherit the same restriction. Declarative subagents don't inherit it—include FilesystemMiddleware(tools=...) in their own middleware field.
Task planning as opt-in capability
Starting in v0.7 task planning is opt-in only. In earlier versions, task planning middleware was included by default. Planning is often useful for long or complicated multi-step tasks, less capable models that benefit from an explicit accountability tool, and UIs that stream progress from agent state. Pass TodoListMiddleware to the middleware parameter to give the agent a write_todos tool for maintaining a structured task list during execution.
Persistent memory for Deep Agents
Deep Agents support persistent memory across conversations, documented in the memory section of the customization guide.
Harness profiles package model-specific configuration
Harness profiles let you package configuration that Deep Agents applies whenever a given provider or specific model is selected. This includes system-prompt tweaks, tool description overrides, excluded tools or middleware, extra middleware, and general-purpose subagent edits. They are the main way to tune how the harness behaves for a particular model without changing the create_deep_agent call site. Use HarnessProfile when building profiles in Python; use HarnessProfileConfig when loading or saving YAML/JSON files. Deep Agents ships built-in harness profiles for OpenAI and Anthropic (Claude) models.
Provider profiles are narrower API for model-construction kwargs
Provider profiles are a narrower companion API for model-construction kwargs, which don't affect the harness. Most callers don't need them. Reach for one when you want init_chat_model defaults, credential checks, or runtime-derived kwargs as defaults with your provider choice, for example when packaging a provider integration. Provider profiles are Python-only; TypeScript SDK supports harness profiles only.
HarnessProfile fields for prompt and tool customization
HarnessProfile fields include: base_system_prompt (string) to replace the base Deep Agents system prompt; system_prompt_suffix (string) to append text after the caller's suffix, placed last in the assembled system prompt; tool_description_overrides (Mapping[str, str]) to override individual tool descriptions keyed by tool name; excluded_tools (frozenset[str]) to remove specific tools from the tool set, matched by tool name; excluded_middleware (frozenset[type[AgentMiddleware] | str]) to strip specific middleware classes from the Deep Agents stack, accepting middleware classes or string names; extra_middleware (Sequence[AgentMiddleware] | Callable[[], Sequence[AgentMiddleware]]) to append middleware to every stack this profile applies to; general_purpose_subagent (GeneralPurposeSubagentProfile) to disable, rename, or re-prompt the general-purpose subagent.
HarnessProfileOptions fields in TypeScript
HarnessProfileOptions fields in TypeScript include: baseSystemPrompt (string) to replace the base Deep Agents system prompt; systemPromptSuffix (string) to append text after the caller's suffix; toolDescriptionOverrides (Record<string, string>) to override individual tool descriptions; excludedTools (string[]) to remove specific tools from the tool set matched by tool name; excludedMiddleware (string[]) to strip specific middleware from the assembled stack, matched against each middleware's .name property, but cannot include required scaffolding names (FilesystemMiddleware, SubAgentMiddleware); extraMiddleware (AgentMiddleware[] | (() => AgentMiddleware[])) for additional middleware appended after user middleware, as a static array or factory function; generalPurposeSubagent (GeneralPurposeSubagentConfig) to disable, rename, or re-prompt the general-purpose subagent with properties enabled, description, and systemPrompt.
Required scaffolding middleware cannot be excluded
Listing FilesystemMiddleware, SubAgentMiddleware, or the internal permission middleware in excluded_middleware raises a ValueError—they're required scaffolding in the Deep Agents stack. To hide their tools from the model without removing the middleware, use excluded_tools instead.
Excluded middleware field accepts two forms in Python
Entries in excluded_middleware accept two forms: a middleware class (matched by exact type), or a plain string that matches AgentMiddleware.name, for use with built-ins and public aliases such as 'SummarizationMiddleware'. Additionally, an module:Class import ref (for example, 'my_pkg.middleware:TelemetryMiddleware') targets an exact middleware class from a config file. Import refs resolve lazily, so use them only for trusted local configuration—loading one imports Python code.
Profile lookup order for preconfigured model instances
When you pass a preconfigured chat model instance instead of a provider:model string, the harness synthesizes the canonical provider:identifier key from the instance and looks it up in this order: 1. Exact provider:identifier match; 2. Identifier-only (only when the identifier already contains :); 3. Provider-only fallback.
Profile registration keys and levels
Both profile types use the same key format: Provider-level—a bare provider name like 'openai' applies to every model from that provider. Model-level—a fully qualified provider:model key like 'openai:gpt-5.5' applies only to that specific model. When both a provider-level and a model-level profile exist, they are merged at resolution time. Unset model-level fields inherit from the provider-level profile; explicit model-level values override them.
Re-registering profile under existing key merges on top of prior one
Re-registering under an existing key merges the new profile on top of the prior one—it does not replace it. Merge semantics apply per-field according to the documented rules.
No wildcard profile key matches every provider
There is no wildcard key that matches every provider. To apply the same overrides everywhere—say, dropping SummarizationMiddleware regardless of which model is selected—register the profile under each provider key you use. Profiles are intended for adjustments that depend on the model being selected. Global adjustments that should apply regardless of model should be made on the create_deep_agent call site.
Harness profile field merge semantics
Merge behavior for HarnessProfile fields: base_system_prompt and system_prompt_suffix—new value wins when set; otherwise inherits. tool_description_overrides—mappings merge per key; new value wins on a shared key. excluded_tools and excluded_middleware—set union. extra_middleware—merged by name: new instance replaces existing at its position, novel entries append. general_purpose_subagent—merged field-wise, unset fields inherit.