Conversation
Signed-off-by: yaron2 <schneider.yaron@live.com>
There was a problem hiding this comment.
Pull request overview
This PR introduces a new @dapr/openclaw extension plus an @dapr/openclaw-plugin package to add Dapr Workflow-backed durability to OpenClaw agent runs, and adds a dedicated GitHub Actions workflow to build/publish those packages. It fits into the repo as a new extension module that bridges OpenClaw agent execution with the existing Dapr workflow runtime.
Changes:
- Adds the OpenClaw runtime extension (
extensions/openclaw/src/*) that patches agent execution to run through Dapr Workflows and activities. - Adds the distributable OpenClaw plugin package, metadata, docs, and local crash-recovery demo assets.
- Adds a dedicated GitHub Actions workflow to build and publish the OpenClaw runtime/plugin packages.
Reviewed changes
Copilot reviewed 21 out of 23 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
extensions/openclaw/tsconfig.json |
TypeScript build config for the new runtime package. |
extensions/openclaw/test/plugins/crasher-tool/package.json |
Defines the test-only crash/retry plugin package. |
extensions/openclaw/test/plugins/crasher-tool/openclaw.plugin.json |
Plugin metadata for the crash-recovery demo tool. |
extensions/openclaw/test/plugins/crasher-tool/index.js |
Implements demo tools used to force and verify crash recovery. |
extensions/openclaw/test/e2e/run-cli-crash-e2e.sh |
Adds a two-phase CLI crash-recovery demo harness. |
extensions/openclaw/test/components/statestore.yaml |
Provides Redis-backed Dapr state store config for workflows. |
extensions/openclaw/src/workflow.ts |
Defines the durable agent-loop workflow orchestration. |
extensions/openclaw/src/types.ts |
Declares workflow/activity/runtime types for the extension. |
extensions/openclaw/src/registry.ts |
Adds in-memory workflow-to-agent runtime context registry. |
extensions/openclaw/src/patch.ts |
Patches OpenClaw agent execution to schedule/resume workflows. |
extensions/openclaw/src/index.ts |
Exposes enable/disable entry points and starts the workflow runtime. |
extensions/openclaw/src/activities.ts |
Implements LLM, tool, and event workflow activities. |
extensions/openclaw/plugin/package.json |
Defines the publishable OpenClaw plugin package. |
extensions/openclaw/plugin/package-lock.json |
Locks plugin package dependencies for local/plugin packaging. |
extensions/openclaw/plugin/openclaw.plugin.json |
Plugin manifest for OpenClaw discovery. |
extensions/openclaw/plugin/index.js |
Plugin entry point that resolves Agent and enables durable execution. |
extensions/openclaw/plugin/README.md |
Usage docs for installing/enabling the plugin package. |
extensions/openclaw/plugin/LICENSE |
License file for the plugin package. |
extensions/openclaw/package.json |
Defines the publishable OpenClaw runtime package. |
extensions/openclaw/README.md |
Main extension documentation and crash-recovery walkthrough. |
extensions/openclaw/LICENSE |
License file for the runtime package. |
.github/workflows/openclaw.yml |
Adds CI/publish automation for the new OpenClaw packages. |
Files not reviewed (1)
- extensions/openclaw/plugin/package-lock.json: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Drain the Agent's steering queue (messages injected while the loop runs) | ||
| if (rt.config.getSteeringMessages) { | ||
| const steering: any[] = await rt.config.getSteeringMessages(); | ||
| if (steering?.length) { | ||
| for (const msg of steering) { | ||
| await rt.emit({ type: "message_start", message: msg }); | ||
| await rt.emit({ type: "message_end", message: msg }); | ||
| messages.push(msg); | ||
| } | ||
| } |
| - name: Install runtime dependencies | ||
| working-directory: extensions/openclaw | ||
| run: npm install | ||
|
|
||
| - name: Build runtime | ||
| working-directory: extensions/openclaw | ||
| run: npm run build | ||
|
|
||
| - name: Pack smoke test (@dapr/openclaw) | ||
| working-directory: extensions/openclaw | ||
| run: npm pack --dry-run | ||
|
|
||
| - name: Pack smoke test (@dapr/openclaw-plugin) | ||
| working-directory: extensions/openclaw/plugin | ||
| run: npm pack --dry-run |
| const sessionId = this.sessionId || "default"; | ||
| const workflowId = `openclaw-${sessionId}`; |
There was a problem hiding this comment.
Pushed a per-prompt workflow ID, but couldn't use idempotencyKey directl since it doesn't reach the Agent in current OpenClaw.
Agent.prompt(input, images) doesn't forward options (pi-agent-core@0.65.0/agent.js:213), and hookCtx exposes runId but not idempotencyKey (and runId is randomUUID() per attempt, so it'd break resume).
Used a SHA-256 over {messages, systemPrompt, toolNames} instead (with timestamp stripped). Same content + retry → same workflow (resume works); different prompts → different workflows. Two distinct calls with an identical content requires an upstream Openclaw cooperation which is a no-go
| // No tool calls → done | ||
| if (!llmResult.toolCalls.length || llmResult.error) { | ||
| yield ctx.callActivity(emitEvent, { | ||
| workflowId, | ||
| event: { | ||
| type: "turn_end", | ||
| message: llmResult.assistantMessage, | ||
| toolResults: [], | ||
| }, | ||
| } as EmitEventInput); | ||
| break; | ||
| } |
| while (iteration < input.maxIterations) { | ||
| // --- turn_start --- | ||
| yield ctx.callActivity(emitEvent, { | ||
| workflowId, | ||
| event: { type: "turn_start" }, | ||
| } as EmitEventInput); |
| // Deterministic per-prompt workflow ID — stable for retries of this | ||
| // exact prompt, distinct from other prompts in the same session. | ||
| const sessionId = this.sessionId || "default"; | ||
| const workflowId = buildWorkflowId(sessionId, messages, context.systemPrompt, toolNames); | ||
|
|
| function buildWorkflowId(sessionId: string, messages: any[], systemPrompt: string, toolNames: string[]): string { | ||
| // Strip volatile fields before hashing. Agent.normalizePromptInput in | ||
| // pi-agent-core stamps `timestamp: Date.now()` on every wrapped user | ||
| // message — including this in the hash would make every retry produce a | ||
| // different workflow ID, breaking crash recovery. The content + role | ||
| // identify the prompt; the timestamp does not. | ||
| const stable = JSON.stringify({ messages, systemPrompt, toolNames }, (key, value) => | ||
| key === "timestamp" ? undefined : value, | ||
| ); | ||
| const fingerprint = createHash("sha256").update(stable).digest("hex").slice(0, 16); | ||
| return `openclaw-${sessionId}-${fingerprint}`; | ||
| } |
| } | ||
|
|
||
| proto.runPromptMessages = async function runPromptMessagesDapr(messages: any[], options: any = {}): Promise<void> { | ||
| console.log("[dapr] runPromptMessages intercepted — routing through Dapr Workflow"); |
| @@ -0,0 +1,129 @@ | |||
| /* | |||
| Copyright 2025 The Dapr Authors | |||
| * and tool call becomes a durable activity with automatic crash recovery. | ||
| * | ||
| * Installation (canonical): | ||
| * openclaw plugin install @dapr/openclaw-plugin |
| try { | ||
| // Check if a previous run left a workflow in RUNNING state (crash recovery). | ||
| let instanceId = workflowId; | ||
| let isResume = false; | ||
| try { | ||
| const existing = await workflowClient.getWorkflowState(workflowId, true); | ||
| if (existing && existing.runtimeStatus === WorkflowRuntimeStatus.RUNNING) { | ||
| console.log(`[dapr] Resuming existing workflow ${workflowId} (crash recovery)`); | ||
| isResume = true; |
This PR adds a Dapr Workflow integration to OpenClaw - allowing users to make their openclaw agents immortal and recover from failures. In addition, this enables cross-device work which OpenClaw doesn't natively support today.
Added here is a GitHub workflow to publish to
dapr/openclawon NPM.cc @WhitWaldo