cloudflare / cloudflare/agents
`Think` isolated agentic inference for `WorkflowEntrypoint` steps
- Dominant language
- TypeScript
- Stars
- 5.6k
- Forks
- 711
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 53
Description
## Related issue
[#1752 — First-class detached sub-agent runs with a durable completion hook](https://github.com/cloudflare/agents/issues/1752)
overlaps on the "don't pollute main history" motivation, but that issue is about async A2A delegation
*within* an agent's own turn. This request is about calling a Think agent *from a Cloudflare Workflow*
step — with the full agentic loop, structured output, and zero conversation pollution — without having
to build a second nested `ThinkWorkflow` as a workaround.
---
## What we want
We want an `agent` workflow step that works **exactly like the `llm` step** — a clean, isolated call that
returns a result — except instead of a single raw `streamText` call it runs the Think agent's full
agentic capabilities: its configured model, its system prompt, its tools, its memory, and the full
inference loop.
| Need | `llm` step today | `agent` step (desired) |
|---|---|---|
| Agent's configured model | uses caller-supplied model | ✅ uses the agent's `getModel()` |
| System prompt / soul | ❌ caller writes the prompt | ✅ agent's own `getSystemPrompt()` |
| Tool use + agentic loop | ❌ single inference call | ✅ multi-turn, tools fire and retry |
| Stateful memory | ❌ | ✅ agent's SQLite memory blocks |
| Structured output (Zod schema) | ✅ | ✅ same `final_answer` enforcement |
| Isolated — no chat history write | ✅ never touches history | ✅ (desired — currently broken) |
| Multiple concurrent workflows | ✅ | ✅ (desired — currently broken) |
In short: **same ergonomics as `llm`, same power as a full Think agent**.
---
## Problem
There is currently no Think primitive that satisfies all of the above.
`step.prompt()` gets us schema enforcement and the agentic loop but **writes every run to the agent's
main `cf_agent_messages` history**. An agent used as a workflow step accumulates every automated
invocation in its chat — mixing operational calls with user-facing conversation. Two concurrent
workflows hitting the same agent DO serialize and interleave in the same history.
---
## Current workaround — and why it is painful
Because no isolated primitive exists, the only way to get the full agentic loop with structured output
from a `WorkflowEntrypoint` step is to build a second durable workflow *inside* the Think agent itself:
1. The outer `WorkflowEntrypoint` calls a `@callable` on the Think agent DO, then immediately calls
`step.waitForEvent` to block.
2. That `@callable` spawns a `ThinkWorkflow` (a nested Cloudflare Workflow bound to the agent DO).
3. The inner `ThinkWorkflow` runs `step.prompt()` to get the full agentic loop and schema enforcement.
4. When the inner workflow finishes, it calls `sendWorkflowEvent` back to the outer
`WorkflowEntrypoint` with the result.
5. The outer workflow wakes up, reads the event payload, and continues.
```
WorkflowEntrypoint (outer)
│
├─ step.do("agent:start")
│ └─ agent.startPrompt(input, schema, parentRef) // @callable — fire and return
│ └─ agent.runWorkflow("AGENT_TASK_WORKFLOW") // spawns a second durable workflow
│
└─ step.waitForEvent("agent:wait", { timeout: "1 hour" })
│
└─ ThinkWorkflow (inner, nested inside agent DO)
└─ step.prompt("respond", { output: schema })
└─ full agent turn: tools, loop, final_answer
└─ sendWorkflowEvent(parentRef) // delivers result back to outer workflow
```
### What this workaround costs
1. **Two nested durable workflows** for every single agent step.
The outer `WorkflowEntrypoint`'s `step.do` already provides checkpointing and replay — the inner
`ThinkWorkflow` adds a second layer of Cloudflare Workflow overhead that solves a problem the
outer layer already solves.
2. **History still polluted.** `step.prompt()` inside the inner `ThinkWorkflow` calls
`submitMessages()`, which appends the turn to `cf_agent_messages`. Every workflow run leaves a
trace in the agent's chat history. Multiple concurrent workflow runs hitting the same agent
serialize at the DO and cross-contaminate each other's context.
3. **Boilerplate every consumer has to rediscover.** The bridge from `WorkflowEntrypoint` → Think
agent → result requires wiring a `@callable`, a nested `ThinkWorkflow`, and a
`sendWorkflowEvent` / `waitForEvent` handshake. There is no SDK primitive that does this — every
team that wants this pattern has to build and maintain it themselves.
---
## Proposed API
```ts
// On the Think class — callable over RPC from any WorkflowEntrypoint
async runTask(
input: string,
options: {
output: Schema // enforced via synthetic final_answer tool (same as step.prompt)
signal?: AbortSignal
}
): Promise>
```
Internally it would:
1. Run inference in a **child facet** (same isolation `startAgentToolRun` already provides —
separate from `cf_agent_messages`, no history writes)
2. Enforce the output schema via the synthetic `final_answer` tool already used by `step.prompt()`
3. Return the validated result synchronously to the caller
4. Require **no nested `ThinkWorkflow`** — durability is owned by the caller's `step.do`
### What the agent step becomes
```ts
// With runTask() — single step.do, isolated, no nested workflow
const result = await step.do(label, { retries: { limit: 2 }, timeout: "15 minutes" }, async () => {
const stub = await getAgentByName(env.UserAgent, agentId)
return stub.runTask(input, { output: schema })
})
```
Additional benefits:
- **Any number of concurrent workflows** can target the **same agent** without serializing or
cross-contaminating history — each `runTask` call runs in its own child facet.
- **`retries:` is safe** — `step.do` replay re-calls `runTask`, starting a fresh child-facet run
rather than appending a duplicate turn to main history (the current `stub.prompt()` retry hazard).
---
## Environment
- `@cloudflare/think` (latest)
- Cloudflare Workers + Durable Objects
- Cloudflare Workflows (`WorkflowEntrypoint`)
Contributor guide
Research direction
Start by tracing the existing step.prompt() path and the startAgentToolRun child-facet path mentioned in the issue, including how the synthetic final_answer tool is enforced. Define how a Think runTask() call can use the agent configuration and tools without writing cf_agent_messages or requiring a nested ThinkWorkflow. Done means the proposed API supports validated structured output, isolated concurrent calls, and safe WorkflowEntrypoint retries.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- ai, backend-api-design, cloud, distributed-systems
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100