You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
PraisonAI PR MervinPraison/PraisonAI#1577 — fix(wrapper): restore #1561 auto-discovery dispatcher in __main__.py (closes #1576) — was merged to main on 2026-04-28 (head SHA 8d373843dc062f358ff5be7722a8cadbe71ce105).
This PR replaces the entire CLI dispatcher implementation that issues #282 and #276 were drafted against. The previous heuristic-based routing (_is_legacy_invocation()) is gone; the routing model is now Typer-first auto-discovery with five explicit precedence rules. Any unmerged doc work targeting the old 3-tier routing diagram in #282 is now factually wrong and must be updated against this PR's source-of-truth.
The dispatcher is the single user-facing entry point for everything typed as praisonai ... — its routing rules are non-obvious (--help and --version go to different paths, a YAML filename routes by command-set membership rather than file existence, etc.) and have never been documented in MervinPraison/PraisonAIDocs. The 26 unit tests added in #1577 pin the contract precisely; treat each test name as a documented behavior.
SDK Truth (read these files before writing the doc)
Per AGENTS.md §1.2 SDK-First cycle — read source, understand, then document. Do not copy from #282/#276 prose; those describe the old model.
Module-path note: In MervinPraison/PraisonAIDocs (synced via update_repos.sh), wrapper sources land at repo-root praisonai/, not src/praisonai/. Verify paths against the synced tree before quoting them in docs.
register_commands() — the registration is now wrapped in the same lock the discoverer uses, and _commands_registered = True is flipped only after the last add_typer() succeeds (no half-registered command tree).
26 tests across 6 classes — each test name is a documented behavior.
Key facts to extract from the source
Routing precedence (from main() in __main__.py, in order):
--version / -V → print version and return — does not import praisonai.cli.* (proven by TestVersionShortCircuit::test_version_does_not_import_typer_or_legacy, which evicts praisonai.cli.* from sys.modules and asserts they stay absent).
--help / -h → Typer (so help auto-discovers all registered subcommands).
No argv → Typer (interactive TUI).
First non-flag positional ∈ _get_typer_commands() → Typer.
Skips leading global flags like --verbose, --debug, --json.
Treats --output-format and -o as value flags — also skips the value that follows them.
Returns None when argv has only flags (rule 5 above falls through to "Typer for global-flag handling").
Free-text prompts (token containing a space) and .yaml paths are returned as-is — the routing decision happens in main() based on command-set membership, not by inspecting the filesystem.
Auto-discovery cache (_get_typer_commands()):
Lazily loads app + calls register_commands(), then introspects via click.Context(typer.main.get_command(app)).list_commands(ctx).
Result cached in module-global _typer_commands_cache under _typer_commands_lock.
Cache is NOT poisoned on failure: a discovery error returns set() for that call but leaves _typer_commands_cache = None so the next caller retries (verified by TestGetTyperCommandsCache::test_failure_does_not_poison_cache).
Concurrent callers get the same cached object — 8-thread stress test in TestGetTyperCommandsCache::test_concurrent_callers_get_same_result.
Failure modes (fail-loud invariant):
register_commands() exceptions inside _run_typerpropagate — they are NOT wrapped in try/except. A missing optional dep surfaces as the underlying ImportError, not as Typer's empty-app behavior. Pinned by TestTyperRegistrationFailureFailsLoud.
_run_typer and _run_legacy both restore sys.argv in finally: even on SystemExit — pinned by TestRunTyperArgvRestoration and TestRunLegacyArgvRestoration (both use argv[0] = "/usr/local/bin/some-launcher" so the assertion has discriminating power against a missing finally).
Why this matters to users (extensibility):
Adding a Typer subcommand → automatic routing. No manual command list, no opt-in. As soon as the new command is registered in praisonai.cli.app, typing praisonai <new-command> routes to Typer.
No filesystem dependency for YAML routing. A typo'd YAML path no longer silently falls into legacy because the file doesn't exist — it falls into legacy because it isn't a registered command. Misroutes that used to depend on os.path.isfile() are gone.
Place at docs/features/cli-dispatcher.mdx. NEVER docs/concepts/.
Add an entry to docs.json under the Features group (not Concepts, not the auto-managed docs/js/ or docs/rust/ groups).
Validate docs.json is still valid JSON after editing.
Required frontmatter
---
title: "CLI Dispatcher"sidebarTitle: "CLI Dispatcher"description: "How praisonai routes your command — version, Typer subcommand, or legacy prompt/YAML"icon: "terminal"
---
Required structure (per AGENTS.md §2)
One-sentence intro — agent-centric, beginner-friendly. Example tone: "praisonai picks one of five paths based on what you type — and adding a new subcommand means it Just Works."
Hero Mermaid diagram (see exact spec below).
## Quick Start with <Steps> — show the five routing paths as five Steps (praisonai --version, praisonai, praisonai --help, praisonai chat "...", praisonai "free-text prompt").
## How It Works — sequenceDiagram showing User → main() → [version|help|empty|known-cmd|fallback] → [Typer|Legacy|exit].
## Routing Rules — the precedence table below, copied verbatim.
## Auto-Discovery — explain why adding a new Typer subcommand "just works" (one short paragraph + 4-line example of registering a command and seeing it route).
## Common Patterns — 3 examples: bare prompt, .yaml file, subcommand with global flags (e.g. praisonai --verbose chat "hi" — show that --verbose is skipped when finding the first positional).
## Best Practices with <AccordionGroup>:
"Why --version is fast" (no praisonai.cli.* imports → still works with broken optional deps).
"Adding a new subcommand" (register in cli/app.py, no dispatcher changes needed).
"Free-text prompts vs. typo'd command names" (a typo routes to legacy, not to a Typer "command not found" error — explain why).
"Failure visibility" (registration errors propagate; the dispatcher does NOT swallow them).
## Related with <CardGroup cols={2}> — link to /docs/cli/cli, /docs/cli/cli-reference, /docs/features/gateway, /docs/cli/version.
Hero diagram (use these exact colors — AGENTS.md §3.1)
graph TB
Start[📋 praisonai argv] --> V{🔍 --version<br/>or -V?}
V -->|Yes| Print[⚡ Print version<br/>no cli.* imports]
V -->|No| H{🔍 --help<br/>or -h?}
H -->|Yes| Typer1[🧰 Typer help]
H -->|No| E{🔍 argv empty?}
E -->|Yes| Typer2[🧰 Typer TUI]
E -->|No| F[🔎 first non-flag positional]
F --> K{🧠 in registered<br/>Typer commands?}
K -->|Yes| Typer3[🧰 Typer subcommand]
K -->|No| Legacy[🤖 Legacy<br/>PraisonAI().main]
classDef input fill:#6366F1,stroke:#7C90A0,color:#fff
classDef check fill:#F59E0B,stroke:#7C90A0,color:#fff
classDef route fill:#10B981,stroke:#7C90A0,color:#fff
classDef agent fill:#8B0000,stroke:#7C90A0,color:#fff
class Start input
class V,H,E,K check
class F input
class Print,Typer1,Typer2,Typer3 route
class Legacy agent
Loading
Routing rules table (copy verbatim — derived from __main__.py:main() post-#1577)
#
What you type
Route
Notes
1
praisonai --version / praisonai -V
Version short-circuit
Prints version and returns. Does not import praisonai.cli.* — stays fast even with broken optional deps. Pinned by TestVersionShortCircuit.
2
praisonai --help / praisonai -h
Typer
Typer's auto-generated help lists every registered subcommand (auto-discovered, no manual list).
Auto-discovered via Click introspection of app. Adding a new subcommand to cli/app.py makes it routable here with zero dispatcher changes.
6
praisonai "Build a weather agent" (free-text — token contains a space)
Legacy PraisonAI().main()
Free-text prompts always fall through to legacy.
7
praisonai agents.yaml (filename, not a registered command)
Legacy PraisonAI().main()
Routing decision is by command-set membership, NOT by os.path.isfile(). A typo'd YAML path also routes to legacy and surfaces there.
8
praisonai totally-unknown (unknown positional)
Legacy PraisonAI().main()
Same as row 7 — anything not in the discovered command set falls through.
Auto-discovery callout
Add a <Note> block:
Adding a new subcommand? Register it in praisonai/cli/app.py (e.g. app.add_typer(my_app, name="mycmd")) and the dispatcher picks it up automatically — praisonai mycmd ... routes to Typer with no changes to __main__.py. The command set is discovered once via click.Context.list_commands() and cached behind a thread-safe lock.
Failure-modes callout
Add a <Warning> block:
Registration errors fail loud. If register_commands() raises (e.g. an ImportError from a missing optional dep), the exception propagates from praisonai ... — you see the real error, not Typer's "no command" page. This is intentional and pinned by tests.
Acceptance criteria
New file docs/features/cli-dispatcher.mdx exists, follows the structure above, passes the AGENTS.md §9 quality checklist (frontmatter, hero diagram, Steps, AccordionGroup, CardGroup all present).
Hero diagram uses the AGENTS.md §3.1 color palette exactly (#6366F1 input, #F59E0B check, #10B981 route, #8B0000 legacy/agent, white text, #7C90A0 strokes).
Routing rules table is the 8-row table above, verbatim.
At least one <Note> covers auto-discovery and one <Warning> covers fail-loud registration.
docs.json updated to add the new page under the Features group; docs.json is still valid JSON.
No edits to docs/concepts/. No edits to docs/js/ or docs/rust/.
No invented APIs — every claim traces to praisonai/__main__.py or praisonai/cli/app.py post-#1577 (head SHA 8d373843dc062f358ff5be7722a8cadbe71ce105).
Document does not reference the old _is_legacy_invocation() heuristic — it was removed in #1577.
Context
PraisonAI PR MervinPraison/PraisonAI#1577 — fix(wrapper): restore #1561 auto-discovery dispatcher in
__main__.py(closes #1576) — was merged tomainon 2026-04-28 (head SHA8d373843dc062f358ff5be7722a8cadbe71ce105).This PR replaces the entire CLI dispatcher implementation that issues #282 and #276 were drafted against. The previous heuristic-based routing (
_is_legacy_invocation()) is gone; the routing model is now Typer-first auto-discovery with five explicit precedence rules. Any unmerged doc work targeting the old 3-tier routing diagram in #282 is now factually wrong and must be updated against this PR's source-of-truth.The dispatcher is the single user-facing entry point for everything typed as
praisonai ...— its routing rules are non-obvious (--helpand--versiongo to different paths, a YAML filename routes by command-set membership rather than file existence, etc.) and have never been documented inMervinPraison/PraisonAIDocs. The 26 unit tests added in #1577 pin the contract precisely; treat each test name as a documented behavior.SDK Truth (read these files before writing the doc)
Per
AGENTS.md§1.2 SDK-First cycle — read source, understand, then document. Do not copy from #282/#276 prose; those describe the old model.src/praisonai/praisonai/__main__.pypraisonai/__main__.pymain(),_get_typer_commands(),_find_first_command(),_run_typer(),_run_legacy(),_typer_commands_cache,_typer_commands_lock.src/praisonai/praisonai/cli/app.pypraisonai/cli/app.pyregister_commands()— the registration is now wrapped in the same lock the discoverer uses, and_commands_registered = Trueis flipped only after the lastadd_typer()succeeds (no half-registered command tree).src/praisonai/tests/unit/cli/test_main_dispatcher.pyKey facts to extract from the source
Routing precedence (from
main()in__main__.py, in order):--version/-V→ print version andreturn— does not importpraisonai.cli.*(proven byTestVersionShortCircuit::test_version_does_not_import_typer_or_legacy, which evictspraisonai.cli.*fromsys.modulesand asserts they stay absent).--help/-h→ Typer (so help auto-discovers all registered subcommands)._get_typer_commands()→ Typer..yaml/.ymlpaths, deprecated flags) → legacyPraisonAI().main().First-positional discovery (
_find_first_command(argv)):--verbose,--debug,--json.--output-formatand-oas value flags — also skips the value that follows them.Nonewhen argv has only flags (rule 5 above falls through to "Typer for global-flag handling")..yamlpaths are returned as-is — the routing decision happens inmain()based on command-set membership, not by inspecting the filesystem.Auto-discovery cache (
_get_typer_commands()):app+ callsregister_commands(), then introspects viaclick.Context(typer.main.get_command(app)).list_commands(ctx)._typer_commands_cacheunder_typer_commands_lock.set()for that call but leaves_typer_commands_cache = Noneso the next caller retries (verified byTestGetTyperCommandsCache::test_failure_does_not_poison_cache).TestGetTyperCommandsCache::test_concurrent_callers_get_same_result.Failure modes (fail-loud invariant):
register_commands()exceptions inside_run_typerpropagate — they are NOT wrapped intry/except. A missing optional dep surfaces as the underlyingImportError, not as Typer's empty-app behavior. Pinned byTestTyperRegistrationFailureFailsLoud._run_typerand_run_legacyboth restoresys.argvinfinally:even onSystemExit— pinned byTestRunTyperArgvRestorationandTestRunLegacyArgvRestoration(both useargv[0] = "/usr/local/bin/some-launcher"so the assertion has discriminating power against a missingfinally).Why this matters to users (extensibility):
praisonai.cli.app, typingpraisonai <new-command>routes to Typer.os.path.isfile()are gone.Task — NEW page:
docs/features/cli-dispatcher.mdxRequired frontmatter
Required structure (per
AGENTS.md§2)## Quick Startwith<Steps>— show the five routing paths as five Steps (praisonai --version,praisonai,praisonai --help,praisonai chat "...",praisonai "free-text prompt").## How It Works—sequenceDiagramshowingUser → main() → [version|help|empty|known-cmd|fallback] → [Typer|Legacy|exit].## Routing Rules— the precedence table below, copied verbatim.## Auto-Discovery— explain why adding a new Typer subcommand "just works" (one short paragraph + 4-line example of registering a command and seeing it route).## Common Patterns— 3 examples: bare prompt,.yamlfile, subcommand with global flags (e.g.praisonai --verbose chat "hi"— show that--verboseis skipped when finding the first positional).## Best Practiceswith<AccordionGroup>:--versionis fast" (nopraisonai.cli.*imports → still works with broken optional deps).cli/app.py, no dispatcher changes needed).## Relatedwith<CardGroup cols={2}>— link to/docs/cli/cli,/docs/cli/cli-reference,/docs/features/gateway,/docs/cli/version.Hero diagram (use these exact colors —
AGENTS.md§3.1)graph TB Start[📋 praisonai argv] --> V{🔍 --version<br/>or -V?} V -->|Yes| Print[⚡ Print version<br/>no cli.* imports] V -->|No| H{🔍 --help<br/>or -h?} H -->|Yes| Typer1[🧰 Typer help] H -->|No| E{🔍 argv empty?} E -->|Yes| Typer2[🧰 Typer TUI] E -->|No| F[🔎 first non-flag positional] F --> K{🧠 in registered<br/>Typer commands?} K -->|Yes| Typer3[🧰 Typer subcommand] K -->|No| Legacy[🤖 Legacy<br/>PraisonAI().main] classDef input fill:#6366F1,stroke:#7C90A0,color:#fff classDef check fill:#F59E0B,stroke:#7C90A0,color:#fff classDef route fill:#10B981,stroke:#7C90A0,color:#fff classDef agent fill:#8B0000,stroke:#7C90A0,color:#fff class Start input class V,H,E,K check class F input class Print,Typer1,Typer2,Typer3 route class Legacy agentRouting rules table (copy verbatim — derived from
__main__.py:main()post-#1577)praisonai --version/praisonai -Vpraisonai.cli.*— stays fast even with broken optional deps. Pinned byTestVersionShortCircuit.praisonai --help/praisonai -hpraisonai(no argv)praisonai --verbose/praisonai -o json(only flags)_find_first_commandreturnsNone→ Typer handles global-flag-only cases.praisonai chat ...(first positional ∈ registered commands)app. Adding a new subcommand tocli/app.pymakes it routable here with zero dispatcher changes.praisonai "Build a weather agent"(free-text — token contains a space)PraisonAI().main()praisonai agents.yaml(filename, not a registered command)PraisonAI().main()os.path.isfile(). A typo'd YAML path also routes to legacy and surfaces there.praisonai totally-unknown(unknown positional)PraisonAI().main()Auto-discovery callout
Add a
<Note>block:Failure-modes callout
Add a
<Warning>block:Acceptance criteria
docs/features/cli-dispatcher.mdxexists, follows the structure above, passes theAGENTS.md§9 quality checklist (frontmatter, hero diagram, Steps, AccordionGroup, CardGroup all present).#6366F1input,#F59E0Bcheck,#10B981route,#8B0000legacy/agent, white text,#7C90A0strokes).<Note>covers auto-discovery and one<Warning>covers fail-loud registration.docs.jsonupdated to add the new page under the Features group;docs.jsonis still valid JSON.docs/concepts/. No edits todocs/js/ordocs/rust/.praisonai/__main__.pyorpraisonai/cli/app.pypost-#1577 (head SHA8d373843dc062f358ff5be7722a8cadbe71ce105)._is_legacy_invocation()heuristic — it was removed in #1577.Out of scope (do NOT do in this PR)
atexitwording indocs/features/gateway.mdx— that lives in [docs] Update wrapper async bridge + CLI dispatcher docs after PraisonAI #1575 #282 and should stay there._async_bridgedocs — also [docs] Update wrapper async bridge + CLI dispatcher docs after PraisonAI #1575 #282.docs/js/ordocs/rust/parity — handled by thegenerate_docs_parity.pyscript, not manually.References
#1548Typer-first dispatch documentation): Docs needed for PR #1548: framework-adapter plugins, Typer-first CLI dispatch, run_sync() loop safety #276