当任务过大、复杂或需并行处理时,允许 Openclaw 代理生成子代理(子 Openclaw 实例)。在……时使用此技能。
Allows an Openclaw agent to spawn child Openclaw agents, passing them exactly the context they need to carry out their piece of work and report results back.
Helper script: scripts/spawn_openclaw.py — copy this into your project and import from it.
It contains spawn_openclaw(), spawn_openclaw_async(), is_error(), and read_result().
Multi-provider: Uses LiteLLM — pass any supported
model string via the model= argument. Default is "anthropic/claude-opus-4-6".
Requires: litellm Python package ( pip install litellm) and the API key env var for
your chosen provider (e.g. ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY).
Tool format: OpenAI function-call format. LiteLLM translates to each provider's native format automatically.
Credential:
spawn_openclaw.pyuses LiteLLM to call your chosen provider. Set the matching API key env var (e.g.ANTHROPIC_API_KEY,OPENAI_API_KEY). Never put keys in a payload or snapshot.
Tool format: Tools must be in OpenAI function-call format. LiteLLM translates them to each provider's native format automatically.
Filesystem access: Passing
tools=to a child agent grants that child whatever capabilities those tools carry. File-access tools allow children to read and write arbitrary paths. Only supply tools you would trust the parent to use directly. When in doubt, omittools=— the child will return results in its summary text.
Snapshot sanitization:
progress_so_faris sent to the Anthropic API and injected into the child's context. Before spawning, review snapshots and strip any secrets, credentials, personal data, or other sensitive information.
Spawn a child agent when any of these are true:
Do not spawn for trivial one-step tasks; keep it in the current agent.
MAX_DEPTH = 3 (configurable in scripts/spawn_openclaw.py).
DepthRole0Root / parent agent1Direct child (default spawn)2Grandchild (only if child's sub_task explicitly permits spawning)3+Blocked — raises ValueError
Always pass depth=<current_depth + 1> when calling spawn_openclaw() from inside a child.
Every spawn call must include these three fields:
json
{
"main_task_title": "<short human-readable title of the overall parent task>",
"progress_so_far": "<markdown summary of what has already been done, decisions made, artefacts produced, and anything the sub-agent must know to avoid redoing work>",
"sub_task": "<clear, self-contained description of exactly what this child agent must do, including expected output format and where/how to return results>"
}
FieldRulesmain_task_title≤ 10 words. Stable across all children of the same parent.progress_so_farInclude: steps completed, key decisions, files written, variables/state the child needs. Exclude: raw data the child doesn't need. Keep it dense but readable.sub_taskMust be self-contained. Assume the child has zero memory of the parent conversation. Include: what to do, inputs, expected output format, where to put results (file path, return value, etc.).
Confirm the sub-task is genuinely separable. If in doubt, handle it yourself.
Before spawning, write a progress snapshot. This becomes progress_so_far for the child
and serves as the parent's own checkpoint in case it needs to resume.
Sanitize before spawning.
progress_so_faris sent to the Anthropic API and injected into the child's context. Remove any secrets, API keys, passwords, tokens, or personal data before including them in the snapshot.
text
## Progress Snapshot — <main_task_title>
**Completed:**
- <step 1>
- <step 2>
**Artifacts produced:**
- <file or output name>: <one-line description>
**Decisions made:**
- <decision>: <rationale>
**Pending (what the child will handle):**
- <sub-task description>
Fill in the three required fields from the snapshot above.
Import from scripts/spawn_openclaw.py:
Important: Without
tools=, the child is a pure language model — it can reason and produce text but cannot read or write files. If the sub_task requires file I/O, pass the appropriate tool definitions (e.g. Anthropic computer-use tools, custom file tools). When tools are omitted, collect the child's result from thesummarystring directly rather than callingread_result().
python
from spawn_openclaw import spawn_openclaw, is_error, read_result
# my_tools = [...] # OpenAI function-call format tools if the child needs file I/O
payload = {
"main_task_title": "Refactor authentication module",
"progress_so_far": (
"## Progress Snapshot\n"
"**Completed:**\n- Audited existing auth flow\n- Identified 3 outdated JWT helpers\n\n"
"**Artifacts produced:**\n- `/tmp/audit_report.md`: full list of issues\n\n"
"**Decisions made:**\n- Use PyJWT 2.x API; drop legacy HS256 fallback\n\n"
"**Pending (child handles):**\n- Rewrite auth/jwt_helpers.py per audit report"
),
"sub_task": (
"Rewrite `/src/auth/jwt_helpers.py` using PyJWT 2.x. "
"Read `/tmp/audit_report.md` for issues to fix. "
"Write the rewritten file to `/tmp/jwt_helpers_new.py` and a "
"one-paragraph summary to `/tmp/jwt_helpers_changes.md`."
),
}
try:
# Swap model= to use any LiteLLM-supported provider:
# "openai/gpt-4o", "gemini/gemini-2.0-flash", "groq/llama-3.3-70b-versatile", etc.
summary = spawn_openclaw(payload, depth=1, model="anthropic/claude-opus-4-6", tools=my_tools)
except (ValueError, FileNotFoundError) as exc:
raise # bad depth, missing payload key, or SKILL.md not found — fix the call
if is_error(summary):
print("Child failed:", summary)
else:
result = read_result("/tmp/jwt_helpers_changes.md")
if result is None:
print("WARNING: child did not write expected result file.")
If SKILL.md is not adjacent to spawn_openclaw.py, pass the path explicitly:
python
import pathlib
summary = spawn_openclaw(payload, depth=1, skill_path=pathlib.Path("/your/path/to/SKILL.md"))
is_error(summary) — handle failures before reading files.read_result(path) to safely read child output; treat None as child failure.StrategyWhen to useParent blocks?SequentialChild output is needed for next parent stepYes, until child doneParallel-gatherMultiple independent children; parent needs all before continuingYes, until all doneFire-and-forgetChild works on a separable track; parent has its own work nowNo — merge later
Use spawn_openclaw(payload, depth=1) as shown in Step 4. Read result, check for errors, continue.
python
import asyncio
from spawn_openclaw import spawn_openclaw_async, is_error, read_result
async def main():
payloads = [
{
"main_task_title": "Generate market research report",
"progress_so_far": "Outline approved. Three sections assigned in parallel.",
"sub_task": "Write 'Competitive Landscape' (600 words). Save to /tmp/section_competitive.md."
},
{
"main_task_title": "Generate market research report",
"progress_so_far": "Outline approved. Three sections assigned in parallel.",
"sub_task": "Write 'Customer Segments' (600 words). Save to /tmp/section_customers.md."
},
{
"main_task_title": "Generate market research report",
"progress_so_far": "Outline approved. Three sections assigned in parallel.",
"sub_task": "Write 'Market Trends' (600 words). Save to /tmp/section_trends.md."
},
]
summaries = await asyncio.gather(
*[spawn_openclaw_async(p, depth=1, model="openai/gpt-4o", tools=my_tools) for p in payloads],
return_exceptions=True,
)
result_paths = [
"/tmp/section_competitive.md",
"/tmp/section_customers.md",
"/tmp/section_trends.md",
]
for summary, path in zip(summaries, result_paths):
if isinstance(summary, BaseException):
print(f"Child raised exception for {path}: {summary}")
continue
if is_error(summary):
print(f"Child failed for {path}:", summary)
continue
content = read_result(path)
if content is None:
print(f"WARNING: no result file at {path}")
else:
print(f"Merging {path} ({len(content)} chars)")
# merge content into parent output ...
The parent delegates a sub-task and immediately continues its own work. The child writes results to a known file path. The parent checks at a planned merge point.
text
Parent: ──[spawn child]──────────────────────────[merge point]──▶ continue
Child: └──[work independently]──[write result file]──▶ done
python
import asyncio
from spawn_openclaw import spawn_openclaw_async, is_error, read_result
async def main():
child_result_path = "/tmp/child_analysis.md"
payload = {
"main_task_title": "Refactor authentication module",
"progress_so_far": (
"Audit complete. Parent is now rewriting core auth logic. "
"Child is assigned to analyse test coverage gaps in parallel."
),
"sub_task": (
f"Read `/tmp/audit_report.md`. Identify which functions lack test coverage. "
f"Write a markdown report of gaps to `{child_result_path}`. "
f"Include function name, file, and suggested test cases for each gap."
),
}
# Spawn — returns immediately, child runs in background
child_task = asyncio.create_task(
spawn_openclaw_async(payload, depth=1, model="anthropic/claude-opus-4-6", tools=my_tools)
)
# Parent does its OWN work right now
await do_parent_work()
# Merge point — collect child
try:
summary = await child_task
except (ValueError, FileNotFoundError) as exc:
print("Child raised configuration error:", exc)
return
if is_error(summary):
print("Child failed:", summary)
else:
content = read_result(child_result_path)
if content is None:
print("WARNING: child did not write expected result file.")
else:
await integrate_child_output(content)
async def do_parent_work():
pass # replace with actual parent steps
async def integrate_child_output(text: str):
print(f"Merging {len(text)} chars from child...")
result_path in sub_task — it is the only rendezvous.read_result() — returns None safely on any filesystem error (missing file, permission denied, etc.).What raises vs. what returns an error string:
SituationBehaviourAnthropic API error (network, rate limit, etc.)Returns JSON error string — never raisesEmpty or non-text API responseReturns JSON error string — never raisesdepth >= MAX_DEPTHRaises ValueError — programmer error, fix your callMissing required payload key ( main_task_title, progress_so_far, sub_task)Raises ValueError — fix your payloadNon-JSON-serializable value in payloadRaises ValueError — fix your payloadSKILL.md not foundRaises FileNotFoundError — fix your path config
Always check with is_error() after a successful call. Wrap the call itself in try/except if you need to handle the two programmer-error exceptions gracefully:
python
try:
summary = spawn_openclaw(payload, depth=1)
except (ValueError, FileNotFoundError) as exc:
print("Configuration error:", exc)
raise # or handle
if is_error(summary):
import json
err = json.loads(summary)
print("Runtime error:", err["error"])
print("Partial results at:", err.get("partial_results")) # may be None
# decide: retry, fallback, abort parent
else:
# success — read result files
result = read_result("/tmp/some_output.md")
if result is not None:
# merge result ...
pass
else:
print("WARNING: child did not write expected result file.")
Anti-PatternWhy It's BadFixSending the full conversation history as progress_so_farWastes tokens; child gets confusedSummarize: only what the child needsIncluding secrets in progress_so_farSnapshot is sent to Anthropic API and visible to childStrip API keys, passwords, tokens, and personal data before spawningPassing overly-permissive tools to childrenChildren gain filesystem or network access beyond what their sub_task needsScope tools to minimum required capability; omit tools= if the task only needs text outputVague sub_task like "handle the rest"Child doesn't know what to doBe explicit: inputs, steps, output locationSpawning for a 2-line taskOverhead > benefitDo it in the parentNot writing a progress snapshot before spawningParent loses state if it crashesAlways freeze state firstOmitting tools= when sub_task requires file I/OChild is a pure LM — it cannot read or write files; read_result() always returns NonePass tool definitions or collect results from the summary string insteadIgnoring is_error() on child summarySilent failures; parent merges nothingAlways check before reading result filesFire-and-forget with no result fileNo rendezvous; parent can't collect outputAlways specify result_path in sub_taskAwaiting fire-and-forget child immediately after spawnDefeats the purposePut await child_task at the merge pointOmitting depth= when spawning from inside a childDepth check never triggers; runaway treesAlways pass depth=current_depth + 1Child spawning further children without explicit permissionRunaway tree; hard to debugOnly spawn if sub_task explicitly says so
text
SPAWN CHECKLIST
───────────────────────────────────────────
[ ] Sub-task is genuinely separable?
[ ] depth= will stay within MAX_DEPTH (default 3)?
[ ] Progress snapshot written (parent state frozen)?
[ ] main_task_title: ≤ 10 words, stable
[ ] progress_so_far: dense summary, no raw dumps
[ ] sub_task: self-contained, explicit result_path
STRATEGY SELECTION
[ ] Parent needs result before next step? → Sequential (A)
[ ] Multiple children, all needed before merge? → Parallel-gather (B)
[ ] Parent has its own work to do right now? → Fire-and-forget (C)
AFTER EVERY SPAWN
[ ] is_error(summary) checked?
[ ] read_result(path) used (handles missing files safely)?
[ ] None result handled — don't merge silently?
FIRE-AND-FORGET EXTRAS
[ ] result_path agreed before spawning?
[ ] merge point placed after parent's own work?