leonard_cli 0.3.0
leonard_cli: ^0.3.0 copied to clipboard
CLI frontend for Leonard.
leonard_cli #
Command-line entrypoint for Leonard. Connects to a running Flutter VM service, drives the perception-action loop, and streams a trajectory file to disk.
Model providers #
The --model flag selects a ModelTier which
the provider_factory maps to a
concrete ModelProvider:
| Tier | Provider | Required env |
|---|---|---|
claude |
AnthropicModelProvider |
ANTHROPIC_API_KEY (default) |
qwen-mlx |
SwiftInferModelProvider (local swift-infer gateway) |
SWIFT_INFER_AGENT_TOKEN (when the gateway requires auth), SWIFT_INFER_ENDPOINT (optional, defaults to http://localhost:8080), SWIFT_INFER_MODEL (optional, defaults to qwen3.6-35b-a3b-8bit), SWIFT_INFER_REASONING_EFFORT (optional), SWIFT_INFER_MAX_TOKENS (optional, defaults to 16384) |
openai |
OpenAiModelProvider |
OPENAI_API_KEY |
swift-infer gateway (qwen-mlx) #
When --model qwen-mlx is selected, the CLI talks to the user's local
swift-infer gateway. The wire contract is intentionally identical to
fs agent (factoryskills' agent implementation in
factoryskills/internal/agent/agent.go) so the same gateway and
inspection tooling work for both clients.
Environment variables #
SWIFT_INFER_AGENT_TOKEN— forwarded asAuthorization: Bearer <token>. Same namefs agentuses; share one shell export for both. When unset (or empty), the CLI sends an unauthenticated request — useful when pointing at a gateway that has auth disabled, but the production gateway requires this.SWIFT_INFER_ENDPOINT— base URL of the gateway. Defaults tohttp://localhost:8080when unset or empty.SWIFT_INFER_MODEL— model id requested from the gateway. Defaults toqwen3.6-35b-a3b-8bitwhen unset or empty. Set it to any node the gateway serves (curl "$SWIFT_INFER_ENDPOINT/v1/models"lists them), e.g.qwen3.8-40b-a3b-8bit. The--model-idflag outranks this variable, which outranks the default; capabilities resolve from the id itself, so anyqwen3.*node keeps vision + preserved thinking.SWIFT_INFER_REASONING_EFFORT—reasoning_effortsent on every/v1/messagesrequest when resolved: one ofnone,low,medium,high,xhigh. Unset (or empty) means the field is omitted and the node's model-card default applies — except for aqwen3.8*model id, which defaults tomediumbecause that template otherwise runsxhigh. The--reasoning-effortflag outranks this variable; an unparseable value fails the run loudly.SWIFT_INFER_MAX_TOKENS—max_tokensfor a single response. Defaults to16384(thinking plus answer share this budget). The--max-tokensflag outranks this variable; a non-positive or non-numeric value fails the run loudly.
Every other sampling knob (temperature, top_p, top_k,
presence_penalty, repetition_penalty) is deliberately NOT sent by the CLI,
so the swift-infer node's per-model card values apply. preserve_thinking is
always sent — it is a reasoning-replay contract, not a sampling knob.
Per-run conversation tracing #
Every run mints a stable sessionId (cli-<utc-iso8601> slugged to
header-safe characters) and stamps every request with:
X-Session-Id: <sessionId>X-Conversation-Id: leonard-<sessionId>-<unixMs>— one conversation per run, groups every turn for inspection. Mirrorsfs agent'sfsagent-<beadID>-<unixtime>convention.X-Swift-Infer-Capture-Bodies: true—captureBodiesis on by default for dev/PoC. The gateway captures both the request and response bodies soGET $SWIFT_INFER_ENDPOINT/v1/conversations/<id>returns the captured turn for inspection without re-running the agent.Accept: text/event-stream— SSE streaming for live<think>…</think>surfaces.
Example (default — cloud backend):
export ANTHROPIC_API_KEY=sk-ant-…
dart run leonard_cli \
--vm-uri ws://127.0.0.1:54321/abc=/ws \
--goal "open settings"
Example (opt-in — local swift-infer):
export SWIFT_INFER_AGENT_TOKEN=sk-…
export SWIFT_INFER_ENDPOINT=http://localhost:8080 # optional
export SWIFT_INFER_MODEL=qwen3.8-40b-a3b-8bit # optional
export SWIFT_INFER_REASONING_EFFORT=medium # optional
export SWIFT_INFER_MAX_TOKENS=16384 # optional
dart run leonard_cli \
--model qwen-mlx \
--vm-uri ws://127.0.0.1:54321/abc=/ws \
--goal "open settings"
# Inspect captured turn:
curl "$SWIFT_INFER_ENDPOINT/v1/conversations/leonard-cli-…"
Captured frames and image goldens #
Every live-driving run writes PNG frames already carried by its trajectory turns. By default, the frame directory is derived from the trajectory path:
trajectories/
├── 20260507T141503Z.jsonl
└── 20260507T141503Z.frames/
├── turn-0000.png
└── turn-0001.png
The same rule keeps frames inside the station's existing per-run directory:
panel-selfdrive-20260507T141503Z/
├── outer.jsonl
├── outer.frames/
│ ├── turn-0000.png
│ └── turn-0001.png
├── driver.log
└── driver.status
--frames-dir <dir> overrides only frame placement. The CLI never deletes or
prunes frame or run directories; the caller owns retention. With no
--goldens-dir, the run is capture-only.
To compare the current run against PNG baselines, supply their directory:
dart run leonard_cli \
--vm-uri ws://127.0.0.1:54321/abc=/ws \
--goal "open settings" \
--goldens-dir image_goldens
Frames are paired with goldens by filename. Image dimensions must match. A
pixel differs when any RGBA channel delta exceeds
--golden-channel-tolerance (default 8), and the frame fails when the
differing-pixel ratio exceeds --golden-max-diff-ratio (default 0.0).
To establish or replace baselines, run:
dart run leonard_cli \
--vm-uri ws://127.0.0.1:54321/abc=/ws \
--goal "open settings" \
--goldens-dir image_goldens --update-goldens
Update mode creates the baseline directory, overwrites same-named files, and
prints every baseline written. It never compares in the same invocation. The
repository's comparator fixtures are PNGs under
packages/leonard_cli/test/image_goldens/; they are distinct from the JSON
observation goldens used by the Flutter perception-equivalence tests.
Nightly dogfood #
The nightly e2e test (packages/leonard_agent/integration_test/dogfood/dogfood_e2e_test.dart) is
self-pinned to local inference via its own SwiftInferConfig construction and does not
depend on leonard_cli's --model default. The launchagent that runs the nightly
test (scripts/launchd/run-dogfood.sh) invokes dart test directly on the e2e test
file, bypassing the CLI entirely, so the default change has no effect on nightly behavior.
See also #
factoryskills/internal/agent/agent.go— the reference implementation of this wire contract. lenny's swift-infer client is intentionally header-for-header symmetric withfs agent; if you add a new header to one, add it to the other.lib/src/provider/swift_infer/swift_infer_config.dart(inleonard_agent) — provider-side config surface, including theextraHeadersforward-compat bag.