create-leafmesh 2.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. create_leafmesh/__init__.py +3 -0
  2. create_leafmesh/cli.py +252 -0
  3. create_leafmesh/create.py +106 -0
  4. create_leafmesh/templates/Dockerfile +21 -0
  5. create_leafmesh/templates/README.md +309 -0
  6. create_leafmesh/templates/agency/__init__.py +0 -0
  7. create_leafmesh/templates/agency/advisor_agent.py +151 -0
  8. create_leafmesh/templates/agency/external_agents.py +278 -0
  9. create_leafmesh/templates/agency/fallback_researcher_agent.py +80 -0
  10. create_leafmesh/templates/agency/greeter_agent.py +79 -0
  11. create_leafmesh/templates/agency/processor_agent.py +90 -0
  12. create_leafmesh/templates/agency/researcher_agent.py +99 -0
  13. create_leafmesh/templates/agency/scheduler_agent.py +67 -0
  14. create_leafmesh/templates/agency/tools.py +123 -0
  15. create_leafmesh/templates/claude_skills/leafmesh/SKILL.md +2049 -0
  16. create_leafmesh/templates/claude_skills/leafmesh/agent-config-fields.md +1309 -0
  17. create_leafmesh/templates/claude_skills/leafmesh/examples.md +537 -0
  18. create_leafmesh/templates/claude_skills/leafmesh/reference.md +492 -0
  19. create_leafmesh/templates/configs/config.yaml +1028 -0
  20. create_leafmesh/templates/docker-compose.yml +28 -0
  21. create_leafmesh/templates/dockerignore +17 -0
  22. create_leafmesh/templates/env +109 -0
  23. create_leafmesh/templates/gitignore +33 -0
  24. create_leafmesh/templates/hitl_stub_receiver.py +149 -0
  25. create_leafmesh/templates/main.py +105 -0
  26. create_leafmesh/templates/requirements.txt +10 -0
  27. create_leafmesh-2.1.0.dist-info/METADATA +6 -0
  28. create_leafmesh-2.1.0.dist-info/RECORD +31 -0
  29. create_leafmesh-2.1.0.dist-info/WHEEL +5 -0
  30. create_leafmesh-2.1.0.dist-info/entry_points.txt +2 -0
  31. create_leafmesh-2.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,2049 @@
1
+ ---
2
+ name: leafmesh
3
+ description: Wire multi-agent meshes, HITL webhooks, can_call routing, decorators, and YAML config for the LeafMesh SDK. Use when adding agents, configuring flows, or debugging mesh issues.
4
+ allowed-tools: Read, Grep, Glob, Edit, Write, Bash
5
+ ---
6
+
7
+ # LeafMesh SDK Development Skill
8
+
9
+ You are an expert at building multi-agent orchestration systems with the LeafMesh SDK.
10
+
11
+ ## When the user asks you to... do this:
12
+
13
+ | User says | Action |
14
+ |-----------|--------|
15
+ | "Add a new LLM agent" | 1. Add YAML block in `configs/config.yaml` 2. Create `agency/<name>_agent.py` (optional -- pure YAML works) 3. Wire `can_call` from upstream agents 4. Add entry point if needed |
16
+ | "Add a programmatic agent" | 1. Add YAML block with `agent_type: "programmatic"` 2. If connector-only: add `integration` + `connector_config` (no Python needed) 3. If Python logic: create `agency/<name>_agent.py` 4. Wire `can_call` |
17
+ | "Add an external agent" | 1. Add YAML block with `agent_type: "external"`, `framework`, and `connector_config` (no Python needed) 2. Optionally add `agency/<name>_agent.py` to post-process connector result 3. Wire `can_call` |
18
+ | "Integrate with Zapier/n8n/Composio/MCP" | Programmatic: `integration: "zapier"` + `connector_config`. External: `framework: "n8n"` + `connector_config`. Pre-compose helper: `@pre_compose(context_processor=zapier(...))`. See reference.md for all connector fields. |
19
+ | "Set up HITL / human review" | Pick **one** `human_interface`: `default` (inbox), `webhook` (channels/HTTP), or `api` (Python callback). Each one accepts a *different* set of config fields — see HITL section below. Mixing them (e.g. `default` + `webhook_config`) is rejected at YAML load. |
20
+ | "Connect agents" / "wire routing" | Add `can_call` entries with conditions. Use `calling_agent_response.field` in conditions. |
21
+ | "Add a tool" | Create `@global_tool` in `agency/tools.py`, add tool name to agent's `tools:` list in YAML |
22
+ | "Fan-out / fan-in" | Add multiple agents in `can_call` (fan-out), add `wait_for` expression on aggregator (fan-in) |
23
+ | "Schedule an agent" | Add `wake_up: "cron expression"` to agent YAML |
24
+ | "Debug why agent X isn't called" | Check `can_call` conditions, verify `calling_agent_response` fields match, check `communication_type` |
25
+ | "Validate my config" | POST the config to `/api/yaml/validate` or read `configs/config.yaml` and check structure |
26
+
27
+ ## Current project config
28
+ !`cat configs/config.yaml 2>/dev/null | head -30`
29
+
30
+ ## How This Project Works
31
+
32
+ ```
33
+ configs/config.yaml <- Agent definitions, mesh topology, entry points, HITL config
34
+ agency/*_agent.py <- Agent logic (auto-discovered by filename match)
35
+ agency/tools.py <- Custom tools (@global_tool, @tool)
36
+ main.py <- Entry point: loads config, starts mesh + API server
37
+ hitl_stub_receiver.py <- Webhook stub for testing HITL locally
38
+ .env <- API keys, Redis, license key
39
+ ```
40
+
41
+ **Auto-discovery**: The SDK matches Python function names to YAML agent names. `greeter_agent()` in `agency/greeter_agent.py` binds to the `greeter_agent:` block in `config.yaml`.
42
+
43
+ **Execution flow**: Entry point -> agent function runs -> SDK handles mesh communication (can_call), session state, Redis persistence, and observability automatically.
44
+
45
+ ## Core SDK Pattern
46
+
47
+ ```python
48
+ from leafmesh import LeafMesh
49
+
50
+ sdk = LeafMesh.from_yaml("configs/config.yaml")
51
+ await sdk.start() # Starts Redis, agent registry, API server, scheduler
52
+ result = await sdk.mesh_call("entry_point_name", input_data={"message": "Hello"}, session_id="optional")
53
+ await sdk.stop()
54
+ ```
55
+
56
+ ## User-Facing APIs
57
+
58
+ The SDK auto-starts a FastAPI server. These are the APIs users call to trigger workflows and interact with the mesh:
59
+
60
+ | Method | Endpoint | Purpose |
61
+ |--------|----------|---------|
62
+ | POST | `/api/mesh/request` | Trigger a workflow via entry point |
63
+ | POST | `/api/mesh/stream` | SSE stream of LLM response via entry point |
64
+ | POST | `/webhook/{entry_point}` | Webhook: new task OR HITL human response |
65
+ | POST | `/callback/{agent_name}` | Connector callback: async response from external system (n8n, Zapier, etc.) |
66
+ | GET | `/api/mesh/entry_points` | List available entry points |
67
+ | GET | `/api/webhook/secret` | Get HMAC signing secret for webhooks |
68
+ | POST | `/api/yaml/validate` | Validate a full config (for frontend editors) |
69
+ | POST | `/api/sessions/{session_id}/agents/{agent_name}/rerun` | Re-run an agent in an existing session, with optional feedback / new input |
70
+ | GET | `/health` | Health check |
71
+ | GET | `/docs` | Interactive API docs (ReDoc) |
72
+
73
+ ### Triggering a workflow
74
+
75
+ ```bash
76
+ # Via API
77
+ curl -X POST http://127.0.0.1:18820/api/mesh/request \
78
+ -H "Content-Type: application/json" \
79
+ -d '{"entry_point": "greet_user", "data": {"message": "Hello"}}'
80
+
81
+ # Via webhook (external systems: Slack, Zapier, n8n, etc.)
82
+ curl -X POST http://127.0.0.1:18820/webhook/greet_user \
83
+ -H "Content-Type: application/json" \
84
+ -H "X-LeafMesh-Signature: sha256=<hmac>" \
85
+ -d '{"message": "Hello"}'
86
+ ```
87
+
88
+ ### Webhook smart routing
89
+
90
+ The webhook endpoint routes automatically based on the payload:
91
+
92
+ | Payload | Behavior |
93
+ |---------|----------|
94
+ | No `session_id` | **New task** -- routes to the entry point's target agent |
95
+ | `session_id` + agent is paused (HITL) | **Resume** -- delivers human response to waiting agent |
96
+ | `session_id` + agent is busy (mid-chain) | **Rejected** -- returns `status: "busy"` |
97
+ | `session_id` + agent is idle | **New task on same session** -- preserves conversation history |
98
+
99
+ ### HMAC webhook signing
100
+
101
+ ```bash
102
+ # Get secret
103
+ SECRET=$(curl -s http://127.0.0.1:18820/api/webhook/secret | jq -r .secret)
104
+
105
+ # Sign a payload
106
+ BODY='{"session_id": "sess1", "decision": "approved", "message": "Looks good"}'
107
+ SIG=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
108
+
109
+ # Send signed webhook
110
+ curl -X POST http://127.0.0.1:18820/webhook/greet_user \
111
+ -H "Content-Type: application/json" \
112
+ -H "X-LeafMesh-Signature: sha256=$SIG" \
113
+ -d "$BODY"
114
+ ```
115
+
116
+ ### Rerunning an agent
117
+
118
+ Re-run a single agent inside an existing session, optionally with feedback so it can self-correct (`leafmesh >= 1.0.299`). Use this for a "Rerun" button in your UI, custom retry rules outside Manager analysis, or debugging from a script.
119
+
120
+ ```python
121
+ # Python — same input as last time, no feedback
122
+ result = await sdk.rerun_agent(
123
+ agent_name="advisor_agent",
124
+ session_id="sess-123",
125
+ )
126
+
127
+ # Python — with feedback (caller spotted a bad shape)
128
+ result = await sdk.rerun_agent(
129
+ agent_name="advisor_agent",
130
+ session_id="sess-123",
131
+ feedback={"error": "missing action_items", "expected_shape": {"action_items": "list"}},
132
+ reason="schema_mismatch",
133
+ )
134
+
135
+ # Python — deliberately steer with new input
136
+ result = await sdk.rerun_agent(
137
+ agent_name="processor_agent",
138
+ session_id="sess-123",
139
+ new_input={"message": "Now check refunds instead", "request_type": "refund"},
140
+ )
141
+ ```
142
+
143
+ ```bash
144
+ # HTTP — same primitive, for non-Python clients (e.g. ADK Studio rerun button)
145
+ curl -X POST http://127.0.0.1:18820/api/sessions/sess-123/agents/advisor_agent/rerun \
146
+ -H "Content-Type: application/json" \
147
+ -d '{"feedback": {"error": "lacks specifics"}, "reason": "user_request"}'
148
+ ```
149
+
150
+ `feedback` is rendered per agent type — LLM agents see a correction note in their prompt, human agents see it in their inbox/channel UI, external connectors receive `data._rerun_context`, programmatic agents receive `input_data._rerun_context`. Both Python and HTTP forms route through `Manager.execute_state` — same conductor as strict yields enforcement (`enforce_yields: true`).
151
+
152
+ Returns dispatch metadata (the agent runs asynchronously — subscribe to events on `session_id` for the result):
153
+
154
+ ```json
155
+ {"status": "dispatched", "agent": "advisor_agent", "session_id": "sess-123", "input_source": "stored_original", "reason": "user_request"}
156
+ ```
157
+
158
+ When `new_input` is omitted, the SDK pulls the agent's most recent stored input from `auto_store_agent_input`. If neither exists, the call raises `LeafMeshError`.
159
+
160
+ ## Agent Types
161
+
162
+ | Type | Use When | LLM? | Pure YAML? | Example |
163
+ |------|----------|------|------------|---------|
164
+ | `llm` | Need AI reasoning, generation, analysis | Yes | Yes | Conversation, research, advisory |
165
+ | `human` | Need human decisions, approvals, HITL review | No | Yes | Approval gates, chat interfaces |
166
+ | `programmatic` | Deterministic logic, API calls, data transforms | No | Yes (with connector) | Data processing, Zapier/n8n actions |
167
+ | `external` | Wrap existing framework (CrewAI, LangGraph, n8n, etc.) | Varies | Yes (with connector) | Framework integration |
168
+
169
+ All agent types work from pure YAML. For programmatic and external agents, a connector (`integration` or `framework` + `connector_config`) can be the entire execution engine -- no Python code needed. The connector response is returned as-is. Optionally add `@sdk.intelligence()` to post-process the connector result.
170
+
171
+ ## STRICT — fields by `agent_type`
172
+
173
+ YAML load rejects fields that don't apply to the declared `agent_type`. Set only the fields that match.
174
+
175
+ | `agent_type` | Allowed type-specific fields |
176
+ |---|---|
177
+ | `llm` | `model`, `prompt`, `temperature`, `max_tokens`, `max_completion_tokens`, `reasoning`, `thinking`, `thinking_budget`, `enable_prompt_caching`, `response_format`, `optimization_strategy`, `context_parts`, `tools`, `tool_choice`, `max_tool_calls_per_message`, `tool_call_timeout`, `allow_parallel_tool_calls`, `tool_categories` |
178
+ | `human` | `human_interface`, `human_timeout_seconds`, `human_context_template`, `human_prompt_template`, `fallback_on_timeout`, `fallback_response`, `require_human_confirmation`, `human_escalation_triggers`, `operator_ids`, `webhook_config`, `channels` |
179
+ | `external` | `framework` (**required**), `connector_config` |
180
+ | `programmatic` | `integration`; `connector_config` allowed only when `integration` is set |
181
+
182
+ **Universal fields** (any type): `name`, `description`, `agent_type`, `communication_type`, `parallel`, `max_concurrent`, `wake_up`, `yields`, `inputs`, `can_call`, `narration`, `wait_for`, `wait_for_timeout`, `auto_store_response`, `auto_store_yields`, `enforce_yields`, `enforce_yields_retry`, `memory`, `knowledge`.
183
+
184
+ **Do not set `is_human_powered` manually** — it's auto-derived from `agent_type` and is silently overwritten by the validator.
185
+
186
+ ## STRICT — `human_interface` rules
187
+
188
+ A human agent picks **exactly one** interface. The fields below depend on which one.
189
+
190
+ | `human_interface` | Path | Required fields | Forbidden together |
191
+ |---|---|---|---|
192
+ | `default` | ADK-Frontend HITL inbox (hosted only) | none | do NOT set `webhook_config` or `channels` — they're ignored at runtime |
193
+ | `webhook` | Outbound HTTP / channel adapters | `webhook_config.outbound_url` OR `channels` (one is enough) | — |
194
+ | `api` / `custom` | Python callback registered via `sdk.register_human_handler()` | none | do NOT set `webhook_config` or `channels` |
195
+
196
+ `channels` only fires when `human_interface: webhook`. Setting `channels` with `default` or `api` is silently ignored at runtime — don't do it.
197
+
198
+ ## Human-in-the-Loop (HITL)
199
+
200
+ The human agent is a full mesh node -- not just an approval step. It participates in the agent chain like any other agent, with `can_call` conditions that route based on context.
201
+
202
+ ### HITL YAML Config — pick **one** of these three blocks
203
+
204
+ > Don't mix interfaces. The validator rejects fields that don't apply to the chosen `human_interface`.
205
+
206
+ #### Option A — `human_interface: webhook` (outbound webhook OR channel adapter)
207
+
208
+ ```yaml
209
+ agents:
210
+ client:
211
+ agent_type: "human"
212
+ human_interface: "webhook" # outbound HTTP / channels
213
+ communication_type: "dual" # respond + wait for inbound response
214
+ human_timeout_seconds: 300
215
+ # operator_ids: ["alice@co.com"] # restrict who sees this in inbox (empty = all)
216
+
217
+ webhook_config:
218
+ outbound_url: "http://127.0.0.1:9999/human-notify"
219
+ outbound_headers: {Content-Type: "application/json"}
220
+ outbound_timeout: 30
221
+ max_retries: 1
222
+ retry_delay: 2
223
+ # inbound_endpoint is auto-derived from entry_points
224
+
225
+ # OR (instead of webhook_config) use a native channel adapter:
226
+ # channels:
227
+ # slack:
228
+ # bot_token: "${SLACK_BOT_TOKEN}"
229
+ # signing_secret: "${SLACK_SIGNING_SECRET}"
230
+ # listen_channels: ["${SLACK_CHANNEL_ID}"]
231
+ # post_channel: "${SLACK_POST_CHANNEL}"
232
+
233
+ can_call:
234
+ - agent: "greeter_agent"
235
+ condition: "not calling_agent_response.from_agent"
236
+ - agent: "processor_agent"
237
+ condition: "calling_agent_response.from_agent == 'greeter_agent'"
238
+
239
+ yields: {request_data: "object"}
240
+ inputs: {user_message: "string"}
241
+
242
+ entry_points:
243
+ - name: "greet_user"
244
+ target: "greeter_agent"
245
+ - name: "human_contact"
246
+ target: "client"
247
+ ```
248
+
249
+ #### Option B — `human_interface: default` (ADK-Frontend HITL inbox, hosted only)
250
+
251
+ ```yaml
252
+ agents:
253
+ client:
254
+ agent_type: "human"
255
+ human_interface: "default" # writes to Redis inbox + stream — that's it
256
+ communication_type: "dual"
257
+ human_timeout_seconds: 300
258
+ # operator_ids: ["alice@co.com"]
259
+ # NO webhook_config, NO channels — they're ignored on this interface
260
+ can_call:
261
+ - agent: "greeter_agent"
262
+ condition: "not calling_agent_response.from_agent"
263
+ - agent: "processor_agent"
264
+ condition: "calling_agent_response.from_agent == 'greeter_agent'"
265
+ yields: {request_data: "object"}
266
+ inputs: {user_message: "string"}
267
+ ```
268
+
269
+ #### Option C — `human_interface: api` (Python callback)
270
+
271
+ ```yaml
272
+ agents:
273
+ client:
274
+ agent_type: "human"
275
+ human_interface: "api" # call into Python — no HTTP, no inbox
276
+ communication_type: "dual"
277
+ human_timeout_seconds: 300
278
+ # NO webhook_config, NO channels
279
+ can_call:
280
+ - agent: "greeter_agent"
281
+ condition: "not calling_agent_response.from_agent"
282
+ yields: {request_data: "object"}
283
+ inputs: {user_message: "string"}
284
+ ```
285
+
286
+ ```python
287
+ # Register the Python handler for human_interface: api
288
+ async def my_human_handler(context, session_id, timeout):
289
+ return {"human_decision": "approved", "human_message": "Looks good"}
290
+ sdk.register_human_handler("client", my_human_handler)
291
+ ```
292
+
293
+ ### HITL Scenarios
294
+
295
+ **Scenario 1 (System-initiated):** System triggers workflow, human reviews mid-flow
296
+ ```
297
+ POST /api/mesh/request {"entry_point": "greet_user", "data": {"message": "..."}}
298
+ -> greeter_agent (LLM) -> client (HITL, outbound webhook sent)
299
+ -> [human reviews, responds via webhook]
300
+ -> from_agent == "greeter_agent" -> processor_agent -> researcher + fallback -> advisor
301
+ ```
302
+
303
+ **Scenario 2 (Human-initiated):** Human contacts mesh first via webhook
304
+ ```
305
+ POST /webhook/human_contact {"message": "I need help with..."}
306
+ -> client (no from_agent -> routes to greeter)
307
+ -> greeter_agent (LLM, dual callback -> client)
308
+ -> client (HITL, outbound webhook sent)
309
+ -> [human reviews, responds via webhook]
310
+ -> from_agent == "greeter_agent" -> processor_agent -> researcher + fallback -> advisor
311
+ ```
312
+
313
+ **Scenario 3 (Same session, new message):** Human sends another message after workflow completes
314
+ ```
315
+ POST /webhook/human_contact {"session_id": "existing-session", "message": "Now check my refund"}
316
+ -> Session not paused -> treated as new request on same session
317
+ -> Conversation history preserved from previous interaction
318
+ ```
319
+
320
+ ### How from_agent Routing Works
321
+
322
+ When an agent calls the human agent, the SDK stores `called_by` in Redis. When the human responds via webhook, the SDK includes `from_agent` in the output data so `can_call` conditions can route based on who called.
323
+
324
+ ```yaml
325
+ # In output_data available to can_call conditions:
326
+ calling_agent_response.from_agent # Who called the human ("greeter_agent" or "")
327
+ calling_agent_response.human_message # What the human said
328
+ calling_agent_response.human_decision # Human's decision field
329
+ calling_agent_response.human_data # Any data from the human
330
+ calling_agent_response.human_initiated # true (always for human output)
331
+ calling_agent_response.source_agent # The human agent name ("client")
332
+ ```
333
+
334
+ ### Channel Adapters (Slack, Telegram, etc.)
335
+
336
+ `channels` only fires when `human_interface: webhook`. Other interfaces silently ignore it. To make a channel actually deliver messages, the human agent must declare the webhook interface:
337
+
338
+ ```yaml
339
+ agents:
340
+ client:
341
+ agent_type: "human"
342
+ human_interface: "webhook" # REQUIRED for channels to fire
343
+ communication_type: "dual"
344
+ channels:
345
+ slack:
346
+ bot_token: "${SLACK_BOT_TOKEN}"
347
+ signing_secret: "${SLACK_SIGNING_SECRET}"
348
+ listen_channels: ["${SLACK_LISTEN_CHANNEL}"]
349
+ post_channel: "${SLACK_POST_CHANNEL}"
350
+ telegram:
351
+ bot_token: "${TELEGRAM_BOT_TOKEN}"
352
+ ```
353
+
354
+ The HITL flow works identically across all channels -- the SDK handles transport, the agent handles routing. With multiple channels configured, the SDK tries each in order; if all fail, it falls back to `webhook_config.outbound_url` (when set).
355
+
356
+ ## YAML Agent Config (All Fields)
357
+
358
+ ```yaml
359
+ agents:
360
+ # ── LLM Agent ──
361
+ my_agent:
362
+ name: my_agent
363
+ agent_type: "llm" # llm | human | programmatic | external
364
+ description: "What this agent does"
365
+ model: "gpt-4o-mini" # Any supported model
366
+ prompt: "You are a helpful..." # System prompt
367
+ temperature: 0.1
368
+ max_tokens: 800
369
+ max_completion_tokens: 1000 # For o1/o3/gpt-5.x reasoning models
370
+ communication_type: "dual" # dual | chain | execute
371
+
372
+ # Mesh routing
373
+ can_call:
374
+ - agent: "next_agent"
375
+ condition: "calling_agent_response.status == 'ready'"
376
+ wait_for: "agent_a AND (agent_b OR agent_c)"
377
+ wait_for_timeout: 60
378
+
379
+ # Tools
380
+ tools: ["word_count", "timestamp"]
381
+ tool_categories: ["data", "utility"]
382
+ tool_choice: "auto"
383
+ max_tool_calls_per_message: 5
384
+ allow_parallel_tool_calls: true
385
+ tool_call_timeout: 30
386
+
387
+ # Features
388
+ reasoning: true # SDK chain-of-thought tool injection (any model)
389
+ thinking: true # Native model thinking (Anthropic/OpenAI o-series/Gemini 2.5+)
390
+ thinking_budget: 8192 # Max thinking tokens (1024-32768)
391
+ enable_prompt_caching: true # Provider-native prompt caching (~90% savings on Anthropic)
392
+ parallel: true # Parallel execution
393
+ max_concurrent: 3 # Max concurrent invocations
394
+ wake_up: "0 9 * * *" # Cron schedule
395
+ optimization_strategy: "performance" # performance | cost | speed
396
+
397
+ # Structured output — force LLM to respond with valid JSON schema
398
+ # response_format:
399
+ # type: "object"
400
+ # properties:
401
+ # summary: { type: "string" }
402
+ # score: { type: "number" }
403
+ # required: ["summary"]
404
+
405
+ # Smart memory
406
+ memory:
407
+ strategy: "hybrid" # recency | relevance | hybrid
408
+ limit: 10
409
+ cross_session: true
410
+ cross_session_limit: 50
411
+ relevance_weight: 0.6
412
+ recency_weight: 0.4
413
+ decay_hours: 24
414
+
415
+ # Yields & inputs
416
+ yields: {summary: string, score: number}
417
+ inputs: {query: string, context: object}
418
+
419
+ # Context parts (shape LLM tone)
420
+ context_parts:
421
+ care: "Be empathetic and patient"
422
+ sentiment_analysis: "Detect user frustration"
423
+ guardrails: "Never discuss competitors"
424
+
425
+ # auto_store_response: true # Auto-store responses in Redis (default: true)
426
+ # auto_store_yields: true # Auto-store yields in Redis (default: true)
427
+
428
+ # ── Programmatic Agent (connector-only, no Python needed) ──
429
+ zapier_agent:
430
+ agent_type: "programmatic"
431
+ integration: "zapier" # zapier | composio | n8n | mcp
432
+ connector_config:
433
+ connection: "google_sheets"
434
+ action: "create_spreadsheet_row"
435
+ api_key: "${ZAPIER_API_KEY}"
436
+ # mode: "callback" # For async workflows
437
+ # callback_timeout: 120
438
+ parallel: true # Parallel execution
439
+ max_concurrent: 3 # Max concurrent invocations
440
+ yields: {status: string}
441
+ inputs: {data: object}
442
+
443
+ # ── External Agent (connector-only, no Python needed) ──
444
+ crew_agent:
445
+ agent_type: "external"
446
+ framework: "crewai" # crewai | langgraph | autogen | a2a | mcp | n8n | zapier | composio | custom
447
+ connector_config:
448
+ endpoint: "http://localhost:9000"
449
+ api_key: "${CREWAI_API_KEY}" # Bearer Token
450
+ # user_api_key: "${CREWAI_USER_API_KEY}" # User Bearer Token (preferred over api_key)
451
+ yields: {result: object}
452
+ inputs: {task: string}
453
+
454
+ # ── Human Agent — webhook interface (with optional channels) ──
455
+ reviewer:
456
+ agent_type: "human"
457
+ human_interface: "webhook" # default | webhook | api | custom
458
+ communication_type: "dual"
459
+ human_timeout_seconds: 300
460
+ # operator_ids: ["alice@co.com"] # restrict inbox (empty = all)
461
+ # fallback_on_timeout: true
462
+ # fallback_response: {decision: "timeout_default", message: "Request timed out"}
463
+ # require_human_confirmation: false
464
+ # human_escalation_triggers: ["urgent", "high_value"]
465
+
466
+ # webhook_config — required when human_interface: webhook AND no channels
467
+ webhook_config:
468
+ outbound_url: "http://127.0.0.1:9999/human-notify"
469
+ outbound_headers: {Content-Type: "application/json"}
470
+ outbound_timeout: 30
471
+ # inbound_endpoint: "/webhook/review" # auto-derived from entry_points
472
+ # inbound_auth_token: "${WEBHOOK_AUTH_TOKEN}"
473
+ max_retries: 1
474
+ retry_delay: 2
475
+ # response_mapping: {user_reply: "response"}
476
+
477
+ # channels — only valid when human_interface: webhook
478
+ channels:
479
+ slack:
480
+ bot_token: "${SLACK_BOT_TOKEN}"
481
+ signing_secret: "${SLACK_SIGNING_SECRET}"
482
+ listen_channels: ["${SLACK_CHANNEL_ID}"]
483
+ post_channel: "${SLACK_POST_CHANNEL}"
484
+
485
+ can_call:
486
+ - agent: "publisher"
487
+ condition: "calling_agent_response.human_decision == 'approved'"
488
+ yields: {decision: string}
489
+ inputs: {request: object}
490
+
491
+ # ── Human Agent — default (inbox) interface — hosted only ──
492
+ # reviewer_inbox:
493
+ # agent_type: "human"
494
+ # human_interface: "default" # writes only to ADK-Frontend HITL inbox
495
+ # communication_type: "dual"
496
+ # human_timeout_seconds: 300
497
+ # # NO webhook_config, NO channels — runtime ignores them
498
+ # can_call: [...]
499
+ # yields: {decision: string}
500
+ # inputs: {request: object}
501
+
502
+ # ── Human Agent — api interface (Python callback) ──
503
+ # reviewer_api:
504
+ # agent_type: "human"
505
+ # human_interface: "api" # routed to a registered Python handler
506
+ # communication_type: "dual"
507
+ # human_timeout_seconds: 300
508
+ # # NO webhook_config, NO channels
509
+ # can_call: [...]
510
+ # yields: {decision: string}
511
+ # inputs: {request: object}
512
+ ```
513
+
514
+ ## Condition Syntax (can_call conditions)
515
+
516
+ Conditions evaluate agent output data:
517
+
518
+ ```yaml
519
+ can_call:
520
+ - agent: "specialist"
521
+ condition: "calling_agent_response.status == 'needs_specialist'"
522
+ - agent: "escalation"
523
+ condition: "calling_agent_response.priority == 'high'"
524
+ - agent: "greeter"
525
+ condition: "not calling_agent_response.from_agent" # Falsy check
526
+ - agent: "processor"
527
+ condition: "calling_agent_response.from_agent == 'greeter_agent'"
528
+ - agent: "default"
529
+ condition: "true" # Always matches
530
+ - agent: "urgent"
531
+ condition: "calling_agent_response.item_count > 0" # Numeric comparison
532
+ ```
533
+
534
+ **Operators**: `==`, `!=`, `>`, `<`, `>=`, `<=`, `and`, `or`, `not`
535
+ **Access**: `calling_agent_response.field_name` for the upstream agent's output
536
+
537
+ ## Decorators -- The 5 Pipelines
538
+
539
+ ### @pre_compose -- Prepare inputs BEFORE the LLM
540
+ ```python
541
+ from leafmesh import pre_compose
542
+
543
+ @pre_compose(
544
+ context_processor=enrich_context, # -> context["prepared_data"]["business_context"]
545
+ input_processor=clean_input, # -> context["prepared_data"]["clean_user_input"]
546
+ others_processor=load_extras, # -> context["prepared_data"]["others"]
547
+ )
548
+ async def my_agent(llm_response, input_data, context):
549
+ prepared = context.get("prepared_data", {})
550
+ return {"result": llm_response}
551
+ ```
552
+
553
+ ### @chain -- Sequential post-processing AFTER the LLM
554
+ ```python
555
+ from leafmesh import chain
556
+
557
+ @chain(validate, format_output)
558
+ async def my_agent(llm_response, input_data, context):
559
+ return {"recommendations": llm_response}
560
+ # Runs: agent() -> validate() -> format_output()
561
+ ```
562
+
563
+ ### @chain_with_results -- Chain that collects intermediate results
564
+ ```python
565
+ from leafmesh import chain_with_results
566
+
567
+ @chain_with_results(step1, step2, step3)
568
+ async def my_agent(llm_response, input_data, context):
569
+ return {"main": llm_response}
570
+ # Returns: {"main_result": ..., "chain_results": [step1_result, step2_result, step3_result]}
571
+ ```
572
+
573
+ ### @conditional_chain -- Chain only if condition is met
574
+ ```python
575
+ from leafmesh import conditional_chain
576
+
577
+ @conditional_chain(
578
+ lambda result, ctx: result.get("needs_review"),
579
+ review_step, approval_step
580
+ )
581
+ async def my_agent(llm_response, input_data, context):
582
+ return {"needs_review": True, "data": llm_response}
583
+ ```
584
+
585
+ ### @compose -- Shape output per downstream agent
586
+ ```python
587
+ from leafmesh import compose
588
+
589
+ @compose(
590
+ billing_agent=lambda result, ctx: {"invoice": result["invoice_id"]},
591
+ support_agent=lambda result, ctx: {"ticket": result["summary"]},
592
+ )
593
+ async def my_agent(llm_response, input_data, context):
594
+ return {"invoice_id": "INV-123", "summary": "Issue resolved"}
595
+ ```
596
+
597
+ **Combining decorators** (order matters -- bottom-up execution):
598
+ ```python
599
+ @chain(validate, score) # 3. Post-process
600
+ @compose(report=shape_report) # 2. Shape per-target
601
+ async def advisor(llm_response, input_data, context):
602
+ return {...} # 1. Agent logic
603
+ ```
604
+
605
+ ## Communication Types
606
+
607
+ | Type | Behavior |
608
+ |------|----------|
609
+ | `dual` | Agent responds immediately, then calls downstream agents asynchronously |
610
+ | `chain` | Routes to downstream agent, waits for its result, returns combined |
611
+ | `execute` | Calls downstream, uses result inline, continues processing |
612
+
613
+ ## Fan-In Patterns (wait_for)
614
+
615
+ ```yaml
616
+ wait_for: "A AND B" # Wait for both
617
+ wait_for: "A OR B" # First one wins (race)
618
+ wait_for: "A AND B?" # A required, B optional
619
+ wait_for: "A AND (B OR C)" # A required + race between B and C
620
+ ```
621
+
622
+ Access upstream yields in agent function:
623
+ ```python
624
+ upstream_yields = input_data.get("upstream_yields", {})
625
+ agent_a_data = upstream_yields.get("agent_a", {})
626
+ ```
627
+
628
+ ## Tools
629
+
630
+ ```python
631
+ from leafmesh import global_tool, tool
632
+
633
+ @global_tool(name="lookup", description="Look up a record", category="data",
634
+ allowed_agents=["researcher_agent"], requires_confirmation=True)
635
+ def lookup(record_id: str) -> dict:
636
+ return {"id": record_id, "data": "..."}
637
+
638
+ @tool(name="format_md", description="Format as markdown")
639
+ def format_md(items: list) -> str:
640
+ return "\n".join(f"- {item}" for item in items)
641
+ ```
642
+
643
+ ## Manager (Coordination + Escalation)
644
+
645
+ ```yaml
646
+ manager:
647
+ enabled: true
648
+ model: "gpt-4o-mini" # Summarizer model
649
+ domain: "generic" # generic | ecommerce | data_analysis
650
+ routing:
651
+ mode: "learning" # static | learning (adaptive routing)
652
+ confidence_threshold: 0.7
653
+ fallback: "all"
654
+ escalation:
655
+ targets:
656
+ - type: "human_agent"
657
+ agent: "client"
658
+ # - type: "webhook"
659
+ # url: "${ESCALATION_WEBHOOK_URL}"
660
+ # - type: "channel"
661
+ # provider: "slack"
662
+ # channel_id: "${ESCALATION_SLACK_CHANNEL}"
663
+ auto_escalate:
664
+ max_retries: 3
665
+ max_errors_per_session: 5
666
+ timeout_threshold: 2
667
+ ```
668
+
669
+ ## LLM Providers
670
+
671
+ | Provider | Model Prefix | Config |
672
+ |----------|-------------|--------|
673
+ | OpenAI | `gpt-`, `o1-`, `o3-`, `o4-` | `OPENAI_API_KEY` env var |
674
+ | Anthropic | `claude-` | `ANTHROPIC_API_KEY` env var |
675
+ | Google | `gemini-` | `GOOGLE_API_KEY` env var |
676
+ | DeepSeek | `deepseek-` | `DEEPSEEK_API_KEY` env var |
677
+ | AWS Bedrock | `bedrock/model-name` | `mesh.bedrock.region` in YAML |
678
+ | Google Vertex | `vertex/model-name` | `mesh.vertex.project` + `location` |
679
+ | Azure Foundry | `foundry/model-name` | `mesh.foundry.endpoint` in YAML |
680
+ | Local (vLLM, Ollama, etc.) | any name | `mesh.local.endpoint` + `server_type` in YAML |
681
+
682
+ ## Building New Agents -- Step by Step
683
+
684
+ 1. **Add YAML config** in `configs/config.yaml` under `agents:`
685
+ 2. **For connector-only agents** (programmatic with `integration` or external with `framework`): done -- no Python needed
686
+ 3. **For agents with custom logic**: create `agency/<name>_agent.py` -- function name must match agent name
687
+ 4. **Add to can_call** of upstream agents that should route to it
688
+ 5. **Add entry point** if it should be directly invocable
689
+ 6. **Restart** the mesh (`python main.py`)
690
+
691
+ ### Common Patterns
692
+
693
+ **Hub-and-spoke**: One router agent that calls specialists based on conditions
694
+ ```yaml
695
+ router_agent:
696
+ can_call:
697
+ - agent: "sales_agent"
698
+ condition: "calling_agent_response.intent == 'sales'"
699
+ - agent: "support_agent"
700
+ condition: "calling_agent_response.intent == 'support'"
701
+ ```
702
+
703
+ **Pipeline**: Linear A -> B -> C chain
704
+ ```yaml
705
+ intake_agent:
706
+ can_call: [{agent: "analyzer_agent"}]
707
+ analyzer_agent:
708
+ can_call: [{agent: "responder_agent"}]
709
+ ```
710
+
711
+ **Fan-out/fan-in**: Parallel processing with aggregation
712
+ ```yaml
713
+ splitter_agent:
714
+ can_call:
715
+ - {agent: "worker_a"}
716
+ - {agent: "worker_b"}
717
+ aggregator_agent:
718
+ wait_for: "worker_a AND worker_b"
719
+ ```
720
+
721
+ **Race pattern**: Multiple approaches, first wins
722
+ ```yaml
723
+ consumer_agent:
724
+ wait_for: "fast_agent OR slow_agent"
725
+ ```
726
+
727
+ **HITL approval gate**: Agent -> human review -> continue or revise
728
+ ```yaml
729
+ draft_agent:
730
+ can_call: [{agent: "reviewer"}]
731
+ reviewer:
732
+ agent_type: "human"
733
+ communication_type: "dual"
734
+ can_call:
735
+ - agent: "publisher"
736
+ condition: "calling_agent_response.human_decision == 'approved'"
737
+ - agent: "draft_agent"
738
+ condition: "calling_agent_response.human_decision == 'revision_needed'"
739
+ ```
740
+
741
+ ## Session & Upstream Yields
742
+
743
+ ```python
744
+ async def my_agent(llm_response, input_data, context):
745
+ upstream = input_data.get("upstream_yields", {})
746
+ caller_data = upstream.get("caller_agent_name", {})
747
+ session_id = context.get("session_id")
748
+ memory_posts = context.get("memory_posts", [])
749
+ prepared = context.get("prepared_data", {})
750
+ ```
751
+
752
+ ## Additional Resources
753
+
754
+ - **[agent-config-fields.md](agent-config-fields.md)** — **Authoritative field reference for every YAML field**. Lists every option for every agent type (`llm`, `human`, `external`, `programmatic`), plus `WebhookConfig`, `ChannelConfig`, `Memory`, `EscalationConfig`, `EscalationTarget`, `LeafMeshConfig`, `ManagerConfig`, `MeshConfig` (Bedrock / Vertex / Foundry / Local), `RedisConfig`, `EvolutionConfig`, `DataStructure`, `Entry Points`, all per-framework `connector_config` schemas, and the **Field Applicability by Agent Type** matrix. Read this file when in doubt about any field, accepted values, default, or what's allowed where.
755
+ - **[reference.md](reference.md)** — SDK Python API (`sdk.start()`, `sdk.mesh_call()`, `@global_tool`, decorators, error classes, env vars, etc.).
756
+ - **[examples.md](examples.md)** — copy-paste agent patterns (HITL, fan-out, hub-and-spoke, race, etc.).
757
+
758
+ > When the user asks "what fields can I put on a programmatic agent?" / "what's the default for `temperature`?" / "what does `wait_for: A AND B?` mean?" / "how do I configure n8n callback mode?" / "what fields does `EscalationTarget` accept?" — the answer lives in **agent-config-fields.md**. Don't guess; quote the file.
759
+
760
+
761
+ ---
762
+
763
+ # Complete Agent Config Field Reference
764
+
765
+ _Inlined from `agent-config-fields.md` — every field, every default, every allowed value._
766
+
767
+ This document lists every configuration field, its type, default value, and accepted values. Use this to build frontend forms, dropdowns, and validation.
768
+
769
+ ---
770
+
771
+
772
+ ## Agent Types
773
+
774
+ | Value | Description |
775
+ |-------|-------------|
776
+ | `llm` | LLM-powered agent (default). Executes via OpenAI, Claude, Bedrock, Vertex, or Foundry. |
777
+ | `human` | Human operator agent. Routes to a person via API, webhook, or channel. |
778
+ | `programmatic` | Python function with business logic. No LLM calls. |
779
+ | `external` | Delegates to an external framework (CrewAI, LangGraph, AutoGen, etc.). |
780
+
781
+ ---
782
+
783
+ ## AgentConfig — Core Fields (All Types)
784
+
785
+ These fields apply to every agent regardless of `agent_type`.
786
+
787
+ | Field | Type | Default | Accepted Values | Required | Description |
788
+ |-------|------|---------|-----------------|----------|-------------|
789
+ | `name` | string | — | any string | **yes** | Unique agent name within the mesh |
790
+ | `description` | string | `null` | any string | no | Agent description and purpose |
791
+ | `agent_type` | string | `"llm"` | `llm`, `human`, `programmatic`, `external` | no | Agent execution type (see note below) |
792
+ | `communication_type` | string | `"dual"` | `dual`, `chain`, `execute` | no | How agent communicates with the mesh |
793
+ | `parallel` | bool | `false` | `true`, `false` | no | Enable parallel processing |
794
+ | `max_concurrent` | int | `null` | 1 – unlimited | no | Max concurrent calls when `parallel: true` (null = unlimited) |
795
+ | `wake_up` | string | `null` | cron expression (e.g. `"0 9 * * *"`) | no | Schedule for periodic wake-up |
796
+ | `yields` | dict | `{}` | key: field name, value: type string or nested object | no | Output schema — what agent produces |
797
+ | `inputs` | dict | `{}` | key: field name, value: type string or nested object | no | Input schema — what agent expects |
798
+ | `can_call` | list | `[]` | list of `{"agent": "name"}` or `{"agent": "name", "condition": "expr"}` | no | Agents this agent can invoke |
799
+ | `narration` | string | `null` | any string (multiline supported) | no | Plain-English routing hints for the Manager — evaluated by the Summarizer when conditions don't cover everything (see [Narration Routing](#narration-routing)) |
800
+ | `wait_for` | string or list | `[]` | agent names or expression string | no | Fan-in/join condition |
801
+ | `wait_for_timeout` | int | `60` | 1 – unlimited (seconds) | no | Hard timeout for fan-in |
802
+ | `auto_store_response` | bool | `true` | `true`, `false` | no | Auto-store responses in Redis |
803
+ | `auto_store_yields` | bool | `true` | `true`, `false` | no | Auto-store yields in Redis |
804
+ | `memory` | bool or dict | `false` | `true`, `false`, or memory config dict | no | Agent memory — see [Memory Config](#memory-config) |
805
+ | `memory_limit` | int | `10` | 1 – 100 | no | Legacy: max recent feed posts (use `memory.limit` instead) |
806
+ | `knowledge` | bool or dict | `false` | `false`, or `{serviceName, enabled, groupName}` | no | Knowledge/RAG — see [Knowledge Config](#knowledge-config) |
807
+ | `enforce_yields` | bool | `false` | `true`, `false` | no | Strictly validate this agent's output against the declared `yields:` schema. `false` (default) fills missing keys with type defaults and logs warnings; `true` triggers a Manager-driven retry up to `enforce_yields_retry` times, then escalates. See [Yields Enforcement](#yields-enforcement). |
808
+ | `enforce_yields_retry` | int | `0` | 0 – unlimited | no | Maximum self-correction attempts when `enforce_yields: true`. `0` fails on first contract violation. Each retry passes the previous output + validation errors as feedback so LLM/external/human/programmatic agents can self-correct. Honored by every agent type. |
809
+
810
+ ### Yields Enforcement
811
+
812
+ `enforce_yields` and `enforce_yields_retry` work together to make `yields:` an enforceable contract on the producer side, without coupling agents to each other's shapes.
813
+
814
+ **Default behavior (`enforce_yields: false`)** — lenient mode, backwards-compatible:
815
+
816
+ - Missing yield keys → filled with type defaults (`""`, `0`, `[]`, `{}`, `false`).
817
+ - Type mismatches → kept verbatim, WARNING logged.
818
+ - `can_call` conditions evaluate on a known shape (no more silent skips on undefined keys).
819
+
820
+ **Strict mode (`enforce_yields: true`)** — for production-critical agents:
821
+
822
+ - On contract violation, the SDK fires a Manager-driven retry through `Manager.execute_state(...)`.
823
+ - Up to `enforce_yields_retry` attempts. Each retry sees the previous (wrong) output + validation errors as `_rerun_context`:
824
+ - **LLM agents** — prompt builder appends a correction note.
825
+ - **Human agents** — outbound payload exposes `_rerun_context`; inbox/channel UI surfaces what's needed.
826
+ - **External connectors** — `data._rerun_context` is added to the workflow payload.
827
+ - **Programmatic agents** — `input_data._rerun_context` is available alongside the (possibly Summarizer-corrected) inputs.
828
+ - After the retry budget is exhausted, fires `AGENT_ERROR` with `error_type="YieldContractFailure"` + `retry_exhausted=true`. Routes through `manager.escalation:` if configured.
829
+
830
+ **Example:**
831
+
832
+ ```yaml
833
+ agents:
834
+ client:
835
+ agent_type: human
836
+ human_interface: webhook
837
+ yields:
838
+ request_data: object
839
+ decision: string
840
+ enforce_yields: true # strict
841
+ enforce_yields_retry: 3 # 3 self-correction attempts before escalating
842
+ ```
843
+
844
+ **Programmatic agents are retryable too** — the Summarizer can inspect the failure and produce a `corrected_input` (e.g. fix `"ORGANIZATION"` → `"Organization"`). The retry runs the same deterministic function with the corrected input and produces a different result. See [Manager — Rerun Flow](../core-concepts/manager#rerun) for the full path.
845
+
846
+ ### Changing `agent_type`
847
+
848
+ You can change an agent's type via `PATCH /api/yaml/agents/{name}`. When `agent_type` changes, the ADK automatically **removes fields that don't belong to the new type**:
849
+
850
+ | Switching away from... | Fields removed |
851
+ |------------------------|----------------|
852
+ | `llm` | `model`, `prompt`, `temperature`, `max_tokens`, `max_completion_tokens`, `reasoning`, `thinking`, `thinking_budget`, `tools`, `tool_categories`, `context_parts`, `tool_choice`, `response_format` |
853
+ | `human` | `webhook_config`, `human_interface`, `human_timeout_seconds`, `channels`, `is_human_powered` |
854
+ | `external` | `framework`, `connector_config` |
855
+ | `programmatic` | `integration` |
856
+
857
+ This prevents stale configuration from the old type interfering with the new type's execution.
858
+
859
+ ### `yields` and `inputs` Type Strings
860
+
861
+ Values can be a simple type string (flat field) or a nested object definition.
862
+
863
+ **Flat fields** — value is a type string:
864
+
865
+ | Type String | Description |
866
+ |-------------|-------------|
867
+ | `"string"` | Text value |
868
+ | `"number"` | Numeric value |
869
+ | `"boolean"` | True/false |
870
+ | `"list"` | Array/list |
871
+ | `"object"` | Dictionary/object (unstructured) |
872
+
873
+ **Nested fields** — value is an object with `type` and `fields`:
874
+
875
+ ```yaml
876
+ inputs:
877
+ # Flat fields
878
+ name: string
879
+ email: string
880
+
881
+ # Nested object with defined sub-fields
882
+ arguments:
883
+ type: object
884
+ fields:
885
+ summary: string
886
+ start_datetime: string
887
+ end_datetime: string
888
+ timezone: string
889
+
890
+ yields:
891
+ # Flat
892
+ status: string
893
+
894
+ # Nested
895
+ data:
896
+ type: object
897
+ fields:
898
+ result: string
899
+ metadata: object
900
+ ```
901
+
902
+ When a field uses the nested format, the frontend renders individual sub-field editors instead of a single text input. This is useful for external framework integrations (Composio, Zapier, etc.) where the request body has a known structure.
903
+
904
+ ### `wait_for` Expression Syntax
905
+
906
+ ```yaml
907
+ # Simple list — all required (AND)
908
+ wait_for:
909
+ - agent_a
910
+ - agent_b
911
+
912
+ # Expression strings
913
+ wait_for: "agent_a AND agent_b" # Wait for both
914
+ wait_for: "agent_a OR agent_b" # Wait for first
915
+ wait_for: "agent_a AND agent_b?" # agent_b is optional (? suffix)
916
+ wait_for: "agent_a AND (agent_b OR agent_c)" # Nested logic
917
+ wait_for: "agent_a AND (agent_b OR agent_c) AND agent_d?" # Complex expression
918
+ ```
919
+
920
+ ### `can_call` Format
921
+
922
+ ```yaml
923
+ # Simple
924
+ can_call:
925
+ - agent: "next_agent"
926
+
927
+ # With condition
928
+ can_call:
929
+ - agent: "next_agent"
930
+ - agent: "conditional_agent"
931
+ condition: "calling_agent_response.status == 'ready'"
932
+ ```
933
+
934
+ ### Narration Routing
935
+
936
+ `narration` is an agent-level field for routing hints you **can't express as conditions**. Conditions handle definitive routes ("if category is billing, call billing_agent"). Narration handles non-definitive routes ("if the customer sounds frustrated and mentions cancelling, maybe call retention_agent").
937
+
938
+ ```yaml
939
+ agents:
940
+ triage:
941
+ yields:
942
+ category: "string"
943
+ urgency: "number"
944
+ can_call:
945
+ - agent: "billing_agent"
946
+ condition: "category == 'billing'"
947
+ - agent: "technical_agent"
948
+ condition: "category == 'technical'"
949
+ narration: >
950
+ If the customer mentions cancelling their subscription, route to retention_agent.
951
+ If the customer mentions a competitor by name, route to win_back_agent.
952
+ If the customer asks about enterprise plans, route to sales_agent.
953
+ ```
954
+
955
+ **How it works:**
956
+
957
+ 1. Conditions are evaluated first by the control plane (AST, instant, deterministic)
958
+ 2. Condition targets are dispatched immediately
959
+ 3. The Summarizer — which already analyzes every agent output via LLM — sees the narration in its prompt context
960
+ 4. The Summarizer's `next_agents` recommendation now reflects both condition-routed agents and narration-suggested agents
961
+ 5. The Manager compares `next_agents` against what conditions already dispatched, and calls the difference
962
+
963
+ **Key rules:**
964
+
965
+ - Conditions are the authority — narration never overrides a condition result
966
+ - Narration targets are **additive** — they add to condition targets, never remove
967
+ - Narration can reference **any agent** in the mesh, not just those in `can_call`
968
+ - No narration = zero overhead (the Summarizer's prompt is unchanged)
969
+ - If the Manager is disabled, narrations are ignored
970
+
971
+ See **[Manager — Narration Routing](../core-concepts/manager#narration-routing)** and **[Message Routing](../messages/routing#narration-routing)** for the full flow.
972
+
973
+ ### Knowledge Config
974
+
975
+ `knowledge` enables RAG-powered context injection from a vector database. The agent gets both pre-call injection (automatic) and a `query_knowledge` tool (on-demand) — same dual pattern as memory.
976
+
977
+ ```yaml
978
+ agents:
979
+ support_agent:
980
+ knowledge:
981
+ serviceName: "mongo_main"
982
+ enabled: true
983
+ groupName: "product_docs"
984
+ ```
985
+
986
+ | Field | Type | Required | Default | Description |
987
+ |-------|------|----------|---------|-------------|
988
+ | `serviceName` | string | yes | — | Provider name (configured via Knowledge API, stored in Redis) |
989
+ | `enabled` | bool | no | `true` | Enable/disable knowledge for this agent |
990
+ | `groupName` | string | no | `null` | Query a specific group. Omit to query all groups. |
991
+
992
+ **Key points:**
993
+
994
+ - Provider connection details (connection strings, API keys, embedding model) are configured via the Knowledge API, not in YAML
995
+ - When enabled, both retrieval paths are active automatically — no mode flag
996
+ - `query_knowledge` tool is stripped from agents without knowledge enabled
997
+ - The Manager can also have knowledge for SOP awareness (configured under `manager.knowledge`)
998
+
999
+ See **[Manager — Narration Routing](../core-concepts/manager#narration-routing)** for how knowledge integrates with the Summarizer.
1000
+
1001
+ ---
1002
+
1003
+ ## LLM Agent Fields (`agent_type: "llm"`)
1004
+
1005
+ These fields are used when `agent_type` is `"llm"`. Ignored for other types.
1006
+
1007
+ | Field | Type | Default | Accepted Values | Description |
1008
+ |-------|------|---------|-----------------|-------------|
1009
+ | `model` | string | `"gpt-4o-mini"` | see [Model List](#model-list) below | LLM model name |
1010
+ | `prompt` | string | `null` | any string (multiline supported) | System prompt |
1011
+ | `temperature` | float | `0.1` | `0.0` – `2.0` | LLM temperature (creativity/randomness) |
1012
+ | `max_tokens` | int | `800` | 1 – model max | Max output tokens (legacy models) |
1013
+ | `max_completion_tokens` | int | `null` | 1 – model max | Max completion tokens (o1, gpt-5.x models) |
1014
+ | `reasoning` | bool | `false` | `true`, `false` | Enable ADK-level chain-of-thought reasoning (tool injection — works with any model) |
1015
+ | `thinking` | bool | `false` | `true`, `false` | Enable native model-level extended thinking (requires model support — see below) |
1016
+ | `thinking_budget` | int | `null` | 1024 – 32768 (tokens) | Max thinking tokens. Provider defaults apply when omitted. |
1017
+ | `enable_prompt_caching` | bool | `false` | `true`, `false` | Enable provider-native prompt caching for cost reduction (see below) |
1018
+ | `response_format` | dict | `null` | JSON Schema object | Structured output — forces LLM to respond with valid JSON matching this schema |
1019
+ | `optimization_strategy` | string | `null` | `performance`, `cost`, `speed` | Per-agent model selection strategy |
1020
+ | `context_parts` | dict | `null` | see below | Optional context parts |
1021
+ | `tools` | list | `[]` | tool name strings | Available tools |
1022
+ | `tool_choice` | string | `"auto"` | `auto`, `none`, or specific tool name | Tool selection strategy |
1023
+ | `max_tool_calls_per_message` | int | `5` | 0 – 20 | Max tool calls per LLM message |
1024
+ | `tool_call_timeout` | float | `30.0` | 0.1 – 300 (seconds) | Tool execution timeout |
1025
+ | `allow_parallel_tool_calls` | bool | `true` | `true`, `false` | Allow parallel tool execution |
1026
+ | `tool_categories` | list | `[]` | category name strings | Tool categories agent can access |
1027
+
1028
+ ### `context_parts` Keys
1029
+
1030
+ Each key is injected as a separate system message with a bracketed label, in the order below. Custom keys are also supported — they receive an auto-generated label from their name (`MY_KEY` → `[MY KEY]`).
1031
+
1032
+ | Key | Label injected | Description |
1033
+ |-----|---------------|-------------|
1034
+ | `care` | `[EMPATHY & TONE]` | Warmth/empathy instructions — shapes how the agent expresses itself |
1035
+ | `sentiment_analysis` | `[SENTIMENT ANALYSIS]` | Tone detection instructions — tells the agent to read user mood |
1036
+ | `guardrails` | `[SAFETY GUARDRAILS]` | Safety and compliance rules — what the agent must never do |
1037
+ | `flows` | `[FLOW INSTRUCTIONS]` | **Per-caller routing behaviour** — what the agent should do differently depending on who called it and where in the mesh it is |
1038
+
1039
+ Values are free text strings. All keys are optional — use any combination.
1040
+
1041
+ ```yaml
1042
+ context_parts:
1043
+ care: |
1044
+ Always respond with empathy. Acknowledge frustration before solving.
1045
+ guardrails: |
1046
+ Never share internal system details. No PII disclosure.
1047
+ flows: |
1048
+ When called from the entry point (no from_agent):
1049
+ - This is a new user. Greet warmly and gather requirements.
1050
+ When called from client (human agent):
1051
+ - The human has already responded. Don't re-greet. Summarise and proceed.
1052
+ When called from scheduler_agent:
1053
+ - This is a scheduled run. Skip greeting, produce a structured summary.
1054
+ ```
1055
+
1056
+ ### Model List
1057
+
1058
+ **OpenAI:**
1059
+ - `gpt-4o-mini`, `gpt-4o`
1060
+ - `gpt-4.1`, `gpt-4.1-mini`, `gpt-4.1-nano`
1061
+ - `gpt-5.1`, `gpt-5.2`
1062
+ - `o1`, `o1-mini`, `o3`, `o3-mini`, `o4-mini`
1063
+
1064
+ **Anthropic:**
1065
+ - `claude-opus-4-6`, `claude-sonnet-4-6`
1066
+ - `claude-haiku-4-5-20251001`
1067
+
1068
+ **Google:**
1069
+ - `gemini-2.0-flash`, `gemini-2.5-pro`
1070
+
1071
+ **DeepSeek:**
1072
+ - `deepseek-chat`, `deepseek-reasoner`
1073
+
1074
+ **AWS Bedrock:**
1075
+ - Any Bedrock model ID (e.g. `anthropic.claude-3-sonnet-20240229-v1:0`, `amazon.titan-text-premier-v1:0`)
1076
+ - Requires `mesh.bedrock` config
1077
+
1078
+ **Google Vertex AI:**
1079
+ - Any Vertex model ID (e.g. `gemini-1.5-pro`)
1080
+ - Requires `mesh.vertex` config
1081
+
1082
+ **Azure Foundry:**
1083
+ - Any Azure deployment name
1084
+ - Requires `mesh.foundry` config
1085
+
1086
+ **Local Models (vLLM, SGLang, Ollama, llama.cpp, etc.):**
1087
+ - Any model name supported by your local server
1088
+ - Requires `mesh.local` config (or `LOCAL_MODEL_ENDPOINT` env var)
1089
+ - See [LocalModelConfig](#localmodelconfig) for server setup
1090
+
1091
+ ### `reasoning` vs `thinking`
1092
+
1093
+ These are two **separate** features:
1094
+
1095
+ | Feature | `reasoning: true` | `thinking: true` |
1096
+ |---------|-------------------|-------------------|
1097
+ | **What it does** | ADK injects chain-of-thought tool calls into the prompt | Enables provider-native extended thinking/reasoning |
1098
+ | **Works with** | Any model (tool injection) | Only models that support native thinking |
1099
+ | **Token cost** | Tool call overhead | Dedicated thinking tokens (billed as output) |
1100
+ | **Quality** | Good for structured reasoning | Best quality — model's internal reasoning |
1101
+
1102
+ You can use both together — `reasoning` adds ADK tools while `thinking` enables native model thinking.
1103
+
1104
+ ### Native Thinking — Provider Support
1105
+
1106
+ | Provider | Model Requirement | Behavior |
1107
+ |----------|------------------|----------|
1108
+ | **Anthropic** | Claude Sonnet 4.6, Opus 4.6 | Adaptive/extended thinking with `budget_tokens` |
1109
+ | **OpenAI** | o1, o3, o3-mini, o4-mini | `reasoning.effort` parameter (low/medium/high) |
1110
+ | **Google** | Gemini 2.5+, Gemini 3.x | `thinkingConfig` with `thinkingBudget` |
1111
+ | **DeepSeek** | DeepSeek-R1 | Native chain-of-thought (auto-enabled for R1 models) |
1112
+ | **Bedrock** | Claude models on Bedrock | Extended thinking with `budgetTokens` |
1113
+ | **Vertex** | Claude + Gemini on Vertex | Both thinking APIs supported |
1114
+ | **Foundry** | Azure o-series models | `reasoning.effort` parameter |
1115
+ | **Local** | Depends on model/server | Passthrough if server supports it |
1116
+
1117
+ ### Prompt Caching — Provider Support
1118
+
1119
+ | Provider | How it works | Savings |
1120
+ |----------|-------------|---------|
1121
+ | **Anthropic** | `cache_control: ephemeral` on system prompt + tools | ~90% on cached reads |
1122
+ | **Bedrock** | `promptCaching` parameter | ~90% on cached reads |
1123
+ | **Vertex (Claude)** | `cache_control: ephemeral` on system prompt | ~90% on cached reads |
1124
+ | **OpenAI** | Automatic — no config needed (stats in response) | ~50% on cached |
1125
+ | **Google** | Context caching API (requires separate setup) | Varies |
1126
+
1127
+ ### `response_format` — Structured Output
1128
+
1129
+ Forces the LLM to respond with valid JSON matching a JSON Schema. Supported across all providers — each provider translates the schema to its native structured output API.
1130
+
1131
+ ```yaml
1132
+ agents:
1133
+ data_extractor:
1134
+ agent_type: llm
1135
+ model: gpt-4o
1136
+ prompt: "Extract structured data from the user's message."
1137
+ response_format:
1138
+ type: json_schema
1139
+ json_schema:
1140
+ name: extracted_data
1141
+ strict: true
1142
+ schema:
1143
+ type: object
1144
+ properties:
1145
+ name:
1146
+ type: string
1147
+ email:
1148
+ type: string
1149
+ priority:
1150
+ type: string
1151
+ enum: [low, medium, high]
1152
+ required: [name, email, priority]
1153
+ additionalProperties: false
1154
+ ```
1155
+
1156
+ | Provider | Native API used |
1157
+ |----------|----------------|
1158
+ | **OpenAI** | `response_format` parameter (structured outputs) |
1159
+ | **Anthropic** | Tool-based JSON extraction with schema |
1160
+ | **Google** | `response_schema` in generation config |
1161
+ | **DeepSeek** | `response_format` parameter |
1162
+ | **Bedrock** | Schema injected into system prompt |
1163
+ | **Foundry** | `response_format` parameter |
1164
+ | **Local** | `response_format` passthrough |
1165
+
1166
+ ### Example
1167
+
1168
+ ```yaml
1169
+ agents:
1170
+ analyst:
1171
+ agent_type: llm
1172
+ model: claude-sonnet-4-6
1173
+ thinking: true # native model thinking
1174
+ thinking_budget: 8192 # max 8K thinking tokens
1175
+ reasoning: true # also inject ADK chain-of-thought tools
1176
+ enable_prompt_caching: true # cache system prompt + tools
1177
+ prompt: |
1178
+ You are a data analyst. Analyze the provided data thoroughly.
1179
+ ```
1180
+
1181
+ ---
1182
+
1183
+ ## Human Agent Fields (`agent_type: "human"`)
1184
+
1185
+ These fields are used when `agent_type` is `"human"`. `is_human_powered` is auto-set to `true`.
1186
+
1187
+ | Field | Type | Default | Accepted Values | Description |
1188
+ |-------|------|---------|-----------------|-------------|
1189
+ | `is_human_powered` | bool | `false` | `true`, `false` | Auto-synced to `true` when agent_type="human" |
1190
+ | `human_interface` | string | `"api"` | `default`, `api`, `webhook`, `custom` | How human receives/submits input (see below) |
1191
+ | `human_timeout_seconds` | int | `300` | 1 – 3600 (seconds) | Human response timeout |
1192
+ | `human_context_template` | string | `null` | any string | Template for presenting context to human |
1193
+ | `human_prompt_template` | string | `null` | any string | Template for human prompts |
1194
+ | `fallback_on_timeout` | bool | `true` | `true`, `false` | Use fallback response when human doesn't respond |
1195
+ | `fallback_response` | dict | `null` | arbitrary JSON | Default response on human timeout |
1196
+ | `require_human_confirmation` | bool | `false` | `true`, `false` | Require approval before proceeding |
1197
+ | `human_escalation_triggers` | list | `[]` | free text strings | Conditions triggering human escalation |
1198
+ | `operator_ids` | list | `[]` | list of strings (email or ID) | Operators who can see this agent's HITL requests. Empty = broadcast (all operators see it). |
1199
+ | `webhook_config` | object | `null` | see [WebhookConfig](#webhookconfig) | Webhook settings (required for `webhook` interface) |
1200
+ | `channels` | dict | `{}` | see [ChannelConfig](#channelconfig) | Native channel adapters (Slack, Telegram, etc.) |
1201
+
1202
+ ### `human_interface` — Interface Types
1203
+
1204
+ | Value | Description | How it works |
1205
+ |-------|-------------|-------------|
1206
+ | `default` | **ADK-Frontend HITL Inbox** (recommended) | Writes request to Redis, emits stream event. ADK-Frontend renders an inbox with conversation thread. Human replies via the UI. Supports parallel requests per session. |
1207
+ | `webhook` | **External webhook** | POSTs request to `webhook_config.outbound_url`. Human responds via inbound webhook endpoint. Also supports native channel adapters (Slack, Telegram, etc.). |
1208
+ | `api` | **Python callback** | Calls a Python handler registered via `sdk.register_human_handler()`. No outbound HTTP. Used for custom integrations and testing. |
1209
+ | `custom` | **Custom handler** | Same as `api` — uses the registered `human_interface_handler` callback. |
1210
+
1211
+ > **Note:** `default` is only available on the LeafMesh hosted platform. For self-hosted deployments, use `webhook` with your own `outbound_url`, or `api` with a Python callback.
1212
+
1213
+ ### Example — Default (ADK-Frontend Inbox)
1214
+
1215
+ ```yaml
1216
+ agents:
1217
+ support_human:
1218
+ agent_type: human
1219
+ human_interface: default # ADK-Frontend inbox
1220
+ human_timeout_seconds: 300
1221
+ yields:
1222
+ resolution: string
1223
+ action_taken: string
1224
+ ```
1225
+
1226
+ ### Example — Webhook with Channel Adapter
1227
+
1228
+ ```yaml
1229
+ agents:
1230
+ support_human:
1231
+ agent_type: human
1232
+ human_interface: webhook
1233
+ human_timeout_seconds: 600
1234
+ webhook_config:
1235
+ outbound_url: "https://my-app.com/api/human-requests"
1236
+ channels:
1237
+ slack:
1238
+ bot_token: "${SLACK_BOT_TOKEN:}"
1239
+ signing_secret: "${SLACK_SIGNING_SECRET:}"
1240
+ post_channel: "C123456"
1241
+ ```
1242
+
1243
+ ### Two Scenarios for Human Agent Sessions
1244
+
1245
+ Human agents support two interaction patterns via the same webhook endpoint:
1246
+
1247
+ **Scenario 1 — Resume (session_id present):** When a POST to `/webhook/{entry_point}` includes a `session_id` that matches a pending HITL request, the ADK resumes that session. The operator's response is routed via `can_call` to the next agent.
1248
+
1249
+ **Scenario 2 — New (no session_id or not found):** When a POST has no `session_id` or the session has no pending expectation, the ADK creates a new workflow. If the human agent has no upstream caller, the operator's message is immediately routed via `can_call` — no HITL pending step is created.
1250
+
1251
+ ---
1252
+
1253
+ ## External Agent Fields (`agent_type: "external"`)
1254
+
1255
+ These fields are used when `agent_type` is `"external"`.
1256
+
1257
+ | Field | Type | Default | Accepted Values | Required | Description |
1258
+ |-------|------|---------|-----------------|----------|-------------|
1259
+ | `framework` | string | `null` | `crewai`, `langgraph`, `autogen`, `a2a`, `mcp`, `zapier`, `composio`, `n8n`, `custom` | **yes** | External framework name |
1260
+ | `connector_config` | dict | `{}` | framework-specific key-values | no | Connection configuration — passed as `**kwargs` to the connector |
1261
+
1262
+ > **One agent = one action/workflow.** Each agent targets one specific endpoint, graph, tool, or action. Create multiple agents with different `connector_config` values to call different workflows.
1263
+ >
1264
+ > All `connector_config` fields can also be overridden per-call via `request.connector_config` at runtime.
1265
+
1266
+ ### Common connector_config fields (all frameworks)
1267
+
1268
+ These fields are available on **every** connector type:
1269
+
1270
+ | Field | Type | Default | Description |
1271
+ |-------|------|---------|-------------|
1272
+ | `mode` | string | `"sync"` | Execution mode: `"sync"` (wait for HTTP response) or `"callback"` (fire request, wait for external system to POST back) |
1273
+ | `callback_timeout` | float | `120.0` | Seconds to wait for a callback response before timing out (only used when `mode: "callback"`) |
1274
+
1275
+ #### Sync vs Callback Mode
1276
+
1277
+ **Sync mode** (default): The connector POSTs to the external system and holds the HTTP connection open until the response arrives. This works when the external system returns the actual result in the same HTTP response.
1278
+
1279
+ **Callback mode**: The connector POSTs to the external system with a `_leafmesh_callback_url` and `_leafmesh_session_id` injected into the payload. The connector then blocks internally until the external system POSTs the result back to `/callback/{agent_name}`. Use this when:
1280
+ - The external workflow takes longer than the HTTP timeout
1281
+ - The external system uses "fire and forget" (e.g., n8n's "Respond Immediately" mode)
1282
+ - You need the external system to process asynchronously and deliver results later
1283
+
1284
+ **How external systems use callbacks:**
1285
+
1286
+ The LeafMesh connector injects these fields into the outbound payload:
1287
+ - `_leafmesh_callback_url` — the URL to POST the result back to
1288
+ - `_leafmesh_session_id` — the session ID to include in the callback
1289
+
1290
+ The external system should POST to `_leafmesh_callback_url` with a JSON body containing:
1291
+ ```json
1292
+ {
1293
+ "session_id": "<the _leafmesh_session_id value>",
1294
+ "result": { ... }
1295
+ }
1296
+ ```
1297
+
1298
+ ### connector_config fields per framework
1299
+
1300
+ #### crewai
1301
+
1302
+ | Field | Type | Default | Required | Env Fallback | Description |
1303
+ |-------|------|---------|----------|-------------|-------------|
1304
+ | `endpoint` | string | `""` | **yes** | `CREWAI_ENDPOINT` | HTTP endpoint for deployed CrewAI crew |
1305
+ | `api_key` | string | `""` | no | `CREWAI_API_KEY` | Bearer Token for authentication |
1306
+ | `user_api_key` | string | `""` | no | `CREWAI_USER_API_KEY` | User Bearer Token (preferred over `api_key` when both are set) |
1307
+ | `poll_interval` | float | `2.0` | no | — | Seconds between status polls |
1308
+ | `max_poll_seconds` | float | `300.0` | no | — | Max total polling time (seconds) |
1309
+ | `http_timeout` | float | `30.0` | no | — | HTTP request timeout (seconds) |
1310
+
1311
+ #### langgraph
1312
+
1313
+ | Field | Type | Default | Required | Env Fallback | Description |
1314
+ |-------|------|---------|----------|-------------|-------------|
1315
+ | `endpoint` | string | `""` | **yes** | `LANGGRAPH_ENDPOINT`, `LANGCHAIN_ENDPOINT` | LangGraph Platform deployment URL |
1316
+ | `api_key` | string | `""` | no | `LANGCHAIN_API_KEY`, `LANGGRAPH_API_KEY` | API key |
1317
+ | `graph_id` | string | `"agent"` | no | — | **Which graph to run** — this is the workflow selector |
1318
+ | `poll_interval` | float | `1.0` | no | — | Seconds between status polls |
1319
+ | `max_poll_seconds` | float | `300.0` | no | — | Max total polling time (seconds) |
1320
+ | `http_timeout` | float | `30.0` | no | — | HTTP request timeout (seconds) |
1321
+
1322
+ #### autogen
1323
+
1324
+ Connects to an external AutoGen Studio or custom AutoGen API service via HTTP.
1325
+
1326
+ | Field | Type | Default | Required | Env Fallback | Description |
1327
+ |-------|------|---------|----------|-------------|-------------|
1328
+ | `endpoint` | string | `""` | **yes** | `AUTOGEN_ENDPOINT` | AutoGen service base URL (e.g. `http://localhost:8081`) |
1329
+ | `api_key` | string | `""` | no | `AUTOGEN_API_KEY` | Bearer token for authentication |
1330
+ | `workflow_id` | string | `""` | no | — | Workflow/agent ID to execute on the AutoGen service |
1331
+ | `timeout` | float | `120.0` | no | — | HTTP request timeout (seconds) |
1332
+ | `poll_interval` | float | `2.0` | no | — | Seconds between status poll requests |
1333
+ | `max_poll_seconds` | float | `300.0` | no | — | Max total polling time (seconds) |
1334
+
1335
+ #### a2a
1336
+
1337
+ | Field | Type | Default | Required | Env Fallback | Description |
1338
+ |-------|------|---------|----------|-------------|-------------|
1339
+ | `url` | string | `""` | **yes** | `A2A_AGENT_URL` | A2A-compatible agent server base URL |
1340
+ | `auth_token` | string | `""` | no | `A2A_AUTH_TOKEN` | Bearer token for authentication |
1341
+ | `auth_scheme` | string | `"Bearer"` | no | — | Authorization header scheme |
1342
+ | `poll_interval` | float | `2.0` | no | — | Seconds between task status polls |
1343
+ | `max_poll_seconds` | float | `300.0` | no | — | Max total polling time (seconds) |
1344
+ | `http_timeout` | float | `30.0` | no | — | HTTP request timeout (seconds) |
1345
+
1346
+ #### mcp
1347
+
1348
+ MCP supports two transport modes. `tool_name` is always required.
1349
+
1350
+ **Common fields (both transports):**
1351
+
1352
+ | Field | Type | Default | Required | Env Fallback | Description |
1353
+ |-------|------|---------|----------|-------------|-------------|
1354
+ | `tool_name` | string | `""` | **yes** | — | **Which MCP tool to call** — the workflow selector |
1355
+ | `transport` | string | `"stdio"` | no | — | Transport mode: `"stdio"` or `"http"` |
1356
+ | `timeout` | float | `60.0` | no | — | Request timeout (seconds) |
1357
+
1358
+ **stdio transport fields:**
1359
+
1360
+ | Field | Type | Default | Required | Description |
1361
+ |-------|------|---------|----------|-------------|
1362
+ | `command` | string | `""` | **yes** (stdio) | Executable to launch (e.g. `"npx"`) |
1363
+ | `args` | list | `[]` | no | Command arguments (e.g. `["-y", "@mcp/server-npm"]`) |
1364
+ | `env` | dict | `null` | no | Environment variables for the subprocess |
1365
+
1366
+ **http transport fields:**
1367
+
1368
+ | Field | Type | Default | Required | Description |
1369
+ |-------|------|---------|----------|-------------|
1370
+ | `url` | string | `""` | **yes** (http) | MCP server HTTP/SSE endpoint |
1371
+ | `auth_token` | string | `""` | no | Bearer token |
1372
+
1373
+ #### zapier
1374
+
1375
+ Tool name is built as `{connection}_{action}` (e.g. `google_sheets_create_row`).
1376
+
1377
+ | Field | Type | Default | Required | Env Fallback | Description |
1378
+ |-------|------|---------|----------|-------------|-------------|
1379
+ | `connection` | string | `""` | yes* | — | Zapier app name (e.g. `"google_sheets"`, `"slack"`, `"gmail"`) |
1380
+ | `action` | string | `""` | yes* | — | Action name (e.g. `"create_row"`, `"send_message"`) |
1381
+ | `mcp_key` | string | `""` | yes† | `ZAPIER_MCP_KEY` | Zapier MCP key — used first if `prefer_mcp=true` |
1382
+ | `api_key` | string | `""` | yes† | `ZAPIER_API_KEY` | Zapier REST API key — used as fallback |
1383
+ | `prefer_mcp` | bool | `true` | no | — | Try MCP path first; fall back to REST on failure |
1384
+ | `instructions` | string | `""` | no | — | Optional natural language instructions (REST path only) |
1385
+ | `timeout` | float | `60.0` | no | — | HTTP request timeout (seconds) |
1386
+
1387
+ *At least one of `connection` or `action` required for the tool name. †At least one of `mcp_key` or `api_key` required.
1388
+
1389
+ #### composio
1390
+
1391
+ | Field | Type | Default | Required | Env Fallback | Description |
1392
+ |-------|------|---------|----------|-------------|-------------|
1393
+ | `action` | string | `""` | **yes** | — | **Composio action enum** (e.g. `"GITHUB_STAR_A_REPOSITORY"`) — the workflow selector |
1394
+ | `entity_id` | string | `"default"` | no | `COMPOSIO_ENTITY_ID` | User/entity context for managed auth |
1395
+ | `api_key` | string | `""` | no | `COMPOSIO_API_KEY` | Composio API key |
1396
+ | `timeout` | float | `60.0` | no | — | Execution timeout (seconds) |
1397
+
1398
+ #### n8n
1399
+
1400
+ | Field | Type | Default | Required | Env Fallback | Description |
1401
+ |-------|------|---------|----------|-------------|-------------|
1402
+ | `webhook_url` | string | `""` | **yes** | `N8N_WEBHOOK_URL` | Full webhook trigger URL — **one URL per n8n workflow** |
1403
+ | `auth_token` | string | `""` | no | `N8N_AUTH_TOKEN` | Bearer token |
1404
+ | `timeout` | float | `60.0` | no | — | HTTP request timeout (seconds, sync mode only) |
1405
+
1406
+ **n8n webhook URL types:**
1407
+ - **Production:** `https://your-instance.app.n8n.cloud/webhook/<id>` — works when workflow is **activated** (toggle ON)
1408
+ - **Test:** `https://your-instance.app.n8n.cloud/webhook-test/<id>` — only works while n8n editor has "Listen for Test Event" active (one-shot, for development only)
1409
+
1410
+ **n8n + callback mode:**
1411
+
1412
+ When `mode: "callback"`, the n8n workflow should:
1413
+ 1. Start with a Webhook trigger node (receives the payload including `_leafmesh_callback_url`)
1414
+ 2. Configure the Webhook node to "Respond Immediately" (optional — sync mode works too)
1415
+ 3. Process the workflow
1416
+ 4. End with an HTTP Request node that POSTs back to `{{ $json._leafmesh_callback_url }}` with body:
1417
+ ```json
1418
+ { "session_id": "{{ $json._leafmesh_session_id }}", "result": { ... } }
1419
+ ```
1420
+
1421
+ ---
1422
+
1423
+ ## Programmatic Agent Fields (`agent_type: "programmatic"`)
1424
+
1425
+ These fields are used when `agent_type` is `"programmatic"`.
1426
+
1427
+ | Field | Type | Default | Accepted Values | Description |
1428
+ |-------|------|---------|-----------------|-------------|
1429
+ | `integration` | string | `null` | `zapier`, `composio`, `n8n`, `mcp` | Integration connector (optional) |
1430
+ | `connector_config` | dict | `{}` | integration-specific key-values | Same fields as the matching external framework connector — see tables above |
1431
+
1432
+ **Validation rule:** `integration` is only valid when `agent_type` is `"programmatic"`.
1433
+
1434
+ When `integration` is set, `connector_config` is passed as `**kwargs` to the connector's `__init__`. Use the same fields as the matching framework in the tables above (zapier → zapier fields, mcp → mcp fields, etc.).
1435
+
1436
+ The common fields (`mode`, `callback_timeout`) are also available here — programmatic agents with connectors support the same sync/callback modes as external agents.
1437
+
1438
+ ---
1439
+
1440
+ ## WebhookConfig
1441
+
1442
+ Used inside `webhook_config` for human agents.
1443
+
1444
+ | Field | Type | Default | Description |
1445
+ |-------|------|---------|-------------|
1446
+ | `outbound_url` | string | `null` | URL to POST responses to external system |
1447
+ | `outbound_headers` | dict | `{}` | Headers for outbound webhook (key-value string pairs) |
1448
+ | `outbound_timeout` | int | `30` | Timeout for outbound calls (seconds) |
1449
+ | `inbound_endpoint` | string | `null` | Endpoint path for inbound responses (e.g. `"/webhook/human_contact"`). If not set, derived from entry points. |
1450
+ | `inbound_auth_token` | string | `null` | Auth token for validating inbound requests |
1451
+ | `response_mapping` | dict | `{}` | Field mapping for webhook response transformation |
1452
+ | `max_retries` | int | `3` | Max retry attempts for failed outbound webhooks |
1453
+ | `retry_delay` | int | `5` | Delay between retries (seconds) |
1454
+
1455
+ ---
1456
+
1457
+ ## ChannelConfig
1458
+
1459
+ Used inside `channels` dict for human agents. Keys are provider names.
1460
+
1461
+ ### Supported Provider Keys
1462
+
1463
+ | Key | Provider |
1464
+ |-----|----------|
1465
+ | `slack` | Slack Bot API |
1466
+ | `telegram` | Telegram Bot API |
1467
+ | `discord` | Discord Bot API |
1468
+ | `whatsapp` | WhatsApp Business API (Meta Cloud API) |
1469
+ | `teams` | Microsoft Teams Bot Framework |
1470
+
1471
+ ### Fields Per Channel
1472
+
1473
+ | Field | Type | Default | Description |
1474
+ |-------|------|---------|-------------|
1475
+ | `bot_token` | string | `null` | Bot/API token for the provider (see per-provider notes below) |
1476
+ | `signing_secret` | string | `null` | Request verification secret (see per-provider notes below) |
1477
+ | `listen_channels` | list | `[]` | Channel/chat IDs to accept inbound messages from (empty = all) |
1478
+ | `post_channel` | string | `null` | Default channel/chat ID for outbound messages (see per-provider notes below) |
1479
+ | `verify_token` | string | `null` | Webhook verification token — **WhatsApp only** (`hub.verify_token`) |
1480
+
1481
+ **Note:** `ChannelConfig` allows extra fields (`extra="allow"`) for any provider-specific config.
1482
+
1483
+ ### Per-Provider Field Semantics
1484
+
1485
+ | Provider | `bot_token` | `signing_secret` | `post_channel` | `verify_token` |
1486
+ |----------|------------|------------------|----------------|----------------|
1487
+ | `slack` | Bot OAuth token (`xoxb-…`) | Slack signing secret (HMAC-SHA256) | Channel ID (e.g. `C123456`) | — |
1488
+ | `telegram` | Bot token from @BotFather | Secret token set when registering webhook | Chat ID | — |
1489
+ | `discord` | Bot token (without `Bot ` prefix) | App public key (Ed25519 — requires `pynacl`) | Channel ID | — |
1490
+ | `whatsapp` | Meta Graph API access token | Meta app secret (HMAC-SHA256) | Phone number ID | `hub.verify_token` for webhook registration |
1491
+ | `teams` | Bot Framework App ID | Bot Framework App password | Conversation ID | — |
1492
+
1493
+ ### Inbound Route Registered Per Provider
1494
+
1495
+ | Provider | Route |
1496
+ |----------|-------|
1497
+ | `slack` | `POST /channels/slack/{agent_name}/events` |
1498
+ | `telegram` | `POST /channels/telegram/{agent_name}/webhook` |
1499
+ | `discord` | `POST /channels/discord/{agent_name}/interactions` |
1500
+ | `whatsapp` | `GET /channels/whatsapp/{agent_name}/webhook` (verification) + `POST` (messages) |
1501
+ | `teams` | `POST /channels/teams/{agent_name}/messages` |
1502
+
1503
+ ### Example
1504
+
1505
+ ```yaml
1506
+ channels:
1507
+ slack:
1508
+ bot_token: "${SLACK_BOT_TOKEN:}"
1509
+ signing_secret: "${SLACK_SIGNING_SECRET:}"
1510
+ listen_channels: ["C123456", "C789012"]
1511
+ post_channel: "C123456"
1512
+
1513
+ telegram:
1514
+ bot_token: "${TELEGRAM_BOT_TOKEN:}"
1515
+ signing_secret: "${TELEGRAM_SECRET_TOKEN:}" # optional
1516
+ listen_channels: [] # empty = all chats
1517
+ post_channel: ""
1518
+
1519
+ discord:
1520
+ bot_token: "${DISCORD_BOT_TOKEN:}"
1521
+ signing_secret: "${DISCORD_PUBLIC_KEY:}" # Ed25519 public key
1522
+ listen_channels: ["987654321098765432"]
1523
+ post_channel: "987654321098765432"
1524
+
1525
+ whatsapp:
1526
+ bot_token: "${WHATSAPP_ACCESS_TOKEN:}"
1527
+ signing_secret: "${META_APP_SECRET:}"
1528
+ post_channel: "${WHATSAPP_PHONE_NUMBER_ID:}" # phone number ID, not a phone number
1529
+ verify_token: "my_verify_token" # set same value in Meta dashboard
1530
+
1531
+ teams:
1532
+ bot_token: "${TEAMS_APP_ID:}"
1533
+ signing_secret: "${TEAMS_APP_PASSWORD:}"
1534
+ post_channel: "" # set at runtime from inbound activity
1535
+ ```
1536
+
1537
+ ---
1538
+
1539
+ ## Memory Config
1540
+
1541
+ Field: `memory` — accepts `bool` or `dict`.
1542
+
1543
+ ### Simple Mode
1544
+
1545
+ ```yaml
1546
+ memory: false # Disabled (default)
1547
+ memory: true # Enabled with defaults
1548
+ ```
1549
+
1550
+ ### Advanced Mode (Dict)
1551
+
1552
+ | Field | Type | Default | Accepted Values | Description |
1553
+ |-------|------|---------|-----------------|-------------|
1554
+ | `strategy` | string | `"recency"` | `recency`, `relevance`, `hybrid` | Memory retrieval strategy |
1555
+ | `limit` | int | `10` | 1 – 100 | Max feed posts per invocation |
1556
+ | `cross_session` | bool | `false` | `true`, `false` | Persist memory across sessions |
1557
+ | `cross_session_limit` | int | `50` | 1 – 500 | Max cross-session posts to retain |
1558
+ | `relevance_weight` | float | `0.6` | 0.0 – 1.0 | Weight for relevance scoring |
1559
+ | `recency_weight` | float | `0.4` | 0.0 – 1.0 | Weight for recency scoring |
1560
+ | `decay_hours` | int | `24` | 1 – unlimited | Hours before entries decay |
1561
+
1562
+ ### Example
1563
+
1564
+ ```yaml
1565
+ memory:
1566
+ strategy: "hybrid"
1567
+ limit: 10
1568
+ cross_session: true
1569
+ cross_session_limit: 50
1570
+ relevance_weight: 0.6
1571
+ recency_weight: 0.4
1572
+ decay_hours: 24
1573
+ ```
1574
+
1575
+ ---
1576
+
1577
+ ## EscalationConfig
1578
+
1579
+ Used inside `manager.escalation`.
1580
+
1581
+ | Field | Type | Default | Description |
1582
+ |-------|------|---------|-------------|
1583
+ | `targets` | list of [EscalationTarget](#escalationtarget) | `[]` | Escalation targets — all fire in parallel |
1584
+ | `auto_escalate` | dict | see below | Auto-escalation rules |
1585
+
1586
+ ### `auto_escalate` Fields
1587
+
1588
+ | Field | Type | Default | Description |
1589
+ |-------|------|---------|-------------|
1590
+ | `max_retries` | int | `3` | Max retry attempts before escalating |
1591
+ | `max_errors_per_session` | int | `5` | Error count threshold per session |
1592
+ | `timeout_threshold` | int | `2` | Consecutive timeouts before escalating |
1593
+
1594
+ ---
1595
+
1596
+ ## EscalationTarget
1597
+
1598
+ Each target in the `escalation.targets` list.
1599
+
1600
+ | Field | Type | Default | Accepted Values | Applies To |
1601
+ |-------|------|---------|-----------------|------------|
1602
+ | `type` | string | **required** | `human_agent`, `webhook`, `channel` | all |
1603
+ | `agent` | string | `null` | agent name | `human_agent` |
1604
+ | `entry_point` | string | `null` | entry point name | `human_agent` |
1605
+ | `url` | string | `null` | URL | `webhook` |
1606
+ | `method` | string | `"POST"` | `POST`, `PUT`, `PATCH` | `webhook` |
1607
+ | `headers` | dict | `{}` | key-value string pairs | `webhook` |
1608
+ | `payload_template` | dict | `null` | JSON with `{{var}}` placeholders | `webhook` |
1609
+ | `provider` | string | `null` | `slack`, `telegram`, `discord`, `whatsapp`, `teams` | `channel` |
1610
+ | `channel_id` | string | `null` | channel ID | `channel` |
1611
+ | `message_template` | string | `null` | text with `{{var}}` placeholders | `channel` |
1612
+
1613
+ ### Example
1614
+
1615
+ ```yaml
1616
+ escalation:
1617
+ targets:
1618
+ - type: "human_agent"
1619
+ agent: "customer_support_team"
1620
+
1621
+ - type: "webhook"
1622
+ url: "https://incident.example.com/api/escalate"
1623
+ method: "POST"
1624
+ headers:
1625
+ Authorization: "Bearer ${ESCALATION_TOKEN:}"
1626
+ payload_template:
1627
+ incident_id: "{{session_id}}"
1628
+ severity: "{{severity_level}}"
1629
+ message: "{{error_message}}"
1630
+
1631
+ - type: "channel"
1632
+ provider: "slack"
1633
+ channel_id: "#critical-incidents"
1634
+ message_template: "Escalation: {{message}} (session: {{session_id}})"
1635
+
1636
+ auto_escalate:
1637
+ max_retries: 3
1638
+ max_errors_per_session: 5
1639
+ timeout_threshold: 2
1640
+ ```
1641
+
1642
+ ---
1643
+
1644
+ ## Top-Level Config (LeafMeshConfig)
1645
+
1646
+ | Field | Type | Default | Accepted Values | Description |
1647
+ |-------|------|---------|-----------------|-------------|
1648
+ | `name` | string | `"default_mesh"` | any string | Mesh name |
1649
+ | `version` | string | `"1.0.0"` | any string | Configuration version |
1650
+ | `architecture` | string | `"managed_mesh"` | `managed_mesh` | Architecture type (only one supported) |
1651
+ | `debug` | bool | `false` | `true`, `false` | Enable debug mode |
1652
+ | `log_level` | string | `"INFO"` | `DEBUG`, `INFO`, `WARNING`, `ERROR` | Logging level |
1653
+ | `environment` | string | `"development"` | `development`, `production` | Environment |
1654
+ | `redis` | object | see [RedisConfig](#redisconfig) | — | Redis connection |
1655
+ | `manager` | object | see [ManagerConfig](#managerconfig) | — | Manager coordination + analysis |
1656
+ | `mesh` | object | see [MeshConfig](#meshconfig--cloud-providers) | — | Mesh network + cloud providers |
1657
+ | `agents` | dict | `{}` | agent name → AgentConfig | Agent configurations |
1658
+ | `entry_points` | list | `[{"name": "default_entry", "target": "summarizer", "condition": "always"}]` | see [Entry Points](#entry-points) | Named portals into mesh |
1659
+ | `data_structures` | dict | `{}` | name → DataStructure | Custom data type definitions |
1660
+ | `auto_discover` | dict | `null` | `{"directory": "path", "pattern": "*.py", "recursive": true}` | Auto-discover agent files |
1661
+ | `evolution` | object | see [EvolutionConfig](#evolutionconfig) | — | Evolutionary optimization |
1662
+
1663
+ **Note:** `LeafMeshConfig` has `extra="forbid"` — unknown top-level keys will raise a validation error.
1664
+
1665
+ ---
1666
+
1667
+ ## ManagerConfig
1668
+
1669
+ | Field | Type | Default | Accepted Values | Description |
1670
+ |-------|------|---------|-----------------|-------------|
1671
+ | `enabled` | bool | `true` | `true`, `false` | Enable manager + summarizer |
1672
+ | `model` | string | `"gpt-4o-mini"` | same as [Model List](#model-list) | LLM model for Summarizer analysis |
1673
+ | `domain` | string | `"generic"` | `generic`, `ecommerce`, `data_analysis` | Summarizer domain specialization |
1674
+ | `prompt` | string | `null` | any string (multiline supported) | **Evaluation criteria** — tell the Manager what success looks like, what to escalate on, and what patterns to watch. Injected into every Summarizer analysis call as an `EVALUATION CRITERIA` section, alongside the domain prompt. |
1675
+ | `can_intervene` | bool | `true` | `true`, `false` | Allow manager interventions (false = read-only) |
1676
+ | `coordination_rules` | dict | `{}` | arbitrary key-values | User-defined business rules |
1677
+ | `chain_completion_timeout` | float | `60.0` | seconds | Wait time before checking chain completeness |
1678
+ | `health_check_interval` | int | `60` | seconds | Seconds between health checks |
1679
+ | `agent_timeout_threshold` | int | `180` | seconds | Seconds before agent is timed out |
1680
+ | `escalation` | object | `null` | see [EscalationConfig](#escalationconfig) | Escalation targets and rules |
1681
+ | `routing` | dict | see below | — | Manager routing configuration |
1682
+
1683
+ ### `manager.prompt` — Evaluation Criteria
1684
+
1685
+ Gives the Manager direct context about your mesh's specific purpose, success criteria, and escalation triggers. The Summarizer reads this on every agent turn alongside its domain template.
1686
+
1687
+ ```yaml
1688
+ manager:
1689
+ model: "gpt-4o-mini"
1690
+ domain: "generic"
1691
+ prompt: |
1692
+ This mesh handles customer support tickets.
1693
+ A successful flow means:
1694
+ - greeter identifies the issue category correctly
1695
+ - processor routes to the right specialist agent
1696
+ - the customer receives a clear resolution within 5 minutes
1697
+
1698
+ Escalate if:
1699
+ - the same issue loops more than twice
1700
+ - sentiment is negative AND no resolution has been proposed
1701
+ - the human agent times out without responding
1702
+
1703
+ Watch for:
1704
+ - advisor_agent confidence scores below 0.6
1705
+ - processor_agent routing to fallback more than 50% of the time
1706
+ ```
1707
+
1708
+ ### `routing` Fields
1709
+
1710
+ | Field | Type | Default | Accepted Values | Description |
1711
+ |-------|------|---------|-----------------|-------------|
1712
+ | `mode` | string | `"static"` | `static`, `learning` | Routing mode (static=YAML only, learning=adaptive) |
1713
+ | `memory_size` | int | `100` | 1 – 1000 | Max routing decisions to remember |
1714
+ | `confidence_threshold` | float | `0.7` | 0.0 – 1.0 | Min confidence to accept learned route |
1715
+ | `fallback` | string | `"all"` | `all` | Fallback when confidence too low |
1716
+ | `decay_days` | int | `30` | 1 – 365 | Days before old routing memory decays |
1717
+
1718
+ ### `human_input_rules` (Defaults)
1719
+
1720
+ | Field | Type | Default |
1721
+ |-------|------|---------|
1722
+ | `max_concurrent_requests` | int | `3` |
1723
+ | `max_agent_requests` | int | `5` |
1724
+ | `enable_request_queuing` | bool | `true` |
1725
+
1726
+ ### `timeout_rules` (Defaults)
1727
+
1728
+ | Field | Type | Default |
1729
+ |-------|------|---------|
1730
+ | `max_timeouts_before_escalation` | int | `2` |
1731
+ | `timeout_escalation_enabled` | bool | `true` |
1732
+ | `escalation_notify_managers` | bool | `true` |
1733
+
1734
+ ### `workflow_pause_rules` (Defaults)
1735
+
1736
+ | Field | Type | Default |
1737
+ |-------|------|---------|
1738
+ | `max_pause_duration_minutes` | int | `30` |
1739
+ | `max_concurrent_paused_workflows` | int | `2` |
1740
+ | `pause_monitoring_enabled` | bool | `true` |
1741
+
1742
+ ### `human_response_rules` (Defaults)
1743
+
1744
+ | Scenario | `requires_manager_review` | `auto_escalate` |
1745
+ |----------|--------------------------|-----------------|
1746
+ | `approval` | `false` | `false` |
1747
+ | `escalation` | `true` | `true` |
1748
+ | `timeout` | `true` | `false` |
1749
+
1750
+ ---
1751
+
1752
+ ## MeshConfig & Cloud Providers
1753
+
1754
+ ### MeshConfig
1755
+
1756
+ | Field | Type | Default | Description |
1757
+ |-------|------|---------|-------------|
1758
+ | `call_timeout` | int | `30` | Call timeout in seconds |
1759
+ | `bedrock` | object | `null` | AWS Bedrock config |
1760
+ | `vertex` | object | `null` | Google Vertex AI config |
1761
+ | `foundry` | object | `null` | Microsoft Foundry/Azure AI config |
1762
+ | `local` | object | `null` | Local model server config (vLLM, SGLang, Ollama, etc.) |
1763
+
1764
+ ### BedrockConfig (AWS)
1765
+
1766
+ | Field | Type | Default | Description |
1767
+ |-------|------|---------|-------------|
1768
+ | `region` | string | `"us-east-1"` | AWS region |
1769
+ | `profile` | string | `null` | AWS profile from `~/.aws/credentials` |
1770
+ | `endpoint_url` | string | `null` | Custom Bedrock endpoint URL |
1771
+
1772
+ Auth: `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY` env vars, IAM role, or `~/.aws/credentials`
1773
+
1774
+ ### VertexConfig (Google Cloud)
1775
+
1776
+ | Field | Type | Default | Required | Description |
1777
+ |-------|------|---------|----------|-------------|
1778
+ | `project` | string | — | **yes** | GCP project ID |
1779
+ | `location` | string | `"us-central1"` | no | GCP region |
1780
+
1781
+ Auth: `GOOGLE_APPLICATION_CREDENTIALS` env var or `gcloud auth`
1782
+
1783
+ ### FoundryConfig (Azure)
1784
+
1785
+ | Field | Type | Default | Required | Description |
1786
+ |-------|------|---------|----------|-------------|
1787
+ | `endpoint` | string | — | **yes** | Foundry endpoint URL (e.g. `https://<resource>.openai.azure.com`) |
1788
+ | `api_version` | string | `null` | no | Azure API version (e.g. `2024-10-21`). Omit for v1 endpoint. |
1789
+
1790
+ Auth: `AZURE_FOUNDRY_API_KEY` or `AZURE_FOUNDRY_TOKEN` env vars
1791
+
1792
+ ### LocalModelConfig
1793
+
1794
+ | Field | Type | Default | Required | Description |
1795
+ |-------|------|---------|----------|-------------|
1796
+ | `endpoint` | string | `"http://localhost:11434/api/generate"` | no | Model server URL |
1797
+ | `server_type` | string | `null` (auto-detect) | no | Server type: `openai`, `ollama`, `textgen`, `huggingface` |
1798
+ | `timeout` | float | `120.0` | no | Request timeout in seconds |
1799
+ | `headers` | dict | `{}` | no | Custom HTTP headers (e.g. for auth) |
1800
+
1801
+ Env fallbacks: `LOCAL_MODEL_ENDPOINT`, `LOCAL_MODEL_SERVER_TYPE`, `LOCAL_MODEL_TIMEOUT`
1802
+
1803
+ **Supported servers and their `endpoint` + `server_type`:**
1804
+
1805
+ | Server | `server_type` | Example `endpoint` |
1806
+ |--------|--------------|-------------------|
1807
+ | vLLM | `openai` | `http://localhost:8000/v1/chat/completions` |
1808
+ | SGLang | `openai` | `http://localhost:30000/v1/chat/completions` |
1809
+ | llama.cpp server | `openai` | `http://localhost:8080/v1/chat/completions` |
1810
+ | LM Studio | `openai` | `http://localhost:1234/v1/chat/completions` |
1811
+ | LocalAI | `openai` | `http://localhost:8080/v1/chat/completions` |
1812
+ | TGI (--api openai) | `openai` | `http://localhost:8080/v1/chat/completions` |
1813
+ | TensorRT-LLM | `openai` | `http://localhost:8000/v1/chat/completions` |
1814
+ | Triton + vLLM | `openai` | `http://localhost:8000/v1/chat/completions` |
1815
+ | Ollama | `ollama` | `http://localhost:11434/api/generate` |
1816
+ | Text Gen Web UI | `textgen` | `http://localhost:5000/api/v1/generate` |
1817
+ | HF Inference | `huggingface` | `https://api-inference.huggingface.co/models/...` |
1818
+
1819
+ > **Tip:** Most production servers (vLLM, SGLang, TGI, llama.cpp) expose OpenAI-compatible endpoints. Use `server_type: openai` for all of them. If `server_type` is omitted, it's auto-detected from the URL.
1820
+
1821
+ ```yaml
1822
+ mesh:
1823
+ local:
1824
+ endpoint: http://localhost:8000/v1/chat/completions
1825
+ server_type: openai
1826
+ timeout: 120
1827
+ headers:
1828
+ Authorization: "Bearer ${VLLM_API_KEY:}"
1829
+ ```
1830
+
1831
+ ---
1832
+
1833
+ ## RedisConfig
1834
+
1835
+ | Field | Type | Default | Description |
1836
+ |-------|------|---------|-------------|
1837
+ | `host` | string | `"localhost"` | Redis server host |
1838
+ | `port` | int | `6379` | Redis server port |
1839
+ | `db` | int | `0` | Redis database number |
1840
+ | `password` | string | `null` | Redis password |
1841
+ | `decode_responses` | bool | `true` | Decode Redis responses |
1842
+ | `auto_storage` | bool | `true` | Enable automatic data storage |
1843
+ | `default_ttl` | int | `3600` | Default TTL (seconds) |
1844
+ | `session_ttl` | int | `7200` | Session TTL (seconds) |
1845
+ | `cluster_mode` | bool | `false` | Use Redis cluster |
1846
+ | `cluster_nodes` | list | `[]` | Cluster node addresses (strings) |
1847
+ | `ssl` | bool | `false` | Enable TLS for Redis connection |
1848
+ | `ssl_cert_reqs` | string | `"required"` | TLS verification mode: `required` \| `optional` \| `none` |
1849
+ | `ssl_ca_certs` | string | `null` | Path to CA bundle for verifying Redis server cert |
1850
+ | `ssl_certfile` | string | `null` | Client TLS cert path (mutual TLS) |
1851
+ | `ssl_keyfile` | string | `null` | Client TLS private key path (mutual TLS) |
1852
+ | `ssl_check_hostname` | bool | `true` | Verify server hostname against certificate |
1853
+
1854
+ ---
1855
+
1856
+ ## APIConfig
1857
+
1858
+ Top-level `api:` block Configures the ADK's HTTP server.
1859
+
1860
+ | Field | Type | Default | Description |
1861
+ |-------|------|---------|-------------|
1862
+ | `cors_origins` | list[string] | `[]` | Additional CORS origins, appended to the ADK's built-in defaults (`https://platform.leafcraft.ai` + localhost dev ports). Each entry must be a full origin (`scheme://host[:port]`). |
1863
+
1864
+ ---
1865
+
1866
+ ## EvolutionConfig
1867
+
1868
+ | Field | Type | Default | Accepted Values | Description |
1869
+ |-------|------|---------|-----------------|-------------|
1870
+ | `enabled` | bool | `false` | `true`, `false` | Enable evolutionary optimization |
1871
+ | `strategy` | string | `"genetic"` | free text | Evolution strategy |
1872
+ | `population_size` | int | `20` | 1+ | Population size per generation |
1873
+ | `generations` | int | `50` | 1+ | Maximum generations |
1874
+ | `mutation_rate` | float | `0.1` | 0.0 – 1.0 | Mutation probability |
1875
+ | `crossover_rate` | float | `0.7` | 0.0 – 1.0 | Crossover probability |
1876
+ | `elite_size` | int | `2` | 1+ | Elite genomes to preserve per generation |
1877
+ | `mutation_types` | list | `["prompt_variation", "temperature_adjustment", "tool_selection"]` | `prompt_variation`, `temperature_adjustment`, `tool_selection` | Mutation types |
1878
+ | `fitness_function` | string | `"task_completion_rate"` | free text | Fitness evaluation function |
1879
+ | `selection_method` | string | `"tournament"` | free text | Selection method |
1880
+ | `test_scenarios` | list | `[]` | list of scenario dicts — see below | Weighted test scenarios for fitness evaluation |
1881
+
1882
+ ### `test_scenarios` — Scenario Dict Fields
1883
+
1884
+ Each scenario in `test_scenarios` is a dict with these fields:
1885
+
1886
+ | Field | Type | Default | Description |
1887
+ |-------|------|---------|-------------|
1888
+ | `name` | string | `"scenario_N"` | Human-readable label — shown in logs and Studio |
1889
+ | `entry_point` | string | `"test"` | Which mesh entry point to call |
1890
+ | `input` | any | `"Test scenario input"` | Input data passed to the mesh call |
1891
+ | `expected_outcome` | dict | `{}` | Key/value pairs the response must contain. All keys checked — partial matches return a partial score. |
1892
+ | `weight` | float | `1.0` | How much this scenario counts toward the overall fitness score. Higher = matters more. |
1893
+ | `timeout` | float | `30.0` | Per-scenario timeout in seconds |
1894
+
1895
+ **Fitness formula:**
1896
+ ```
1897
+ fitness = Σ (weight × scenario_score) / Σ (weights)
1898
+ ```
1899
+ where `scenario_score = outcome_match_ratio × (1 − latency_penalty)`.
1900
+
1901
+ A scenario with `weight: 2.0` counts twice as much as one with `weight: 1.0`. Timed-out scenarios score 0 and still count toward the denominator.
1902
+
1903
+ ### Example
1904
+
1905
+ ```yaml
1906
+ evolution:
1907
+ enabled: true
1908
+ population_size: 20
1909
+ generations: 50
1910
+ elite_size: 2
1911
+ test_scenarios:
1912
+ - name: "happy_path"
1913
+ entry_point: "greet_user"
1914
+ input: { "message": "I need help with my order" }
1915
+ expected_outcome:
1916
+ status: "success"
1917
+ weight: 1.0
1918
+
1919
+ - name: "escalation_path"
1920
+ entry_point: "greet_user"
1921
+ input: { "message": "This is urgent and completely broken" }
1922
+ expected_outcome:
1923
+ escalated: true
1924
+ weight: 2.0 # weighted heavier — escalation correctness matters more
1925
+
1926
+ - name: "hitl_flow"
1927
+ entry_point: "human_contact"
1928
+ input: { "user_message": "I want a refund" }
1929
+ expected_outcome:
1930
+ human_involved: true
1931
+ weight: 1.5
1932
+ ```
1933
+
1934
+ ---
1935
+
1936
+ ## DataStructure
1937
+
1938
+ | Field | Type | Default | Description |
1939
+ |-------|------|---------|-------------|
1940
+ | `type` | string | **required** | `object`, `string`, `number`, `boolean`, `list` |
1941
+ | `properties` | dict | `null` | Object properties (for type=object) |
1942
+ | `required` | list | `[]` | Required field names |
1943
+ | `validation_rules` | dict | `{}` | Validation rules (key-value strings) |
1944
+
1945
+ ---
1946
+
1947
+ ## Entry Points
1948
+
1949
+ Each entry point is a named portal into the mesh.
1950
+
1951
+ | Field | Type | Default | Description |
1952
+ |-------|------|---------|-------------|
1953
+ | `name` | string | **required** | Entry point name |
1954
+ | `target` | string | **required** | Target agent name |
1955
+ | `description` | string | optional | Description |
1956
+ | `condition` | string | `"always"` | Trigger condition |
1957
+
1958
+ ### Example
1959
+
1960
+ ```yaml
1961
+ entry_points:
1962
+ - name: "greet_user"
1963
+ target: "greeter_agent"
1964
+ description: "Main entry — greets user and starts the agent chain"
1965
+ - name: "direct_research"
1966
+ target: "researcher_agent"
1967
+ description: "Skip greeting, go straight to research"
1968
+ ```
1969
+
1970
+ ---
1971
+
1972
+ ## Validation Rules
1973
+
1974
+ These are enforced by Pydantic validators in the ADK:
1975
+
1976
+ | Rule | Constraint |
1977
+ |------|-----------|
1978
+ | `agent_type` | Must be `llm`, `human`, `programmatic`, or `external` |
1979
+ | `communication_type` | Must be `dual`, `chain`, or `execute` |
1980
+ | `optimization_strategy` | Must be `performance`, `cost`, or `speed` (or null) |
1981
+ | `max_tool_calls_per_message` | Must be 0–20 |
1982
+ | `tool_call_timeout` | Must be > 0 and <= 300 seconds |
1983
+ | `tool_choice` | Must be a non-empty string (`auto`, `none`, or tool name) |
1984
+ | `framework` | Must be `crewai`, `langgraph`, `autogen`, `a2a`, `mcp`, `zapier`, `composio`, `n8n`, `custom` (or null) |
1985
+ | `framework` required | When `agent_type` is `external`, `framework` must be set |
1986
+ | `integration` | Must be `zapier`, `composio`, `n8n`, `mcp` (or null) |
1987
+ | `integration` restricted | Only valid when `agent_type` is `programmatic` |
1988
+ | `is_human_powered` sync | Auto-set to `true` when `agent_type="human"`, forced to `false` when `agent_type="llm"` |
1989
+ | `AgentConfig` extras | Allows arbitrary extra fields (`extra="allow"`) |
1990
+ | `LeafMeshConfig` extras | Rejects unknown top-level keys (`extra="forbid"`) |
1991
+
1992
+ ---
1993
+
1994
+ ## Field Applicability by Agent Type
1995
+
1996
+ Shows which fields are **used** (U), **ignored** (—), or **required** (R) for each agent type.
1997
+
1998
+ | Field | `llm` | `human` | `programmatic` | `external` |
1999
+ |-------|-------|---------|----------------|------------|
2000
+ | `name` | R | R | R | R |
2001
+ | `description` | U | U | U | U |
2002
+ | `model` | U | — | — | — |
2003
+ | `prompt` | U | — | — | — |
2004
+ | `temperature` | U | — | — | — |
2005
+ | `max_tokens` | U | — | — | — |
2006
+ | `max_completion_tokens` | U | — | — | — |
2007
+ | `reasoning` | U | — | — | — |
2008
+ | `thinking` | U | — | — | — |
2009
+ | `thinking_budget` | U | — | — | — |
2010
+ | `enable_prompt_caching` | U | — | — | — |
2011
+ | `response_format` | U | — | — | — |
2012
+ | `optimization_strategy` | U | — | — | — |
2013
+ | `context_parts` | U | — | — | — |
2014
+ | `tools` | U | — | — | — |
2015
+ | `tool_choice` | U | — | — | — |
2016
+ | `max_tool_calls_per_message` | U | — | — | — |
2017
+ | `tool_call_timeout` | U | — | — | — |
2018
+ | `allow_parallel_tool_calls` | U | — | — | — |
2019
+ | `tool_categories` | U | — | — | — |
2020
+ | `is_human_powered` | — | auto | — | — |
2021
+ | `human_interface` | — | U | — | — |
2022
+ | `human_timeout_seconds` | — | U | — | — |
2023
+ | `human_context_template` | — | U | — | — |
2024
+ | `human_prompt_template` | — | U | — | — |
2025
+ | `fallback_on_timeout` | — | U | — | — |
2026
+ | `fallback_response` | — | U | — | — |
2027
+ | `require_human_confirmation` | — | U | — | — |
2028
+ | `human_escalation_triggers` | — | U | — | — |
2029
+ | `webhook_config` | — | U | — | — |
2030
+ | `channels` | — | U | — | — |
2031
+ | `framework` | — | — | — | R |
2032
+ | `connector_config` | — | — | — | U |
2033
+ | `integration` | — | — | U | — |
2034
+ | `communication_type` | U | U | U | U |
2035
+ | `parallel` | U | U | U | U |
2036
+ | `max_concurrent` | U | U | U | U |
2037
+ | `wake_up` | U | U | U | U |
2038
+ | `yields` | U | U | U | U |
2039
+ | `inputs` | U | U | U | U |
2040
+ | `can_call` | U | U | U | U |
2041
+ | `narration` | U | U | U | U |
2042
+ | `knowledge` | U | U | U | U |
2043
+ | `wait_for` | U | U | U | U |
2044
+ | `wait_for_timeout` | U | U | U | U |
2045
+ | `auto_store_response` | U | U | U | U |
2046
+ | `auto_store_yields` | U | U | U | U |
2047
+ | `memory` | U | U | U | U |
2048
+
2049
+ **Legend:** R = required, U = used, — = ignored, auto = auto-set