Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/facet-workflow-origins.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"agents": patch
---

Support starting Agent workflows from sub-agent facets by preserving the originating facet path for callbacks and workflow Agent RPC.
44 changes: 22 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,28 +100,28 @@ The agent is a Durable Object, so it needs a binding and a SQLite migration in `

## Features

| Feature | Description |
| ----------------------- | ------------------------------------------------------------------------------- |
| **Persistent State** | Syncs to all connected clients, survives restarts |
| **Callable Methods** | Type-safe RPC via the `@callable()` decorator |
| **Sub-agents** | Parent/child DO composition via facets, nested routing, and typed parent lookup |
| **Agent Tools** | Run chat-capable sub-agents as tools with streaming child timelines |
| **Scheduling** | One-time, recurring, and cron-based tasks |
| **WebSockets** | Real-time bidirectional communication with lifecycle hooks |
| **AI Chat** | Message persistence, resumable streaming, server/client tool execution |
| **MCP** | Act as MCP servers or connect as MCP clients (HTTP, SSE, RPC, elicitation) |
| **WebMCP** | Expose browser-side tools to agents over WebSocket |
| **Workflows** | Durable multi-step tasks with human-in-the-loop approval |
| **Email** | Send, receive, and reply via Cloudflare Email Service |
| **Voice** | Continuous STT, streaming TTS, VAD, interruption, SFU utilities |
| **Browser Agents** | Run agents in the browser tab with `agents/browser` |
| **Code Mode** | LLMs generate executable TypeScript instead of individual tool calls |
| **Sandboxed Execution** | Run generated code inside an isolated Worker with a virtual filesystem |
| **x402 Payments** | Pay-per-call APIs and tools via the x402 protocol |
| **Observability** | Built-in tracing, metrics, and structured logs |
| **SQL** | Direct SQLite queries via Durable Objects |
| **React Hooks** | `useAgent`, `useAgentChat`, `useVoiceAgent` for frontend integration |
| **Vanilla JS Client** | `AgentClient` and `VoiceClient` for non-React environments |
| Feature | Description |
| ----------------------- | ---------------------------------------------------------------------------------------------------- |
| **Persistent State** | Syncs to all connected clients, survives restarts |
| **Callable Methods** | Type-safe RPC via the `@callable()` decorator |
| **Sub-agents** | Parent/child DO composition via facets, nested routing, and typed parent lookup |
| **Agent Tools** | Run chat-capable sub-agents as tools with streaming child timelines |
| **Scheduling** | One-time, recurring, and cron-based tasks |
| **WebSockets** | Real-time bidirectional communication with lifecycle hooks |
| **AI Chat** | Message persistence, resumable streaming, server/client tool execution |
| **MCP** | Act as MCP servers or connect as MCP clients (HTTP, SSE, RPC, elicitation) |
| **WebMCP** | Expose browser-side tools to agents over WebSocket |
| **Workflows** | Durable multi-step tasks with human-in-the-loop approval, including facet-local runs from sub-agents |
| **Email** | Send, receive, and reply via Cloudflare Email Service |
| **Voice** | Continuous STT, streaming TTS, VAD, interruption, SFU utilities |
| **Browser Agents** | Run agents in the browser tab with `agents/browser` |
| **Code Mode** | LLMs generate executable TypeScript instead of individual tool calls |
| **Sandboxed Execution** | Run generated code inside an isolated Worker with a virtual filesystem |
| **x402 Payments** | Pay-per-call APIs and tools via the x402 protocol |
| **Observability** | Built-in tracing, metrics, and structured logs |
| **SQL** | Direct SQLite queries via Durable Objects |
| **React Hooks** | `useAgent`, `useAgentChat`, `useVoiceAgent` for frontend integration |
| **Vanilla JS Client** | `AgentClient` and `VoiceClient` for non-React environments |

## Packages

Expand Down
22 changes: 11 additions & 11 deletions design/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ The goal is to give contributors (and future-us) a quick way to understand _why_

## Contents

| File | Scope |
| -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| [think.md](./think.md) | Think — chat agent base class, sessions, streaming, tools, execution ladder |
| [visuals.md](./visuals.md) | UI component library choice, Kumo usage, custom patterns |
| [readonly-connections.md](./readonly-connections.md) | Readonly connection enforcement, storage, tradeoffs, and caveats |
| [workspace.md](./workspace.md) | Workspace — hybrid SQLite+R2 filesystem, bash, symlinks |
| [agent-tools.md](./agent-tools.md) | Agent tools — chat sub-agent orchestration, parent registry, replay |
| [sub-agent-routing.md](./sub-agent-routing.md) | Sub-agent routing as shipped — facets, nested URLs, registry, parent lookup |
| [rfc-sub-agents.md](./rfc-sub-agents.md) | RFC: Sub-agents — child DOs via facets, typed stubs, mixin API |
| [rfc-helper-sub-agent-orchestration.md](./rfc-helper-sub-agent-orchestration.md) | RFC: Agent tool orchestration — `runAgentTool`, `agentTool`, event forwarding |
| [loopback.md](./loopback.md) | Loopback pattern — cross-boundary RPC for sub-agents and dynamic isolates |
| File | Scope |
| -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| [think.md](./think.md) | Think — chat agent base class, sessions, streaming, tools, execution ladder |
| [visuals.md](./visuals.md) | UI component library choice, Kumo usage, custom patterns |
| [readonly-connections.md](./readonly-connections.md) | Readonly connection enforcement, storage, tradeoffs, and caveats |
| [workspace.md](./workspace.md) | Workspace — hybrid SQLite+R2 filesystem, bash, symlinks |
| [agent-tools.md](./agent-tools.md) | Agent tools — chat sub-agent orchestration, parent registry, replay |
| [sub-agent-routing.md](./sub-agent-routing.md) | Sub-agent routing as shipped — facets, nested URLs, registry, workflow origins |
| [rfc-sub-agents.md](./rfc-sub-agents.md) | RFC: Sub-agents — child DOs via facets, typed stubs, mixin API |
| [rfc-helper-sub-agent-orchestration.md](./rfc-helper-sub-agent-orchestration.md) | RFC: Agent tool orchestration — `runAgentTool`, `agentTool`, event forwarding |
| [loopback.md](./loopback.md) | Loopback pattern — cross-boundary RPC for sub-agents and dynamic isolates |
5 changes: 5 additions & 0 deletions design/sub-agent-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ RPC if you need parent-side side effects.
SQLite database, while the root parent keeps a small index of active facet
fibers so alarm housekeeping can route recovery checks back into idle
children.
- `runWorkflow()` works on facets. Workflow params include a versioned
`__agentOrigin` with `kind: "facet"`, the root binding, and the root-first
`selfPath`; `AgentWorkflow` uses it to route `this.agent` RPC and durable
callbacks back to the originating facet. For facet origins, `this.agent` is
RPC-only and does not support `.fetch()`.
- Think chat recovery works on facets; recovered continuations can schedule from
the child and are routed through the top-level parent's alarm.
- `deleteSubAgent()` is idempotent and removes pending schedules for that
Expand Down
2 changes: 2 additions & 0 deletions docs/durable-execution.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ Sub-agents do not have independent alarm slots, so the top-level parent owns the

This keeps recovery local to the child while preserving the single physical alarm slot owned by the parent. A recovered continuation can use `schedule()` from inside the facet; the parent owns the physical alarm and routes the callback back to the child.

Workflows started from sub-agents follow the same locality model for tracking and callbacks. The workflow row lives in the sub-agent's SQLite database, and `AgentWorkflow.agent` routes RPC and workflow callbacks back to the originating facet.

#### Error during execution

```
Expand Down
2 changes: 2 additions & 0 deletions docs/human-in-the-loop.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ export class ExpenseAgent extends Agent<Env, ExpenseState> {
}
```

If a sub-agent starts the workflow, approval and rejection methods are scoped to that sub-agent. Parent agents should resolve the child with `subAgent()` and call child-defined wrapper methods that run `approveWorkflow()` or `rejectWorkflow()` inside the child; `onWorkflowProgress()`, `onWorkflowComplete()`, and related callbacks also run on the originating sub-agent.

### Timeout Handling

Set timeouts to prevent workflows from waiting indefinitely:
Expand Down
5 changes: 4 additions & 1 deletion docs/long-running-agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,8 @@ try {
| Minutes to hours | [Workflows](./workflows.md) |
| Hours to days | Async pattern: start job → hibernate → wake on completion |

If a sub-agent starts a workflow, tracking and workflow controls are scoped to that sub-agent. A parent can still coordinate the workflow by resolving the child with `subAgent()` and calling child-defined wrapper methods that run `getWorkflow()`, `approveWorkflow()`, `terminateWorkflow()`, or cleanup helpers inside the child.

## Surviving crashes: fibers and recovery

An agent can be evicted at any time — a deploy, a platform restart, or hitting resource limits. If the agent was mid-task, that work is lost unless it was checkpointed.
Expand Down Expand Up @@ -595,7 +597,8 @@ export class ProjectManager extends Agent<ProjectState> {
)
});

// Clean up old workflow tracking records
// Clean up old workflow tracking records started by this same agent.
// Child-started workflows must be cleaned up on the child facet.
this.deleteWorkflows({
status: ["complete", "errored"],
createdBefore: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
Expand Down
16 changes: 8 additions & 8 deletions docs/server-driven-messages.md
Original file line number Diff line number Diff line change
Expand Up @@ -379,14 +379,14 @@ The `messageConcurrency` setting on `AIChatAgent` controls how overlapping user

## Combining with other Agent primitives

| Primitive | How to combine |
| ------------------ | --------------------------------------------------------------------------------------------- |
| `schedule()` | Schedule a callback that calls `saveMessages` — see the cron example above |
| `queue()` | Queue a method that calls `saveMessages` for deferred processing |
| `runWorkflow()` | Start a Workflow; use `AgentWorkflow.agent` RPC to call a method that triggers `saveMessages` |
| `onEmail()` | Convert email content to a chat message and call `saveMessages` |
| `onRequest()` | Handle webhooks and call `saveMessages` |
| `this.broadcast()` | Broadcast custom state from `onChatResponse` |
| Primitive | How to combine |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `schedule()` | Schedule a callback that calls `saveMessages` — see the cron example above |
| `queue()` | Queue a method that calls `saveMessages` for deferred processing |
| `runWorkflow()` | Start a Workflow; use `AgentWorkflow.agent` RPC to call a method that triggers `saveMessages`. For workflows started by sub-agents, this stub routes back to the originating facet and is RPC-only; use sub-agent routing for HTTP/WebSocket traffic. |
| `onEmail()` | Convert email content to a chat message and call `saveMessages` |
| `onRequest()` | Handle webhooks and call `saveMessages` |
| `this.broadcast()` | Broadcast custom state from `onChatResponse` |

## Cancelling a server-driven turn

Expand Down
2 changes: 1 addition & 1 deletion docs/state.md
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ async run(event: AgentWorkflowEvent<Params>, step: AgentWorkflowStep) {
}
```

These are durable operations - they persist even if the workflow retries.
These are durable operations - they persist even if the workflow retries. They update the workflow's originating Agent. If a sub-agent started the workflow, the state update applies to that sub-agent facet and broadcasts to that facet's clients.

## Patterns & Best Practices

Expand Down
4 changes: 4 additions & 0 deletions docs/sub-agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ Sub-agents can use `runFiber()` and Think's `chatRecovery` just like top-level a

Because facets do not have independent alarm slots, the top-level parent owns the physical alarm heartbeat for sub-agent fibers. The sub-agent still stores fiber rows and snapshots in its own SQLite database, while the parent stores a small root-side index of active facet fibers. When the parent alarm fires, it checks that index and routes recovery checks back into the owning sub-agent. Think's chat recovery can schedule its recovered continuation from inside the sub-agent; the parent owns the physical alarm and routes the continuation back to the child.

Sub-agents can also start [Workflows](./workflows.md) with `this.runWorkflow()`. Workflow tracking is local to the sub-agent's SQLite database, and `AgentWorkflow.agent` routes RPC, callbacks, state updates, and broadcasts back to the originating sub-agent. Parent agents do not automatically list or control child-started workflows. Because `SubAgentStub<T>` only exposes user-defined child methods, add child wrapper methods for controls such as `getWorkflow()`, `approveWorkflow()`, or `terminateWorkflow()`, then call those wrappers through `await this.subAgent(Child, name)`. If you pass `runWorkflow(..., { agentBinding })` from a sub-agent, use the root Agent binding name, not a child binding name.

For sub-agent workflow origins, `AgentWorkflow.agent` is RPC-only. Use it to call Agent methods, but use `routeSubAgentRequest()` or the nested `/agents/{parent}/{name}/sub/{child}/{name}` URL shape for external HTTP or WebSocket routing instead of `this.agent.fetch()`.

### Shared identity

Sub-agents know who their parent is via `this.parentPath` (root-first ancestor chain) and `this.parentAgent(ParentClass)` (typed stub). A sub-agent with no parent (top-level agent) has `parentPath === []`.
Expand Down
Loading
Loading