Complete the MCP tool call chain: - contractor-agent bridge exposes /mcp/execute endpoint for tool callbacks - openclaw-mcp-server.mjs proxies OpenClaw tool defs to Claude as MCP tools - sdk-adapter passes --mcp-config on first turn with all OpenClaw tools - tool-test plugin registers contractor_echo in globalThis tool handler map - agent-config-writer auto-sets tools.profile=full so OpenClaw sends tool defs - Fix --mcp-config arg ordering: prompt must come before <configs...> flag Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
14 KiB
Claude Contractor Implementation Plan
Purpose
Turn the architecture decisions into an implementation-oriented plan for phase 1.
This document is intentionally practical. It focuses on file structure, core modules, interfaces, and build order for the Claude-only phase.
Phase 1 implementation goal
Deliver a working Claude-backed contractor agent in OpenClaw that can:
- be provisioned through
openclaw contractor-agents add - use
contractor-claude-bridgeas its primary model - map an OpenClaw session to a Claude Code session
- forward the latest user message into Claude Code
- return Claude output through normal OpenClaw response flow
Gemini is out of scope for this phase.
Proposed repository structure
ContractorAgent/
├── docs/
│ ├── claude/
│ │ ├── PLAN.md
│ │ ├── MODEL.md
│ │ ├── CLI.md
│ │ ├── ARCHITECTURE.md
│ │ └── IMPLEMENTATION.md
│ └── gemini/
│ └── .gitkeep
├── src/
│ ├── index.ts
│ ├── config/
│ │ ├── schema.ts
│ │ └── contractor-config.ts
│ ├── cli/
│ │ ├── register-cli.ts
│ │ ├── contractor-agents-add.ts
│ │ └── contractor-agents-status.ts
│ ├── model/
│ │ ├── register-bridge-model.ts
│ │ ├── contractor-claude-bridge.ts
│ │ ├── input-filter.ts
│ │ └── response-normalizer.ts
│ ├── contractor/
│ │ ├── metadata-resolver.ts
│ │ ├── runtime-state.ts
│ │ ├── session-map-store.ts
│ │ ├── bootstrap.ts
│ │ └── recovery.ts
│ ├── claude/
│ │ ├── acp-adapter.ts
│ │ ├── claude-session.ts
│ │ └── claude-types.ts
│ ├── openclaw/
│ │ ├── agent-config-writer.ts
│ │ ├── agents-add-runner.ts
│ │ └── session-context.ts
│ └── types/
│ ├── contractor.ts
│ ├── session-map.ts
│ └── model.ts
├── package.json
├── openclaw.plugin.json
└── tsconfig.json
Not every file needs to exist on day 1, but this is a good target shape.
Module responsibilities
1. src/index.ts
Plugin entry point.
Responsibilities:
- export the OpenClaw plugin entry
- register the bridge model
- register plugin-owned CLI commands
- register any supporting services needed by the plugin
Expected shape:
export default definePluginEntry({
id: "contractor-agent",
name: "Contractor Agent",
description: "Claude-backed contractor agents for OpenClaw",
register(api) {
// register bridge model
// register CLI
// register helpers/services if needed
},
})
2. src/model/register-bridge-model.ts
Registers the custom model id used by contractor-backed agents.
Primary concern:
- expose
contractor-claude-bridgeto OpenClaw as a valid model selection target
This is the key seam that makes contractor-backed agents look normal to OpenClaw.
3. src/model/contractor-claude-bridge.ts
Core bridge model implementation.
Responsibilities:
- receive the model request from OpenClaw
- identify agent id and session key
- resolve contractor metadata
- resolve or create Claude session mapping
- filter input
- call Claude adapter
- normalize result
- return OpenClaw-compatible model output
This file is the heart of the system.
4. src/model/input-filter.ts
Convert raw OpenClaw model input into the payload actually sent to Claude.
Possible exports:
export function extractLatestActionableMessage(...)
export function buildClaudeTurnPayload(...)
This module should be pure and testable.
5. src/model/response-normalizer.ts
Convert Claude-side output into OpenClaw-compatible model response data.
Possible exports:
export function normalizeClaudeResponse(...)
export function normalizeClaudeError(...)
This should also be mostly pure and testable.
6. src/contractor/metadata-resolver.ts
Read agent config and decide whether the current agent is a contractor-backed Claude agent.
Responsibilities:
- inspect agent config
- return structured contractor runtime metadata
- reject unsupported or misconfigured contractor setups
Possible export:
export function resolveContractorAgentMetadata(...): ContractorAgentMetadata
7. src/contractor/session-map-store.ts
Read and write the OpenClaw session to Claude session mapping file.
Responsibilities:
- find by OpenClaw session key
- insert new mapping
- update timestamps
- replace mapping on reset/recovery
- mark mapping orphaned
Possible interface:
export interface SessionMapStore {
get(openclawSessionKey: string): Promise<SessionMapEntry | null>
put(entry: SessionMapEntry): Promise<void>
markOrphaned(openclawSessionKey: string): Promise<void>
remove(openclawSessionKey: string): Promise<void>
list(agentId: string): Promise<SessionMapEntry[]>
}
8. src/contractor/runtime-state.ts
Resolve and initialize plugin runtime state directories.
Responsibilities:
- compute state directory from workspace
- ensure directories exist
- provide path helpers for session-map and related files
Possible exports:
export function getContractorStateDir(workspace: string): string
export function getSessionMapPath(workspace: string): string
export async function ensureContractorStateDir(workspace: string): Promise<void>
9. src/contractor/bootstrap.ts
Create the initial contractor bootstrap payload for new Claude sessions.
Responsibilities:
- generate initial instruction text
- include workspace metadata
- include contractor role expectations
- avoid repeating per-turn content
Possible export:
export function buildClaudeBootstrap(...): string
10. src/contractor/recovery.ts
Handle missing or invalid Claude sessions.
Responsibilities:
- mark mappings orphaned
- trigger fresh Claude session creation
- optionally build a minimal recovery summary
Possible export:
export async function recoverClaudeSession(...)
11. src/claude/acp-adapter.ts
Claude runtime adapter used by the bridge model.
Responsibilities:
- create Claude session
- resume Claude session
- send message to Claude
- close Claude session if needed
Possible interface:
export interface ClaudeAcpAdapter {
createSession(input: CreateClaudeSessionInput): Promise<CreateClaudeSessionResult>
sendMessage(input: SendClaudeMessageInput): Promise<SendClaudeMessageResult>
closeSession?(sessionId: string): Promise<void>
}
This should hide ACP/runtime-specific details from the bridge model.
12. src/openclaw/agents-add-runner.ts
Encapsulate the logic for creating base OpenClaw agents from the contractor CLI.
Responsibilities:
- run or emulate
openclaw agents add <agent-id> --workspace <workspace> --model contractor-claude-bridge --non-interactive - translate failures into contractor CLI errors
Possible export:
export async function createBaseAgent(...): Promise<void>
13. src/openclaw/agent-config-writer.ts
Write contractor metadata into OpenClaw config after the base agent is created.
Responsibilities:
- update target agent config entry
- write contractor runtime stanza
- preserve unrelated config
Possible export:
export async function markAgentAsClaudeContractor(...): Promise<void>
14. src/cli/register-cli.ts
Register the plugin-owned CLI root.
Responsibilities:
- register
openclaw contractor-agents - attach subcommands such as
add, laterstatus,sessions,reset-session
15. src/cli/contractor-agents-add.ts
Implementation of the phase 1 provisioning flow.
Responsibilities:
- parse args
- validate contractor kind
- create base agent
- write contractor metadata
- initialize runtime state directory
- initialize empty session map
- return human-readable or JSON result
Primary types
src/types/contractor.ts
export type ContractorKind = "claude" | "gemini"
export type ContractorAgentMetadata = {
agentId: string
contractor: "claude"
bridgeModel: string
workspace: string
backend: "acp"
mode: "persistent" | "oneshot"
}
src/types/session-map.ts
export type SessionMapState = "active" | "closed" | "orphaned"
export type SessionMapEntry = {
openclawSessionKey: string
agentId: string
contractor: "claude"
claudeSessionId: string
workspace: string
createdAt: string
lastActivityAt: string
state: SessionMapState
}
export type SessionMapFile = {
version: 1
sessions: SessionMapEntry[]
}
src/types/model.ts
export type ContractorBridgeModelRequest = {
agentId: string
openclawSessionKey: string
workspace: string
latestUserMessage: string
rawInput: unknown
}
Exact fields depend on how OpenClaw provider/model requests are exposed in runtime.
Implementation notes from probe testing
See BRIDGE_MODEL_FINDINGS.md for full details. Key constraints that affect implementation:
- The sidecar MUST support SSE streaming (
stream: truealways sent by OpenClaw). Non-streaming responses cause OpenClaw to drop assistant history from subsequent turns. - OpenClaw sends the full system prompt (~28K chars) rebuilt on every turn. The input filter must strip this and extract only the latest user message.
- User message format from OpenClaw:
[Day YYYY-MM-DD HH:MM TZ] message text - Claude session continuation uses UUID from
@anthropic-ai/claude-agent-sdkmessage.session_id. Resume viaoptions: { resume: sessionId }. - Custom model is registered via
openclaw.jsonprovider config, not a plugin SDK call. The install script must write the provider entry and setbaseUrlto the sidecar port.
Revised milestone order
Milestone 3 (bridge model) is moved before Milestone 2 (CLI) because the bridge sidecar is the load-bearing component. Everything else depends on confirming it works end-to-end.
Suggested implementation order
Milestone 1, project skeleton
Create:
- package metadata
- plugin manifest
src/index.ts- empty docs-backed source tree
Success condition:
- plugin package structure exists
- build can run
Milestone 2, CLI provisioning path
Implement:
register-cli.tscontractor-agents-add.tsagents-add-runner.tsagent-config-writer.tsruntime-state.ts- empty
session-map-store.ts
Success condition:
openclaw contractor-agents add ... --contractor claudeprovisions a contractor-marked agent successfully
Milestone 3, bridge model registration
Implement:
register-bridge-model.ts- minimal
contractor-claude-bridge.ts
Success condition:
- OpenClaw recognizes
contractor-claude-bridgeas a valid model id
Milestone 4, session mapping and Claude dispatch
Implement:
- session-map CRUD
- metadata resolver
- initial ACP adapter
- input filter
- response normalizer
Success condition:
- a contractor-backed agent can receive a real turn and return a Claude-generated response
Milestone 5, bootstrap and recovery
Implement:
bootstrap.tsrecovery.ts- reset semantics
Success condition:
- first-turn session creation and broken-session recovery are both handled predictably
Testing strategy
Unit-test first
Best candidates for unit tests:
- metadata resolver
- input filter
- response normalizer
- session-map-store read/write/update behavior
- bootstrap payload builder
Integration-test later
Integration targets:
- contractor CLI add flow
- bridge model request path
- session creation and reuse
- reset and orphan recovery
Failure handling rules
The implementation should clearly separate:
Provisioning failures
Examples:
- invalid workspace
- base agent creation failed
- contractor metadata write failed
Recommended behavior:
- fail loudly
- roll back base agent creation if practical and safe
Runtime failures
Examples:
- Claude session missing
- ACP adapter unavailable
- response normalization failed
Recommended behavior:
- mark runtime mapping state explicitly
- recover when safe
- surface user-visible error when recovery is not possible
Minimum viable internal contracts
To prevent overdesign, phase 1 only needs a few stable internal boundaries:
- bridge model -> metadata resolver
- bridge model -> session map store
- bridge model -> Claude ACP adapter
- contractor CLI -> base agent creator
- contractor CLI -> agent config writer
If these boundaries are clean, later Gemini support can reuse most of the same architecture.
Design choices to keep intentionally simple in phase 1
- single contractor kind implemented, Claude
- JSON file-backed session map
- no aggressive streaming support initially
- no large-scale tool bridge yet
- no complicated migration logic yet
Things to avoid early
- overabstracting for Gemini before Claude works
- mixing static config with dynamic session map state
- rebuilding full OpenClaw context every turn
- making the bridge model pretend to be a normal stateless LLM provider
Open questions to resolve during implementation
- What exact OpenClaw plugin/provider registration API should the bridge model use?
- How should the plugin call underlying agent creation, shelling out to CLI vs internal config mutation?
- What exact payload does OpenClaw pass to a custom model implementation?
- What exact continuation identifier should be stored for Claude sessions?
- How should Claude tool requests later be bridged into OpenClaw-hosted tool execution?
Immediate next coding step
If starting implementation now, I would begin with:
- plugin manifest and
src/index.ts - CLI registration for
contractor-agents add - base agent creation + contractor metadata write
- empty state dir and empty session map initialization
That gets provisioning working before the runtime bridge itself is added.