diff --git a/apps/cockpit/src/components/cockpit-shell.spec.tsx b/apps/cockpit/src/components/cockpit-shell.spec.tsx
index 4db88d3f0..6beac1d78 100644
--- a/apps/cockpit/src/components/cockpit-shell.spec.tsx
+++ b/apps/cockpit/src/components/cockpit-shell.spec.tsx
@@ -13,7 +13,9 @@ import {
ThemeProvider,
} from '@threadplane/ui-react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import { NO_COCKPIT_DOCS_LINK } from '@threadplane/cockpit-registry';
import { getCockpitPageModel } from '../lib/cockpit-page';
+import type { CockpitPageModel } from '../lib/cockpit-page';
import type { UseRuntimeControllerOptions } from '../lib/runtime/use-runtime-controller';
const operationalMocks = vi.hoisted(() => ({
@@ -121,13 +123,16 @@ const renderShell = (runtimeUrl: string | null = null) =>
);
-const renderShellFor = (slug: string[]) => {
+const renderShellFor = (
+ slug: string[],
+ presentationOverrides: Partial = {}
+) => {
const pageModel = getCockpitPageModel(slug);
return render(
@@ -745,9 +750,7 @@ describe('CockpitShell documentation link', () => {
expect(link.getAttribute('rel')).toBe('noopener noreferrer');
});
- it('renders no link for a capability with no published docs page', () => {
- // deep-agents carries the NO_COCKPIT_DOCS_LINK sentinel: the website has no
- // deep-agents library yet, so there is nothing to link to.
+ it('links a deep-agents capability at the deep-agents docs library', () => {
renderShellFor([
'deep-agents',
'core-capabilities',
@@ -756,6 +759,21 @@ describe('CockpitShell documentation link', () => {
'python',
]);
+ const link = screen.getByRole('link', { name: /read docs/i });
+ expect(link.getAttribute('href')).toBe(
+ 'https://threadplane.ai/docs/deep-agents/capabilities/planning'
+ );
+ });
+
+ it('renders no link for a capability with no published docs page', () => {
+ // Every mapped capability now points at a published page, so the sentinel
+ // branch is exercised through a presentation carrying it rather than
+ // through a table entry that happens to be blank today.
+ renderShellFor(
+ ['deep-agents', 'core-capabilities', 'planning', 'overview', 'python'],
+ { docsPath: NO_COCKPIT_DOCS_LINK }
+ );
+
expect(screen.queryByRole('link', { name: /read docs/i })).toBeNull();
});
});
diff --git a/apps/cockpit/src/lib/route-resolution.spec.ts b/apps/cockpit/src/lib/route-resolution.spec.ts
index a72962253..cb50db8ba 100644
--- a/apps/cockpit/src/lib/route-resolution.spec.ts
+++ b/apps/cockpit/src/lib/route-resolution.spec.ts
@@ -176,7 +176,7 @@ describe('getCapabilityPresentation', () => {
expect(getCapabilityPresentation(docsEntry)).toMatchObject({
kind: 'docs-only',
- docsPath: '',
+ docsPath: '/docs/deep-agents/getting-started/introduction',
});
expect(getCapabilityPresentation(capabilityEntry)).toMatchObject({
kind: 'capability',
diff --git a/apps/website/content/docs/deep-agents/capabilities/filesystem.mdx b/apps/website/content/docs/deep-agents/capabilities/filesystem.mdx
new file mode 100644
index 000000000..13270f994
--- /dev/null
+++ b/apps/website/content/docs/deep-agents/capabilities/filesystem.mdx
@@ -0,0 +1,111 @@
+---
+title: Filesystem
+description: StateBackend keeps a Deep Agents workspace on the graph state, and a FilesystemPermission in interrupt mode routes writes through the chat interrupt panel.
+---
+
+# Filesystem
+
+`create_deep_agent` always installs `FilesystemMiddleware`, so a Deep Agents agent always has `ls`, `read_file`, `write_file`, and `edit_file`. What decides whether a user interface can render that workspace is not the middleware. It is the backend.
+
+```python
+from deepagents import create_deep_agent
+from deepagents.backends import StateBackend
+from deepagents.middleware import FilesystemPermission
+
+graph = create_deep_agent(
+ model=ChatOpenAI(model="gpt-4.1", temperature=0),
+ tools=[lookup_field_elevation, lookup_runway_length],
+ system_prompt=(PROMPTS_DIR / "filesystem.md").read_text(),
+ backend=StateBackend(),
+ permissions=[
+ FilesystemPermission(operations=["write"], paths=["/reports/**"], mode="interrupt"),
+ ],
+)
+```
+
+`StateBackend` stores the agent's files on the graph state under `files`, which means every write arrives at the client as a `values` update. A backend that writes anywhere else — a host directory, a remote object store — puts nothing on the state, and a panel bound to `files` stays empty no matter how busy the agent is. The choice of backend is the choice of whether the workspace is renderable at all.
+
+## What the demo shows
+
+The demo is the same dispatch desk, given a task that produces artifacts: gather field data for two airports, keep working notes, then file a report.
+
+The workspace panel renders a directory tree grouped by path. Scratch files under `/notes/` appear the moment the agent writes them, with no ceremony. A write under `/reports/`, however, stops the run.
+
+That is the `FilesystemPermission` above. In `interrupt` mode a matching call pauses for human approval instead of executing, and the pause surfaces through the standard chat interrupt panel — the same component every other LangGraph interrupt uses. There is no Deep Agents specific interrupt UI, because there is no Deep Agents specific interrupt.
+
+Approving the write lets the run continue and the file lands in the tree. Rejecting it returns the model to work without the file.
+
+
+Give the pattern a literal prefix, as `/reports/**` does. Bulk tools such as `ls`, `glob`, and `grep` decide whether to fire the permission based on whether their search subtree could overlap the anchored prefix. A fully unanchored pattern collapses to the root and fires on every listing, which turns an approval gate into an interruption on each directory read.
+
+
+## How it reaches the UI
+
+### The tree
+
+`files` is a flat map from absolute path to contents. Splitting each key on its last slash is enough to group it into directories.
+
+```ts
+protected readonly files = computed(() => {
+ const raw = (this.agent.value() as Record | undefined)?.['files'];
+ const entries = new Map();
+ if (raw && typeof raw === 'object') {
+ for (const [path, contents] of Object.entries(raw as Record)) {
+ entries.set(path, typeof contents === 'string' ? contents : JSON.stringify(contents));
+ }
+ }
+ return [...entries.entries()].map(([path, contents]) => {
+ const slash = path.lastIndexOf('/');
+ return {
+ path,
+ directory: slash > 0 ? path.slice(0, slash) : '/',
+ name: path.slice(slash + 1),
+ contents,
+ };
+ });
+});
+```
+
+### The pending write
+
+While an approval is open, the file does not exist yet — it is an argument on a paused tool call. Reading it off the interrupt lets the tree show the file as a ghost row, so the reviewer sees where it is about to land before deciding.
+
+The interrupt payload is `{ action_requests: [{ name, args }] }`, and for `write_file` the target path is `args.file_path`:
+
+```ts
+protected readonly pendingPath = computed(() => {
+ for (const interrupt of this.agent.langGraphInterrupts() ?? []) {
+ const value = (interrupt as { value?: unknown }).value as
+ | { action_requests?: Array<{ args?: Record }> }
+ | undefined;
+ for (const request of value?.action_requests ?? []) {
+ const path = request.args?.['file_path'];
+ if (typeof path === 'string') return path;
+ }
+ }
+ return null;
+});
+```
+
+### The resume payload
+
+`deepagents` expects a structured decision, not a bare string and not a bare list:
+
+```ts
+protected onInterruptAction(action: InterruptAction): void {
+ if (action === 'accept') {
+ void this.agent.submit({ resume: { decisions: [{ type: 'approve' }] } });
+ } else if (action === 'ignore') {
+ void this.agent.submit({ resume: { decisions: [{ type: 'reject' }] } });
+ }
+}
+```
+
+Passing a bare list raises a `TypeError` on the server rather than a validation error the browser can show, so the failure appears as a dead run rather than as a rejected submission. The shape is worth getting right the first time.
+
+## Next steps
+
+- [Planning](/docs/deep-agents/capabilities/planning) — the todo list the agent keeps while it files.
+- [Skills](/docs/deep-agents/capabilities/skills) — the same backend machinery, mounted read-only.
+- [Interrupts](/docs/langgraph/guides/interrupts) — the interrupt lifecycle underneath the approval.
+- [Chat interrupt panel](/docs/chat/components/chat-interrupt-panel) — the component that renders the approval.
diff --git a/apps/website/content/docs/deep-agents/capabilities/memory.mdx b/apps/website/content/docs/deep-agents/capabilities/memory.mdx
new file mode 100644
index 000000000..ffdc3bb10
--- /dev/null
+++ b/apps/website/content/docs/deep-agents/capabilities/memory.mdx
@@ -0,0 +1,124 @@
+---
+title: Memory
+description: MemoryMiddleware plus StoreBackend give an agent a cross-thread memory file, and memory_contents is private state that a panel has to reach deliberately.
+---
+
+# Memory
+
+Memory in Deep Agents is a file the agent maintains about itself. `memory=["/memories/AGENTS.md"]` installs `MemoryMiddleware`, which loads that file into the system prompt at the start of every turn and instructs the model to keep it current with `edit_file`. Nothing in the application parses the conversation for facts. The agent decides what is worth remembering.
+
+The backend decides how long the memory lasts.
+
+```python
+from deepagents import create_deep_agent
+from deepagents.backends import StoreBackend
+
+MEMORY_NAMESPACE = ("cockpit", "deep-agents-memory")
+
+graph = create_deep_agent(
+ model=ChatOpenAI(model="gpt-4.1", temperature=0),
+ system_prompt=(PROMPTS_DIR / "memory.md").read_text(),
+ backend=StoreBackend(namespace=lambda _runtime: MEMORY_NAMESPACE),
+ memory=["/memories/AGENTS.md"],
+)
+```
+
+`StoreBackend` writes into LangGraph's `BaseStore`, which is shared across threads. `StateBackend` would put the same file on the thread's own state, where a new conversation would never see it. Leaving `store` unset means "resolve the store from the graph execution context", which LangGraph Server supplies.
+
+
+The demo uses a fixed namespace tuple, so every visitor shares one memory. That is deliberate for a demo and wrong everywhere else. A real deployment derives the namespace from the caller's identity.
+
+
+## What the demo shows
+
+The demo is the dispatch desk with a memory panel beside it. Tell it your home base is Denver and that you fly a mid-size business jet, and the panel fills in as the agent writes to `/memories/AGENTS.md`. Start a genuinely new thread and the agent already knows both facts, because the file came from the store rather than from the transcript.
+
+The panel also labels which of two sources it is reading, which turns out to be the interesting part.
+
+What the agent records is a matter of prompt, not code:
+
+```markdown
+`/memories/AGENTS.md` is yours. It is loaded into your context at the start of
+every conversation, including conversations you have not had yet.
+
+Write to it with `edit_file` whenever the user tells you something durable:
+a home base, a fleet type, a standing preference, a correction.
+
+Do not record one-off requests, small talk, or anything stale next week.
+Never record credentials of any kind.
+```
+
+The last line is not decoration. A memory file is a persistent, model-writable document, so what must never go into it belongs in the prompt explicitly.
+
+## How it reaches the UI
+
+Here the framework constrains the answer, and the constraint is worth stating rather than working around quietly.
+
+`MemoryMiddleware` annotates `memory_contents` with `PrivateStateAttr`. That keeps the key out of the `values` stream — correct for a transcript, since the memory file is context for the model rather than conversation — and it is exactly why a panel bound to `agent.value()` shows nothing while the agent is working. The key **is** written to the checkpoint, so it does arrive, but only once the run settles and the client hydrates the latest state.
+
+For a live panel, the graph has to announce the key on a channel the client does receive. A small middleware does that:
+
+```python
+class MemoryVisibilityMiddleware(AgentMiddleware):
+ def _emit(self, state):
+ contents = state.get("memory_contents")
+ if contents is None:
+ return
+ try:
+ writer = get_stream_writer()
+ except (RuntimeError, KeyError):
+ # No streaming context. The value is still on the checkpoint,
+ # which is what the client's settle-time hydration reads.
+ return
+ writer({"name": MEMORY_EVENT, "data": {"memory_contents": contents}})
+
+ def after_model(self, state, runtime):
+ self._emit(state)
+ return None
+```
+
+This is an application-side shim, not a framework change. The key stays private on the state; it is simply announced alongside it.
+
+### Two sources, and knowing which one you are on
+
+A custom event is a live signal and is not replayed when a thread is reopened. The checkpoint is durable but arrives only at settle. A panel that wants both reads both, and it is worth telling them apart rather than blending them:
+
+```ts
+private readonly liveMemory = computed | null>(() => {
+ for (const event of [...this.agent.customEvents()].reverse()) {
+ if (event.name !== MEMORY_EVENT) continue;
+ const contents = (event.data as { memory_contents?: unknown } | undefined)?.[
+ 'memory_contents'
+ ];
+ if (contents && typeof contents === 'object') return contents as Record;
+ }
+ return null;
+});
+
+private readonly settledMemory = computed | null>(() => {
+ const contents = (this.agent.value() as Record | undefined)?.[
+ 'memory_contents'
+ ];
+ return contents && typeof contents === 'object'
+ ? (contents as Record)
+ : null;
+});
+
+protected readonly memorySource = computed<'live' | 'checkpoint' | 'none'>(() => {
+ const live = this.liveMemory();
+ if (live && Object.keys(live).length > 0) return 'live';
+ return this.settledMemory() ? 'checkpoint' : 'none';
+});
+```
+
+Without the middleware the panel still fills in, just a beat later and only at settle. With it, the panel updates while the agent is still writing. `checkpoint` is also what a reopened thread looks like, so the label is genuinely informative rather than a debug artifact.
+
+
+The only assertion that proves cross-thread memory is a genuinely new thread that already knows. Clearing the panel and watching it refill proves the component works, not the store.
+
+
+## Next steps
+
+- [Skills](/docs/deep-agents/capabilities/skills) — the same private-state visibility problem, for `skills_metadata`.
+- [Filesystem](/docs/deep-agents/capabilities/filesystem) — the state-backed workspace that does stream on its own.
+- [Memory](/docs/langgraph/guides/memory) — the LangGraph store this capability is built on.
diff --git a/apps/website/content/docs/deep-agents/capabilities/planning.mdx b/apps/website/content/docs/deep-agents/capabilities/planning.mdx
new file mode 100644
index 000000000..fe98b71b8
--- /dev/null
+++ b/apps/website/content/docs/deep-agents/capabilities/planning.mdx
@@ -0,0 +1,104 @@
+---
+title: Planning
+description: TodoListMiddleware puts a todos array on the graph state, and an Angular panel renders the plan as the agent revises it mid-run.
+---
+
+# Planning
+
+`TodoListMiddleware` gives the model one tool, `write_todos`, and declares one key on the graph state, `todos`. That is the entire capability. Everything a plan panel needs comes from those two facts.
+
+```python
+from deepagents import create_deep_agent
+from langchain.agents.middleware import TodoListMiddleware
+from langchain_openai import ChatOpenAI
+
+graph = create_deep_agent(
+ model=ChatOpenAI(model="gpt-4.1", temperature=0),
+ tools=[lookup_field_elevation, lookup_runway_length, lookup_weather],
+ system_prompt=(PROMPTS_DIR / "planning.md").read_text(),
+ middleware=[TodoListMiddleware()],
+)
+```
+
+`create_deep_agent` installs a default middleware set that already includes the todo list. The demo passes it explicitly anyway, so the source states which component owns `todos` rather than leaving a reader to infer it.
+
+## What the demo shows
+
+The demo is an aviation dispatch desk. Ask it whether a mid-size business jet can operate out of Aspen and San Francisco on the same day, and the run has a visible shape:
+
+1. The agent writes a plan before it does any work. The panel fills with pending rows.
+2. One row flips to in progress, the corresponding lookup tool runs, and the row completes.
+3. When a lookup returns something the plan did not account for — a mountain wave advisory, a runway shorter than the aircraft needs — the agent rewrites the list mid-run. Rows are added, and the panel changes shape while the run is still going.
+
+Step 3 is the reason the panel is worth building. A plan that only ever appends is a progress bar. A plan the agent revises is a window into what the agent is actually reasoning about.
+
+Getting there needs a prompt, not more middleware. `TodoListMiddleware` supplies a tool, not a policy, and a model left to itself will fan out six parallel lookups and never write a todo. The demo's system prompt is explicit:
+
+```markdown
+Your first action on any request is a call to `write_todos`. Do not call a
+lookup tool before the todo list exists. Write one todo per step.
+
+Mark exactly one todo `in_progress` before you start it and mark it `completed`
+the moment it is done. Call `write_todos` again for each transition.
+```
+
+## How it reaches the UI
+
+`todos` is a public key on the graph state, so LangGraph streams it in the `values` channel and `@threadplane/langgraph` projects the latest snapshot into `agent.value()`. The panel is a `computed()` over that snapshot and nothing more.
+
+```ts
+import { Component, computed } from '@angular/core';
+import { injectAgent } from '@threadplane/langgraph';
+
+interface Todo {
+ content: string;
+ status: 'pending' | 'in_progress' | 'completed';
+}
+
+const TODO_STATUSES: Todo['status'][] = ['pending', 'in_progress', 'completed'];
+
+export class PlanningComponent {
+ protected readonly agent = injectAgent();
+
+ protected readonly todos = computed(() => {
+ const todos = (this.agent.value() as Record | undefined)?.['todos'];
+ if (!Array.isArray(todos)) return [];
+ return todos.map((todo) => {
+ const entry = todo as Record;
+ const status = entry['status'] as Todo['status'];
+ return {
+ content: String(entry['content'] ?? ''),
+ status: TODO_STATUSES.includes(status) ? status : 'pending',
+ };
+ });
+ });
+}
+```
+
+Two details in that projection are deliberate.
+
+**The status is normalized.** A graph state key is not a typed contract. Narrowing an unknown status to `pending` keeps an unexpected value from reaching a template that switches on it.
+
+**Every call to `write_todos` replaces the whole list.** There is no partial update and no merge, so the panel never has to reconcile anything. It renders the array it was given.
+
+The template tracks by index, because a todo carries no identifier:
+
+```html
+@for (todo of todos(); track $index) {
+
+ {{ todo.content }}
+
+}
+```
+
+Tracking by content would be worse, not better: content is exactly what changes when the agent rewrites a step.
+
+
+In `deepagents` 0.7.11 a todo is exactly `{ content, status }`. There is no identifier, no timestamp, and no separate present-tense label. A panel that depends on any of those will not survive contact with the framework.
+
+
+## Next steps
+
+- [Subagents](/docs/deep-agents/capabilities/subagents) — the same orchestrator delegating each planned step to a child agent.
+- [Filesystem](/docs/deep-agents/capabilities/filesystem) — the workspace the agent writes into while it works through the plan.
+- [Streaming](/docs/langgraph/guides/streaming) — how the `values` channel reaches `agent.value()`.
diff --git a/apps/website/content/docs/deep-agents/capabilities/skills.mdx b/apps/website/content/docs/deep-agents/capabilities/skills.mdx
new file mode 100644
index 000000000..8fbfcddb1
--- /dev/null
+++ b/apps/website/content/docs/deep-agents/capabilities/skills.mdx
@@ -0,0 +1,111 @@
+---
+title: Skills
+description: SkillsMiddleware loads only SKILL.md frontmatter into the prompt and leaves the body on disk. skills_metadata is private state a panel must reach for.
+---
+
+# Skills
+
+A skill is a folder with a `SKILL.md` whose YAML frontmatter carries a `name` and a `description`, following the [agentskills.io](https://agentskills.io) specification. `SkillsMiddleware` loads **only that frontmatter** into the system prompt — a short index the model can scan — and leaves the body on the filesystem until a request actually matches.
+
+That two-stage load is what progressive disclosure means. The index costs a few tokens per skill. The procedure costs nothing until it is needed.
+
+```markdown
+---
+name: runway-analysis
+description: Decide whether a runway is long enough for a given aircraft at a given field elevation. Use when the user asks about runway suitability, takeoff or landing distance, or operating out of a high-elevation field.
+license: MIT
+---
+
+# Runway Analysis
+
+## Procedure
+
+1. Get the field elevation and the longest runway length.
+2. Read `/skills/runway-analysis/reference/margins.md` for the margin table.
+ Do not work from memory — the table is the authority.
+3. Compare, then state the verdict and the two numbers you compared.
+```
+
+The `description` is doing the routing, so it is written as a matching rule rather than as a summary. Step 2 is the second stage of the disclosure: the reference file costs nothing until the `SKILL.md` sends the agent to it.
+
+## What the demo shows
+
+The dispatch desk carries two skills, runway analysis and a weather brief. The panel lists both from the moment the run starts, because both frontmatter blocks are in the prompt. Ask a runway question and exactly one skill opens: the panel marks `runway-analysis` as read, its reference file is opened a step later, and the weather skill stays closed on disk.
+
+That closed skill is the demonstration. If every skill's files are read on every request, the index is not routing anything and the descriptions need work.
+
+The prompt has to say that the index is an index:
+
+```markdown
+Your procedures are not in this prompt — they are skills under `/skills/`.
+When a request matches a skill, read its `SKILL.md` before you start, and
+follow the procedure it gives you. If the `SKILL.md` points at another file,
+read that too — the numbers in a reference file are the authority, and your
+recollection is not.
+```
+
+## Mounting the skills
+
+`SkillsMiddleware` reads through a backend, and which backend is a deployment decision. The demo seeds a process-local store from the repository and mounts it read-only, which keeps the skill content in version control without giving the agent the host.
+
+```python
+graph = create_deep_agent(
+ model=ChatOpenAI(model="gpt-4.1", temperature=0),
+ tools=[lookup_field_elevation, lookup_runway_length, lookup_weather],
+ system_prompt=(PROMPTS_DIR / "skills.md").read_text(),
+ backend=CompositeBackend(
+ default=StateBackend(),
+ routes={"/skills/": StoreBackend(namespace=..., store=SKILLS_STORE)},
+ ),
+ skills=["/skills/"],
+)
+```
+
+`CompositeBackend` routes by path prefix, longest first. Anything outside `/skills/` falls through to `StateBackend`, so notes the agent writes stay on the thread and never touch the skill mount.
+
+
+Seed the store at `/runway-analysis/SKILL.md`, not `/skills/runway-analysis/SKILL.md`. The composite removes the matched prefix before delegating and re-adds it to the result, so a store seeded with the prefix surfaces to the agent as `/skills/skills/runway-analysis/...` and the skill scan finds nothing.
+
+
+## How it reaches the UI
+
+The panel has two halves, and they arrive by different routes.
+
+**The index is private state.** `skills_metadata` is annotated `PrivateStateAttr`, exactly as `memory_contents` is, so it is absent from the `values` stream and reaches `agent.value()` only once the run settles. A live index needs the same custom-event shim the [memory capability](/docs/deep-agents/capabilities/memory) documents: a small middleware republishes the key on the custom stream, and the client reads the custom event first and falls back to the settled state for a reopened thread.
+
+```ts
+private readonly liveSkills = computed(() => {
+ for (const event of [...this.agent.customEvents()].reverse()) {
+ if (event.name !== SKILLS_EVENT) continue;
+ const metadata = (event.data as { skills_metadata?: unknown } | undefined)?.[
+ 'skills_metadata'
+ ];
+ if (Array.isArray(metadata)) return metadata as SkillMetadata[];
+ }
+ return null;
+});
+```
+
+As with memory, this is an application-side shim rather than a framework feature, and it is worth naming as such.
+
+**What the agent opened needs no shim at all.** A skill body is read with `read_file`, which is an ordinary tool call:
+
+```ts
+private readonly openedPaths = computed(() => {
+ const paths: string[] = [];
+ for (const call of this.agent.toolCalls()) {
+ if (call.name !== 'read_file') continue;
+ const path = (call.args as Record | undefined)?.['file_path'];
+ if (typeof path === 'string' && !paths.includes(path)) paths.push(path);
+ }
+ return paths;
+});
+```
+
+Matching those paths against each skill's directory is what makes the panel show the thing worth showing: one skill opened, the rest still on disk.
+
+## Next steps
+
+- [Memory](/docs/deep-agents/capabilities/memory) — the same private-state visibility problem, in full.
+- [Filesystem](/docs/deep-agents/capabilities/filesystem) — the backends the skill mount is assembled from.
+- [Chat tool calls](/docs/chat/components/chat-tool-calls) — how the `read_file` calls render in the conversation.
diff --git a/apps/website/content/docs/deep-agents/capabilities/subagents.mdx b/apps/website/content/docs/deep-agents/capabilities/subagents.mdx
new file mode 100644
index 000000000..54ec29086
--- /dev/null
+++ b/apps/website/content/docs/deep-agents/capabilities/subagents.mdx
@@ -0,0 +1,86 @@
+---
+title: Subagents
+description: SubAgentMiddleware dispatches child graphs through a task tool, which the Threadplane subagent tracker recognizes by default with no client configuration.
+---
+
+# Subagents
+
+`SubAgentMiddleware` gives an orchestrator a single tool, `task`, taking `{ description, subagent_type }`. Each dispatch runs a real child graph in its own `tools:` namespace, and the child is seeded with the orchestrator's `description` before it emits its first token.
+
+```python
+from deepagents import SubAgent, create_deep_agent
+
+FIELD_RESEARCHER: SubAgent = {
+ "name": "field-researcher",
+ "description": "Gathers field elevation and runway length for one airport.",
+ "system_prompt": "You research airport field data for a dispatch desk. ...",
+ "tools": [lookup_field_elevation, lookup_runway_length],
+}
+
+graph = create_deep_agent(
+ model=ChatOpenAI(model="gpt-4.1", temperature=0),
+ system_prompt=(PROMPTS_DIR / "subagents.md").read_text(),
+ subagents=[FIELD_RESEARCHER, WEATHER_ANALYST],
+)
+```
+
+Passing `subagents` is what installs the middleware and, with it, the `task` tool. The demo's orchestrator is given no lookup tools of its own, so it has no way to answer a question without delegating.
+
+## What the demo shows
+
+Ask the dispatch desk about two airports at once and the run fans out. Two `task` calls go out in a single turn, two child agents work in parallel, and two subagent cards stream side by side in the conversation — each with its own transcript, its own tool calls, and no cross-wiring between them.
+
+The fan-out needs a prompt. A model left to itself will serialize dispatches, so one line in the orchestrator's system prompt changes the shape of the run:
+
+```markdown
+When a request covers more than one airport or more than one kind of data,
+issue every dispatch you need in a single turn so the specialists work in
+parallel. Do not wait for one to report before sending the next.
+```
+
+## How it reaches the UI
+
+This is the capability that needs the least work on the client, because it needs none.
+
+`task` is an ordinary tool call on the wire. What makes it render as a child agent is that the Threadplane subagent tracker recognizes the name — and `['task']` is the tracker's **default** `subagentToolNames`. A Deep Agents graph therefore lights the subagent cards with no configuration at all. Naming it explicitly is still worth doing as documentation, and it is required only when a dispatch tool is called something else:
+
+```ts
+provideAgent({
+ apiUrl: environment.langGraphApiUrl,
+ assistantId: environment.assistantId,
+ // The default. Set it when your dispatch tool is named something else.
+ subagentToolNames: ['task'],
+});
+```
+
+### Why parallel dispatches attribute cleanly
+
+The tracker registers a dispatch from the `task` tool call itself — including the `subagent_type` argument, which becomes the card's name — and then matches the child's `tools:` namespace exactly. Because the dispatch is registered before the child emits anything, attribution is structural rather than a guess from message ordering or text similarity. Two children running at the same time land in two cards because their namespaces differ, not because their output happens to look different.
+
+Each dispatch also carries the orchestrator's `description` verbatim, and the match on it is exact. Two dispatches with identical descriptions are still distinguished by namespace, but a description that names its subject makes the card readable as well as correct.
+
+### What to put in the sidebar
+
+The `` composition already renders each dispatch as a `` inline and keeps it, collapsed, after completion. A separate tray of active subagents would duplicate that. The demo spends the sidebar on the one thing the cards do not show, which is how wide the fan-out went:
+
+```ts
+private readonly dispatches = computed(() => [...this.agent.subagents().values()]);
+
+protected readonly dispatchCount = computed(() => this.dispatches().length);
+
+protected readonly runningCount = computed(
+ () => this.dispatches().filter((subagent) => subagent.status() === 'running').length,
+);
+```
+
+Note that `status` is itself a signal on the `Subagent` record, so it is called rather than read.
+
+
+A dispatch `description` is both the child's opening instruction and the label a reader sees on the card. A vague description costs twice: the child starts with less to go on, and the card says less about what is happening.
+
+
+## Next steps
+
+- [Planning](/docs/deep-agents/capabilities/planning) — the orchestrator's own todo list, which pairs naturally with delegation.
+- [Chat subagent card](/docs/chat/components/chat-subagent-card) — the card component on its own.
+- [Subgraphs](/docs/langgraph/guides/subgraphs) — how namespaced child execution is attributed underneath the tracker.
diff --git a/apps/website/content/docs/deep-agents/getting-started/introduction.mdx b/apps/website/content/docs/deep-agents/getting-started/introduction.mdx
new file mode 100644
index 000000000..5ec0811f8
--- /dev/null
+++ b/apps/website/content/docs/deep-agents/getting-started/introduction.mdx
@@ -0,0 +1,87 @@
+---
+title: Introduction
+description: Render the Deep Agents middleware capabilities — planning, filesystem, subagents, memory, and skills — in an Angular UI over LangGraph.
+---
+
+# Introduction
+
+[Deep Agents](https://github.com/langchain-ai/deepagents) is LangChain's agent-harness library. It is not a protocol and not a runtime: it is a set of LangGraph middleware that assembles a long-horizon agent out of five capabilities, each of which puts something on the graph that a user interface can render.
+
+| Capability | Middleware | What it puts on the graph |
+|---|---|---|
+| [Planning](/docs/deep-agents/capabilities/planning) | `TodoListMiddleware` | A `todos` array the agent rewrites as it works. |
+| [Filesystem](/docs/deep-agents/capabilities/filesystem) | `FilesystemMiddleware` | A `files` map, plus write approvals when a permission is set to interrupt. |
+| [Subagents](/docs/deep-agents/capabilities/subagents) | `SubAgentMiddleware` | A `task` tool that dispatches real child graphs. |
+| [Memory](/docs/deep-agents/capabilities/memory) | `MemoryMiddleware` | A memory file the agent maintains, held across threads. |
+| [Skills](/docs/deep-agents/capabilities/skills) | `SkillsMiddleware` | A skill index loaded from `SKILL.md` frontmatter. |
+
+Every page in this section is written against the real `deepagents` package, version 0.7.11, running as a LangGraph graph. Nothing here is a reimplementation of the framework, and nothing here is a mock of it.
+
+
+These pages document what a Deep Agents graph exposes and how an Angular application reads it. They are not a substitute for the upstream Deep Agents documentation, and Threadplane does not maintain the framework.
+
+
+## The setup story, once
+
+A Deep Agents agent is a LangGraph graph. `create_deep_agent` returns a compiled graph, LangGraph Server serves it like any other, and the browser talks to it over the same protocol. There is no Deep Agents adapter, because none is needed: `@threadplane/langgraph` is the whole client wiring.
+
+That means the setup is identical for all five capabilities, and it is the setup already documented under the LangGraph adapter. Work through the [LangGraph quickstart](/docs/langgraph/getting-started/quickstart) once, and every page below assumes it.
+
+```ts
+// app.config.ts
+import { ApplicationConfig } from '@angular/core';
+import { provideAgent } from '@threadplane/langgraph';
+import { provideChat } from '@threadplane/chat';
+
+export const appConfig: ApplicationConfig = {
+ providers: [
+ provideAgent({
+ apiUrl: environment.langGraphApiUrl,
+ assistantId: environment.assistantId,
+ }),
+ provideChat({}),
+ ],
+};
+```
+
+```ts
+import { Component, computed } from '@angular/core';
+import { ChatComponent } from '@threadplane/chat';
+import { injectAgent } from '@threadplane/langgraph';
+
+@Component({
+ selector: 'app-root',
+ imports: [ChatComponent],
+ template: ``,
+})
+export class App {
+ protected readonly agent = injectAgent();
+}
+```
+
+What changes from capability to capability is not the provider and not the component. It is which part of the agent handle each panel reads: `value()` for state keys, `subagents()` for dispatches, `customEvents()` for keys the framework keeps private, `toolCalls()` for what the agent actually opened.
+
+## Three ways a capability reaches the browser
+
+Deep Agents does not publish all five capabilities the same way, and the difference decides how much work a panel is.
+
+**Public state keys stream on their own.** `todos` and `files` are ordinary keys on the graph state. LangGraph streams them in the `values` channel, `@threadplane/langgraph` projects the latest snapshot into `agent.value()`, and a panel is a `computed()` over that. No configuration, no custom events, no server-side shim.
+
+**Tool calls are already structured.** The subagent `task` tool and the filesystem `read_file` tool arrive as normal tool calls. The subagent tracker recognizes `task` by default, so child agents render as cards with no client configuration at all.
+
+**Private state keys do not stream.** `memory_contents` and `skills_metadata` are annotated `PrivateStateAttr` by their middleware. That annotation keeps them out of the `values` stream by design — they are context for the model, not transcript — and it means a panel bound to `agent.value()` shows nothing while the agent is working. Those keys are written to the checkpoint, so they arrive at settle, but a live panel needs the graph to announce them on a channel the client does receive. The memory and skills pages show the small middleware that does it, and say plainly that it is an application-side shim rather than a framework feature.
+
+That last row is a real constraint of the framework as it stands, not an oversight in the demos. It is stated on both pages it affects.
+
+## The demos
+
+Each capability page describes a standalone example that runs the real framework against a real model. The examples live under [`cockpit/deep-agents`](https://github.com/cacheplane/angular-agent-framework/tree/main/cockpit/deep-agents) and are hosted on the [Threadplane cockpit](https://cockpit.threadplane.ai), which shows the Angular source, the Python graph, and the system prompt beside the running demo.
+
+All five share one scenario — an aviation dispatch desk with a handful of airport lookup tools — so the difference between two pages is the capability under test and nothing else.
+
+## Further reading
+
+- [LangGraph adapter introduction](/docs/langgraph/getting-started/introduction) — the adapter every Deep Agents graph binds through.
+- [Chat subagent card](/docs/chat/components/chat-subagent-card) — the component the `task` dispatches render into.
+- [Chat interrupt panel](/docs/chat/components/chat-interrupt-panel) — the component a filesystem write approval renders into.
+- [Persistence](/docs/langgraph/guides/persistence) — checkpoints and the store, which the memory capability depends on.
diff --git a/apps/website/src/app/docs/page.tsx b/apps/website/src/app/docs/page.tsx
index bad5598a7..9ae030669 100644
--- a/apps/website/src/app/docs/page.tsx
+++ b/apps/website/src/app/docs/page.tsx
@@ -226,6 +226,10 @@ export default function DocsLandingPage() {
Agent runtimes →
+ {' '}Building on the Deep Agents framework?{' '}
+
+ Deep Agents →
+
diff --git a/apps/website/src/components/docs/LibraryMark.tsx b/apps/website/src/components/docs/LibraryMark.tsx
index c80a47f37..daa7641ea 100644
--- a/apps/website/src/components/docs/LibraryMark.tsx
+++ b/apps/website/src/components/docs/LibraryMark.tsx
@@ -1,6 +1,6 @@
import type { LibraryId } from '../../lib/docs-config';
-type GlyphKey = 'chat' | 'middleware' | 'pulse' | 'layers';
+type GlyphKey = 'chat' | 'middleware' | 'pulse' | 'layers' | 'branch';
type MarkEntry =
| { kind: 'logo'; src: string }
@@ -15,6 +15,7 @@ const MARKS: Record = {
middleware: { kind: 'glyph', glyph: 'middleware' },
telemetry: { kind: 'glyph', glyph: 'pulse' },
runtimes: { kind: 'glyph', glyph: 'layers' },
+ 'deep-agents': { kind: 'glyph', glyph: 'branch' },
};
function ChatGlyph({ s }: { s: number }) {
@@ -52,11 +53,23 @@ function LayersGlyph({ s }: { s: number }) {
);
}
+function BranchGlyph({ s }: { s: number }) {
+ return (
+
+ );
+}
+
const GLYPHS: Record React.JSX.Element> = {
chat: ChatGlyph,
middleware: MiddlewareGlyph,
pulse: PulseGlyph,
layers: LayersGlyph,
+ branch: BranchGlyph,
};
interface Props {
diff --git a/apps/website/src/lib/docs-config.ts b/apps/website/src/lib/docs-config.ts
index eba538fb3..29af8890d 100644
--- a/apps/website/src/lib/docs-config.ts
+++ b/apps/website/src/lib/docs-config.ts
@@ -6,7 +6,8 @@ export type LibraryId =
| 'a2ui'
| 'middleware'
| 'telemetry'
- | 'runtimes';
+ | 'runtimes'
+ | 'deep-agents';
export interface DocsPage {
title: string;
@@ -509,6 +510,37 @@ export const docsConfig: DocsLibrary[] = [
},
],
},
+ {
+ id: 'deep-agents',
+ title: 'Deep Agents',
+ description:
+ 'Rendering the Deep Agents middleware capabilities — planning, filesystem, subagents, memory, and skills — in an Angular UI',
+ // The framework is a LangGraph agent harness, so these pages sit behind the
+ // LangGraph adapter rather than beside it. Reference material, not a pick.
+ group: 'library',
+ sections: [
+ {
+ title: 'Getting Started',
+ id: 'getting-started',
+ color: 'blue',
+ pages: [
+ { title: 'Introduction', slug: 'introduction', section: 'getting-started' },
+ ],
+ },
+ {
+ title: 'Capabilities',
+ id: 'capabilities',
+ color: 'blue',
+ pages: [
+ { title: 'Planning', slug: 'planning', section: 'capabilities' },
+ { title: 'Filesystem', slug: 'filesystem', section: 'capabilities' },
+ { title: 'Subagents', slug: 'subagents', section: 'capabilities' },
+ { title: 'Memory', slug: 'memory', section: 'capabilities' },
+ { title: 'Skills', slug: 'skills', section: 'capabilities' },
+ ],
+ },
+ ],
+ },
];
export function getLibraryConfig(libraryId: string): DocsLibrary | undefined {
diff --git a/cockpit/deep-agents/filesystem/angular/src/index.ts b/cockpit/deep-agents/filesystem/angular/src/index.ts
index 493125d36..5d0a9ceff 100644
--- a/cockpit/deep-agents/filesystem/angular/src/index.ts
+++ b/cockpit/deep-agents/filesystem/angular/src/index.ts
@@ -23,9 +23,7 @@ export const deepAgentsFilesystemAngularModule: CockpitCapabilityModule = {
language: 'angular',
},
title: 'Deep Agents Filesystem (Angular)',
- // No `deep-agents` library exists on the website yet; the empty string is
- // the "no published docs page" sentinel and renders no Docs link.
- docsPath: '',
+ docsPath: '/docs/deep-agents/capabilities/filesystem',
promptAssetPaths: [
'cockpit/deep-agents/filesystem/angular/prompts/filesystem.md',
],
diff --git a/cockpit/deep-agents/filesystem/python/src/index.ts b/cockpit/deep-agents/filesystem/python/src/index.ts
index b479c4793..cdaa725de 100644
--- a/cockpit/deep-agents/filesystem/python/src/index.ts
+++ b/cockpit/deep-agents/filesystem/python/src/index.ts
@@ -27,9 +27,7 @@ export const deepAgentsFilesystemPythonModule: CockpitCapabilityModule = {
language: 'python',
},
title: 'Deep Agents Filesystem (Python)',
- // No `deep-agents` library exists on the website yet; the empty string is
- // the "no published docs page" sentinel and renders no Docs link.
- docsPath: '',
+ docsPath: '/docs/deep-agents/capabilities/filesystem',
promptAssetPaths: ['cockpit/deep-agents/filesystem/python/prompts/filesystem.md'],
codeAssetPaths: [
'cockpit/deep-agents/filesystem/angular/src/app/filesystem.component.ts',
diff --git a/cockpit/deep-agents/memory/angular/src/index.ts b/cockpit/deep-agents/memory/angular/src/index.ts
index 396b7d0b4..d2f9714dd 100644
--- a/cockpit/deep-agents/memory/angular/src/index.ts
+++ b/cockpit/deep-agents/memory/angular/src/index.ts
@@ -23,9 +23,7 @@ export const deepAgentsMemoryAngularModule: CockpitCapabilityModule = {
language: 'angular',
},
title: 'Deep Agents Memory (Angular)',
- // No `deep-agents` library exists on the website yet; the empty string is
- // the "no published docs page" sentinel and renders no Docs link.
- docsPath: '',
+ docsPath: '/docs/deep-agents/capabilities/memory',
promptAssetPaths: [
'cockpit/deep-agents/memory/angular/prompts/memory.md',
],
diff --git a/cockpit/deep-agents/memory/python/src/index.ts b/cockpit/deep-agents/memory/python/src/index.ts
index 6bd7e1869..a40127df4 100644
--- a/cockpit/deep-agents/memory/python/src/index.ts
+++ b/cockpit/deep-agents/memory/python/src/index.ts
@@ -27,9 +27,7 @@ export const deepAgentsMemoryPythonModule: CockpitCapabilityModule = {
language: 'python',
},
title: 'Deep Agents Memory (Python)',
- // No `deep-agents` library exists on the website yet; the empty string is
- // the "no published docs page" sentinel and renders no Docs link.
- docsPath: '',
+ docsPath: '/docs/deep-agents/capabilities/memory',
promptAssetPaths: ['cockpit/deep-agents/memory/python/prompts/memory.md'],
codeAssetPaths: [
'cockpit/deep-agents/memory/angular/src/app/memory.component.ts',
diff --git a/cockpit/deep-agents/planning/angular/src/index.ts b/cockpit/deep-agents/planning/angular/src/index.ts
index 5f845f9d9..1c7eaf82f 100644
--- a/cockpit/deep-agents/planning/angular/src/index.ts
+++ b/cockpit/deep-agents/planning/angular/src/index.ts
@@ -23,9 +23,7 @@ export const deepAgentsPlanningAngularModule: CockpitCapabilityModule = {
language: 'angular',
},
title: 'Deep Agents Planning (Angular)',
- // No `deep-agents` library exists on the website yet; the empty string is
- // the "no published docs page" sentinel and renders no Docs link.
- docsPath: '',
+ docsPath: '/docs/deep-agents/capabilities/planning',
promptAssetPaths: [
'cockpit/deep-agents/planning/angular/prompts/planning.md',
],
diff --git a/cockpit/deep-agents/planning/python/src/index.ts b/cockpit/deep-agents/planning/python/src/index.ts
index e9930f9b6..648fb9c0b 100644
--- a/cockpit/deep-agents/planning/python/src/index.ts
+++ b/cockpit/deep-agents/planning/python/src/index.ts
@@ -27,9 +27,7 @@ export const deepAgentsPlanningPythonModule: CockpitCapabilityModule = {
language: 'python',
},
title: 'Deep Agents Planning (Python)',
- // No `deep-agents` library exists on the website yet; the empty string is
- // the "no published docs page" sentinel and renders no Docs link.
- docsPath: '',
+ docsPath: '/docs/deep-agents/capabilities/planning',
promptAssetPaths: ['cockpit/deep-agents/planning/python/prompts/planning.md'],
codeAssetPaths: [
'cockpit/deep-agents/planning/angular/src/app/planning.component.ts',
diff --git a/cockpit/deep-agents/skills/angular/src/index.ts b/cockpit/deep-agents/skills/angular/src/index.ts
index 0498c15d9..11f94c480 100644
--- a/cockpit/deep-agents/skills/angular/src/index.ts
+++ b/cockpit/deep-agents/skills/angular/src/index.ts
@@ -23,9 +23,7 @@ export const deepAgentsSkillsAngularModule: CockpitCapabilityModule = {
language: 'angular',
},
title: 'Deep Agents Skills (Angular)',
- // No `deep-agents` library exists on the website yet; the empty string is
- // the "no published docs page" sentinel and renders no Docs link.
- docsPath: '',
+ docsPath: '/docs/deep-agents/capabilities/skills',
promptAssetPaths: [
'cockpit/deep-agents/skills/angular/prompts/skills.md',
],
diff --git a/cockpit/deep-agents/skills/python/src/index.ts b/cockpit/deep-agents/skills/python/src/index.ts
index 77ad4530b..435c04cd6 100644
--- a/cockpit/deep-agents/skills/python/src/index.ts
+++ b/cockpit/deep-agents/skills/python/src/index.ts
@@ -27,9 +27,7 @@ export const deepAgentsSkillsPythonModule: CockpitCapabilityModule = {
language: 'python',
},
title: 'Deep Agents Skills (Python)',
- // No `deep-agents` library exists on the website yet; the empty string is
- // the "no published docs page" sentinel and renders no Docs link.
- docsPath: '',
+ docsPath: '/docs/deep-agents/capabilities/skills',
promptAssetPaths: ['cockpit/deep-agents/skills/python/prompts/skills.md'],
codeAssetPaths: [
'cockpit/deep-agents/skills/angular/src/app/skills.component.ts',
diff --git a/cockpit/deep-agents/subagents/angular/src/index.ts b/cockpit/deep-agents/subagents/angular/src/index.ts
index 85e182103..3ed76c5d2 100644
--- a/cockpit/deep-agents/subagents/angular/src/index.ts
+++ b/cockpit/deep-agents/subagents/angular/src/index.ts
@@ -23,9 +23,7 @@ export const deepAgentsSubagentsAngularModule: CockpitCapabilityModule = {
language: 'angular',
},
title: 'Deep Agents Subagents (Angular)',
- // No `deep-agents` library exists on the website yet; the empty string is
- // the "no published docs page" sentinel and renders no Docs link.
- docsPath: '',
+ docsPath: '/docs/deep-agents/capabilities/subagents',
promptAssetPaths: [
'cockpit/deep-agents/subagents/angular/prompts/subagents.md',
],
diff --git a/cockpit/deep-agents/subagents/python/src/index.ts b/cockpit/deep-agents/subagents/python/src/index.ts
index a282ca31b..eb2343cbb 100644
--- a/cockpit/deep-agents/subagents/python/src/index.ts
+++ b/cockpit/deep-agents/subagents/python/src/index.ts
@@ -27,9 +27,7 @@ export const deepAgentsSubagentsPythonModule: CockpitCapabilityModule = {
language: 'python',
},
title: 'Deep Agents Subagents (Python)',
- // No `deep-agents` library exists on the website yet; the empty string is
- // the "no published docs page" sentinel and renders no Docs link.
- docsPath: '',
+ docsPath: '/docs/deep-agents/capabilities/subagents',
promptAssetPaths: ['cockpit/deep-agents/subagents/python/prompts/subagents.md'],
codeAssetPaths: [
'cockpit/deep-agents/subagents/angular/src/app/subagents.component.ts',
diff --git a/libs/cockpit-registry/src/lib/docs-links.ts b/libs/cockpit-registry/src/lib/docs-links.ts
index 7e8054cd1..0a0c86c10 100644
--- a/libs/cockpit-registry/src/lib/docs-links.ts
+++ b/libs/cockpit-registry/src/lib/docs-links.ts
@@ -34,13 +34,13 @@ export const NO_COCKPIT_DOCS_LINK = '';
* lanes of one cockpit demo point at the same page.
*/
export const COCKPIT_DOCS_LINKS: Readonly> = {
- // deep-agents — no `deep-agents` library exists on the website yet.
- 'deep-agents/getting-started/overview': NO_COCKPIT_DOCS_LINK,
- 'deep-agents/core-capabilities/planning': NO_COCKPIT_DOCS_LINK,
- 'deep-agents/core-capabilities/filesystem': NO_COCKPIT_DOCS_LINK,
- 'deep-agents/core-capabilities/subagents': NO_COCKPIT_DOCS_LINK,
- 'deep-agents/core-capabilities/memory': NO_COCKPIT_DOCS_LINK,
- 'deep-agents/core-capabilities/skills': NO_COCKPIT_DOCS_LINK,
+ // deep-agents
+ 'deep-agents/getting-started/overview': '/docs/deep-agents/getting-started/introduction',
+ 'deep-agents/core-capabilities/planning': '/docs/deep-agents/capabilities/planning',
+ 'deep-agents/core-capabilities/filesystem': '/docs/deep-agents/capabilities/filesystem',
+ 'deep-agents/core-capabilities/subagents': '/docs/deep-agents/capabilities/subagents',
+ 'deep-agents/core-capabilities/memory': '/docs/deep-agents/capabilities/memory',
+ 'deep-agents/core-capabilities/skills': '/docs/deep-agents/capabilities/skills',
// langgraph
'langgraph/getting-started/overview': '/docs/langgraph/getting-started/introduction',
@@ -104,18 +104,12 @@ export const COCKPIT_DOCS_LINKS: Readonly> = {
/**
* The capabilities that deliberately carry `NO_COCKPIT_DOCS_LINK`.
*
- * Kept as an explicit list so the guard spec can assert that the only blank
- * entries are these — a rename that accidentally blanks a real link fails
- * instead of quietly dropping the "Docs" button from a page.
+ * Currently empty: every mapped capability points at a published page. The list
+ * stays because the guard spec asserts that the blanked entries are exactly
+ * these — so with an empty list, blanking anything at all fails, rather than
+ * quietly dropping the "Docs" button from a page.
*/
-export const COCKPIT_TOPICS_WITHOUT_DOCS: readonly string[] = [
- 'deep-agents/getting-started/overview',
- 'deep-agents/core-capabilities/planning',
- 'deep-agents/core-capabilities/filesystem',
- 'deep-agents/core-capabilities/subagents',
- 'deep-agents/core-capabilities/memory',
- 'deep-agents/core-capabilities/skills',
-];
+export const COCKPIT_TOPICS_WITHOUT_DOCS: readonly string[] = [];
/**
* Resolve the website documentation URL for a cockpit capability.