agent-simplification
Simplify a complex ElevenLabs voice agent (typically a large workflow agent) into a leaner architecture — fewer workflow nodes, deduplicated prompts, procedures where they help — while PROVING with a ground-truth test suite that every behaviour of the original is preserved or improved. Use when the user says things like 'simplify this agent', 'flatten this workflow', 'this agent is too complex', 'consolidate these nodes', 'rebuild this agent with fewer nodes', 'can we replace this workflow with procedures', or asks whether a big workflow agent can be made easier to maintain without losing behaviour."

ElevenLabs Agent Simplification

Turn a complex agent (usually a many-node workflow) into a simpler one that is provably at least as good. The core loop: understand everything the complex agent does → encode it as a test suite (ground truth) → build the simpler agent → iterate until it matches or beats the original on every test → A/B on branches → hand over with an honest report.

The output is never just a simpler agent. It is a simpler agent plus the test suite that proves equivalence, attached natively in the platform so the client can re-run it.

Non-negotiable safety rails

Phase 1 — Understand the complex agent completely

Pull the agent JSON (GET /v1/convai/agents/{id}) and map every layer:

  1. Base prompt (conversation_config.agent.prompt.prompt).
  2. Workflow nodes — read additional_prompt, not just prompt.prompt. This is the #1 trap: an override_agent node's real instructions live in additional_prompt (the studio "Conversation goal" box, APPENDED to the base prompt). conversation_config.agent.prompt.prompt on a node is only set when the "Override prompt" toggle is ON. A node whose prompt.prompt is empty is NOT an empty node. Extract every node's additional_prompt to a file — in a real case 16 "empty-looking" nodes carried ~100k chars of specialized business logic.
  3. Edges — routing conditions (forward_condition.label), in/out degrees. The only dead nodes are orphans (no incoming edge = unreachable) and fully disconnected nodes (no edges at all). Anything with edges is a live decision point — every node has an LLM choosing among its outgoing edges, even a tool node with an empty tool list — so it cannot be considered for removal unless genuine logic duplication is proven.
  4. Per-node scopingadditional_knowledge_base / additional_tool_ids. Check whether the base agent already exposes the same KB (folders in RAG auto mode = every doc) and tools; if so the scoping is redundant, not behaviour.
  5. Say nodes — fixed messages the client wants said verbatim (goodbyes, transfer preambles, mandated phrases). These become wording-parity tests later.
  6. Transfer machinery — compare workflow phone_number node destinations against the base transfer_to_number system tool (destination SIP/number + its condition text). Often identical → the workflow transfer scaffolding is redundant. Check real usage: count tool fires across recent conversations (which transfer path actually runs in prod?).
  7. Dispatch tools (tool nodes in the graph) — these fire deterministically whenever the graph reaches that node, whereas agent-attached tools fire at the LLM's discretion. Examine each one: which paths reach it, and what guarantee does it provide? Flattening removes that guarantee, so every dispatch path needs a test proving the tool still fires in the simplified agent. Some dispatch tools also turn out redundant (e.g. a "say X then transfer" preamble tool duplicating what the transfer_to_number system tool already does) — check real-call tool-fire counts before deciding.
  8. Procedures, system tools (built_in_tools), language config — note language_presets lives at the TOP level of conversation_config, not under .agent.
  9. Real conversations — pull recent calls (GET /v1/convai/conversations?agent_id=&page_size=100, paginate; detail per call). Categorize (use the agent's own call_category data-collection if present), count tool fires, and note failure modes. This tells you which paths matter and at what frequency.

If you find duplicated logic anywhere (repeated boilerplate across nodes, per-node scoping the base already has, scaffolding duplicating a system tool), simply remove it in the simplified build — after confirming it is a true duplicate, not a variant.

Phase 2 — Build the ground-truth test suite BEFORE simplifying

Derive test scenarios from both sources: - The complex agent itself: every distinct behaviour in the node additional_prompts, the base prompt's flows (e.g. an operator-transfer escalation sequence), each qualifying-transfer case, say-node wording, dispatch-tool paths, procedure flows, tool-consent rules, deprecated services, coverage/country rules, language policy. - Real conversations: one scenario per distinct path/category actually observed, phrased as a simulated-user persona (reference the source conv_ id in the scenario for traceability).

Two complementary instruments — pick per behaviour:

simulation tests (multi-turn) llm tests (single-turn)
What Simulated user persona converses N turns; whole conversation judged against success_conditions Fixed chat_history; judge the agent's NEXT reply against success_condition
Use for Procedure-driven and multi-step flows (diagnostics, escalation sequences, retention) Sharp single decision points (don't transfer on first demand; reply language; refusal)
Avoid for Any flow whose first agent turn is a tool call (start_procedure, language_detection): the judge sees no text → unknown. Use a simulation test instead.

Criteria-writing rules (learned the hard way): - Verify every fact in a criterion against the KB before asserting it. Do not label something a hallucination until you've grepped the KB (a "made-up" USSD code turned out to be documented). - Accept reasonable behaviour. If answering a direct question briefly before a still-needed transfer is fine, say so in the criterion. Over-strict criteria create false failures you'll then "fix" wrongly. - Don't let the scenario undermine the premise. If testing "plain SIM fails where partner is 4G-only", pin a country that actually has no 3G (check the KB), or the agent will be correct and the test wrong. - Don't test end_call. Test harnesses don't simulate call termination reliably, and the caller hangs up anyway. Testing that the agent does NOT hang up prematurely is fine. - One criterion per saved test where possible (clean per-behaviour pass/fail in the dashboard).

Mechanics: - Save as native tests (POST /v1/convai/agent-testing/create) and attach to both the original (reference) agent and the simplified copy via platform_settings.testing.attached_tests — dashboard-visible, re-runnable by the client, and the same test objects score both sides of the A/B. - Mock every webhook/client tool with tool_mock_overrides (per-test inline mock bodies keyed by tool id: {tool_id: [{"mock_result": "<json string>"}]} with tool_mock_config: {mocking_strategy: "all", ...}). Nothing hits real endpoints and no global tool object is edited. - Seed dynamic_variables from a real call (scrubbed), and include system__call_sid, system__caller_id, system__conversation_id, system__called_number — simulations error with missing_dynamic_variables otherwise if any tool references them. - Tests already attached to the original agent are shared objects: adding new tests alongside them is fine, but never edit or remove the existing ones.

Baseline the original agent on the suite first. Its failures are improvement opportunities, not excuses ("equivalently bad" is not the goal).

Phase 3 — Simplify

Default target: single-agent (1-node workflow) + base prompt + focused rules + procedures + KB. Keep the workflow only where determinism is genuinely required and prompt enforcement proves unreliable in tests.

Phase 4 — Iterate on failures (the discipline that makes this work)

Run the suite on the copy; then, for every non-passing test (on the copy or the original): 1. Read the transcript/rationale once. Do not re-run to confirm a failure — one failure means there's something to improve. (Re-running to verify a FIX is fine.) 2. Classify: agent bug (fix with a focused rule/procedure edit, then verify) vs test bug (wrong premise, over-strict criterion, wrong instrument — fix the test, never by loosening it past what's actually correct) vs ungradable artifact (tool-only turn in an llm test → convert to a simulation test or retire the duplicate). 3. Failures shared by the original agent are still yours to fix — the tests encode ground truth from real calls, and the goal is a better agent, not parity with its defects. 4. Watch for rule dilution: as prompt rules accumulate, earlier enforcement can weaken. If a previously-passing mandated behaviour regresses, strengthen that rule's self-check rather than adding more prose.

Phase 5 — Final A/B on branches of the original agent

The cleanest comparison is two branches of the SAME agent: - POST /v1/convai/agents/{id}/branches with {parent_version_id, name, description, conversation_config, platform_settings, workflow} → a branch (e.g. "Pete") carrying the simplified config; Main stays untouched. parent_version_id = the agent's current version_id. - GET /v1/convai/agents/{id}?branch_id=... to verify each branch's config. - Run the full suite per branch: POST /v1/convai/agents/{id}/run-tests with {"tests":[{"test_id":...}], "branch_id": ...}; poll GET /v1/convai/test-invocations/{invocation_id} until all runs are terminal; result is condition_result.result (success / failure / unknown; rationale may be a dict with summary). - Report per-branch totals + per-test diffs, with invocation ids (client-verifiable in the dashboard).

Phase 6 — Report honestly

API gotchas appendix

Procedures (draft → commit lifecycle — everything is branch-scoped): - POST /v1/convai/agents/{id}/branches/{br}/procedures {name, content, type: "free_form", trigger?} → returns procedure_id, but creates the procedure as a draft and adds its ref to your user's agent draft. It will NOT appear in the committed agent/branch procedure lists yet — do not conclude the API "didn't attach" (checking the committed view after create is the classic mistake). - GET .../procedures/status → the draft refs; GET agent?include_draft=true → draft config. - POST .../procedures/compilepreview only: validates and returns the compiled workflow + errors. It commits nothing. - Commit = PATCH /v1/convai/agents/{id}?branch_id={br} (an empty JSON body works): it resolves procedure refs from your draft into the new committed version, then deletes the drafts. After this, the procedure appears in GET agent .procedures, the branch list, and direct GET. - Edit content: PATCH .../procedures/{pid}/draft (full body: name, content, type, trigger) → commit PATCH. Delete: DELETE .../procedures/{pid} (removes from draft set) → commit PATCH. - Procedure content = markdown with --- frontmatter (name, trigger) + steps. Reference KB docs as [kb id="..." name="..."], tools as [tool id="..." name="..."], system tools as [system_tool id="transfer_to_number" name="..."]. - Procedures are per-agent (cloned when an agent is duplicated) — but the KB/tools they reference are global.

Testing: - Saved-test update is PUT (full replace — a partial body silently wipes omitted fields), not PATCH (405). - simulate-conversation (ad-hoc, no saved artifact) runs the LIVE agent config — to A/B a candidate config you must actually PATCH it (or use branches + run-tests). - A test whose judged turn is a bare tool call grades unknown — instrument mismatch, not failure. - Multi-turn sims have some single-run flakiness. Don't paper over it with repeat runs — testing has cost; prefer broadening the suite with more varied scenarios, and investigate any failure rather than re-rolling it.

Agent config: - A language preset makes a language switchable by language_detection; but if a {{language}} dynamic variable anchors the default, add an explicit "the spoken language wins" prompt rule. - Conversation-detail endpoint rate-limits hard — keep concurrency ~4-5 with retries.