LLAssemblyCLI.
Code driven agents orchestration
LLAssemblyCLI is a portable skill for agents orchestration that you can run in agentic CLI you already use:
- Compiles your request into an explicit control-flow program to orchestrate sub-agents.
- Runs that deterministic program dispatching sub-agents, feeding their results back into the control-flow state that decides what should be the next step.
- Persists every step to an append-only log so the loop can be paused, inspected, and resumed after a crash without losing its place.
If you've used Claude Code's Dynamic Workflows, the core concept is similar here, except:
- LLAssemblyCLI runs workflows as a skill in any: ClaudeCode, OpenCode, Codex, pi, ...
- Plan is represented in many different ways, including: Python, Mermaid, DSL Assembly.
- Runs locally: workflow can be evaluated and reproduced.
- Reliably executed under other skill definitions (can be nested in other skills).
- If needed LLAssemblyCLI can run itself (and any plan) on top of Claude Workflows.
LLAssemblyCLI ships with 4 variants of the control-flow:
- Python with pydantic-monty
- Pure Python
- Mermaid diagrams
- Assembly-like DSL (based on LLAssembly project)
https://github.com/user-attachments/assets/6b28c830-390a-452e-88ec-08a71cac5a52
⚠️ Work in progress: this library is under active development. Reports in Issues are appreciated.
What it is
The common way to orchestrate agents is to put an orchestrator agent in charge. The control flow lives inside the model's head and depends on the multiple factors during runtime.
You cannot review a decision tree up-front, and you cannot reproduce decision that was made from context you no longer have.
LLAssembly inverts this. Use the model once, up front, to compile your goal into a control-flow program — then let plain deterministic code execute it:
- It spawns sub-agents per workflow step
- Feeds each result back to the code-plan
- Decides on the next step based on the control-flow definitions in the code
When loop is started the pre-generated code — never the model — decides the next branch, retry, or loop.
This buys two things the orchestrator-agent pattern cannot guarantee:
- Reliability of the execution plan. Branches and loops are real code with real conditions, not a model's moment-to-moment judgement. The plan is written once, reviewed, and then followed faithfully.
- Reproducible execution. The same plan plus the same sub-agent results produce the same path every time. Execution state is persisted, so a loop can be paused, inspected, replayed, or resumed after a crash without losing its place.
Here is what a workflow looks like in mermaid plan variant. It is parsed, emulated and the drives your CLI tool to spawn sub-agents as defined:
flowchart TD
start([start]) --> implement[[implement: add retry logic to client.py]]
implement --> tests[[run_tests: run the suite and report failures]]
tests --> passed{"Did every test pass?"}
passed -- true --> done([done])
passed -- false --> fix[[diagnose_and_fix: find the cause and patch it]]
fix --> tests
You can also include such plan in any other skill and say "drive that with llassembly-agentic-workflow" ensuring reliability of how sub-agents spawns.
Installation
No dependencies needed. Git clone -> run a Python script to copy one of the skill variants into the target dir.
python copy_skill_to.py llassembly <target_dir> # build the ASM-emulator variant skill
# or
python copy_skill_to.py python <target_dir> # build the Python-planner variant skill
# or
python copy_skill_to.py monty <target_dir> # build the Pydantic-Monty variant skill
# or
python copy_skill_to.py mermaid <target_dir> # build the mermaid variant skill
npx skills:
# ASM-emulator variant skill
npx skills add https://github.com/electronick1/LLAssemblyCLI/tree/stable-llassembly/llassembly-agentic-workflow
# Python-planner variant skill
npx skills add https://github.com/electronick1/LLAssemblyCLI/tree/stable-python/llassembly-agentic-workflow
# Pydantic-Monty variant skill
npx skills add https://github.com/electronick1/LLAssemblyCLI/tree/stable-monty/llassembly-agentic-workflow
# Mermaid variant skill
npx skills add https://github.com/electronick1/LLAssemblyCLI/tree/stable-mermaid/llassembly-agentic-workflow
The variant you build determines which emulator/planner executes the plan (see more details in further sections). There are four, and they make different trade-offs.
| Variant | Plan language | Sandboxed | Best for |
|---|---|---|---|
mermaid |
Mermaid diagram | Yes, a mermaid graph iteration | Easy use by humans, portability |
llassembly |
Assembly-like instruction set | Yes, a simple emulator (no file/syscall/network instructions) | Small models, CLI tasks |
monty |
Plain Python def main() |
Medium — pydantic-monty (Rust-emulation, WIP) | Complex branching with an added safety layer |
python |
Plain Python def main() |
No — raw exec(), your own sandbox required |
Complex branching, structured JSON outputs |
Configuration
LLAssembly skill runtime:
The plan file (plan_llassembly.<ext>) and the append-only runtime.log are written to
a per-workflow directory <base>/llassembly/<workflow-id>/, where <base> defaults to
/tmp.
To save it elsewhere, set the LLASSEMBLY_WORKFLOW_PATH environment variable before the
first execution — e.g. export LLASSEMBLY_WORKFLOW_PATH=~/.cache writes to
~/.cache/llassembly/<workflow-id>/.
Claude workflow integration:
The skill can also be driven by Claude Code's Dynamic Workflows. Copy
assets/llassembly-claude-workflow.js into <project>/.claude/workflows/, then:
/llassembly-claude-workflow <goal>
LLAssemblyCLI will be executed on top of Claude dynamic workflows - each Claude agent seeks for the next instruction from LLAssemblyCLI planner and executes it, the workflow runs until all steps in the plan are done.
Pre-generate control-flow plan (workflow) and save it.
You can ask llassembly-agentic-workflow to pre-generate and save control-flow (workflow) without
execution. Just say that in the request to the model (e.g. ".... Save workflow without execution.").
CLI/model will ask you about skill name, and it will be saved in the project path to the skills
(depending on the CLI tool you use).
Mermaid-based planner variant
Executes a mermaid flowchart directly as a control-flow graph that spawns and orchestrates agents.
| Shape | Meaning |
|---|---|
start([start]) |
Entry point. Exactly one per plan. |
done([done]) / abort([abort]) |
Success / failure exits. At least one required. |
id[[name: objective]] |
Invoke the sub-agent name. |
id{"Question?"} |
Decision answered by the preceding sub-agent — free. |
id{{"Question?"}} |
Decision answered by its own sub-agent — costs one call. |
- A plain decision costs no sub-agent call. A
{ }node is attached to the single sub-agent above it as an extra output key on that agent's contract. - User Friendly The nice way to work with mermaid, as an orchestrator, is to ask model to generate plan/sub-agents and verify/extend/edit it by yourself. It is user friendly way to navigate through workflow before execution.
- Nested skills Mermaid plans are great when you want to use LLAssemblyCLI from other skills - include mermaid diagram to your skill, tell it to use llassembly-agentic-workflow and you get workflow running by LLAssemblyCLI or Claude Dynamic Workflows.
More info: mermaid_skill_parts/references/workers/llassembly-control-flow.md.
Assembly-based planner variant
Executes Assembly-like plan emitted by LLM in a lightweight emulator with a deliberately
limited instruction set (MOV, PUSH/POP, ADD/SUB, CMP,
conditional jumps, CALL/RET, and a handful of macro/db directives). Because
the plan can only express this tiny, well-defined set of operations:
- No extra hardening layers required. The emulator (see emulator.py) can only do what its
instruction table allows — there are no file, syscall, or internet-related
instructions; mainly: CMP, jumps, numbers/strings manipulation, that are emulated
in a limited and controlled way, only to connect sub-agents together,
see:
asm_skill_parts/references/workers/llassembly-control-flow.mdfor more details. - Plans stay stable even on small models. The narrow grammar is easy for weak models to emit correctly and to keep consistent across a long loop.
- Emulator is based on LLAssembly project.
This plan type shines when you want to run CLI based skills without extra infrastructure, and it performs quite well even on very small models like qwen3.6:35b.
Monty-based planner variant
Uses the same Python plan language as the Python variant, but replaces raw
exec() with pydantic_monty as
the execution engine.
pydantic-montydoes not guarantee 100% safety, since full Python emulation in Rust may contain security issues in its implementation and pydantic-monty project is itself a work-in-progress.pydantic-montyis a non-stdlib dependency. Before running this variant, ensurepydantic_montyis available in your Python environment. You can also specify the runtime in your prompt — for example: "Useuv runto execute Python".
This plan type is well suited when the orchestration logic is complex and benefits from a full programming language, with an additional layer of security compared to the YOLO Python variant.
YOLO Python-based planner variant
Executes raw Python emitted by the LLM: the plan is a small script that
declares sub-agents and drives them from a def main() entry point,
branching with ordinary if/while and returning rich values.
- ⚠️ WARNING! The Python-based planner runs LLM generated Python code, it must run in a safe sandboxed environment. Treat the python-based planner code as untrusted code! Running LLM generated code is never safe — and you are solely responsible for securing the environment in which it executes.
- Good for control flows with many branches and JSON outputs. Full Python expressiveness makes complex branching and structured (JSON) sub-agent results natural to handle.
This plan type is well suited when the orchestration logic is complex and benefits from a real programming language, but extra secure infrastructure is required.
Demo
The following shows what happens when you use the skill in Opencode as an example.
The goal in this example is: "implement the retry logic in client.py, run the test suite,
diagnose failures, fix them, and verify all tests pass — up to 5 attempts."
1. Install the skill into your OpenCode config dir
git clone https://github.com/electronick1/LLAssemblyCLI
cd LLAssemblyCLI
python copy_skill_to.py llassembly .opencode/skills/
# ✓ Successfully created skill at: .opencode/skills/llassembly-agentic-workflow
2. Start an OpenCode session with the skill loaded and state your goal
> Implement retry logic in client.py, run the tests, diagnose and fix failures,
verify all tests pass — up to 5 attempts.
3. The skill compiles the goal into a control-flow plan
The orchestrator calls get_next_instruction.py and is told to run the
llassembly-control-flow sub-agent, which writes plan_llassembly.<ext> (in one of 4
variants: assembly-like, mermaid, python, python for pydantic-monty):
%macro agent_implement
%include "general/implement"
%define OBJECTIVE "Implement retry logic in client.py"
%define OUTPUT_1 "status: \"ok\" or \"error\""
%endmacro
%macro agent_test
%include "general/test"
%define OBJECTIVE "Run the test suite and report results"
%define OUTPUT_1 "passed: \"true\" or \"false\""
%define OUTPUT_2 "failure_summary: brief description of failures if any"
%endmacro
%macro agent_diagnose_and_fix
%include "general/diagnose_and_fix"
%define OBJECTIVE "Diagnose test failures and apply fixes"
%define OUTPUT_1 "status: \"ok\" or \"error\""
%endmacro
MOV R20, 0 ; initialize retry counter to zero
loop: ; loop head: start of retry cycle
agent_implement ; invoke implement sub-agent
MOV R1, OUTPUT_1 ; copy implement result into R1
CMP R1, "ok" ; compare implement status against "ok"
JNE done_fail ; if implementation failed, jump to failure exit
agent_test ; invoke test sub-agent
MOV R2, OUTPUT_1 ; copy test result into R2 (distinct register)
CMP R2, "true" ; compare test pass flag against "true"
JE done_success ; if all tests passed, jump to success exit
ADD R20, 1 ; increment retry counter by one
CMP R20, 5 ; compare retry counter against max of 5 attempts
JGE done_fail ; if retry budget exhausted, jump to failure exit
agent_diagnose_and_fix ; invoke diagnose_and_fix sub-agent
JMP loop ; jump back to loop head to retry
done_success: ; success exit label
MOV R10, 0 ; set exit code to zero (success)
JMP done ; jump to single completion path
done_fail: ; failure exit label
MOV R10, 1 ; set exit code to one (failure)
done: ; single completion label for all exit paths
RET ; return from main execution
4. The driver generates any missing sub-agent definitions
Before execution begins, the driver scans the plan for every declared sub-agent and checks
whether a definition file already exists. For each one that is missing it runs the
llassembly-generate-sub-agents agent, which writes a ready-to-use .md definition
into the workflow's references/agents/ directory.
[driver] → missing agent: implement → running llassembly-generate-sub-agents...
✓ references/agents/implement.md written
[driver] → missing agent: test → running llassembly-generate-sub-agents...
✓ references/agents/test.md written
[driver] → missing agent: diagnose_and_fix → running llassembly-generate-sub-agents...
✓ references/agents/diagnose_and_fix.md written
[driver] → all agents present, starting execution
5. The driver executes the plan — one sub-agent at a time
[driver] → run sub-agent: implement (writes retry logic to client.py)
[driver] → run sub-agent: test (2 failures found)
[driver] → run sub-agent: diagnose_and_fix (fixes import error + off-by-one)
[driver] → run sub-agent: test (all tests pass)
[driver] Execution finished. Goal is achieved.
Every step is appended to /tmp/llassembly/<workflow-id>/runtime.log as JSONL. If the session
crashes between any two steps, re-running the driver replays the log and resumes from exactly
where it left off — no work is repeated.
What plan_llassembly.<ext> looks like
All four variants express the same kind of plan — declare the sub-agents the
control flow invokes, then orchestrate them with a bounded, verify-driven loop —
in their own language. The plan file is always plan_llassembly.<ext>; the ASM
variant fills it with assembly, the mermaid variant with a flowchart, and the
Python and Monty variants with a Python script.
Assembly variant
%macro agent_build ; Declare the build sub-agent (interface only)
%include "general/build" ; Where the build sub-agent's specification belongs
%define OBJECTIVE "Build the project artifact from source" ; One-sentence objective
%define OUTPUT_1 "status: \"ok\" on success or \"error\" on failure" ; Build status contract
%endmacro ; End of build sub-agent declaration
%macro agent_verify ; Declare the verifier sub-agent (interface only)
%include "general/verify" ; Where the verifier sub-agent's specification belongs
%define OBJECTIVE "Verify the built artifact passes all checks" ; One-sentence objective
%define OUTPUT_1 "all_passed: \"true\" if every check passed, else \"false\"" ; Verify contract
%endmacro ; End of verifier sub-agent declaration
MOV R20, 0 ; Initialize retry counter to zero
build_and_verify: ; Loop head: build the artifact then verify it
agent_build ; Invoke the build sub-agent
MOV R1, OUTPUT_1 ; Copy build status into R1 before it is overwritten
CMP R1, "ok" ; Compare build status output against success indicator
JNE handle_failure ; If build failed, go to the failure/retry handler
agent_verify ; Invoke the verifier sub-agent
MOV R2, OUTPUT_1 ; Copy verifier result into R2 (distinct register)
CMP R2, "true" ; Compare verifier pass flag against "true"
JE done_success ; If verification passed, the goal is achieved
handle_failure: ; Failure handler: spend one retry if budget allows
ADD R20, 1 ; Increment the retry counter
CMP R20, 3 ; Compare retry counter against the maximum of 3 attempts
JGE done_giveup ; If budget exhausted, stop retrying and give up
JMP build_and_verify ; Otherwise loop back and rebuild then re-verify
done_success: ; Success exit: verifier confirmed the goal
MOV R10, 0 ; Set exit code to zero indicating success
JMP done ; Jump to the single completion path
done_giveup: ; Give-up exit: retry budget exhausted
MOV R10, 1 ; Set exit code to one indicating failure
done: ; Single completion label for all exit paths
RET ; Return from main execution reaching completion
Python (or python monty) variant
# Declare the build sub-agent (interface only — no behavior implemented).
setup_sub_agent(
name="build",
objective="Build the project artifact from source",
output_spec={"status": 'status: "ok" on success or "error" on failure'},
existing=False,
)
# Declare the verifier sub-agent (interface only — no behavior implemented).
setup_sub_agent(
name="verify",
objective="Verify the built artifact passes all checks",
output_spec={"all_passed": 'all_passed: "true" if every check passed, else "false"'},
existing=False,
)
def main():
attempts = 0 # initialize the retry counter
max_attempts = 3 # bound the loop so the plan always terminates
succeeded = False # track whether the goal was achieved
while attempts < max_attempts: # loop until verified or budget exhausted
attempts += 1 # spend one retry on this iteration
build_result = run_sub_agent("build") # invoke the build sub-agent
build_status = build_result["status"] # capture build status before reuse
if build_status != "ok": # build failed
continue # retry the build on the next iteration
verify_result = run_sub_agent("verify") # invoke the verifier sub-agent
verify_passed = verify_result["all_passed"] # capture verifier flag distinctly
if verify_passed == "true": # verification confirmed the goal
succeeded = True # record success
break # the goal is achieved; stop looping
# verification failed; loop back to rebuild and verify again
exit_code = 0 if succeeded else 1 # zero on success, one when the budget ran out
return exit_code
Mermaid variant
flowchart TD
%% A gate before any work. Nothing precedes it, so it needs its own sub-agent ({{ }})
start([start]) --> q_clean
q_clean{{"Is the working tree clean and on the release branch?"}}
q_clean -- true --> w_build
q_clean -- false --> abort
%% An error edge is worth drawing only because a real recovery follows it
w_build[[build: Build the release artifact from source]] --> w_test
w_build -- error --> w_deps
w_deps[[repair_deps: Reinstall dependencies and clear the build cache]] --> w_build
%% run_tests reports its own status and answers the question below, in one call
w_test[[run_tests: Run the full test suite against the built artifact]] --> d_pass
d_pass{"Did every test pass?"}
d_pass -- true --> w_publish
d_pass -- false --> w_fix
w_fix[[fix_tests: Diagnose the failing tests and patch the source]] --> w_build
%% Verify the outcome rather than trusting the step that produced it
w_publish[[publish: Publish the artifact to the release channel]] --> d_live
d_live{"Is the published release reachable and serving the new version?"}
d_live -- true --> finish
d_live -- false --> w_rollback
w_rollback[[rollback: Restore the release channel to the previous version]] --> abort
finish([done])
abort([abort])
flowchart TD
%% A gate before any work. Nothing precedes it, so it needs its own sub-agent ({{ }})
start([start]) --> q_clean
q_clean{{"Is the working tree clean and on the release branch?"}}
q_clean -- true --> w_build
q_clean -- false --> abort
%% An error edge is worth drawing only because a real recovery follows it
w_build[[build: Build the release artifact from source]] --> w_test
w_build -- error --> w_deps
w_deps[[repair_deps: Reinstall dependencies and clear the build cache]] --> w_build
%% run_tests reports its own status and answers the question below, in one call
w_test[[run_tests: Run the full test suite against the built artifact]] --> d_pass
d_pass{"Did every test pass?"}
d_pass -- true --> w_publish
d_pass -- false --> w_fix
w_fix[[fix_tests: Diagnose the failing tests and patch the source]] --> w_build
%% Verify the outcome rather than trusting the step that produced it
w_publish[[publish: Publish the artifact to the release channel]] --> d_live
d_live{"Is the published release reachable and serving the new version?"}
d_live -- true --> finish
d_live -- false --> w_rollback
w_rollback[[rollback: Restore the release channel to the previous version]] --> abort
finish([done])
abort([abort])
No comments yet
Be the first to share your take.