@cuylabs/agent-runtime-dapr
v0.9.0
Published
Dapr-backed workload runtime and host adapters for @cuylabs/agent-runtime
Maintainers
Readme
@cuylabs/agent-runtime-dapr
Run AI agents with Dapr durability — crash-safe workflows, persistent state,
and scheduled jobs. Built on @cuylabs/agent-core and @cuylabs/agent-runtime.
Package Boundary
Use @cuylabs/agent-runtime-dapr when you want Dapr-backed infrastructure for workloads or agents:
- Dapr runtime driver for scheduled and manually triggered jobs
- durable workflow decomposition for agent turns
- persistent execution snapshots and checkpoints
- hosted HTTP runners and multi-agent hosts
- Dapr service invocation and workflow clients
This package does not redefine agent semantics or generic workload orchestration. It builds on:
agent-corefor task and turn execution semanticsagent-runtimefor the outer workload runtime contract
Why Dapr?
Dapr provides the durable infrastructure while your agent owns the intelligence:
Your code (agent + tools) ←→ Dapr sidecar (state, workflows, jobs)- Crash recovery — if the process dies mid-turn, Dapr resumes from the last checkpoint.
- Persistent state — execution history, sessions, and checkpoints survive restarts.
- Scheduled jobs — trigger agent work on a cron schedule via Dapr Jobs API.
- Zero vendor lock-in — Dapr runs anywhere: local Docker, Kubernetes, cloud.
Installation
pnpm add @cuylabs/agent-runtime-dapr @cuylabs/agent-core @dapr/daprFocused imports are also available when you want the package surface to mirror the internal modules:
import { createDaprExecutionObserver } from "@cuylabs/agent-runtime-dapr/execution";
import { createDaprAgentRunner } from "@cuylabs/agent-runtime-dapr/host";
import { DaprWorkflowClient } from "@cuylabs/agent-runtime-dapr/workflow";Under the hood, this package now exposes two layers:
createDaprWorkloadRuntime(...)for any workload that fits the neutralagent-runtimecontractcreateDaprAgentRuntime(...)andcreateDaprAgentRunner(...)as theagent-core-specific adapters built on top of that
Quick Start
Step 1: Define your agent
import { createAgent, Tool } from "@cuylabs/agent-core";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";
const greetTool = Tool.define("greet", {
description: "Greet someone by name.",
parameters: z.object({ name: z.string() }),
execute: async ({ name }) => ({
title: `Greeted ${name}`,
output: `Hello, ${name}!`,
metadata: {},
}),
});
const agent = createAgent({
model: openai("gpt-4o"),
cwd: process.cwd(),
systemPrompt: "You are a helpful assistant. Use the greet tool when asked.",
maxSteps: 10,
tools: [greetTool],
});Step 2: Create the runner
import { WorkflowRuntime } from "@dapr/dapr";
import { createDaprAgentRunner } from "@cuylabs/agent-runtime-dapr";
const runner = createDaprAgentRunner({
agent,
name: "my-agent",
workflowRuntime: new WorkflowRuntime(),
});
await runner.serve({ port: 3000 });Step 3: Start with Dapr
dapr run --app-id my-agent --dapr-grpc-port 50001 -- npx tsx my-agent.tsStep 4: Send requests
# Direct execution (synchronous)
curl -s http://localhost:3000/agents/run \
-H "Content-Type: application/json" \
-d '{"message": "Greet Carlos"}' | jq
# Durable workflow (async, crash-recoverable)
curl -s http://localhost:3000/agents/workflow \
-H "Content-Type: application/json" \
-d '{"message": "Greet Carlos"}' | jqThat's it. The runner handles workflow registration, HTTP server, runtime wiring, logging, and graceful shutdown.
Multi-Agent Host
Run multiple agents in a single process with createDaprMultiAgentRunner():
import { createDaprMultiAgentRunner } from "@cuylabs/agent-runtime-dapr";
const runner = createDaprMultiAgentRunner({
agents: [
{ agent: greeter, name: "greeter" },
{ agent: calculator, name: "calculator" },
],
workflowRuntime: new WorkflowRuntime(),
});
await runner.serve({ port: 3000 });Each agent gets its own workflow definition, execution store, and logging — but they share a single Dapr sidecar, workflow worker, and HTTP port.
# Target a specific agent by URL path
curl -s http://localhost:3000/agents/greeter/run \
-H "Content-Type: application/json" \
-d '{"message": "Say hi to Alice"}' | jqTwo Execution Modes
Every agent host exposes two ways to run a turn:
| Mode | Endpoint | Behavior |
|------|----------|----------|
| Direct | POST /agents/run | Synchronous. Returns result in the HTTP response. State is persisted, but execution is not crash-recoverable. |
| Workflow | POST /agents/workflow | Asynchronous. Returns 202 with an instanceId immediately. The turn runs as a Dapr workflow — crash-safe with activity-level checkpoints. |
The workflow decomposes each turn into four activities:
model-step → tool-call → step-commit → output-commitEach activity is a checkpoint. If the process crashes after tool-call, Dapr
replays from that point — the model call and tool execution don't repeat.
HTTP API Reference
| Method | Path | Description |
|--------|------|-------------|
| GET | /health | Liveness check |
| GET | /healthz | Liveness alias |
| GET | /ready | Readiness check |
| GET | /readyz | Readiness alias |
| GET | /agents | List registered agents |
| POST | /agents/run | Run agent turn (direct) |
| POST | /agents/workflow | Run agent turn (durable workflow) |
| POST | /agents/:id/run | Run specific agent (direct) |
| POST | /agents/:id/workflow | Run specific agent (durable workflow) |
| GET | /agents/:id/executions/:sessionId | Get execution details |
| GET | /agents/:id/executions/:sessionId/checkpoints | Get execution checkpoints |
| GET | /agents/:id/workflows/:instanceId | Get workflow state |
| POST | /job/:name | Handle Dapr scheduled job trigger |
Runner Options
createDaprAgentRunner() accepts these options:
| Option | Required | Default | Description |
|--------|----------|---------|-------------|
| agent | Yes | — | The Agent instance |
| name | No | agent.name | Agent ID in the Dapr ecosystem |
| workflowRuntime | Yes | — | new WorkflowRuntime() from @dapr/dapr |
| daprHttpEndpoint | No | http://$DAPR_HOST:$DAPR_HTTP_PORT | Sidecar HTTP endpoint |
| stateStoreName | No | "statestore" | Dapr state store component |
| driverOptions | No | — | Advanced Dapr runtime driver options: API token, retries, timeouts, custom fetch, sidecar verification |
| observers | No | [] | Extra execution lifecycle observers |
| logging | No | true | Enable/disable console logging |
| logPrefix | No | [${name}] | Log line prefix |
| aliases | No | [] | Alternative names for agent lookup |
The runner returns an object with:
start()— start runtime and workflow workerserve(options?)— start HTTP server, block on SIGINT/SIGTERMrun(message, options?)— run a task programmaticallystop()— graceful shutdown
Runner startup is transactional: if the workflow worker fails to start, the runtime is stopped before the error is returned.
Retention and Cleanup
Durability needs explicit retention. The package exposes cleanup methods on both durable stores so hosts can trim history without reaching into raw Dapr state.
await runtimeBundle.executionStore.cleanup({
maxAgeMs: 7 * 24 * 60 * 60 * 1000,
maxCheckpointsPerExecution: 20,
});
await runStore.cleanup({
maxAgeMs: 30 * 24 * 60 * 60 * 1000,
maxRuns: 10_000,
});The intended production pattern is to schedule cleanup as ordinary runtime work rather than relying on ad hoc scripts.
Going Deeper
For advanced use cases (custom workflow shapes, custom HTTP handlers, cross-service invocation), the package also exports the lower-level building blocks:
| Helper | Purpose |
|--------|---------|
| createDaprAgentWorkflowHost() | Wrap an Agent into a workflow host |
| createDaprWorkflowWorker() | Register workflow hosts in a WorkflowRuntime |
| createDaprWorkloadRuntime() | Dapr-backed runtime bundle for generic workloads |
| createDaprAgentRuntime() | Create runtime bundle (scheduling + runner + store) |
| startDaprHostHttpServer() | Start the HTTP control surface |
| DaprWorkflowClient | Manage workflow instances via HTTP API |
| DaprRuntimeDriver | Low-level RuntimeDriver for Dapr state |
| DaprExecutionStore | Persistent execution snapshots and checkpoints |
| createDaprExecutionObserver() | Persist execution events to the store |
| createDaprLoggingObserver() | Console logging for execution lifecycle |
| DaprServiceInvoker | Call agents across Dapr service boundaries |
See the docs/ folder for detailed guides:
- Architecture — how the three packages compose
- Workflow Internals — the 4-activity decomposition
- API Reference — all exported types and functions
- Advanced Patterns — cross-service invocation, custom observers, etc.
Runtime Boundary
The package layering is:
agent-core: agent turn/task semanticsagent-runtime: generic workload orchestration contractagent-runtime-dapr: Dapr-backed implementation of that contract
agent-runtime-dapr integrates with those lower layers in two different ways:
- outer workload path: it uses
agent-runtimeto schedule, dispatch, retry, and observe jobs - inner durable turn path: it uses
agent-coreruntime primitives to split one agent turn into durable workflow activities such asmodel-step,tool-call,step-commit, andoutput-commit
So this package does not only sit "on top of" agent-runtime. It also reaches
into the reusable turn/task surface exported by agent-core when it needs
fine-grained durable execution.
If you are running ordinary jobs or non-agent workloads, use
createDaprWorkloadRuntime(...).
If you are running agent-core tasks, use createDaprAgentRuntime(...) or the
higher-level createDaprAgentRunner(...).
Examples
The examples/ directory has complete, runnable scripts:
| Script | Lines | Description |
|--------|-------|-------------|
| simple-agent.ts | ~55 | Minimal agent with one tool |
| coding-agent.ts | ~45 | File-system tools via @cuylabs/agent-code |
| multi-agent.ts | ~85 | Two agents in one process |
| maintenance-host.ts | ~200 | Scheduled cleanup worker with /metrics and Dapr job callbacks |
See the examples README for step-by-step setup and usage.
Production Notes
DaprRuntimeDriververifies the sidecar is reachable on startup (verifySidecarOnStart, defaulttrue)- Per-request timeout: 15s default (
requestTimeoutMs) - Automatic retries for transient failures (
maxRequestRetries, default2) - Supports Dapr API token authentication (
dapr-api-tokenheader) GET /readyandGET /readyzreport runtime, worker, sidecar, and state-store readiness- Dapr Jobs API calls are isolated behind an internal adapter so scheduler changes stay local to the Dapr package
- Use
DaprExecutionStore.cleanup(...)andDaprOrchestratorRunStore.cleanup(...)to enforce retention budgets - For a concrete operational service, see
examples/maintenance-host.ts - For containers: run one sidecar per app process, point
daprHttpEndpointat the local sidecar
License
Apache-2.0
