Compare commits

..
Author SHA1 Message Date
praisonai-triage-agent[bot]andMervin Praison eb73d528a8 fix(code): bind headless basic tools to workspace, harden lifecycle
- Bind basic file/shell tools (read/write/list/execute) to --workspace via
  Workspace containment (fail-closed), matching the edit group, so an
  auto-approved headless run cannot escape the configured workspace.
- Move workspace resolution + tool loading inside the protected try/finally in
  _run_print_code so init failures still emit the JSON error envelope and always
  run cleanup_runtime().
- Use a private mktemp log in the terminal-bench example instead of a
  predictable /tmp path; drop the stale log_path metadata.
- Add --dangerously-skip-approval to the opt-in real-agent write test and a
  fast regression test for basic-group workspace containment.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Mervin Praison <[email protected]>
2026-08-14 19:52:54 +00:00
2283 changed files with 33353 additions and 374888 deletions

No files matched your search

+330
View File
@@ -0,0 +1,330 @@
<!-- wp:paragraph -->
<p>praisonai-mcp is Tier 2e — the heavy MCP server host: full <code>MCPServer</code>, tool/resource/prompt registries, recipe bridge, HTTP-stream auth, and MCP client config management. Depends only on <code>praisonaiagents</code>. v0.0.2. Protocol: MCP 2025-11-25.</p>
<!-- /wp:paragraph -->
<!-- wp:merpress/mermaidjs {"align":"full"} -->
<div class="wp-block-merpress-mermaidjs diagram-source-mermaid alignfull"><pre class="mermaid">%%{init: {"theme": "base", "themeVariables": {"background": "transparent", "lineColor": "#000000"}}}%%
flowchart TB
CLIENT[Cursor Claude Desktop]
subgraph T1 [praisonaiagents]
SDK[MCP client ToolsMCPServer]
SEC[mcp_security shared]
end
subgraph T2e [praisonai-mcp]
HOST[MCPServer]
REG[tool resource prompt registry]
TR[stdio http-stream]
AUTH[api-key oauth scopes]
CFG[mcp list add sync]
end
subgraph T2a [praisonai-code optional]
LIGHT[serve mcp light]
BRIDGE[_mcp_bridge]
end
subgraph T3 [praisonai wrapper optional]
CAP[capabilities recipe]
end
CLIENT --> TR
TR --> HOST
HOST --> REG
REG -.->|adapters| CAP
CFG -.->|_code_bridge| LIGHT
BRIDGE --> CFG
SDK --> LIGHT
SEC --> TR
classDef agent fill:#8B0000,color:#fff
classDef tool fill:#189AB4,color:#fff
class CLIENT agent
class SDK agent
class HOST agent
class REG tool
class TR tool
class AUTH tool
class CFG tool
class LIGHT tool
class SEC tool
</pre></div>
<!-- /wp:merpress/mermaidjs -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Three MCP layers (do not conflate)</h2>
<!-- /wp:heading -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Layer</th><th>Package</th><th>Role</th><th>Entry</th></tr></thead><tbody><tr><td>Client</td><td><code>praisonaiagents/mcp/</code></td><td>Connect agents to external MCP servers; OAuth storage</td><td><code>pip install "praisonaiagents[mcp]"</code></td></tr><tr><td>Light server</td><td><code>praisonai-code</code></td><td>Config-driven basic tool hosting</td><td><code>praisonai serve mcp</code></td></tr><tr><td>Heavy host</td><td><code>praisonai-mcp</code></td><td>Full server, adapters, recipe bridge, HTTP auth</td><td><code>praisonai mcp serve</code></td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Install matrix</h2>
<!-- /wp:heading -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Install</th><th><code>praisonai mcp</code></th><th><code>praisonai serve mcp</code></th><th><code>praisonai-mcp</code> script</th></tr></thead><tbody><tr><td><code>pip install praisonai-mcp</code></td><td>—</td><td>—</td><td>✅</td></tr><tr><td><code>pip install "praisonai-mcp[all]"</code></td><td>—</td><td>—</td><td>✅ HTTP + auth</td></tr><tr><td><code>pip install praisonai-code</code> only</td><td>hidden</td><td>✅ light</td><td>—</td></tr><tr><td><code>pip install "praisonai[mcp]"</code></td><td>✅</td><td>✅</td><td>✅</td></tr><tr><td><code>pip install praisonai</code></td><td>✅</td><td>✅</td><td>✅</td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:paragraph -->
<p>1. Standalone heavy host</p>
<!-- /wp:paragraph -->
<!-- wp:code -->
<pre class="wp-block-code"><code>pip install "praisonai-mcp[all]"
export OPENAI_API_KEY=sk-...
praisonai-mcp --version
praisonai-mcp serve --transport stdio
praisonai-mcp list-tools
praisonai-mcp doctor</code></pre>
<!-- /wp:code -->
<!-- wp:paragraph -->
<p>2. Full stack (same commands via <code>praisonai mcp …</code>)</p>
<!-- /wp:paragraph -->
<!-- wp:code -->
<pre class="wp-block-code"><code>pip install praisonai
# or MCP-only extra
pip install "praisonai[mcp]"
praisonai mcp serve --transport stdio
praisonai mcp list-tools
praisonai mcp doctor</code></pre>
<!-- /wp:code -->
<!-- wp:paragraph -->
<p>3. Light server only (no praisonai-mcp required)</p>
<!-- /wp:paragraph -->
<!-- wp:code -->
<pre class="wp-block-code"><code>pip install praisonai-code "praisonaiagents[mcp]"
praisonai serve mcp --port 8080
# Uses agents ToolsMCPServer — not the heavy MCPServer</code></pre>
<!-- /wp:code -->
<!-- wp:separator -->
<hr class="wp-block-separator has-alpha-channel-opacity"/>
<!-- /wp:separator -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Section 1 — STDIO host (Cursor / Claude Desktop)</h2>
<!-- /wp:heading -->
<!-- wp:code -->
<pre class="wp-block-code"><code># Heavy host — full PraisonAI tool registry
praisonai-mcp serve --transport stdio
# or
praisonai mcp serve --transport stdio
# Generate client config
praisonai mcp config-generate --client cursor --transport stdio
praisonai mcp config-generate --client claude-desktop --transport stdio</code></pre>
<!-- /wp:code -->
<!-- wp:paragraph -->
<p>STDIO uses JSON-RPC over stdin/stdout. Logs go to stderr only — never corrupt the protocol stream.</p>
<!-- /wp:paragraph -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Section 2 — HTTP Stream host</h2>
<!-- /wp:heading -->
<!-- wp:paragraph -->
<p>Streamable HTTP (MCP 2025-11-25) — not legacy standalone SSE. Requires <code>[server]</code> extra (starlette + uvicorn).</p>
<!-- /wp:paragraph -->
<!-- wp:code -->
<pre class="wp-block-code"><code>praisonai mcp serve \
--transport http-stream \
--host 127.0.0.1 \
--port 8080 \
--endpoint /mcp \
--api-key my-secret-key \
--response-mode batch
# Endpoints: POST/GET/DELETE /mcp, plus /health</code></pre>
<!-- /wp:code -->
<!-- wp:paragraph -->
<p>Non-localhost binds require <code>--api-key</code> or <code>--keys-file</code>. Origin validation uses shared helpers from <code>praisonaiagents.mcp.mcp_security</code>.</p>
<!-- /wp:paragraph -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Section 3 — Recipe → MCP bridge</h2>
<!-- /wp:heading -->
<!-- wp:paragraph -->
<p>Turn any PraisonAI recipe into a scoped MCP server with allow/deny lists and safe mode.</p>
<!-- /wp:paragraph -->
<!-- wp:code -->
<pre class="wp-block-code"><code>praisonai mcp serve-recipe support-reply --transport stdio
praisonai mcp validate-recipe support-reply
praisonai mcp inspect-recipe support-reply
praisonai mcp config-generate-recipe support-reply --client claude-desktop</code></pre>
<!-- /wp:code -->
<!-- wp:paragraph -->
<p>Default denied tools include <code>shell.exec</code>, <code>file.delete</code>, <code>eval</code>, and similar destructive operations.</p>
<!-- /wp:paragraph -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Section 4 — MCP client config management</h2>
<!-- /wp:heading -->
<!-- wp:paragraph -->
<p>Config subcommands (<code>list</code>, <code>add</code>, <code>remove</code>, <code>sync</code>, <code>test</code>) require <code>praisonai-code</code> via <code>_code_bridge</code>.</p>
<!-- /wp:paragraph -->
<!-- wp:code -->
<pre class="wp-block-code"><code>praisonai mcp list
praisonai mcp add filesystem \
--command npx \
--args "-y @modelcontextprotocol/server-filesystem ."
praisonai mcp test filesystem
praisonai mcp sync
praisonai mcp tools
praisonai mcp status</code></pre>
<!-- /wp:code -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Section 5 — Built-in registry (tools, resources, prompts)</h2>
<!-- /wp:heading -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Type</th><th>Examples</th><th>Adapter</th></tr></thead><tbody><tr><td>Tools</td><td><code>praisonai.chat.*</code>, <code>praisonai.workflow.*</code>, agent run helpers</td><td><code>mcp_server/adapters/capabilities.py</code>, <code>cli_tools.py</code></td></tr><tr><td>Resources</td><td><code>praisonai://memory/sessions</code>, <code>praisonai://workflows</code>, <code>praisonai://agents</code></td><td><code>adapters/resources.py</code></td></tr><tr><td>Prompts</td><td><code>deep-research</code>, <code>code-review</code>, <code>workflow-auto</code></td><td><code>adapters/prompts.py</code></td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:code -->
<pre class="wp-block-code"><code># Inspect what the host exposes
praisonai-mcp list-tools
praisonai-mcp list-resources
praisonai-mcp list-prompts
# Host tool search (MCPServerCLI)
praisonai-mcp tools search "workflow"</code></pre>
<!-- /wp:code -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Section 6 — Security &amp; auth</h2>
<!-- /wp:heading -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Concern</th><th>Package</th><th>Module</th></tr></thead><tbody><tr><td>Origin validation, session IDs</td><td><code>praisonaiagents</code></td><td><code>mcp/mcp_security.py</code></td></tr><tr><td>API keys, scoped keys file</td><td><code>praisonai-mcp</code></td><td><code>mcp_server/auth/api_key.py</code></td></tr><tr><td>OAuth 2.1 + PKCE</td><td><code>praisonai-mcp</code></td><td><code>mcp_server/auth/oauth.py</code></td></tr><tr><td>OIDC discovery</td><td><code>praisonai-mcp</code></td><td><code>mcp_server/auth/oidc.py</code></td></tr><tr><td>Scope enforcement</td><td><code>praisonai-mcp</code></td><td><code>mcp_server/auth/scopes.py</code></td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:code -->
<pre class="wp-block-code"><code># Scoped keys file (--keys-file)
[
{"key": "monitor-key", "name": "monitor", "scopes": ["tools:read", "resources:read"]},
{"key": "admin-key", "name": "admin", "scopes": ["*"]}
]
# Env vars
export MCP_API_KEY=your-key
export PRAISONAI_MCP_MAX_SESSIONS=1000</code></pre>
<!-- /wp:code -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Section 7 — CLI routing (lazy load)</h2>
<!-- /wp:heading -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Registry</th><th>Commands</th><th>Gate</th></tr></thead><tbody><tr><td>MCP-resident Typer</td><td>list, add, remove, sync, test, serve, doctor</td><td><code>mcp_package_available()</code></td></tr><tr><td>MCPServerCLI host</td><td>serve, list-tools, list-resources, config-generate, tasks, auth</td><td>Direct via <code>praisonai-mcp &lt;cmd&gt;</code></td></tr><tr><td>Code-resident light</td><td><code>serve mcp</code></td><td>Always available with code + agents</td></tr><tr><td>Wrapper-resident</td><td>dashboard, schedule, langfuse</td><td><code>wrapper_package_available()</code></td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:paragraph -->
<p>Standalone <code>praisonai-code</code> hides <code>mcp</code> from <code>--help</code> when <code>praisonai-mcp</code> is not installed. Host commands like <code>praisonai-mcp serve</code> skip the <code>mcp</code> prefix; <code>praisonai mcp serve</code> goes through the Typer group.</p>
<!-- /wp:paragraph -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Section 8 — Python API</h2>
<!-- /wp:heading -->
<!-- wp:code -->
<pre class="wp-block-code"><code>import praisonai_mcp
from praisonai_mcp.mcp_server import MCPServer
from praisonai_mcp.mcp_server.registry import register_tool
from praisonai_mcp.mcp_server.adapters import register_all
@register_tool("custom.greet")
def greet(name: str) -> str:
"""Greet a person by name."""
return f"Hello, {name}!"
register_all()
server = MCPServer(name="praisonai")
server.run(transport="stdio")</code></pre>
<!-- /wp:code -->
<!-- wp:code -->
<pre class="wp-block-code"><code># Recipe host
from praisonai_mcp.mcp_server import RecipeMCPAdapter
adapter = RecipeMCPAdapter("support-reply")
adapter.load()
adapter.to_mcp_server().run(transport="stdio")
# Shim path still works
from praisonai.mcp_server import MCPServer</code></pre>
<!-- /wp:code -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Section 9 — Standalone (no wrapper)</h2>
<!-- /wp:heading -->
<!-- wp:code -->
<pre class="wp-block-code"><code>pip install praisonaiagents "praisonai-mcp[all]"
export OPENAI_API_KEY=sk-...
praisonai-mcp serve --transport stdio
praisonai-mcp list-tools
praisonai-mcp doctor
praisonai-mcp --help</code></pre>
<!-- /wp:code -->
<!-- wp:paragraph -->
<p>Heavy MCP hosting needs only <code>praisonaiagents</code> + <code>praisonai-mcp</code>. Full capability registry (workflows, deploy, extended tools) activates when the wrapper is co-installed via <code>_wrapper_bridge</code>.</p>
<!-- /wp:paragraph -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Bridges (no PyPI cycles)</h2>
<!-- /wp:heading -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Bridge</th><th>Direction</th><th>Use</th></tr></thead><tbody><tr><td><code>_mcp_bridge</code></td><td>code → mcp</td><td>Route <code>praisonai mcp</code> into Typer group</td></tr><tr><td><code>_code_bridge</code></td><td>mcp → code</td><td>Config loader, paths, output controller</td></tr><tr><td><code>_wrapper_bridge</code></td><td>mcp → wrapper</td><td>Capabilities, recipe, deploy adapters</td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Backward-compat shims</h2>
<!-- /wp:heading -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Old path</th><th>Still works via</th></tr></thead><tbody><tr><td><code>praisonai.mcp_server.*</code></td><td><code>alias_package</code> → <code>praisonai_mcp.mcp_server</code></td></tr><tr><td><code>praisonai.cli.commands.mcp</code></td><td><code>sys.modules</code> shim → mcp Typer group</td></tr><tr><td><code>from praisonai.mcp_server import MCPServer</code></td><td>Lazy re-export through shim</td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Seven-package publish order</h2>
<!-- /wp:heading -->
<!-- wp:code -->
<pre class="wp-block-code"><code>praisonaiagents → praisonai-code → praisonai-bot → praisonai-train → praisonai-browser → praisonai-mcp → praisonai (wrapper)</code></pre>
<!-- /wp:code -->
<!-- wp:paragraph -->
<p>Wrapper pins <code>praisonai-mcp&gt;=X</code> after mcp is on PyPI. CI: <code>.github/workflows/pypi-release.yml</code>. Import gate: <code>scripts/check_c12_mcp_imports.sh</code>.</p>
<!-- /wp:paragraph -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Summary</h2>
<!-- /wp:heading -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Item</th><th>Value</th></tr></thead><tbody><tr><td>Tier</td><td>2e</td></tr><tr><td>PyPI</td><td><code>praisonai-mcp</code> v0.0.2</td></tr><tr><td>Runtime deps</td><td><code>praisonaiagents</code>, <code>mcp</code>, rich, typer</td></tr><tr><td>HTTP extra</td><td><code>[server]</code> — starlette, uvicorn</td></tr><tr><td>Auth extra</td><td><code>[auth]</code> — httpx</td></tr><tr><td>Transports</td><td><code>stdio</code>, <code>http-stream</code></td></tr><tr><td>Light alternative</td><td><code>praisonai serve mcp</code> (code tier)</td></tr><tr><td>CLI entry</td><td><code>praisonai-mcp</code> or <code>praisonai mcp</code></td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:paragraph -->
<p>Previous: <a href="https://mer.vin/2026/07/praisonai-train-praisonai-train-fine-tuning-agent-training-in-depth/">praisonai-train</a> · Next: praisonai wrapper</p>
<!-- /wp:paragraph -->
+460
View File
@@ -0,0 +1,460 @@
<!-- wp:paragraph -->
<p>praisonai is Tier 3 — the integration wrapper that wires <code>praisonaiagents</code> + five Tier-2 packages (<code>praisonai-code</code>, <code>praisonai-bot</code>, <code>praisonai-train</code>, <code>praisonai-browser</code>, <code>praisonai-mcp</code>) into one install, with dashboard, framework adapters, jobs API, serve orchestration, and backward-compat shims. v4.6.150.</p>
<!-- /wp:paragraph -->
<!-- wp:merpress/mermaidjs {"align":"full"} -->
<div class="wp-block-merpress-mermaidjs diagram-source-mermaid alignfull"><pre class="mermaid">%%{init: {"theme": "base", "themeVariables": {"background": "transparent", "lineColor": "#000000"}}}%%
flowchart TB
subgraph T1 [Tier 1 praisonaiagents]
A[Agent tools memory MCP client]
end
subgraph T2 [Tier 2 packages]
C[praisonai-code run chat code]
B[praisonai-bot gateway channels]
TR[praisonai-train fine-tune agents]
BR[praisonai-browser CDP Playwright]
MCP[praisonai-mcp server host]
end
subgraph T3 [Tier 3 praisonai wrapper]
W[dashboard jobs frameworks serve shims]
end
A --> C
A --> B
A --> TR
A --> BR
A --> MCP
W --> C
W --> B
W --> TR
W --> BR
W --> MCP
W --> A
classDef agent fill:#8B0000,color:#fff
classDef tool fill:#189AB4,color:#fff
class A agent
class W agent
class C tool
class B tool
class TR tool
class BR tool
class MCP tool
</pre></div>
<!-- /wp:merpress/mermaidjs -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Seven-package stack</h2>
<!-- /wp:heading -->
<!-- wp:code -->
<pre class="wp-block-code"><code>praisonaiagents → praisonai-code + praisonai-bot + praisonai-train + praisonai-browser + praisonai-mcp → praisonai (wrapper)</code></pre>
<!-- /wp:code -->
<!-- wp:paragraph -->
<p>Each Tier-2 package is installable standalone with its own console script. The wrapper pins all of them and preserves every old import path via shims. Use the wrapper when you need several surfaces together — HTTP serve, dashboard, framework YAML, jobs/recipe — without managing tier pins yourself.</p>
<!-- /wp:paragraph -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Product catalog (C0–C12 shipped)</h2>
<!-- /wp:heading -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Package</th><th>Epic</th><th>Choose this when…</th><th>Standalone install</th></tr></thead><tbody><tr><td><code>praisonaiagents</code></td><td>Tier 1</td><td>Building or embedding agents in code — Agent, tools, memory, hooks</td><td><code>pip install praisonaiagents</code></td></tr><tr><td><code>praisonai-code</code></td><td>C0–C8</td><td>Terminal agentic CLI — run, chat, code, warm daemon</td><td><code>pip install praisonai-code</code></td></tr><tr><td><code>praisonai-bot</code></td><td>C9</td><td>Messaging bots + WebSocket gateway</td><td><code>pip install "praisonai-bot[gateway,bot]"</code></td></tr><tr><td><code>praisonai-train</code></td><td>C10</td><td>Agent training loops + GPU LLM fine-tuning</td><td><code>pip install praisonai-train</code> or <code>[llm]</code></td></tr><tr><td><code>praisonai-browser</code></td><td>C11</td><td>Chrome extension bridge, CDP, Playwright agents</td><td><code>pip install "praisonai-browser[all]"</code></td></tr><tr><td><code>praisonai-mcp</code></td><td>C12</td><td>MCP server for Cursor, Claude Desktop, Windsurf</td><td><code>pip install "praisonai-mcp[all]"</code></td></tr><tr><td><code>praisonai</code></td><td>Tier 3</td><td>Full product / umbrella — serve, dashboard, frameworks, jobs</td><td><code>pip install praisonai</code></td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Three-layer products (do not conflate)</h2>
<!-- /wp:heading -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Product</th><th>Layer 1 (agents)</th><th>Layer 2 (dedicated pkg)</th><th>Layer 3 (wrapper)</th></tr></thead><tbody><tr><td>Browser</td><td><code>tools/protocols/browser.py</code></td><td><code>praisonai-browser</code></td><td>shims + optional full stack</td></tr><tr><td>MCP</td><td><code>praisonaiagents/mcp</code> client + light server</td><td><code>praisonai-mcp</code> heavy host</td><td>capability bodies via <code>_wrapper_bridge</code></td></tr><tr><td>Bots</td><td>bots/gateway protocols</td><td><code>praisonai-bot</code></td><td>shims only</td></tr><tr><td>Train</td><td>Agent + hooks in agents</td><td><code>praisonai-train</code></td><td>shims + <code>setup-conda-env</code></td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Install matrix</h2>
<!-- /wp:heading -->
<!-- wp:paragraph -->
<p>1. Base install (full seven-package stack)</p>
<!-- /wp:paragraph -->
<!-- wp:code -->
<pre class="wp-block-code"><code>pip install praisonai
export OPENAI_API_KEY=sk-...
praisonai doctor
praisonai version show</code></pre>
<!-- /wp:code -->
<!-- wp:paragraph -->
<p>2. Targeted extras</p>
<!-- /wp:paragraph -->
<!-- wp:code -->
<pre class="wp-block-code"><code>pip install "praisonai[claw]" # dashboard + bots + gateway
pip install "praisonai[all]" # onboarding + messaging + search stack
pip install "praisonai[train]" # LLM fine-tuning (Unsloth stack)
pip install "praisonai[mcp]" # full MCP server host
pip install "praisonai[browser]" # extension bridge + CDP + Playwright
pip install "praisonai[crewai]" # multi-framework YAML</code></pre>
<!-- /wp:code -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Package</th><th>Min version (wrapper pin)</th><th>Console script</th></tr></thead><tbody><tr><td><code>praisonaiagents</code></td><td>≥ 1.6.153</td><td>—</td></tr><tr><td><code>praisonai-code</code></td><td>≥ 0.0.49</td><td><code>praisonai-code</code></td></tr><tr><td><code>praisonai-bot</code></td><td>≥ 0.0.34</td><td><code>praisonai-bot</code></td></tr><tr><td><code>praisonai-train</code></td><td>≥ 0.0.5</td><td><code>praisonai-train</code></td></tr><tr><td><code>praisonai-browser</code></td><td>≥ 0.0.2</td><td><code>praisonai-browser</code></td></tr><tr><td><code>praisonai-mcp</code></td><td>≥ 0.0.2</td><td><code>praisonai-mcp</code></td></tr><tr><td><code>praisonai</code></td><td>4.6.150</td><td><code>praisonai</code></td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:separator -->
<hr class="wp-block-separator has-alpha-channel-opacity"/>
<!-- /wp:separator -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Section 1 — What wrapper adds (not in Tier-2 alone)</h2>
<!-- /wp:heading -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Feature</th><th>Command / path</th><th>Owner</th><th>Extracted?</th></tr></thead><tbody><tr><td>Unified CLI router</td><td><code>praisonai …</code></td><td>code Typer + 6 resident registries</td><td>—</td></tr><tr><td>HTTP serve orchestration</td><td><code>praisonai serve agents|recipe|a2a|…</code></td><td>wrapper <code>cli/features/serve.py</code></td><td>Deferred (C15 epic)</td></tr><tr><td>Dashboard launcher</td><td><code>praisonai dashboard</code></td><td>wrapper</td><td>Stay wrapper</td></tr><tr><td>Framework adapters</td><td><code>praisonai --framework crewai</code></td><td>wrapper <code>framework_adapters/</code></td><td>Stay wrapper (impls in external PraisonAI-Frameworks)</td></tr><tr><td>Jobs API</td><td><code>praisonai.jobs</code> · port 8005</td><td>wrapper</td><td>Deferred (with serve/recipe)</td></tr><tr><td>Recipe NL → YAML</td><td><code>praisonai recipe …</code></td><td>wrapper</td><td>Deferred</td></tr><tr><td>Scheduler CLI</td><td><code>praisonai schedule …</code></td><td>wrapper</td><td>Stay wrapper (3-layer split with bot)</td></tr><tr><td>Sandbox execution</td><td><code>praisonai sandbox …</code></td><td>wrapper <code>sandbox/</code></td><td>C13 candidate</td></tr><tr><td>Cloud deploy</td><td><code>praisonai deploy …</code></td><td>wrapper <code>deploy/</code></td><td>C13 candidate</td></tr><tr><td>Langfuse / Langflow / n8n</td><td><code>langfuse</code>, <code>flow</code>, <code>n8n</code></td><td>wrapper</td><td>Stay wrapper</td></tr><tr><td>Backward-compat shims</td><td><code>praisonai.bots</code> → <code>praisonai_bot</code></td><td>wrapper <code>cli/_shim.py</code></td><td>Permanent</td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:paragraph -->
<p>Tier-2 packages own their domains; the wrapper owns integration surfaces that span multiple tiers.</p>
<!-- /wp:paragraph -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Section 2 — Terminal hot path (via code tier)</h2>
<!-- /wp:heading -->
<!-- wp:code -->
<pre class="wp-block-code"><code>pip install praisonai
export OPENAI_API_KEY=sk-...
praisonai run "Summarise the latest AI agent trends"
praisonai chat
praisonai code
praisonai daemon start --background
praisonai run "Hello" # auto-forwards to warm runtime</code></pre>
<!-- /wp:code -->
<!-- wp:code -->
<pre class="wp-block-code"><code># Python SDK (Tier 1 — same everywhere)
from praisonaiagents import Agent
agent = Agent(instructions="You are a helpful AI assistant")
agent.start("Write a movie script about a robot on Mars")</code></pre>
<!-- /wp:code -->
<!-- wp:paragraph -->
<p>Deep dive: <a href="https://mer.vin/2026/07/praisonai-code-praisonai-code-terminal-cli-in-depth/">praisonai-code in-depth</a></p>
<!-- /wp:paragraph -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Section 3 — Train (via praisonai-train tier)</h2>
<!-- /wp:heading -->
<!-- wp:code -->
<pre class="wp-block-code"><code># LLM fine-tuning (Unsloth) — lazy ML deps
praisonai train llm dataset.json
praisonai train llm --model llama-3.1 dataset.json
# Agent training — LLM-as-judge
praisonai train agents --input "What is Python?" --iterations 3
# Human feedback loop
praisonai train agents --input "Explain quantum computing" --human
# Session management
praisonai train list
praisonai train apply train-abc123 --run "And Germany?"
# Standalone script (no wrapper prefix)
praisonai-train agents --input "Hello" --iterations 2</code></pre>
<!-- /wp:code -->
<!-- wp:paragraph -->
<p>Deep dive: <a href="https://mer.vin/2026/07/praisonai-train-praisonai-train-fine-tuning-agent-training-in-depth/">praisonai-train in-depth</a></p>
<!-- /wp:paragraph -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Section 4 — Bots &amp; gateway (via bot tier)</h2>
<!-- /wp:heading -->
<!-- wp:code -->
<pre class="wp-block-code"><code>pip install "praisonai[bot]"
export OPENAI_API_KEY=sk-...
export TELEGRAM_BOT_TOKEN=...
praisonai onboard
praisonai bot start --platform telegram
praisonai gateway start --config ~/.praisonai/bot.yaml
praisonai gateway install --config ~/.praisonai/bot.yaml
praisonai pairing approve telegram ABC12345</code></pre>
<!-- /wp:code -->
<!-- wp:paragraph -->
<p>Deep dive: <a href="https://mer.vin/2026/07/praisonai-bot-praisonai-bot-gateway-channels-in-depth/">praisonai-bot in-depth</a></p>
<!-- /wp:paragraph -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Section 5 — MCP server host (via mcp tier)</h2>
<!-- /wp:heading -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Command</th><th>Layer</th><th>Package</th></tr></thead><tbody><tr><td><code>praisonai serve mcp</code></td><td>Light server</td><td><code>praisonai-code</code> + agents</td></tr><tr><td><code>praisonai mcp serve</code></td><td>Heavy host</td><td><code>praisonai-mcp</code></td></tr><tr><td><code>praisonai-mcp serve</code></td><td>Heavy host (direct)</td><td><code>praisonai-mcp</code></td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:code -->
<pre class="wp-block-code"><code># Heavy MCP host — full tool/resource/prompt registry
praisonai mcp serve --transport stdio
praisonai mcp serve --transport http-stream --port 8080 --api-key secret
# Recipe → scoped MCP server
praisonai mcp serve-recipe support-reply --transport stdio
# Client config management (needs praisonai-code co-installed)
praisonai mcp list
praisonai mcp add filesystem --command npx --args "-y @modelcontextprotocol/server-filesystem ."
# Generate Cursor / Claude Desktop config
praisonai mcp config-generate --client cursor --transport stdio</code></pre>
<!-- /wp:code -->
<!-- wp:paragraph -->
<p>Deep dive: <a href="https://mer.vin/2026/07/praisonai-mcp-praisonai-mcp-server-host-client-config-in-depth/">praisonai-mcp in-depth</a></p>
<!-- /wp:paragraph -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Section 6 — Browser automation (via browser tier)</h2>
<!-- /wp:heading -->
<!-- wp:code -->
<pre class="wp-block-code"><code>pip install "praisonai[browser]"
# Extension bridge server + LLM browser agent
praisonai browser start
praisonai browser run "Find the latest Python release notes"
# Diagnostics
praisonai browser doctor
# Lightweight tool shim (code tier — no praisonai-browser required)
praisonai browser-tool snapshot https://example.com
# Standalone script
praisonai-browser start
praisonai-browser run "Click the login button"</code></pre>
<!-- /wp:code -->
<!-- wp:paragraph -->
<p>Three layers: agents protocols → <code>praisonai browser-tool</code> (praisonai-tools) → <code>praisonai browser</code> (extension bridge, CDP, Playwright).</p>
<!-- /wp:paragraph -->
<!-- wp:paragraph -->
<p>Deep dive: <a href="https://mer.vin/2026/07/praisonai-browser-praisonai-browser-browser-automation-in-depth/">praisonai-browser in-depth</a></p>
<!-- /wp:paragraph -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Section 7 — Dashboard stack</h2>
<!-- /wp:heading -->
<!-- wp:code -->
<pre class="wp-block-code"><code>pip install "praisonai[claw,flow,ui]"
# Individual services
praisonai claw # :8082 — ops dashboard
praisonai flow # :7861 — Langflow
praisonai ui # :8081 — clean chat
# Unified launcher (wrapper-only)
praisonai dashboard # :3000
praisonai dashboard --port 9000 --no-auto-start</code></pre>
<!-- /wp:code -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Port</th><th>Service</th></tr></thead><tbody><tr><td>3000</td><td>Dashboard hub</td></tr><tr><td>7860/7861</td><td>Langflow</td></tr><tr><td>8081</td><td>UI chat</td></tr><tr><td>8082</td><td>Claw</td></tr><tr><td>8765</td><td>Gateway WebSocket</td></tr><tr><td>8005</td><td>Jobs API</td></tr><tr><td>8080</td><td>MCP HTTP stream (optional)</td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Section 8 — Multi-framework YAML</h2>
<!-- /wp:heading -->
<!-- wp:code -->
<pre class="wp-block-code"><code>pip install "praisonai[crewai]"
export OPENAI_API_KEY=sk-...
praisonai --framework crewai --auto "Create a movie script"
praisonai run --file agents.yaml --framework crewai
pip install "praisonai[autogen]"
praisonai --framework autogen agents.yaml</code></pre>
<!-- /wp:code -->
<!-- wp:paragraph -->
<p>Protocols live in <code>praisonaiagents/frameworks/</code>; adapter implementations in wrapper <code>framework_adapters/</code>. Entry-point group: <code>praisonai.framework_adapters</code>.</p>
<!-- /wp:paragraph -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Section 9 — Scheduler (24/7 agents)</h2>
<!-- /wp:heading -->
<!-- wp:code -->
<pre class="wp-block-code"><code>praisonai schedule add --agent my_agent.yaml --expr "hourly"
praisonai schedule list
praisonai schedule start</code></pre>
<!-- /wp:code -->
<!-- wp:code -->
<pre class="wp-block-code"><code># Python
from praisonai.scheduler import AgentScheduler
from praisonaiagents import Agent
agent = Agent(name="NewsChecker", instructions="Check latest AI news")
scheduler = AgentScheduler(agent=agent, task="Summarise top 3 AI stories")
scheduler.start("hourly", run_immediately=True)</code></pre>
<!-- /wp:code -->
<!-- wp:paragraph -->
<p>Three scheduler layers: job store (<code>praisonaiagents.scheduler</code>) → CLI (<code>praisonai schedule</code>, wrapper) → gateway tick delivery (<code>praisonai_bot.scheduler.executor</code>).</p>
<!-- /wp:paragraph -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Section 10 — Jobs API &amp; wrapper-only surfaces</h2>
<!-- /wp:heading -->
<!-- wp:paragraph -->
<p>Jobs, recipe, serve, sandbox, and deploy remain wrapper-owned today (deferred or C13 candidates).</p>
<!-- /wp:paragraph -->
<!-- wp:code -->
<pre class="wp-block-code"><code># Jobs API (wrapper — port 8005)
python -m uvicorn praisonai.jobs.server:create_app --port 8005 --factory
praisonai run submit "Analyse AI trends"
praisonai run submit "Analyse news" --recipe news-analyzer --wait
export PRAISONAI_JOBS_API_KEY=your-secret
# HTTP serve (wrapper orchestration — Typer shell in code tier)
praisonai serve agents --port 8080
praisonai serve recipe support-reply
# Sandbox / deploy (wrapper today — C13 extraction candidates)
praisonai sandbox run "print hello" --backend docker
praisonai deploy agents.yaml --provider aws</code></pre>
<!-- /wp:code -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Section 11 — Lazy CLI routing (six resident registries)</h2>
<!-- /wp:heading -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Registry</th><th>Count</th><th>Examples</th><th>Loaded from</th><th>Gate</th></tr></thead><tbody><tr><td>Code-resident</td><td>~40</td><td>run, chat, code, daemon, serve, config</td><td><code>praisonai_code.cli.commands.*</code></td><td>always</td></tr><tr><td>Bot-resident</td><td>8</td><td>bot, gateway, onboard, kanban, claw</td><td><code>praisonai_bot.cli.commands.*</code></td><td><code>bot_package_available()</code></td></tr><tr><td>Train-resident</td><td>1</td><td>train</td><td><code>praisonai_train.cli.commands.*</code></td><td><code>train_package_available()</code></td></tr><tr><td>Browser-resident</td><td>1</td><td>browser</td><td><code>praisonai_browser.cli.commands.*</code></td><td><code>browser_package_available()</code></td></tr><tr><td>MCP-resident</td><td>1</td><td>mcp</td><td><code>praisonai_mcp.cli.commands.*</code></td><td><code>mcp_package_available()</code></td></tr><tr><td>Wrapper-resident</td><td>22</td><td>dashboard, schedule, flow, langfuse, recipe</td><td><code>praisonai.cli.commands.*</code></td><td><code>wrapper_available()</code></td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:code -->
<pre class="wp-block-code"><code>praisonai --help # 70+ commands, lazy-loaded
praisonai doctor
praisonai train --help
praisonai mcp --help
praisonai browser --help
praisonai dashboard --help</code></pre>
<!-- /wp:code -->
<!-- wp:paragraph -->
<p>Standalone <code>praisonai-code</code> hides bot/train/browser/mcp/wrapper commands from <code>--help</code> when those packages are absent.</p>
<!-- /wp:paragraph -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Section 12 — Shim map (backward compat)</h2>
<!-- /wp:heading -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Old import / path</th><th>Canonical target</th></tr></thead><tbody><tr><td><code>praisonai.cli.main</code></td><td><code>praisonai_code.cli.main</code></td></tr><tr><td><code>praisonai.runtime.*</code></td><td><code>praisonai_code.runtime.*</code></td></tr><tr><td><code>praisonai.bots.*</code></td><td><code>praisonai_bot.bots.*</code></td></tr><tr><td><code>praisonai.gateway.*</code></td><td><code>praisonai_bot.gateway.*</code></td></tr><tr><td><code>praisonai.train.*</code></td><td><code>praisonai_train.train.*</code></td></tr><tr><td><code>praisonai.browser.*</code></td><td><code>praisonai_browser.*</code></td></tr><tr><td><code>praisonai.mcp_server.*</code></td><td><code>praisonai_mcp.mcp_server.*</code></td></tr><tr><td><code>praisonai.scheduler.executor</code></td><td><code>praisonai_bot.scheduler.executor</code></td></tr><tr><td><code>setup-conda-env</code></td><td><code>praisonai_train.setup.setup_conda_env</code></td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:code -->
<pre class="wp-block-code"><code># These must keep working
from praisonai.cli.main import PraisonAI
from praisonai.bots import Bot
from praisonai.gateway import WebSocketGateway
from praisonai.train.agents import AgentTrainer
from praisonai.mcp_server import MCPServer
python -m praisonai run "hello"
python -m praisonai.runtime</code></pre>
<!-- /wp:code -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Section 13 — Bridges (no PyPI cycles)</h2>
<!-- /wp:heading -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Bridge</th><th>Direction</th><th>Use</th></tr></thead><tbody><tr><td><code>_wrapper_bridge</code></td><td>code → wrapper</td><td>serve handlers, frameworks, jobs</td></tr><tr><td><code>_bot_bridge</code></td><td>code → bot</td><td>bot/gateway CLI routing</td></tr><tr><td><code>_train_bridge</code></td><td>code → train</td><td>train CLI routing</td></tr><tr><td><code>_browser_bridge</code></td><td>code → browser</td><td>browser CLI routing</td></tr><tr><td><code>_mcp_bridge</code></td><td>code → mcp</td><td>mcp CLI routing</td></tr><tr><td><code>_code_bridge</code></td><td>tier-2 → code</td><td>config kernel, legacy dispatch</td></tr><tr><td><code>_wrapper_bridge</code></td><td>tier-2 → wrapper</td><td>capabilities, recipe, jobs/UI</td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:heading -->
<h2 class="wp-block-heading">When to use wrapper vs a Tier-2 package alone</h2>
<!-- /wp:heading -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Your goal</th><th>Install</th></tr></thead><tbody><tr><td>Only terminal agents</td><td><code>praisonai-code</code> (+ optional <code>praisonaiagents</code>)</td></tr><tr><td>Only bots on Telegram/Slack</td><td><code>praisonai-bot</code></td></tr><tr><td>Only train / fine-tune</td><td><code>praisonai-train</code></td></tr><tr><td>Only automate browser</td><td><code>praisonai-browser</code></td></tr><tr><td>Only MCP server for IDE</td><td><code>praisonai-mcp</code></td></tr><tr><td>Several of the above + serve / dashboard / framework YAML</td><td><code>pip install praisonai</code></td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:heading -->
<h2 class="wp-block-heading">What stays in wrapper (not extracted)</h2>
<!-- /wp:heading -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Surface</th><th>Why it stays</th></tr></thead><tbody><tr><td><code>dashboard</code> / <code>flow</code> / <code>ui</code></td><td>Langflow extra + launcher glue — not a standalone PyPI product</td></tr><tr><td><code>claw</code></td><td>Default app lives in <code>praisonai-bot</code>; wrapper is a thin shim</td></tr><tr><td><code>framework_adapters/</code></td><td>Thin registry; heavy impls in external PraisonAI-Frameworks</td></tr><tr><td><code>schedule</code> CLI</td><td>Three-layer split: agents store + wrapper CLI + bot executor</td></tr><tr><td><code>serve</code> / <code>recipe</code> / <code>jobs</code></td><td>~14k LOC deferred epic — high coupling (C15)</td></tr><tr><td><code>sandbox</code> / <code>deploy</code></td><td>Still in wrapper today; C13 extraction candidates</td></tr><tr><td><code>standardise</code> / suite runner</td><td>Maintainer/docs tooling — internal only</td></tr><tr><td><code>persist</code> / enterprise adapters</td><td><strong>Rejected</strong> — agents already ship File/SQLite defaults; no crisp standalone story</td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Roadmap — C13+ candidates</h2>
<!-- /wp:heading -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Package</th><th>Status</th><th>Choose when…</th><th>Standalone install (planned)</th></tr></thead><tbody><tr><td><code>praisonai-sandbox</code></td><td><strong>C13 preferred</strong> (execution goal)</td><td>Isolated agent code execution — Docker/E2B/Modal/Daytona/Sandlock</td><td><code>pip install praisonai-sandbox[docker]</code></td></tr><tr><td><code>praisonai-deploy</code></td><td><strong>C13 alt</strong> (deploy goal)</td><td>Ship <code>agents.yaml</code> to Docker/AWS/GCP/Azure</td><td><code>pip install praisonai-deploy</code></td></tr><tr><td><code>praisonai-replay</code></td><td>Conditional C14+</td><td>Deterministic replay from run ledger / traces</td><td>After product sign-off</td></tr><tr><td><code>praisonai-serve</code></td><td>Deferred C15</td><td>HTTP API hosting without full umbrella (~14k LOC)</td><td>With recipe/jobs decision</td></tr><tr><td><code>praisonai-recipe</code></td><td>Deferred</td><td>NL → agent YAML without jobs UI</td><td>Tied to jobs + serve</td></tr><tr><td><code>praisonai-jobs</code></td><td>Deferred</td><td>Async long-running agent jobs API</td><td>Extract with serve or stay wrapper</td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:paragraph -->
<p>C13 decision rule: <strong>sandbox</strong> = where code runs; <strong>deploy</strong> = where the agent service lives. Ship one first after product sign-off.</p>
<!-- /wp:paragraph -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Standalone extraction litmus test</h2>
<!-- /wp:heading -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Criterion</th><th>Pass (browser/MCP)</th><th>Fail (example)</th></tr></thead><tbody><tr><td>Named user goal without full <code>praisonai</code></td><td><code>pip install praisonai-browser</code> → automate Chrome</td><td><code>pip install praisonai-persist</code> → ???</td></tr><tr><td>Coherent product boundary</td><td>One Typer group / one console script</td><td>Whole <code>cli/features/</code> orchestration layer</td></tr><tr><td>PyPI deps: <code>praisonaiagents</code> only</td><td>Tier-2 + lazy <code>_wrapper_bridge</code></td><td>Hard <code>import praisonai</code> at module level</td></tr><tr><td>Worth a separate install</td><td>User who never wants dashboard/serve/bots</td><td>Internal docs tooling</td></tr><tr><td>Extraction shape</td><td>C10 single PR (~3–11k LOC)</td><td>Mega-epic without product sign-off</td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:heading -->
<h2 class="wp-block-heading">When to use which tier</h2>
<!-- /wp:heading -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Scenario</th><th>Install</th></tr></thead><tbody><tr><td>Pure Python agents, minimal deps</td><td><code>pip install praisonaiagents</code></td></tr><tr><td>Terminal coding agent, CI, headless</td><td><code>pip install praisonai-code</code></td></tr><tr><td>Telegram/Discord gateway only</td><td><code>pip install "praisonai-bot[gateway,bot]"</code></td></tr><tr><td>Agent training (no GPU)</td><td><code>pip install praisonai-train</code></td></tr><tr><td>LLM fine-tuning (GPU)</td><td><code>pip install "praisonai-train[llm]"</code></td></tr><tr><td>MCP server for Cursor/Claude</td><td><code>pip install "praisonai-mcp[all]"</code></td></tr><tr><td>Browser automation (extension/CDP)</td><td><code>pip install "praisonai-browser[all]"</code></td></tr><tr><td>CrewAI/AutoGen YAML</td><td><code>pip install "praisonai[crewai]"</code></td></tr><tr><td>Full ops + channels + dashboard</td><td><code>pip install "praisonai[claw]"</code></td></tr><tr><td>Everything in one install</td><td><code>pip install praisonai</code></td></tr><tr><td>Library author / embedding</td><td><code>praisonaiagents</code> only</td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Seven-package publish order</h2>
<!-- /wp:heading -->
<!-- wp:code -->
<pre class="wp-block-code"><code>praisonaiagents
→ praisonai-code + praisonai-bot + praisonai-train + praisonai-browser + praisonai-mcp
→ praisonai (wrapper pins all tier-2 deps last)</code></pre>
<!-- /wp:code -->
<!-- wp:paragraph -->
<p>CI: <code>.github/workflows/pypi-release.yml</code> · selective publish: <code>publish_all.py --changed-only</code> (default). Playbook per package: manifest → TDD compat tests → git mv → shims → <code>_X_RESIDENT_COMMANDS</code> → CI shard → PyPI.</p>
<!-- /wp:paragraph -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Summary</h2>
<!-- /wp:heading -->
<!-- wp:table -->
<figure class="wp-block-table"><table><thead><tr><th>Item</th><th>Value</th></tr></thead><tbody><tr><td>Tier</td><td>3 (integration wrapper)</td></tr><tr><td>PyPI</td><td><code>praisonai</code> v4.6.150</td></tr><tr><td>Shipped tier-2 deps</td><td>code, bot, train, browser, mcp (C0–C12)</td></tr><tr><td>Resident registries</td><td>6 (code + bot + train + browser + mcp + wrapper)</td></tr><tr><td>Wrapper-only (~140k LOC)</td><td>serve, recipe, jobs, dashboard, sandbox, deploy, frameworks</td></tr><tr><td>Next extraction</td><td>C13: sandbox or deploy (product sign-off)</td></tr><tr><td>One-liner still works</td><td><code>pip install praisonai</code></td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:paragraph -->
<p>Architecture overview: Seven-package C7–C12 extraction · Post-C12 roadmap (C13+)</p>
<!-- /wp:paragraph -->
<!-- wp:paragraph -->
<p>Series: <a href="https://mer.vin/2026/07/praisonai-code-praisonai-code-terminal-cli-in-depth/">praisonai-code</a> · <a href="https://mer.vin/2026/07/praisonai-bot-praisonai-bot-gateway-channels-in-depth/">praisonai-bot</a> · <a href="https://mer.vin/2026/07/praisonai-train-praisonai-train-fine-tuning-agent-training-in-depth/">praisonai-train</a> · <a href="https://mer.vin/2026/07/praisonai-browser-praisonai-browser-browser-automation-in-depth/">praisonai-browser</a> · <a href="https://mer.vin/2026/07/praisonai-mcp-praisonai-mcp-server-host-client-config-in-depth/">praisonai-mcp</a> · praisonai wrapper</p>
<!-- /wp:paragraph -->
-1
View File
@@ -17,7 +17,6 @@ GROQ_API_KEY=gsk_... # Groq (fast inference)
COHERE_API_KEY=... # Cohere models
AZURE_OPENAI_API_KEY=... # Azure OpenAI
AZURE_OPENAI_ENDPOINT=... # Azure OpenAI endpoint
EVOLINK_API_KEY=... # EvoLink.AI OpenAI-compatible models. Get yours at https://evolink.ai/dashboard/keys
# === Optional: Memory backends ===
MEM0_API_KEY=... # Mem0 memory service
+1 -7
View File
@@ -6,11 +6,6 @@ labels: bug
assignees: ''
---
## SDK / package
- [ ] Python (`pip install praisonai` / `praisonaiagents`)
- [ ] TypeScript / JavaScript (`npm install praisonai`) — fixes in `src/praisonai-ts/`
- [ ] Cross-language / parity (file here on PraisonAI)
## Environment
- Provider (select one):
- [ ] Anthropic
@@ -19,7 +14,6 @@ assignees: ''
- [ ] AWS Bedrock
- [ ] Other: <!-- specify -->
- PraisonAI version: <!-- if known -->
- Node.js version (TS/JS only): <!-- e.g. 20.x -->
- Operating System: <!-- e.g. macOS 14.3, Windows 11, Ubuntu 22.04 -->
## Full Code
@@ -37,4 +31,4 @@ assignees: ''
<!-- What actually happened -->
## Additional Context
<!-- Add any other context about the problem here, such as screenshots, logs, etc. -->
<!-- Add any other context about the problem here, such as screenshots, logs, etc. -->
@@ -228,7 +228,6 @@ runs:
3. Tools → MervinPraison/PraisonAI-Tools: agent-callable integrations at runtime (Composio, Slack, APIs). Do NOT add to praisonaiagents/.
4. Plugins → MervinPraison/PraisonAI-Plugins: lifecycle extensions (tracing, logging, hooks, guardrails). Not agent tools.
5. Sandbox backends → PraisonAI-Plugins via `praisonai.sandbox` entry point (core protocol only).
6. TypeScript SDK → src/praisonai-ts/ in this monorepo (read src/praisonai-ts/AGENTS.md for TS rules).
- Does it duplicate existing functionality? Check if Agent already supports this via existing params (reflection, planning, tools, hooks, memory).
- Do not bloat the Agent class with additional params — only if absolutely required; we already support many params.
- Does it inherit from Agent properly? New agent types MUST inherit Agent, not wrap it with composition.
@@ -288,7 +287,7 @@ runs:
fi
# Add allowed tools (include GitHub tools by default)
TOOLS="mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__search_issues,mcp__github__list_issues,mcp__github__add_issue_comment,mcp__github__get_pull_request,mcp__github__get_pull_request_files,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,Read,Write,Edit,Bash"
TOOLS="mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__search_issues,mcp__github__list_issues,mcp__github__create_comment,Read,Write,Edit,Bash"
if [[ -n "$INPUT_ALLOWED_TOOLS" ]]; then
TOOLS="$TOOLS,$INPUT_ALLOWED_TOOLS"
fi
@@ -328,7 +327,6 @@ runs:
# Execute Claude Code with timeout, using stdin for the prompt
TIMEOUT_SECONDS=$(($INPUT_TIMEOUT_MINUTES * 60))
timeout $TIMEOUT_SECONDS claude "${CMD_ARGS[@]}" < "$PROMPT_FILE" \
| tee "$GITHUB_WORKSPACE/.claude-assess-output.jsonl"
timeout $TIMEOUT_SECONDS claude "${CMD_ARGS[@]}" < "$PROMPT_FILE"
echo "✅ Claude Code execution completed"
@@ -1,36 +0,0 @@
name: Pick Claude OAuth token
description: Probe primary/secondary Claude OAuth tokens and select a working one (dual OAuth Option B).
inputs:
token_primary:
description: Primary Claude OAuth token (CLAUDE_CODE_OAUTH_TOKEN)
required: false
token_secondary:
description: Secondary Claude OAuth token (CLAUDE_CODE_OAUTH_TOKEN_B)
required: false
active_slot:
description: Preferred slot — primary or secondary (repo variable CLAUDE_OAUTH_ACTIVE)
required: false
default: primary
outputs:
token:
description: Selected OAuth token
value: ${{ steps.pick.outputs.token }}
slot:
description: Slot used (primary, secondary, or *-unprobed fallback)
value: ${{ steps.pick.outputs.slot }}
runs:
using: composite
steps:
- id: pick
shell: bash
env:
CLAUDE_CODE_OAUTH_TOKEN: ${{ inputs.token_primary }}
CLAUDE_CODE_OAUTH_TOKEN_B: ${{ inputs.token_secondary }}
CLAUDE_OAUTH_ACTIVE: ${{ inputs.active_slot }}
run: |
if [[ -n "${HUB_SCRIPTS:-}" && -f "${HUB_SCRIPTS}/oauth-pick-token.sh" ]]; then
SCRIPT="${HUB_SCRIPTS}/oauth-pick-token.sh"
else
SCRIPT="${GITHUB_ACTION_PATH}/../../scripts/oauth-pick-token.sh"
fi
bash "$SCRIPT"
+2 -4
View File
@@ -33,20 +33,18 @@ steps:
Second, read the issue: execute_command 'cat .github/triage-context.md'. Do NOT pass cwd.
Third, read architecture guidelines: execute_command 'cat src/praisonai-agents/AGENTS.md'.
If the issue is TypeScript/JavaScript SDK, also execute_command 'cat src/praisonai-ts/AGENTS.md'.
Fourth, decide routing before planning:
- TypeScript/JavaScript SDK → src/praisonai-ts/ in this monorepo (read src/praisonai-ts/AGENTS.md).
- Tools → PraisonAI-Tools: agent-callable integrations at runtime (Composio, Slack, APIs).
- Plugins → PraisonAI-Plugins: lifecycle extensions (tracing, logging, hooks, guardrails).
- Sandbox backends → PraisonAI-Plugins via `praisonai.sandbox` entry point.
Core SDK/wrapper for Python protocols and CLI; external repos for Tools/Plugins/Docs only.
Core SDK/wrapper only for protocols and CLI — external repos for Tools/Plugins.
Fifth, decide if this issue requires code changes:
- If YES: identify the exact files and lines that must change. Output a concrete file-level plan.
- If NO (question, discussion, duplicate, invalid): say so clearly. No code changes needed.
Output: routing decision (Core / Wrapper / TS-monorepo / Tools-external / Plugins-external / No-change), validation result, and exact files/lines to change.
Output: routing decision (Core / Wrapper / Tools-external / Plugins-external / No-change), validation result, and exact files/lines to change.
expected_output: A decision on whether code changes are needed, and if so a concrete file-level plan.
- name: Implement Fix
+1 -1
View File
@@ -3,7 +3,7 @@
* @see .github/workflows/auto-pr-comment.yml, claude.yml, bot-pr-recovery.yml
*/
const KICK_AUTHORS = new Set(['MervinPraison', 'github-actions[bot]', 'praisonai-triage-agent[bot]']);
const KICK_AUTHORS = new Set(['MervinPraison', 'github-actions[bot]']);
const BOT_PR_AUTHORS = new Set(['praisonai-triage-agent[bot]', 'github-actions[bot]']);
async function listAllComments(github, owner, repo, issueNumber) {
+2 -114
View File
@@ -213,53 +213,6 @@ assert(
mg.findMergeGateVerdict(recentVerdictOnHead, null, '2026-06-27T09:55:00Z') === 'APPROVE'
);
assert(
'scan kickoff text is not a verdict',
mg.findMergeGateVerdict([
{
body: '**Merge gate scan** — eligible for assessment. Claude merge gate will assess and may auto-merge if `MERGE_GATE_VERDICT: APPROVE`.',
created_at: '2026-06-27T10:05:00Z',
},
], null, '2026-06-27T09:55:00Z') === null
);
// merge-only must not use a short time window — fresh Opus APPROVE after assess wins on HEAD.
assert(
'merge path finds APPROVE on HEAD without minCreatedAt window',
mg.findMergeGateVerdict(
[{ body: 'MERGE_GATE_VERDICT: APPROVE', created_at: '2026-06-12T10:00:00Z' }],
null,
'2026-06-12T09:00:00Z',
{ excludeAutomatedFallback: true }
) === 'APPROVE'
);
assert(
'25min minCreatedAt excludes older APPROVE (scan/dispatch paths only)',
mg.findMergeGateVerdict(
[{ body: 'MERGE_GATE_VERDICT: APPROVE', created_at: '2026-06-12T08:00:00Z' }],
'2026-06-12T09:30:00Z',
'2026-06-12T07:00:00Z',
{ excludeAutomatedFallback: true }
) === null
);
const finalWithFinished = [
{ user: { login: 'MervinPraison' }, body: '@claude You are the FINAL architecture reviewer.', created_at: '2026-06-27T10:00:00Z' },
{ user: { login: 'praisonai-triage-agent[bot]' }, body: 'Claude finished', created_at: '2026-06-27T10:05:00Z' },
];
assert('final complete requires Claude finished reply', mg.finalClaudeCompletedOnSha(finalWithFinished, '2026-06-27T09:55:00Z'));
assert('final trigger alone is incomplete', !mg.finalClaudeCompletedOnSha(
[{ user: { login: 'MervinPraison' }, body: '@claude You are the FINAL architecture reviewer.', created_at: '2026-06-27T10:00:00Z' }],
'2026-06-27T09:55:00Z'
));
assert('recent scan comment detected', mg.hasRecentMergeGateScanComment([
{ body: '**Merge gate scan** — eligible', created_at: new Date().toISOString() },
]));
assert('old scan comment not recent', !mg.hasRecentMergeGateScanComment([
{ body: '**Merge gate scan** — eligible', created_at: '2020-01-01T00:00:00Z' },
]));
// Sensitive + secrets
assert('workflow path sensitive', mg.sensitivePathReasons([{ filename: '.github/workflows/foo.yml' }]).length === 1);
assert('ci-only label exempts workflows', mg.sensitivePathReasons(
@@ -304,13 +257,8 @@ const finalAfterRebase = {
body: '@claude You are the FINAL architecture reviewer.',
created_at: conflictIso(-20 * 60 * 1000),
};
const claudeFinishedAfterRebase = {
user: { login: 'praisonai-triage-agent[bot]' },
body: 'Claude finished',
created_at: conflictIso(-19 * 60 * 1000),
};
const headAfterRebase = conflictIso(-21 * 60 * 1000);
const rebaseComments = [conflictTrigger, rebaseDone, finalAfterRebase, claudeFinishedAfterRebase];
const rebaseComments = [conflictTrigger, rebaseDone, finalAfterRebase];
assert('conflict blocks before rebase done', mg.hasRecentConflictComment([conflictTrigger], headAfterRebase));
assert('conflict clears after rebase + FINAL on HEAD', !mg.hasRecentConflictComment(rebaseComments, headAfterRebase));
assert('conflict still blocks without FINAL on HEAD', mg.hasRecentConflictComment(
@@ -324,16 +272,6 @@ assert('sdk with tests ok', mg.missingTestsReason([
{ filename: 'src/praisonai-agents/a/b.py', additions: 3 },
{ filename: 'src/praisonai-agents/tests/test_x.py', additions: 10 },
]) === null);
assert('ts sdk without tests', mg.missingTestsReason([
{ filename: 'src/praisonai-ts/src/agent/simple.ts', additions: 5 },
]) !== null);
assert('ts sdk with tests ok', mg.missingTestsReason([
{ filename: 'src/praisonai-ts/src/agent/simple.ts', additions: 5 },
{ filename: 'src/praisonai-ts/tests/unit/agent/history-restore.test.ts', additions: 10 },
]) === null);
assert('touches ts sdk', mg.touchesSdk([{ filename: 'src/praisonai-ts/src/agent/simple.ts' }]));
assert('final body requires sdk value table', mg.FINAL_CLAUDE_REVIEW_BODY.includes('| **SDK value** |'));
assert('final body requires ts agents md', mg.FINAL_CLAUDE_REVIEW_BODY.includes('src/praisonai-ts/AGENTS.md'));
// PR size
assert('large PR blocked', mg.prSizeReasons([{ additions: 900 }]).length > 0);
@@ -349,54 +287,4 @@ assert('fork sync PR link rejected', !mg.isInternalPullRequestLink(
'PraisonAI'
));
assert('dispatch blocked when merge gate active', mg.mergeGateDispatchBlockedReason({
labels: ['claude-merge-gate-active'],
pendingRuns: 0,
}) === 'merge gate already active on PR');
assert('dispatch blocked when queue saturated', mg.mergeGateDispatchBlockedReason({
labels: [],
pendingRuns: 3,
})?.includes('already queued or in progress'));
assert('dispatch allowed when queue below cap', mg.mergeGateDispatchBlockedReason({
labels: [],
pendingRuns: 2,
}) == null);
// Pending count excludes workflow_run auto-dispatch probes (PR #4534)
assert('workflow_run probes excluded from pending count', mg.countSubstantiveMergeGateRuns([
{ event: 'workflow_dispatch' },
{ event: 'workflow_run' },
{ event: 'workflow_run' },
]) === 1);
assert('all-probe runs count as zero pending', mg.countSubstantiveMergeGateRuns([
{ event: 'workflow_run' },
{ event: 'workflow_run' },
]) === 0);
assert('substantive dispatches still counted', mg.countSubstantiveMergeGateRuns([
{ event: 'workflow_dispatch' },
{ event: 'schedule' },
]) === 2);
assert('countSubstantiveMergeGateRuns null-safe', mg.countSubstantiveMergeGateRuns(null) === 0);
assert('rate limit threshold is 200', mg.MIN_CORE_RATE_LIMIT_REMAINING === 200);
(async () => {
const skipLow = await mg.shouldSkipMergeGateDispatch(
{
rest: {
rateLimit: {
get: async () => ({ data: { resources: { core: { remaining: 50 } } } }),
},
},
},
'owner',
'repo',
1,
{ info: () => {} }
);
assert('rate limit low skips dispatch', skipLow === true);
process.exit(failed ? 1 : 0);
})().catch((err) => {
console.error(err);
process.exit(1);
});
process.exit(failed ? 1 : 0);
+25 -275
View File
@@ -16,12 +16,8 @@ const PR_MAX_AUTO_ADDITIONS = 800;
const PR_MAX_AUTO_FILES = 30;
const MANUAL_ONLY_LABELS = new Set(['security', 'breaking-change', 'needs-manual-review', 'release']);
const WORKFLOW_ONLY_LABEL = 'merge-gate-ci-only';
const EXTERNAL_EXAMPLE_PATH_PREFIX = 'examples/tools/external/';
const EXTERNAL_EXAMPLE_LABEL = 'pipeline/external-example';
const MAINTAINER_ACCEPT_EXAMPLE_LABEL = 'maintainer-accept-example';
const CI_ONLY_PATH_PREFIXES = ['.github/workflows/', '.github/actions/', '.github/scripts/merge-gate'];
const SDK_PATH_PREFIXES = ['src/praisonai-agents/', 'src/praisonai/', 'src/praisonai-ts/'];
const TS_SDK_SOURCE_PREFIX = 'src/praisonai-ts/src/';
const SDK_PATH_PREFIXES = ['src/praisonai-agents/', 'src/praisonai/'];
const SENSITIVE_PATH_PATTERNS = [
/^\.github\/workflows\//,
/praisonaiagents\/(auth|approval|policy|sandbox)\//,
@@ -30,17 +26,7 @@ const SENSITIVE_PATH_PATTERNS = [
/\.env(\.|$)/,
/credentials\.json$/i,
];
const REQUIRED_SDK_CHECK_PATTERNS = [
/test/i,
/smoke/i,
/core/i,
/python package/i,
/comprehensive/i,
/optimized/i,
/praisonai-ts/i,
/npm/i,
/webview/i,
];
const REQUIRED_SDK_CHECK_PATTERNS = [/test/i, /smoke/i, /core/i, /python package/i, /comprehensive/i, /optimized/i];
const SECRET_PATTERNS = [
/sk-[a-zA-Z0-9]{20,}/,
/AKIA[0-9A-Z]{16}/,
@@ -88,11 +74,7 @@ function isClaudeTriggerNoise(c) {
function isClaudeFinalReplyComment(c) {
const login = (c.user?.login || '').toLowerCase();
const fromAutomation =
login.includes('praisonai-triage') ||
login.includes('github-actions') ||
AUTO_ACTORS.some((a) => a.toLowerCase() === login);
if (!fromAutomation) return false;
if (!login.includes('praisonai-triage')) return false;
return (c.body || '').includes('Claude finished');
}
@@ -114,47 +96,15 @@ function isConflictRebaseTriggerComment(c) {
function isConflictRebaseCompletionComment(c) {
const login = (c.user?.login || '').toLowerCase();
const fromAutomation =
login.includes('praisonai-triage') ||
login.includes('github-actions') ||
AUTO_ACTORS.some((a) => a.toLowerCase() === login);
if (!fromAutomation) return false;
if (!login.includes('praisonai-triage') && !login.includes('github-actions')) return false;
const body = (c.body || '').toLowerCase();
return (
body.includes('rebase complete') ||
body.includes('rebase onto') ||
body.includes('force-pushed') ||
(body.includes('conflict') && body.includes('resolved'))
);
}
/** HEAD landed after conflict rebase but no FINAL @claude since that push. */
function needsFinalReReviewAfterConflictRebase(comments, headPushedAt) {
if (!headPushedAt) return false;
const headTime = new Date(headPushedAt).getTime();
const conflictTriggers = comments.filter(isConflictRebaseTriggerComment);
if (conflictTriggers.length === 0) return false;
const latestConflict = conflictTriggers.reduce((a, b) =>
new Date(a.created_at) > new Date(b.created_at) ? a : b
);
const conflictTime = new Date(latestConflict.created_at).getTime();
if (headTime <= conflictTime) return false;
const rebaseDone = comments.some(
(c) =>
new Date(c.created_at).getTime() > conflictTime && isConflictRebaseCompletionComment(c)
);
if (!rebaseDone) return false;
const finalsAfterHead = comments.filter(
(c) =>
isFinalClaudeTriggerComment(c) &&
new Date(c.created_at).getTime() >= headTime - 60000
);
return finalsAfterHead.length === 0;
}
function conflictRebaseQuiescent(comments, headPushedAt) {
const conflictTriggers = comments.filter(isConflictRebaseTriggerComment);
if (conflictTriggers.length === 0) return true;
@@ -295,9 +245,6 @@ function shouldSkipStaleFinalRecovery(comments, headPushedAt, headPusherLogin =
if (headPusherLogin && isClaudeAutomationLogin(headPusherLogin)) {
return { skip: true, reason: 'head pushed by Claude automation' };
}
if (needsFinalReReviewAfterConflictRebase(comments, headPushedAt)) {
return { skip: false, reason: '' };
}
if (isPushSoonAfterLatestFinal(comments, headPushedAt)) {
return {
skip: true,
@@ -318,20 +265,11 @@ function shouldSkipStaleFinalRecovery(comments, headPushedAt, headPusherLogin =
function finalClaudeCompletedOnSha(comments, headPushedAt) {
if (!hasFinalClaudeReviewTrigger(comments)) return false;
if (isStaleFinalAfterPush(comments, headPushedAt)) return false;
const finals = comments.filter(isFinalClaudeTriggerComment);
if (finals.length === 0) return false;
const latestFinal = finals.reduce((a, b) =>
new Date(a.created_at) > new Date(b.created_at) ? a : b
);
const finalTime = new Date(latestFinal.created_at).getTime();
return comments.some((c) => {
if (!isClaudeFinalReplyComment(c)) return false;
return new Date(c.created_at).getTime() >= finalTime - 60000;
});
return true;
}
const FINAL_CLAUDE_REVIEW_BODY =
'@claude You are the FINAL architecture reviewer. If the branch is under MervinPraison/PraisonAI (not a fork), you are able to make modifications to this branch and push directly. SCOPE: Review changes in this PR. Python SDK: praisonaiagents, praisonai. TypeScript SDK: src/praisonai-ts/. Do NOT modify src/praisonai-rust. Read ALL comments above from Gemini, Qodo, CodeRabbit, and Copilot carefully before responding.\n\n**MANDATORY READ (before reviewing):**\n- Always read src/praisonai-agents/AGENTS.md\n- If this PR touches src/praisonai-ts/, also read src/praisonai-ts/AGENTS.md §2.1.2 (TS triage + PR review checklist)\n\n**Phase 1: Review per AGENTS.md**\n1. Protocol-driven: check heavy implementations vs core SDK\n2. Backward compatible: ensure zero feature regressions\n3. Performance: no hot-path regressions\n4. SDK value: review in depth whether the change genuinely adds value to the SDK — never add features for the sake of adding them. It must strengthen the SDK (simpler, more user-friendly, robust, world-class, secure). If it does not clearly add value, request changes or recommend rejecting/closing rather than merging scope creep\n5. Do not bloat the Agent class with additional params — only if absolutely required; we already support many params.\n6. Repo routing: agent-callable tools → PraisonAI-Tools; lifecycle plugins → PraisonAI-Plugins; optional sandbox backends → PraisonAI-Plugins (`praisonai.sandbox` entry point) — request changes if wrongly added to praisonaiagents/\n6b. **External tool examples (`examples/tools/external/**`):** If the diff is confined to that path, mark **SDK value** ❌ for monorepo merge — redirect the contributor to PraisonAI-Tools; do **not** Approve for merge unless the PR has the `maintainer-accept-example` label.\n\n**MANDATORY COMMENT FORMAT — include this Phase 1 table in your review comment:**\n#### Phase 1 — AGENTS.md review\n| Check | Result |\n|---|---|\n| Protocol-driven / no heavy impl in core | ✅ or ❌ + one-line rationale |\n| Backward compatible | ✅ or ❌ + one-line rationale |\n| Performance (hot path) | ✅ or ❌ + one-line rationale |\n| **SDK value** | ✅ or ❌ + one-line rationale (explicitly judge whether the change strengthens the SDK) |\n| No Agent param bloat | ✅ or ❌ + one-line rationale |\n| Repo routing | ✅ or ❌ + one-line rationale |\n\nFor TypeScript PRs (src/praisonai-ts/), also add:\n| TS types / parity / tests | ✅ or ❌ + one-line rationale (npm run build && npm test) |\n\n**Phase 2: FIX Valid Issues**\n7. For any VALID bugs or architectural flaws found by Gemini, CodeRabbit, Qodo, Copilot, or any other reviewer: implement the fix\n8. Also independently identify and fix any gaps or issues you find in the changed code — do not rely only on prior reviewer feedback\n9. Push all code fixes directly to THIS branch (do NOT create a new PR)\n10. Comment a summary of exact files modified and what you skipped\n\n**Phase 3: Final Verdict**\n11. If all issues are resolved, approve the PR / close the Issue\n12. If blocking issues remain, request changes / leave clear action items';
'@claude You are the FINAL architecture reviewer. If the branch is under MervinPraison/PraisonAI (not a fork), you are able to make modifications to this branch and push directly. SCOPE: Focus ONLY on Python packages (praisonaiagents, praisonai). Do NOT modify praisonai-rust or praisonai-ts. Read ALL comments above from Gemini, Qodo, CodeRabbit, and Copilot carefully before responding.\n\n**Phase 1: Review per AGENTS.md**\n1. Protocol-driven: check heavy implementations vs core SDK\n2. Backward compatible: ensure zero feature regressions\n3. Performance: no hot-path regressions\n4. SDK value: review in depth whether the change genuinely adds value to the SDK — never add features for the sake of adding them. It must strengthen the SDK (simpler, more user-friendly, robust, world-class, secure). If it does not clearly add value, request changes or recommend rejecting/closing rather than merging scope creep\n5. Do not bloat the Agent class with additional params — only if absolutely required; we already support many params.\n6. Repo routing: agent-callable tools → PraisonAI-Tools; lifecycle plugins → PraisonAI-Plugins; optional sandbox backends → PraisonAI-Plugins (`praisonai.sandbox` entry point) — request changes if wrongly added to praisonaiagents/\n\n**Phase 2: FIX Valid Issues**\n7. For any VALID bugs or architectural flaws found by Gemini, CodeRabbit, Qodo, Copilot, or any other reviewer: implement the fix\n8. Also independently identify and fix any gaps or issues you find in the changed code — do not rely only on prior reviewer feedback\n9. Push all code fixes directly to THIS branch (do NOT create a new PR)\n10. Comment a summary of exact files modified and what you skipped\n\n**Phase 3: Final Verdict**\n11. If all issues are resolved, approve the PR / close the Issue\n12. If blocking issues remain, request changes / leave clear action items';
async function getMergeState(github, owner, repo, prNumber) {
const query = `
@@ -487,79 +425,6 @@ function hasBlockingClaudeRunForPr(runs, headRef) {
return (runs || []).some((r) => claudeRunBlocksPr(r, headRef));
}
const MERGE_GATE_WORKFLOW_FILE = 'claude-merge-gate.yml';
const MERGE_GATE_ACTIVE_LABEL = 'claude-merge-gate-active';
const MAX_PENDING_MERGE_GATE_RUNS = 3;
const MIN_CORE_RATE_LIMIT_REMAINING = 200;
function mergeGateDispatchBlockedReason({ labels, pendingRuns, maxPending = MAX_PENDING_MERGE_GATE_RUNS }) {
if ((labels || []).includes(MERGE_GATE_ACTIVE_LABEL)) {
return 'merge gate already active on PR';
}
if (pendingRuns >= maxPending) {
return `${pendingRuns} merge gate run(s) already queued or in progress (max ${maxPending})`;
}
return null;
}
// workflow_run probes (auto-dispatch) share concurrency and must not count
// toward backpressure — they blocked dispatches for merge-ready PRs.
function countSubstantiveMergeGateRuns(runs) {
return (runs || []).filter((r) => r && r.event !== 'workflow_run').length;
}
async function countPendingMergeGateRuns(github, owner, repo) {
let pending = 0;
for (const status of ['queued', 'in_progress']) {
try {
const { data } = await github.rest.actions.listWorkflowRuns({
owner,
repo,
workflow_id: MERGE_GATE_WORKFLOW_FILE,
status,
per_page: 30,
});
pending += countSubstantiveMergeGateRuns(data.workflow_runs);
} catch {
// Best-effort backpressure only.
}
}
return pending;
}
async function getCoreRateLimitRemaining(github) {
try {
const { data } = await github.rest.rateLimit.get();
return data?.resources?.core?.remaining ?? null;
} catch {
return null;
}
}
async function shouldSkipMergeGateDispatch(github, owner, repo, prNumber, core, options = {}) {
const maxPending = options.maxPending ?? MAX_PENDING_MERGE_GATE_RUNS;
const minCoreRemaining = options.minCoreRemaining ?? MIN_CORE_RATE_LIMIT_REMAINING;
const remaining = await getCoreRateLimitRemaining(github);
if (remaining !== null && remaining < minCoreRemaining) {
core?.info?.(
`Skip merge gate dispatch for PR #${prNumber}: rate limit low (${remaining} core remaining)`
);
return true;
}
const ctx = await loadPrContext(github, owner, repo, prNumber);
const pendingRuns = await countPendingMergeGateRuns(github, owner, repo);
const reason = mergeGateDispatchBlockedReason({
labels: ctx.labels,
pendingRuns,
maxPending,
});
if (reason) {
core?.info?.(`Skip merge gate dispatch for PR #${prNumber}: ${reason}`);
return true;
}
return false;
}
async function hasInProgressClaudeAssistant(github, owner, repo, prNumber = null) {
try {
const { data } = await github.rest.actions.listWorkflowRuns({
@@ -586,11 +451,6 @@ function isCiOnlyChange(files) {
return files.every((f) => CI_ONLY_PATH_PREFIXES.some((p) => f.filename.startsWith(p)));
}
function isExternalExampleOnlyChange(files) {
if (!files.length) return false;
return files.every((f) => f.filename.startsWith(EXTERNAL_EXAMPLE_PATH_PREFIX));
}
function isInternalPullRequestLink(link, owner, repo) {
if (link?.base?.repo?.full_name === `${owner}/${repo}`) return true;
const baseUrl = link?.base?.repo?.url || '';
@@ -728,35 +588,18 @@ function prSizeReasons(files) {
}
function missingTestsReason(files) {
const pySdkAdds = files.filter(
const sdkAdds = files.filter(
(f) =>
f.filename.startsWith('src/praisonai-agents/') &&
f.filename.endsWith('.py') &&
!f.filename.endsWith('__init__.py') &&
(f.additions || 0) > 0
);
const tsSdkAdds = files.filter(
(f) =>
f.filename.startsWith(TS_SDK_SOURCE_PREFIX) &&
f.filename.endsWith('.ts') &&
!f.filename.endsWith('.d.ts') &&
(f.additions || 0) > 0
);
if (pySdkAdds.length === 0 && tsSdkAdds.length === 0) return null;
const hasPyTestChange = files.some(
if (sdkAdds.length === 0) return null;
const hasTestChange = files.some(
(f) => /\/tests?\//.test(f.filename) || /test_.*\.py$/.test(f.filename) || /_test\.py$/.test(f.filename)
);
const hasTsTestChange = files.some(
(f) =>
f.filename.startsWith('src/praisonai-ts/tests/') ||
/\.(test|spec)\.ts$/.test(f.filename)
);
if (pySdkAdds.length > 0 && !hasPyTestChange) {
return 'Python SDK code added without test file changes — requires manual review';
}
if (tsSdkAdds.length > 0 && !hasTsTestChange) {
return 'TS SDK code added without test file changes — requires manual review';
}
if (!hasTestChange) return 'SDK code added without test file changes — requires manual review';
return null;
}
@@ -805,67 +648,22 @@ function manualReviewReasonForAgentPy(agentChange) {
return null;
}
async function listAllComments(github, owner, repo, issueNumber, options = {}) {
const { since } = options;
const params = {
async function listAllComments(github, owner, repo, issueNumber) {
if (typeof github.paginate === 'function') {
return github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number: issueNumber,
per_page: 100,
});
}
const { data } = await github.rest.issues.listComments({
owner,
repo,
issue_number: issueNumber,
per_page: 100,
};
if (since) params.since = since;
// Always page explicitly — github-script's paginate helper can return only the
// first page on busy PRs, so fresh MERGE_GATE_VERDICT comments on page 2+ are missed.
const all = [];
for (let page = 1; page <= 50; page++) {
const { data } = await github.rest.issues.listComments({ ...params, page });
all.push(...data);
if (data.length < 100) break;
}
return all;
}
/** Poll until Opus posts MERGE_GATE_VERDICT on HEAD (merge-only runs right after assess). */
async function waitForMergeGateVerdict(github, owner, repo, prNumber, options = {}) {
const {
headPushedAt = null,
excludeAutomatedFallback = true,
maxAttempts = 18,
intervalMs = 10000,
since = null,
core: log = null,
} = options;
let resolvedHeadPushedAt = headPushedAt;
const commentSince =
since ||
(resolvedHeadPushedAt
? new Date(new Date(resolvedHeadPushedAt).getTime() - 5 * 60 * 1000).toISOString()
: null);
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
if (!resolvedHeadPushedAt) {
const ctx = await loadPrContext(github, owner, repo, prNumber);
resolvedHeadPushedAt = ctx.headPushedAt;
}
const comments = await listAllComments(github, owner, repo, prNumber, {
since: commentSince || undefined,
});
const verdict = findMergeGateVerdict(comments, null, resolvedHeadPushedAt, {
excludeAutomatedFallback,
});
if (verdict) {
return { verdict, headPushedAt: resolvedHeadPushedAt };
}
if (attempt < maxAttempts) {
log?.info?.(
`Verdict not visible yet (attempt ${attempt}/${maxAttempts}), waiting ${intervalMs / 1000}s...`
);
await new Promise((r) => setTimeout(r, intervalMs));
}
}
return { verdict: null, headPushedAt: resolvedHeadPushedAt };
});
return data;
}
async function getHeadCommitDate(github, owner, repo, prNumber) {
@@ -981,15 +779,6 @@ async function evaluatePipelineQuiescent(github, owner, repo, prNumber, core, op
const pullFiles = await listPullFiles(github, owner, repo, prNumber);
if (
isExternalExampleOnlyChange(pullFiles) &&
!ctx.labels.includes(MAINTAINER_ACCEPT_EXAMPLE_LABEL)
) {
reasons.push(
'external example only — belongs in PraisonAI-Tools (add maintainer-accept-example to opt in)'
);
}
const agentChange = getAgentPyChangeFromFiles(pullFiles);
const agentManual = manualReviewReasonForAgentPy(agentChange);
if (agentManual) reasons.push(agentManual);
@@ -1036,13 +825,6 @@ async function listPrNumbersForMergeGateScan(github, owner, repo, core) {
}
const mergeReady = mergeReadyIssues
.filter((issue) => issue.pull_request)
.filter((issue) => {
const labels = (issue.labels || []).map((l) => (typeof l === 'string' ? l : l.name));
if (labels.includes(EXTERNAL_EXAMPLE_LABEL) && !labels.includes(MAINTAINER_ACCEPT_EXAMPLE_LABEL)) {
return false;
}
return true;
})
.map((issue) => issue.number)
.sort((a, b) => a - b);
@@ -1098,33 +880,18 @@ async function selectMergeGateCandidates(github, owner, repo, prNumbers, maxCand
}
const AUTOMATED_FALLBACK_MARKER = 'Automated fallback —';
const MERGE_GATE_SCAN_MARKER = 'Merge gate scan';
function isAutomatedFallbackVerdict(body) {
return (body || '').includes(AUTOMATED_FALLBACK_MARKER);
}
function isMergeGateVerdictComment(body) {
const text = body || '';
if (text.includes(MERGE_GATE_SCAN_MARKER)) return false;
return /(?:^|\n)\s*MERGE_GATE_VERDICT:\s*(APPROVE|BLOCK)\b/m.test(text);
}
function hasRecentMergeGateScanComment(comments, withinMinutes = 30) {
const cutoff = Date.now() - withinMinutes * 60 * 1000;
return (comments || []).some((c) => {
if (!(c.body || '').includes(MERGE_GATE_SCAN_MARKER)) return false;
return new Date(c.created_at).getTime() > cutoff;
});
}
function findMergeGateVerdict(comments, minCreatedAt = null, headPushedAt = null, options = {}) {
const { excludeAutomatedFallback = false } = options || {};
const minTime = minCreatedAt ? new Date(minCreatedAt).getTime() : 0;
const headTime = headPushedAt ? new Date(headPushedAt).getTime() - 60000 : 0;
const gateComments = comments
.filter((c) => {
if (!isMergeGateVerdictComment(c.body)) return false;
if (!(c.body || '').includes('MERGE_GATE_VERDICT:')) return false;
if (excludeAutomatedFallback && isAutomatedFallbackVerdict(c.body)) return false;
const created = new Date(c.created_at).getTime();
if (minTime && created < minTime) return false;
@@ -1134,8 +901,8 @@ function findMergeGateVerdict(comments, minCreatedAt = null, headPushedAt = null
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
if (gateComments.length === 0) return null;
const body = gateComments[0].body || '';
if (/MERGE_GATE_VERDICT:\s*APPROVE\b/.test(body)) return 'APPROVE';
if (/MERGE_GATE_VERDICT:\s*BLOCK\b/.test(body)) return 'BLOCK';
if (body.includes('MERGE_GATE_VERDICT: APPROVE')) return 'APPROVE';
if (body.includes('MERGE_GATE_VERDICT: BLOCK')) return 'BLOCK';
return null;
}
@@ -1149,7 +916,6 @@ module.exports = {
hasRecentConflictComment,
isConflictRebaseTriggerComment,
isConflictRebaseCompletionComment,
needsFinalReReviewAfterConflictRebase,
conflictRebaseQuiescent,
hasHumanChangesRequested,
hasAnyChangesRequested,
@@ -1170,10 +936,6 @@ module.exports = {
claudeRunBlocksPr,
hasBlockingClaudeRunForPr,
isCiOnlyChange,
isExternalExampleOnlyChange,
EXTERNAL_EXAMPLE_PATH_PREFIX,
EXTERNAL_EXAMPLE_LABEL,
MAINTAINER_ACCEPT_EXAMPLE_LABEL,
isInternalPullRequestLink,
resolvePrNumberFromLinkedPullRequests,
resolvePrNumberFromHeadBranch,
@@ -1192,7 +954,6 @@ module.exports = {
secretScanReasons,
sdkTestChecksReason,
listAllComments,
waitForMergeGateVerdict,
getHeadCommitDate,
isStaleFinalAfterPush,
needsStaleFinalRecovery,
@@ -1211,16 +972,5 @@ module.exports = {
selectMergeGateCandidates,
isAutomatedFallbackVerdict,
AUTOMATED_FALLBACK_MARKER,
isMergeGateVerdictComment,
hasRecentMergeGateScanComment,
findMergeGateVerdict,
MERGE_GATE_WORKFLOW_FILE,
MERGE_GATE_ACTIVE_LABEL,
MAX_PENDING_MERGE_GATE_RUNS,
MIN_CORE_RATE_LIMIT_REMAINING,
getCoreRateLimitRemaining,
mergeGateDispatchBlockedReason,
countSubstantiveMergeGateRuns,
countPendingMergeGateRuns,
shouldSkipMergeGateDispatch,
};
-101
View File
@@ -1,101 +0,0 @@
#!/usr/bin/env bash
# Pick a working Claude Code OAuth token (primary/secondary with failover).
# Env: CLAUDE_CODE_OAUTH_TOKEN, CLAUDE_CODE_OAUTH_TOKEN_B, CLAUDE_OAUTH_ACTIVE
# Out: GITHUB_OUTPUT token, slot
set -euo pipefail
PRIMARY="${CLAUDE_CODE_OAUTH_TOKEN:-}"
SECONDARY="${CLAUDE_CODE_OAUTH_TOKEN_B:-}"
ACTIVE="${CLAUDE_OAUTH_ACTIVE:-primary}"
ACTIVE_LC=$(echo "$ACTIVE" | tr '[:upper:]' '[:lower:]')
case "$ACTIVE_LC" in
secondary|b|2) PREFERRED=secondary ;;
*) PREFERRED=primary ;;
esac
probe_token() {
local token=$1
[[ -z "$token" ]] && return 1
local body='{"model":"claude-3-5-haiku-latest","max_tokens":1,"messages":[{"role":"user","content":"."}]}'
local tmp http_code body_resp
tmp=$(mktemp)
http_code=$(curl -sS -o "$tmp" -w "%{http_code}" \
-X POST "https://api.anthropic.com/v1/messages" \
-H "Authorization: Bearer ${token}" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d "$body" 2>/dev/null || echo "000")
body_resp=$(cat "$tmp")
rm -f "$tmp"
if [[ "$http_code" == "200" ]] || [[ "$http_code" == "400" ]]; then
return 0
fi
if [[ "$http_code" == "401" ]] || [[ "$http_code" == "429" ]]; then
echo "::warning::OAuth probe failed (HTTP ${http_code})"
return 1
fi
if echo "$body_resp" | grep -qiE 'revoked|invalid.*token|authentication|weekly limit|rate_limit|overloaded|billing'; then
echo "::warning::OAuth probe rejected (HTTP ${http_code})"
return 1
fi
return 0
}
try_token() {
local slot=$1 token=$2
[[ -z "$token" ]] && return 1
if probe_token "$token"; then
SELECTED_SLOT=$slot
SELECTED_TOKEN=$token
return 0
fi
return 1
}
SELECTED_SLOT=""
SELECTED_TOKEN=""
if [[ "$PREFERRED" == "primary" ]]; then
try_token primary "$PRIMARY" || try_token secondary "$SECONDARY" || true
else
try_token secondary "$SECONDARY" || try_token primary "$PRIMARY" || true
fi
if [[ -z "$SELECTED_TOKEN" ]]; then
if [[ "$PREFERRED" == "primary" && -n "$PRIMARY" ]]; then
SELECTED_TOKEN="$PRIMARY"
SELECTED_SLOT="primary-unprobed"
elif [[ -n "$SECONDARY" ]]; then
SELECTED_TOKEN="$SECONDARY"
SELECTED_SLOT="secondary-unprobed"
elif [[ -n "$PRIMARY" ]]; then
SELECTED_TOKEN="$PRIMARY"
SELECTED_SLOT="primary-unprobed"
else
echo "::error::No Claude OAuth token configured"
exit 1
fi
echo "::warning::OAuth probe failed for all tokens; using ${SELECTED_SLOT}"
fi
echo "::notice::Claude OAuth slot: ${SELECTED_SLOT}"
if [[ -z "${GITHUB_OUTPUT:-}" ]]; then
echo "slot=${SELECTED_SLOT}"
exit 0
fi
{
echo "token<<EOF"
echo "$SELECTED_TOKEN"
echo "EOF"
echo "slot=${SELECTED_SLOT}"
} >> "$GITHUB_OUTPUT"
@@ -73,33 +73,4 @@ assert(
)
);
assert(
'external example only path',
mg.isExternalExampleOnlyChange([{ filename: 'examples/tools/external/arxiv/tool.py' }])
);
assert(
'mixed paths not external-example only',
!mg.isExternalExampleOnlyChange([
{ filename: 'examples/tools/external/foo.py' },
{ filename: 'src/praisonai-agents/foo.py' },
])
);
assert(
'upstream head repo',
ps.isUpstreamHeadRepo(
{ head: { repo: { full_name: 'MervinPraison/PraisonAI' } } },
'MervinPraison',
'PraisonAI'
)
);
assert(
'fork head repo still synced (not upstream)',
!ps.isUpstreamHeadRepo(
{ head: { repo: { full_name: 'ai-mrscraper/PraisonAI' } } },
'MervinPraison',
'PraisonAI'
)
);
process.exit(failed ? 1 : 0);
+7 -27
View File
@@ -37,17 +37,8 @@ const LABEL_SPECS = [
{ name: 'pipeline/blocked:cooldown', color: 'fef2c0', description: 'Blocked: post-push or @claude cooldown' },
{ name: 'pipeline/blocked:stale-final', color: 'f9d0c4', description: 'Blocked: FINAL stale after new commits' },
{ name: 'pipeline/blocked:no-final', color: 'ededed', description: 'Blocked: no FINAL @claude trigger yet' },
{
name: 'pipeline/external-example',
color: 'bfd4f2',
description: 'Diff only under examples/tools/external/ — not merge-ready without maintainer-accept-example',
},
];
// PRs with pipeline/external-example cannot reach pipeline/merge-ready unless
// maintainers add maintainer-accept-example (enforced in merge-gate evaluatePipelineQuiescent,
// same fail-closed pattern as fork PRs).
function deriveStage(comments, evalResult) {
if (evalResult.ready) return 'pipeline/merge-ready';
if (!chain.chainKickPosted(comments)) return 'pipeline/reviews-pending';
@@ -72,8 +63,7 @@ function reasonToBlockerLabel(reason) {
r.includes('without test') ||
r.includes('files changed') ||
r.includes('possible secret') ||
r.includes('agent.py') ||
r.includes('external example only')
r.includes('agent.py')
) return 'pipeline/blocked:manual-review';
if (r.includes('recent @claude') || r.includes('post-push buffer')) return 'pipeline/blocked:cooldown';
if (r.includes('stale final')) return 'pipeline/blocked:stale-final';
@@ -143,16 +133,11 @@ async function syncPipelineLabels(github, owner, repo, prNumber, core) {
github, owner, repo, prNumber, core
);
const { stage, blockers, all } = computePipelineLabels(ctx.comments, evalResult);
const pullFiles = await mergeGate.listPullFiles(github, owner, repo, prNumber);
const auxiliary = mergeGate.isExternalExampleOnlyChange(pullFiles)
? [mergeGate.EXTERNAL_EXAMPLE_LABEL]
: [];
const labelSet = [...all, ...auxiliary];
const current = ctx.labels.filter((l) => l.startsWith(PIPELINE_PREFIX));
const desired = new Set(labelSet);
const desired = new Set(all);
const toRemove = current.filter((l) => !desired.has(l) && l !== 'pipeline/merged');
const toAdd = labelSet.filter((l) => !current.includes(l));
const toAdd = all.filter((l) => !current.includes(l));
for (const name of toRemove) {
try {
@@ -196,10 +181,10 @@ async function syncPipelineLabels(github, owner, repo, prNumber, core) {
async function dispatchMergeGateForOldestReady(github, owner, repo, readyCandidates, core) {
if (!readyCandidates.length) return 0;
const mergeGate = require('./merge-gate.js');
readyCandidates.sort((a, b) => a.createdAt - b.createdAt);
for (const cand of readyCandidates) {
if (await mergeGate.shouldSkipMergeGateDispatch(github, owner, repo, cand.prNumber, core)) {
if ((cand.labels || []).includes('claude-merge-gate-active')) {
core?.info?.(`Skip dispatch PR #${cand.prNumber}: merge gate already active`);
continue;
}
await github.rest.repos.createDispatchEvent({
@@ -214,11 +199,6 @@ async function dispatchMergeGateForOldestReady(github, owner, repo, readyCandida
return 0;
}
function isUpstreamHeadRepo(pr, owner, repo) {
const headFull = pr.head?.repo?.full_name;
return !headFull || headFull === `${owner}/${repo}`;
}
async function syncOpenPullRequests(github, owner, repo, options, core) {
const { maxPrs = 20, dispatchMergeGate = true } = options || {};
await ensurePipelineLabels(github, owner, repo, core);
@@ -243,8 +223,9 @@ async function syncOpenPullRequests(github, owner, repo, options, core) {
for (const pr of prs) {
if (synced >= maxPrs) break;
if (pr.draft) continue;
if (pr.head?.repo?.full_name && pr.head.repo.full_name !== `${owner}/${repo}`) continue;
const result = await syncPipelineLabels(github, owner, repo, pr.number, core);
if (result.ready && isUpstreamHeadRepo(pr, owner, repo)) {
if (result.ready) {
readyCandidates.push({
prNumber: pr.number,
createdAt: result.createdAt || new Date(pr.created_at).getTime(),
@@ -267,7 +248,6 @@ module.exports = {
STAGE_LABELS,
BLOCKER_LABELS,
ALL_PIPELINE_LABELS,
isUpstreamHeadRepo,
deriveStage,
deriveBlockerLabels,
computePipelineLabels,
-142
View File
@@ -1,142 +0,0 @@
# Why this exists
#
# praisonai-agents has ~11,900 tests. Until this gate, CI executed 15 of them:
# one smoke step in test-optimized.yml naming two files. Everything else was
# collected by nobody.
#
# The cost of that showed up as two test modules that could not even be
# IMPORTED -- they referenced symbols moved or replaced months earlier, so 19
# tests silently contributed nothing while still appearing to exist. A failing
# test is loud; a test that never collects is not.
#
# This job started as collection-only, because running the suite would have gone
# red on arrival: 297 pre-existing failures nothing had been enforcing, and a job
# that is red the day it lands teaches people to ignore it. Then a curated
# green-set, ratcheted upward.
#
# Both are gone. The unit suite now runs in full and passes: 9373 tests, 0
# failures. The `collect` job stays because it is the cheaper, stricter signal --
# it cannot pass while a test file is unimportable, and it says so in 40s.
#
name: agents-suite
on:
push:
branches: [ main ]
paths:
- 'src/praisonai-agents/**'
- '.github/workflows/agents-suite.yml'
pull_request:
paths:
- 'src/praisonai-agents/**'
- '.github/workflows/agents-suite.yml'
jobs:
collect:
name: every praisonai-agents test file imports
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install
run: |
set -euo pipefail
python -m pip install --upgrade pip
pip install -e src/praisonai-agents
pip install pytest pytest-asyncio pytest-timeout
- name: Collect the whole suite
working-directory: src/praisonai-agents
env:
# A key must be present or some modules bail at import; it is never
# used, because collection does not execute test bodies.
OPENAI_API_KEY: 'sk-collect-only-not-a-real-key'
run: |
set -euo pipefail
# No pipe here: piping pytest into tail discards its exit code, which
# is how a gate silently stops gating.
python -m pytest tests/ --collect-only -q --disable-warnings
unit:
name: praisonai-agents unit tests
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install
run: |
set -euo pipefail
python -m pip install --upgrade pip
# Extras, not decoration. Tests across workflows/embedding/eval/context
# patch litellm and construct LLM(...), and the knowledge suite needs
# chonkie for chunking -- without [knowledge] 22 tests fail on
# "chonkie package not found", an install gap that reads as 22 code
# failures.
pip install -e "src/praisonai-agents[llm,knowledge]"
# praisonai-sandbox is a MONOREPO sibling, not a third-party optional.
# Without it 47 tests fail with "No module named
# praisonai_sandbox.compute" -- an install gap reported as 47 code
# failures. Installing it took the suite from 297 failures to 250.
pip install -e src/praisonai-sandbox
# Same story: a stale praisonai-bot in the environment made 22 gateway
# tests fail on a malformed pairing file. The monorepo version already
# handles it; the INSTALLED one did not.
pip install -e src/praisonai-bot
# The wrapper. tests/unit/test_managed_backend.py imports
# praisonai.integrations.managed_agents; without it 47 tests fail with
# "No module named 'praisonai'". Found by running the suite on 3.11 in
# a CI-shaped environment rather than trusting a developer machine
# where every sibling is already installed.
pip install -e src/praisonai
pip install pytest pytest-asyncio pytest-timeout
- name: Run the unit suite
working-directory: src/praisonai-agents
env:
# Never used: PRAISONAI_ALLOW_NETWORK=0 and the live marker keep these
# offline. Present because some modules bail at import without a key.
OPENAI_API_KEY: 'sk-not-a-real-key'
PRAISONAI_ALLOW_NETWORK: '0'
run: |
set -euo pipefail
# The whole suite, not a curated subset. tests/green-set.txt and
# tests/known-failures.txt are gone: 9373 pass, 0 fail.
#
# The 297 failures this job was built around were, in the end:
# 83 environment -- monorepo siblings not installed, dirty HOME
# 19 ONE line -- a module-level os.environ.setdefault in
# tests/unit/tools/test_delegation_tools.py that
# turned auto-approval on for the whole process
# at COLLECTION time
# ~5 deprecation warnings that fire once per process
# ~85 tests asserting contracts the product had deliberately changed
# 5 real product bugs
# the rest: tests built on Mocks whose auto-created attributes
# defeated the very code path under test
#
# Three traps worth knowing before adding a test here, each of which
# cost a full-suite bisect:
# 1. Module-level env mutation persists for the entire process, and
# runs at collection. Use monkeypatch.
# 2. The approval registry is a process-global singleton. Anything
# you add_requirement() stays for every later test.
# 3. `'litellm' in module_name` also matches praisonai's own
# _litellm_loader; evicting it from sys.modules creates a second
# module object with separate globals.
#
# No pipe: piping pytest into anything discards its exit code, which is
# how a gate silently stops gating.
#
# Nothing is excluded. test_prefetch.py used to be, recorded as "passes
# on macOS, fails on Linux" -- wrong variable. It was Python 3.12 vs the
# 3.11 pinned above: isinstance() against a runtime_checkable Protocol
# used hasattr on 3.11 (which any MagicMock satisfies) and
# inspect.getattr_static from 3.12 (which does not see Mock's
# auto-created attributes). The mock now names its methods, so both
# versions take the same branch.
python -m pytest tests/unit/ -q -p no:randomly --timeout=120 \
--disable-warnings
+1 -1
View File
@@ -42,5 +42,5 @@ jobs:
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '🤖 Auto-triaging with Claude. Analyzing issue and implementing a fix...\n\nClaude will:\n1. Route correctly: TypeScript → src/praisonai-ts/; agent tools → PraisonAI-Tools; lifecycle plugins → PraisonAI-Plugins\n2. Search codebase for root cause\n3. Implement a minimal fix following AGENTS.md\n4. Create a PR with the changes'
body: '🤖 Auto-triaging with Claude. Analyzing issue and implementing a fix...\n\nClaude will:\n1. Route correctly: agent tools → PraisonAI-Tools; lifecycle plugins → PraisonAI-Plugins\n2. Search codebase for root cause\n3. Implement a minimal fix following AGENTS.md\n4. Create a PR with the changes'
});
+7 -7
View File
@@ -53,7 +53,7 @@ jobs:
- name: Post Claude FINAL after prior reviewers
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_TOKEN != '' && secrets.GH_TOKEN || github.token }}
github-token: ${{ secrets.GH_TOKEN }}
script: |
const path = require('path');
const chain = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/pr-review-chain.js'));
@@ -97,7 +97,7 @@ jobs:
- name: Post Claude FINAL (skip Copilot)
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_TOKEN != '' && secrets.GH_TOKEN || github.token }}
github-token: ${{ secrets.GH_TOKEN }}
script: |
const path = require('path');
const chain = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/pr-review-chain.js'));
@@ -126,7 +126,7 @@ jobs:
- name: Re-post FINAL Claude review when stale or missing
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_TOKEN != '' && secrets.GH_TOKEN || github.token }}
github-token: ${{ secrets.GH_TOKEN }}
script: |
const path = require('path');
const mergeGate = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/merge-gate.js'));
@@ -182,7 +182,7 @@ jobs:
- name: Sync pipeline status labels
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_TOKEN != '' && secrets.GH_TOKEN || github.token }}
github-token: ${{ secrets.GH_TOKEN }}
script: |
const path = require('path');
const ps = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/pipeline-status.js'));
@@ -203,7 +203,7 @@ jobs:
- name: Post missing/stale FINAL @claude on open PRs
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_TOKEN != '' && secrets.GH_TOKEN || github.token }}
github-token: ${{ secrets.GH_TOKEN }}
script: |
const path = require('path');
const mergeGate = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/merge-gate.js'));
@@ -275,7 +275,7 @@ jobs:
- name: Trigger CodeRabbit and Qodo reviews
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_TOKEN != '' && secrets.GH_TOKEN || github.token }}
github-token: ${{ secrets.GH_TOKEN }}
script: |
const path = require('path');
const chain = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/bot-pr-review-chain.js'));
@@ -285,7 +285,7 @@ jobs:
- name: Sync pipeline status labels
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_TOKEN != '' && secrets.GH_TOKEN || github.token }}
github-token: ${{ secrets.GH_TOKEN }}
script: |
const path = require('path');
const ps = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/pipeline-status.js'));
+1 -9
View File
@@ -28,16 +28,8 @@ jobs:
with:
token: ${{ steps.app-token.outputs.token }}
- name: Pick Claude OAuth token
id: oauth
uses: MervinPraison/github-automation-template/.github/actions/[email protected]
with:
token_primary: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
token_secondary: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN_B }}
active_slot: ${{ vars.CLAUDE_OAUTH_ACTIVE != '' && vars.CLAUDE_OAUTH_ACTIVE || 'primary' }}
- name: Run Claude Issue Triage
uses: ./.github/actions/claude-issue-triage-action
with:
claude_code_oauth_token: ${{ steps.oauth.outputs.token }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
github_token: ${{ steps.app-token.outputs.token }}
+35 -140
View File
@@ -33,8 +33,7 @@ on:
- claude-merge-gate
concurrency:
# Serialize workflow_run auto-dispatches (Core Tests / Claude Assistant fan-out).
group: claude-merge-gate-${{ github.repository }}-${{ github.event.inputs.pr_number || github.event.client_payload.pr_number || (github.event_name == 'workflow_run' && 'auto-dispatch') || 'scan' }}
group: claude-merge-gate-${{ github.repository }}-${{ github.event.inputs.pr_number || github.event.client_payload.pr_number || github.event.workflow_run.id || 'scan' }}
cancel-in-progress: false
env:
@@ -108,10 +107,6 @@ jobs:
return;
}
if (await mergeGate.shouldSkipMergeGateDispatch(github, owner, repo, prNumber, core)) {
return;
}
await github.rest.repos.createDispatchEvent({
owner,
repo,
@@ -201,26 +196,17 @@ jobs:
'**Actions:** wait for CI and the Claude review chain, or add label `needs-manual-review` and merge manually.',
'**Opt out:** label `no-auto-merge`.',
].join('\n');
const ctx = await mergeGate.loadPrContext(github, owner, repo, skipped[0].pr);
if (!mergeGate.hasRecentMergeGateScanComment(ctx.comments)) {
await github.rest.issues.createComment({
owner, repo, issue_number: skipped[0].pr, body,
});
}
await github.rest.issues.createComment({
owner, repo, issue_number: skipped[0].pr, body,
});
} else if (inputPr && candidates.length === 1) {
const prNum = candidates[0].pr_number;
const ctx = await mergeGate.loadPrContext(github, owner, repo, prNum);
if (!mergeGate.hasRecentMergeGateScanComment(ctx.comments)) {
await github.rest.issues.createComment({
owner, repo, issue_number: prNum,
body: '**Merge gate scan** — eligible for assessment. Claude merge gate will assess and may auto-merge on an APPROVE verdict.',
});
}
await github.rest.issues.createComment({
owner, repo, issue_number: candidates[0].pr_number,
body: '**Merge gate scan** — eligible for assessment. Claude merge gate will assess and may auto-merge if `MERGE_GATE_VERDICT: APPROVE`.',
});
}
await ps.syncOpenPullRequests(
github, owner, repo, { maxPrs: 10, dispatchMergeGate: false }, core
);
await ps.syncOpenPullRequests(github, owner, repo, { maxPrs: 10 }, core);
claude-assess:
needs: scan-candidates
@@ -267,14 +253,6 @@ jobs:
- name: Stash merge gate helpers
run: cp .github/scripts/merge-gate.js /tmp/merge-gate.js
- name: Pick Claude OAuth token
id: oauth
uses: MervinPraison/github-automation-template/.github/actions/[email protected]
with:
token_primary: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
token_secondary: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN_B }}
active_slot: ${{ vars.CLAUDE_OAUTH_ACTIVE != '' && vars.CLAUDE_OAUTH_ACTIVE || 'primary' }}
- name: Fetch PR branch
env:
PR_NUMBER: ${{ matrix.pr_number }}
@@ -284,14 +262,13 @@ jobs:
- name: Claude merge gate assessment (read-only)
id: assess
continue-on-error: true
uses: ./.github/actions/claude-code-action
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
CLAUDE_CODE_SUBAGENT_MODEL: inherit
ANTHROPIC_DEFAULT_SONNET_MODEL: claude-sonnet-4-6
with:
claude_code_oauth_token: ${{ steps.oauth.outputs.token }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
github_token: ${{ steps.app-token.outputs.token }}
model: claude-opus-4-8
trigger_phrase: '@claude-merge-gate-internal'
@@ -311,7 +288,7 @@ jobs:
Review checklist:
1. Read AGENTS.md and the PR diff on the current branch.
2. Read all PR comments and reviews (FINAL architecture reviewer / Lead Engineer must have run).
3. Confirm SDK value gate: change strengthens the relevant SDK (Python: praisonaiagents/praisonai; TypeScript: src/praisonai-ts/) — no scope creep. The aim is a LIGHTWEIGHT AND POWERFUL package — BLOCK changes that add params/modules/exports duplicating existing Agent capabilities (instructions/backstory/tools/hooks/memory), or add knobs/fields with no live consumer, i.e. scope creep for the sake of it. FINAL architecture review must include the Phase 1 table with an explicit **SDK value** row.
3. Confirm SDK value gate: change strengthens praisonaiagents (no scope creep). The aim is a LIGHTWEIGHT AND POWERFUL package — BLOCK changes that add params/modules/exports duplicating existing Agent capabilities (instructions/backstory/tools/hooks/memory), or add knobs/fields with no live consumer, i.e. scope creep for the sake of it.
4. Repo routing: BLOCK if agent-callable tools (→ PraisonAI-Tools), lifecycle plugins (→ PraisonAI-Plugins), or optional sandbox backends (→ PraisonAI-Plugins via `praisonai.sandbox`) were added to praisonaiagents/ instead of the external repo.
5. Do not bloat the Agent class with additional params — only if absolutely required; we already support many params.
6. If agent.py adds >100 lines or new Agent __init__ params, respond MERGE_GATE_VERDICT: BLOCK (manual review required).
@@ -326,97 +303,15 @@ jobs:
Write
Replace
allowed_tools: |
mcp__github__add_issue_comment
mcp__github__get_pull_request
mcp__github__get_pull_request_files
mcp__github__get_pull_request_reviews
mcp__github__get_pull_request_status
mcp__github__get_issue
mcp__github__get_issue_comments
Glob
Grep
View
GlobTool
GrepTool
Read
timeout_minutes: 15
- name: Post Opus verdict from assess output if MCP missed
id: relay_verdict
if: always() && steps.assess.conclusion != 'skipped'
uses: actions/github-script@v7
with:
github-token: ${{ steps.app-token.outputs.token }}
script: |
const fs = require('fs');
const mergeGate = require('/tmp/merge-gate.js');
const owner = context.repo.owner;
const repo = context.repo.repo;
const prNumber = ${{ matrix.pr_number }};
const ctx = await mergeGate.loadPrContext(github, owner, repo, prNumber);
const minCreatedAt = new Date(Date.now() - 25 * 60 * 1000).toISOString();
const existing = mergeGate.findMergeGateVerdict(
ctx.comments, minCreatedAt, ctx.headPushedAt, { excludeAutomatedFallback: true }
);
if (existing) {
core.info(`Opus verdict already on PR: ${existing}`);
return;
}
const outPath = `${process.env.GITHUB_WORKSPACE}/.claude-assess-output.jsonl`;
if (!fs.existsSync(outPath)) {
core.info('No assess output file to relay');
return;
}
const lines = fs.readFileSync(outPath, 'utf8').split('\n').filter(Boolean);
let verdict = null;
let rationale = '';
for (let i = lines.length - 1; i >= 0; i--) {
const line = lines[i];
if (line.includes('MERGE_GATE_VERDICT: APPROVE')) {
verdict = 'APPROVE';
rationale = line;
break;
}
if (line.includes('MERGE_GATE_VERDICT: BLOCK')) {
verdict = 'BLOCK';
rationale = line;
break;
}
try {
const evt = JSON.parse(line);
const text = JSON.stringify(evt);
if (text.includes('MERGE_GATE_VERDICT: APPROVE')) {
verdict = 'APPROVE';
rationale = text;
break;
}
if (text.includes('MERGE_GATE_VERDICT: BLOCK')) {
verdict = 'BLOCK';
rationale = text;
break;
}
} catch {
// Not JSON — keep scanning.
}
}
if (!verdict) {
core.info('Assess output had no MERGE_GATE_VERDICT to relay');
return;
}
const body = [
`MERGE_GATE_VERDICT: ${verdict}`,
'',
'Relayed from Opus merge gate assess output (GitHub MCP comment post was unavailable).',
].join('\n');
await github.rest.issues.createComment({
owner, repo, issue_number: prNumber, body,
});
core.info(`Relayed Opus verdict to PR comment: ${verdict}`);
- name: Post fallback verdict if Opus did not comment
- name: Post fallback BLOCK if Opus did not comment
id: fallback_verdict
if: always() && steps.assess.conclusion != 'skipped'
if: steps.assess.outcome != 'success'
uses: actions/github-script@v7
with:
github-token: ${{ steps.app-token.outputs.token }}
@@ -432,21 +327,20 @@ jobs:
ctx.comments, minCreatedAt, ctx.headPushedAt, { excludeAutomatedFallback: true }
);
if (existing) {
core.info(`Verdict already posted: ${existing}`);
core.info(`Opus verdict already posted: ${existing}`);
return;
}
const check = await mergeGate.evaluatePipelineQuiescent(
github, owner, repo, prNumber, core, { forMergeStep: true }
);
const verdict = check.ready ? 'APPROVE' : 'BLOCK';
const lines = [
`MERGE_GATE_VERDICT: ${verdict}`,
'MERGE_GATE_VERDICT: BLOCK',
'',
'Automated fallback — Opus merge gate did not post a PR comment.',
'Automated fallback — Opus merge gate assessment did not complete.',
];
if (check.ready) {
lines.push('All deterministic merge gates passed.');
lines.push('Deterministic gates passed, but Opus MERGE_GATE_VERDICT is required before merge.');
} else {
lines.push('Blockers:', ...check.reasons.map((r) => `- ${r}`));
}
@@ -454,10 +348,9 @@ jobs:
owner, repo, issue_number: prNumber,
body: lines.join('\n'),
});
core.info(`Posted fallback verdict: ${verdict}`);
core.info('Posted fallback BLOCK — Opus verdict required');
- name: Capture Opus MERGE_GATE_VERDICT
id: capture_verdict
- name: Require Opus MERGE_GATE_VERDICT
uses: actions/github-script@v7
with:
github-token: ${{ steps.app-token.outputs.token }}
@@ -473,14 +366,14 @@ jobs:
const verdict = mergeGate.findMergeGateVerdict(
comments, minCreatedAt, ctx.headPushedAt, { excludeAutomatedFallback: true }
);
core.setOutput('verdict', verdict || 'NONE');
if (!verdict) {
core.warning(
'Opus MERGE_GATE_VERDICT not posted on PR (GitHub MCP tools may be blocked). Merge waits for APPROVE.'
core.setFailed(
'Opus merge gate assessment must post MERGE_GATE_VERDICT on this PR (automated fallback APPROVE is not accepted).'
);
} else {
core.info(`Opus merge gate verdict: ${verdict}`);
return;
}
core.info(`Opus merge gate verdict: ${verdict}`);
core.setOutput('verdict', verdict);
- name: Remove merge gate active label
if: always()
@@ -501,6 +394,10 @@ jobs:
core.info(`Label claude-merge-gate-active already removed or missing on PR #${prNumber}`);
}
- name: Fail job if assessment failed
if: steps.assess.outcome == 'failure'
run: exit 1
merge-only:
needs: [scan-candidates, claude-assess]
if: |
@@ -555,12 +452,10 @@ jobs:
try {
const ctx = await mergeGate.loadPrContext(github, owner, repo, prNumber);
const { verdict } = await mergeGate.waitForMergeGateVerdict(
github, owner, repo, prNumber, {
headPushedAt: ctx.headPushedAt,
excludeAutomatedFallback: true,
core,
}
const comments = ctx.comments;
const verdictMin = new Date(Date.now() - 25 * 60 * 1000).toISOString();
const verdict = mergeGate.findMergeGateVerdict(
comments, verdictMin, ctx.headPushedAt, { excludeAutomatedFallback: true }
);
if (verdict !== 'APPROVE') {
core.info(`Skip merge: verdict=${verdict || 'NONE'}`);
+10 -25
View File
@@ -100,19 +100,18 @@ jobs:
'',
'_Bug and enhancement reports are auto-triaged with Claude. Questions and other issues: a maintainer can comment `@claude` to trigger analysis._',
'',
'**Routing:** Agent-callable tools → [PraisonAI-Tools](https://github.com/MervinPraison/PraisonAI-Tools); lifecycle plugins (tracing/hooks/guardrails) → [PraisonAI-Plugins](https://github.com/MervinPraison/PraisonAI-Plugins). TypeScript/JavaScript SDK fixes land in `src/praisonai-ts/` in this repo.'
'**Routing:** Agent-callable tools → [PraisonAI-Tools](https://github.com/MervinPraison/PraisonAI-Tools); lifecycle plugins (tracing/hooks/guardrails) → [PraisonAI-Plugins](https://github.com/MervinPraison/PraisonAI-Plugins).'
].join('\n')
});
}
// --- Auto-trigger Claude for bug/enhancement reports ---
// --- Auto-trigger Claude for owner issues and actionable external bug/enhancement reports ---
const titleLower = (issue.title || '').toLowerCase();
const bodyRaw = issue.body || '';
const hasBugMarker = /#?\s*\[bug\]/i.test(issue.title || '') || /#?\s*\[bug\]/i.test(bodyRaw);
const hasActionableTitle = /^(\[[^\]]+\]|bug:|fix[:(]|feat:|ux:|enhancement:)/i.test(titleLower);
const isBugReport = labels.includes('bug') || hasBugMarker;
const isEnhancement = labels.includes('enhancement') && !labels.includes('question');
const autoClaude = isOwner || isBugReport || (hasActionableTitle && isEnhancement);
const autoClaude = isOwner || (hasActionableTitle && (
labels.includes('bug') ||
(labels.includes('enhancement') && !labels.includes('question'))
));
if (autoClaude) {
await github.rest.issues.addLabels({
issue_number: issue.number,
@@ -249,14 +248,6 @@ jobs:
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
- name: Pick Claude OAuth token
id: oauth
uses: MervinPraison/github-automation-template/.github/actions/[email protected]
with:
token_primary: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
token_secondary: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN_B }}
active_slot: ${{ vars.CLAUDE_OAUTH_ACTIVE != '' && vars.CLAUDE_OAUTH_ACTIVE || 'primary' }}
- name: Fetch PR branch and Setup Remote
if: steps.check_fork.outputs.is_pr == 'true'
env:
@@ -283,7 +274,7 @@ jobs:
GH_TOKEN: ${{ steps.check_fork.outputs.can_push_fork == 'true' && secrets.GH_TOKEN || steps.app-token.outputs.token }}
with:
allowed_bots: 'praisonai-triage-agent[bot]'
claude_code_oauth_token: ${{ steps.oauth.outputs.token }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
github_token: ${{ steps.app-token.outputs.token }}
model: claude-opus-4-8
trigger_phrase: "@claude"
@@ -302,7 +293,7 @@ jobs:
STEP 1 — READ GUIDELINES:
Read AGENTS.md to understand the architecture rules.
SCOPE: Python SDK (praisonaiagents, praisonai) and TypeScript SDK (src/praisonai-ts/) live in this monorepo. Do NOT modify src/praisonai-rust. For TS issues read src/praisonai-ts/AGENTS.md.
SCOPE: Focus ONLY on Python packages (praisonaiagents, praisonai). Do NOT modify or touch praisonai-rust or praisonai-ts packages.
AIM: This package must stay LIGHTWEIGHT AND POWERFUL. Do a CRITICAL REVIEW at each stage (issue triage, fix planning, PR review/implementation) and reject scope creep for the sake of adding features. Prefer existing Agent capabilities (instructions/backstory/tools/hooks/memory) over new params/modules/exports. Even when fixing a raised issue, keep the fix minimal and lightweight — do NOT expand the surface area or add knobs with no live consumer.
STEP 2 — ARCHITECTURE VALIDATION & ROUTING (MANDATORY before writing code):
@@ -314,7 +305,6 @@ jobs:
4. Documentation (PraisonAIDocs): Documentation pages, guides, API references.
5. Plugins → MervinPraison/PraisonAI-Plugins: framework lifecycle extensions (tracing, logging, metrics, hooks, guardrails). Not agent tools — use STEP 3-ALT.
6. Sandbox backends (optional/niche, e.g. Capsule) → PraisonAI-Plugins via `praisonai.sandbox` entry point; core keeps protocol/config/manager only.
7. TypeScript/JavaScript SDK → src/praisonai-ts/ in this monorepo (not praisonaiagents/).
For items routed to an EXTERNAL repository (Tools, Docs, Plugins), follow STEP 3-ALT below.
- Does it duplicate existing functionality? Check if Agent already supports this via existing params (reflection, planning, tools, hooks, memory). If it does, reject the change as scope creep and prefer the existing capability — the aim is lightweight and powerful, not more knobs.
- Do not bloat the Agent class with additional params — only if absolutely required; we already support many params.
@@ -346,11 +336,9 @@ jobs:
gh pr create -R MervinPraison/<REPO_NAME> --head claude/issue-$ISSUE_NUMBER-$(date +%Y%m%d) --title "feat: <title>" --body "Fixes MervinPraison/PraisonAI#$ISSUE_NUMBER
<body>"
h) Comment on THIS PraisonAI issue with the external PR URL.
STEP 4 — TEST:
- For Python SDK changes: cd src/praisonai-agents && PYTHONPATH=. python -m pytest tests/ -x -q --timeout=30
- For TypeScript SDK changes: cd src/praisonai-ts && npm install && npm run build && npm test
- For SDK changes: cd src/praisonai-agents && PYTHONPATH=. python -m pytest tests/ -x -q --timeout=30
- For docs changes: verify files exist and are valid markdown/MDX
- Ensure no regressions
@@ -363,8 +351,6 @@ jobs:
Bash(conda:*)
Bash(pytest:*)
Bash(gh:*)
Bash(npm:*)
Bash(node:*)
Bash(python -m pytest:*)
Bash(python -m pip:*)
Bash(poetry:*)
@@ -384,10 +370,9 @@ jobs:
steps.check_fork.outputs.is_pr != 'true' &&
(github.event_name == 'issues' || github.event_name == 'issue_comment') &&
github.event.issue != null
continue-on-error: true
uses: actions/github-script@v7
with:
github-token: ${{ steps.app-token.outputs.token }}
github-token: ${{ secrets.GH_TOKEN }}
script: |
const path = require('path');
const chain = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/bot-pr-review-chain.js'));
-267
View File
@@ -1,267 +0,0 @@
# Build the desktop app for macOS, Windows and Linux, and attach it to the
# release.
#
# Releases carried no assets at all -- v4.7.1 shipped zero -- so there was
# nothing for anyone to download. The macOS half of this shipped first; this
# adds the other two, which were not merely missing from CI but impossible:
# the bundle targets were `["app", "dmg"]`, both macOS-only, so a Windows or
# Linux build produced zero artifacts and still exited zero.
#
# Unsigned, deliberately, on every platform. macOS signing needs a Developer ID
# and Windows signing now needs a hardware-backed certificate or a hosted
# signing service -- neither is in secrets. Shipping builds people can open
# past the warning is better than shipping none, and the release notes say so
# rather than letting Gatekeeper and SmartScreen say it for us.
name: Desktop release
on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: 'Existing release tag to attach the build to'
required: true
permissions:
contents: write
concurrency:
# Queue rather than cancel: a cancelled release build looks identical to a
# release that simply never produced assets, which is the bug being fixed.
group: desktop-release-${{ github.event.release.tag_name || inputs.tag }}
cancel-in-progress: false
jobs:
build:
strategy:
fail-fast: false
matrix:
include:
# macos-15, not macos-14: that image entered deprecation on
# 2026-07-06 and is fully unsupported from 2026-11-02, with brownout
# periods failing jobs before then. This is the only Apple-silicon
# leg, so losing it means no arm64 DMG at all.
- runner: macos-15 # Apple silicon
target: aarch64-apple-darwin
bundles: dmg
ext: dmg
asset: macos-apple-silicon
# macos-15-intel, not macos-13: that image was retired on
# 2025-12-04, so the leg would never schedule and the verify job
# below would correctly report a missing asset on every release,
# forever. macos-15-intel is GitHub's named x86_64 replacement.
- runner: macos-15-intel # Intel
target: x86_64-apple-darwin
bundles: dmg
ext: dmg
asset: macos-intel
- runner: windows-latest
target: x86_64-pc-windows-msvc
bundles: nsis
ext: exe
asset: windows-x64-setup
# ubuntu-22.04 rather than -latest on purpose: glibc is forward
# compatible only, so the build host sets the *oldest* distribution
# the .deb can run on. Building on 24.04 would produce a package that
# refuses to start on 22.04 and Debian 12 with a "GLIBC_2.38 not
# found" that no runtime flag can fix.
#
# This image begins deprecation on 2026-09-17 and is retired on
# 2027-04-17. When it goes, move to `ubuntu-latest` with
# `container: debian:12` rather than bumping the runner -- that keeps
# the glibc floor decided by us instead of by GitHub's image
# lifecycle.
- runner: ubuntu-22.04
target: x86_64-unknown-linux-gnu
bundles: deb
ext: deb
asset: linux-x64
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v4
- name: Install the Linux build dependencies
if: runner.os == 'Linux'
run: |
set -euo pipefail
# Retried: a degraded mirror otherwise burns the whole job and
# reports "cancelled", which reads as our failure rather than apt's.
for attempt in 1 2 3; do
sudo apt-get update && break || sleep 15
done
sudo apt-get install -y --no-install-recommends \
libwebkit2gtk-4.1-dev \
libappindicator3-dev \
librsvg2-dev \
libxdo-dev \
libssl-dev \
patchelf \
xdg-utils
# libappindicator3-dev, not the ayatana package: the .deb's generated
# Depends: follows whichever one the binary linked against, so the
# two must not be mixed between builds.
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: swatinem/rust-cache@v2
with:
workspaces: src/praisonai-desktop/src-tauri -> target
# Saving the Windows target directory costs more minutes than the
# rebuild it saves.
save-if: ${{ runner.os != 'Windows' }}
- name: Install the Tauri CLI
shell: bash
# `|| true` swallowed a genuine install failure and the next step then
# died with an unrelated "no such command: tauri". It also behaved
# differently per leg, since Windows defaults to pwsh where `true` is
# not a command. Skip the install if it is already there; otherwise
# let a failure be a failure.
run: command -v cargo-tauri >/dev/null || cargo install tauri-cli --version '^2' --locked
- name: Set the desktop version from the release tag
shell: bash
env:
RELEASE_TAG: ${{ github.event.release.tag_name || inputs.tag }}
run: node src/praisonai-desktop/tools/set-release-version.mjs "$RELEASE_TAG"
- name: Build the bundle
working-directory: src/praisonai-desktop/src-tauri
run: cargo tauri build --target ${{ matrix.target }} --bundles ${{ matrix.bundles }}
- name: The Windows binary must carry the release version
if: runner.os == 'Windows'
shell: pwsh
working-directory: src/praisonai-desktop/src-tauri
env:
RELEASE_TAG: ${{ github.event.release.tag_name || inputs.tag }}
run: |
$expected = $env:RELEASE_TAG -replace '^v', ''
$core = ($expected -split '[-+]')[0]
$exe = "target/${{ matrix.target }}/release/praisonai-desktop.exe"
$version = (Get-Item $exe).VersionInfo
Write-Host "ProductVersion=$($version.ProductVersion) FileVersion=$($version.FileVersion)"
if ($version.ProductVersion -ne $expected) {
throw "ProductVersion $($version.ProductVersion) does not match $expected"
}
# Compare the three components that carry meaning, not the string.
#
# This gate blocked the v4.7.4 Windows build with
# FileVersion 4.7.4 does not match 4.7.4.0
# It appended ".0" and compared literally, but Windows reports what
# was stamped -- three parts here. The stamping was correct and the
# check was not, so the release shipped with no Windows installer at
# all: the one platform it was cut to fix.
#
# [version] normalises a missing fourth component to -1, so both
# "4.7.4" and "4.7.4.0" compare equal on major.minor.build.
$got = [version]$version.FileVersion
$want = [version]$core
if ($got.Major -ne $want.Major -or
$got.Minor -ne $want.Minor -or
$got.Build -ne $want.Build) {
throw "FileVersion $($version.FileVersion) does not match $core"
}
- name: The macOS bundle must carry a valid signature
if: runner.os == 'macOS'
working-directory: src/praisonai-desktop/src-tauri
run: |
set -euo pipefail
dmg="$(find "target/${{ matrix.target }}/release/bundle" -name '*.dmg' -print -quit)"
[ -n "$dmg" ] || { echo "no .dmg was produced"; exit 1; }
# Verify inside the DMG, not the .app beside it.
#
# Tauri deletes the app bundle once the DMG is built -- the build log
# says so plainly ("Cleaning .../bundle/macos/PraisonAI.app") -- so a
# gate that looked for the .app found nothing and failed every macOS
# leg. Reading the DMG is also the better check: it is the artifact
# people download, and the one that was broken.
#
# v4.7.2 shipped a DMG that macOS called "damaged and can't be
# opened" on every Mac. The cause was not the missing Developer ID:
# the only signature was the one rustc's linker puts on the bare
# executable, whose CodeDirectory declares a resource seal that an
# unsigned bundle does not have --
#
# Sealed Resources=none
# code has no resources but signature indicates they must be present
#
# That is an *invalid* signature, not an unsigned one, and macOS
# reports invalid as "damaged". Right-click -> Open cannot bypass it,
# so the instructions shipped alongside it could not work either.
mnt="$(mktemp -d)"
hdiutil attach -nobrowse -readonly -quiet -mountpoint "$mnt" "$dmg"
trap 'hdiutil detach "$mnt" -quiet || true' EXIT
app="$(find "$mnt" -maxdepth 1 -name '*.app' -print -quit)"
[ -n "$app" ] || { echo "the DMG contains no .app"; ls -la "$mnt"; exit 1; }
echo "verifying $app"
codesign --verify --deep --strict --verbose=2 "$app"
# Printed, not asserted: --verify already fails on the exact bundle
# that shipped, so a second check for the same thing would only be a
# second way to say it. This is here to make the log readable.
codesign -dv --verbose=2 "$app" 2>&1 | grep -E 'Sealed Resources|flags=' || true
- name: Name the artifact for the platform it runs on
id: artifact
shell: bash
working-directory: src/praisonai-desktop/src-tauri
run: |
set -euo pipefail
tag="${{ github.event.release.tag_name || inputs.tag }}"
root="target/${{ matrix.target }}/release/bundle"
found="$(find "$root" -name "*.${{ matrix.ext }}" -print -quit || true)"
# Fail loudly rather than uploading nothing: an empty release asset
# list is exactly the state this workflow exists to fix, and a bundler
# given a target it does not support exits zero having written none.
if [ -z "$found" ]; then
echo "no .${{ matrix.ext }} was produced under $root"
find "$root" -type f -print || echo "(the bundle directory does not exist)"
exit 1
fi
out="PraisonAI-${tag}-${{ matrix.asset }}.${{ matrix.ext }}"
mv "$found" "../$out"
echo "path=src/praisonai-desktop/$out" >> "$GITHUB_OUTPUT"
- name: Attach it to the release
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release upload \
"${{ github.event.release.tag_name || inputs.tag }}" \
"${{ steps.artifact.outputs.path }}" --clobber
verify:
# A release with a missing platform is worse than an obviously failed
# build, because nobody notices. This says which one is absent.
needs: build
if: always()
runs-on: ubuntu-latest
steps:
- name: Check every platform produced an asset
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ github.event.release.tag_name || inputs.tag }}
run: |
set -euo pipefail
assets="$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" \
--json assets -q '.assets[].name')"
echo "$assets"
missing=""
for want in macos-apple-silicon macos-intel windows-x64-setup linux-x64; do
echo "$assets" | grep -q -- "$want" || missing="$missing $want"
done
if [ -n "$missing" ]; then
echo "::error::the release is missing:$missing"
exit 1
fi
echo "all four platform builds are attached"
-115
View File
@@ -1,115 +0,0 @@
# The desktop app's tests.
#
# These existed and ran nowhere: no workflow referenced praisonai-desktop, and
# the frontend suite could not even be installed -- jsdom was undeclared, so it
# ran only on machines that happened to have one further up the tree, at
# whatever version that happened to be.
name: desktop
on:
push:
branches: [main]
paths: ['src/praisonai-desktop/**', '.github/workflows/desktop.yml',
'.github/workflows/desktop-release.yml']
pull_request:
paths: ['src/praisonai-desktop/**', '.github/workflows/desktop.yml',
'.github/workflows/desktop-release.yml']
concurrency:
group: desktop-${{ github.ref }}
cancel-in-progress: true
defaults:
run:
working-directory: src/praisonai-desktop
jobs:
engine:
name: engine (${{ matrix.os }})
# The engine is the part that has to behave the same on all three, which is
# the entire point of the portability suite -- so it is run on all three.
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-22.04, macos-15, windows-latest]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Run the engine tests
shell: bash
env:
# Never touch a real keychain on a shared runner.
PRAISONAI_KEYCHAIN_SERVICE: ai.praison.desktop.ci
run: |
set -euo pipefail
failed=0
for suite in engine/test_*.py; do
echo "::group::$suite"
python "$suite" -v || failed=1
echo "::endgroup::"
done
exit $failed
frontend:
name: frontend
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- run: npm ci
# `test:ci` is every suite except lifecycle, which drives real
# conversations against a live engine and a real model -- neither of
# which exists on a runner. It is excluded by name rather than left to
# skip quietly, so that a skipped test here is a genuine problem.
- name: Run the UI tests
# A suite that skips everything still exits 0 -- that is how thirteen
# tests came to look like a pass while proving nothing. So the skip
# count is asserted, not just the exit status.
run: |
set -euo pipefail
set -o pipefail
npm run test:ci 2>&1 | tee "$RUNNER_TEMP/ui.log"
skipped=$(grep -E '^. skipped ' "$RUNNER_TEMP/ui.log" | grep -oE '[0-9]+$' | head -1)
echo "skipped: ${skipped:-unknown}"
test "${skipped:-1}" -eq 0
shell:
name: shell (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-22.04, macos-15, windows-latest]
defaults:
run:
working-directory: src/praisonai-desktop/src-tauri
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@v2
with:
workspaces: src/praisonai-desktop/src-tauri
# The Windows cache is large and slow enough to cost more than it saves.
save-if: ${{ runner.os != 'Windows' }}
- name: Install the Linux build dependencies
if: runner.os == 'Linux'
working-directory: .
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev \
libxdo-dev libssl-dev patchelf
- name: Test
# Debug info is the bulk of a Tauri target directory and nothing here
# reads a backtrace, so building without it keeps the runner's disk
# from filling on the Windows leg.
run: cargo test --config 'profile.test.debug=false' --config 'profile.dev.debug=false'
- name: Clippy
run: cargo clippy --all-targets -- -D warnings
+5 -8
View File
@@ -21,9 +21,6 @@ on:
- '.github/actions/install-monorepo-packages/**'
workflow_dispatch:
permissions:
contents: read
jobs:
cross-repo-integration:
runs-on: ubuntu-latest
@@ -44,13 +41,13 @@ jobs:
ref: main
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: python -m pip install --upgrade pip
- uses: ./PraisonAI/.github/actions/install-monorepo-packages
- uses: PraisonAI/.github/actions/install-monorepo-packages
with:
repo-root: PraisonAI/
wrapper-extras: ".[ui,dev]"
@@ -141,13 +138,13 @@ jobs:
ref: main
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: python -m pip install --upgrade pip
- uses: ./PraisonAI/.github/actions/install-monorepo-packages
- uses: PraisonAI/.github/actions/install-monorepo-packages
with:
repo-root: PraisonAI/
wrapper-extras: ".[ui,dev]"
@@ -165,4 +162,4 @@ jobs:
python -m pytest tests/integration/test_aiui_host_agentic.py -v
else
echo "OPENAI_API_KEY not available, skipping agentic tests"
fi
fi
+7 -19
View File
@@ -117,25 +117,13 @@ jobs:
const labels = issue.labels.map((l) => l.name);
if (status !== 'DIRTY' && labels.includes(LABEL)) {
try {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: prNumber,
name: LABEL,
});
core.info(`Removed ${LABEL} from PR #${prNumber}`);
} catch (err) {
// Fork PRs may return 403/404 when the token cannot mutate labels on the head repo's issue.
// For upstream PRs a 403/404 is a genuine failure that must surface, otherwise the
// merge-blocking label stays in place while the check reports success.
const isFork = headRepo && headRepo !== baseRepo;
if (isFork && (err.status === 403 || err.status === 404)) {
core.info(`Skip remove ${LABEL} on fork PR #${prNumber}: ${err.status}`);
} else {
throw err;
}
}
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: prNumber,
name: LABEL,
});
core.info(`Removed ${LABEL} from PR #${prNumber}`);
}
if (status !== 'DIRTY') return { skipped: true, reason: 'not dirty' };
-351
View File
@@ -1,351 +0,0 @@
# The mobile package's PLATFORM builds: a real iOS app and a real Android app.
#
# mobile.yml proves the crate type-checks for a phone (`cargo check` against
# the two mobile targets). This proves the two generated platform projects,
# src-tauri/gen/apple and src-tauri/gen/android, still turn that crate into an
# installable app -- which is a different thing, and the thing that had never
# happened: until #4673 neither project existed.
#
# What each job produces, and why that shape:
#
# ios-simulator -- an UNSIGNED arm64 simulator .app. A device build needs
# an Apple Developer Team and a signing identity, which are
# the maintainer's and not in this repo (bundle.iOS.
# developmentTeam is unset in tauri.conf.json). A simulator
# build needs neither, so it is the build that can run on
# every PR. It is not decoration: it links the Rust
# staticlib into the Xcode target, packages the webview
# into the bundle and exercises Info.plist, i.e. every step
# short of the certificate.
# android-apk -- a debug APK, arm64-v8a. Debug because a release APK
# needs a signing keystore, which is likewise the
# maintainer's; the debug keystore is generated on the
# runner. It installs on any arm64 Android 8.0+ device with
# `adb install`.
#
# Both jobs fail if the thing they exist to produce is missing. The bundler
# exits zero on a target it silently skipped (desktop-release.yml learned this
# the hard way), so "the step passed" is not evidence; "the file exists and
# contains the Rust library" is.
#
# The Xcode project is generated on the macOS runner the first time (this
# laptop has no Xcode, only the Command Line Tools) and then committed, so
# the init step is a no-op once gen/apple is in the tree. The Android project
# was generated locally and is committed; the same guard keeps that job
# honest if someone deletes it.
name: mobile platforms
on:
workflow_dispatch:
pull_request:
paths: &watched
- 'src/praisonai-mobile/**'
- 'src/praisonai-ts/src/**'
- '.github/workflows/mobile-platforms.yml'
# Without this, no platform build had ever run on main -- so a break
# introduced by two PRs that were each green apart could sit on main
# unnoticed. That is exactly what happened: #4737 branched from before
# #4727 added the `file:` dependency, so the two were never installed
# together until they had both landed.
push:
branches: [main]
paths: *watched
concurrency:
# Per-ref on a PR, per-SHA on main. This is the crux of the fix, not
# cancel-in-progress alone: a concurrency group holds at most ONE
# running run and ONE pending run, and a newer arrival always replaces
# the pending one -- cancel-in-progress: false spares only the RUNNING
# run, never the queued one. So with a single mobile-platforms-main
# group, three merges in one ~3-minute window would still drop the
# middle commit's run (it is only ever pending), which is precisely the
# blindness the push trigger was added to remove. Giving each main SHA
# its own group makes every commit a candidate release with its own
# answer, with nothing to contend for and so nothing to cancel.
group: mobile-platforms-${{ github.ref == 'refs/heads/main' && github.sha || github.ref }}
# On a PR, superseding an in-flight build is right: only the tip
# matters and these jobs cost ~3 minutes on two runners. On main each
# group has a single run, so this is a no-op there -- left true because
# the condition that mattered is now expressed in the group name above,
# and this keeps the PR behaviour explicit.
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
defaults:
run:
working-directory: src/praisonai-mobile
jobs:
ios-simulator:
name: ios simulator (.app, unsigned)
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22.18'
cache: npm
cache-dependency-path: src/praisonai-mobile/package-lock.json
- uses: dtolnay/rust-toolchain@stable
with:
# Both: `tauri ios init` installs whatever it thinks is missing via
# rustup, and having them already present keeps that step quiet and
# deterministic. aarch64-apple-ios-sim is the one the build below
# actually links against.
targets: aarch64-apple-ios,aarch64-apple-ios-sim
- uses: Swatinem/rust-cache@v2
with:
workspaces: src/praisonai-mobile/src-tauri
key: ios-sim
- name: "Build praisonai-ts (the file: dependency)"
# praisonai-mobile depends on praisonai by `file:`, so `npm ci`
# here runs praisonai-ts's own `prepare` -> `rimraf dist`. On a
# fresh runner praisonai-ts has no node_modules, so rimraf is not
# installed and `npm ci` dies with code 127 before a single
# platform step runs. mobile.yml gives all four of its jobs this
# step; these two were written against the tree from before the
# dependency existed and never got it.
working-directory: src/praisonai-ts
run: |
npm install --no-audit --no-fund --legacy-peer-deps
npm run build
- run: npm ci
- name: Generate the Xcode project (only if gen/apple is not committed)
# `tauri ios init` renders the template and then runs `xcodegen`, a
# brew package the CLI would otherwise try to install itself, mid-run,
# with a prompt. Installing it up front is the same outcome without
# the prompt. cocoapods is only invoked when the config declares pods;
# it declares none.
run: |
set -euo pipefail
if [ -d src-tauri/gen/apple ]; then
echo "src-tauri/gen/apple is committed; not regenerating it"
exit 0
fi
brew list xcodegen >/dev/null 2>&1 || brew install xcodegen
npx tauri ios init --ci --skip-targets-install
- name: Put the real AppIcon set into the Xcode project
# `tauri ios init` renders the CLI's OWN placeholder AppIcon PNGs into
# gen/apple/Assets.xcassets, and nothing in the CLI ever replaces them:
# `tauri icon` writes to src-tauri/icons/ios/ and stops there (it wires
# up gen/android, but there is no iOS equivalent). So the copy is done
# here, and every filename matches one-for-one -- both sides are named
# by the same Contents.json.
#
# Idempotent, and it runs whether or not init just ran, so a
# regenerated project cannot silently ship Tauri's icon as ours.
run: |
set -euo pipefail
dest=src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset
test -d "$dest" || { echo "::error::$dest does not exist"; find src-tauri/gen/apple -maxdepth 3 -print; exit 1; }
cp src-tauri/icons/ios/*.png "$dest/"
ls "$dest" | wc -l
- name: Build the simulator app, unsigned
# --target aarch64-sim: the iphonesimulator SDK, arm64 (the runner is
# Apple silicon, and so is every Mac that will run the simulator).
# --no-sign: xcodebuild with CODE_SIGNING_ALLOWED=NO. For the
# simulator SDK the CLI then lifts PraisonAI.app straight out of the
# xcarchive instead of exporting an IPA (crates/tauri-cli/src/mobile/
# ios/build.rs, the `target.sdk == "iphonesimulator"` branch), which
# is exactly the artifact a simulator installs.
# --debug: the dev profile links in seconds rather than minutes, and a
# simulator build is never the one that ships.
run: npx tauri ios build --debug --target aarch64-sim --no-sign --ci
- name: Is there an app, and is the Rust inside it?
id: app
run: |
set -euo pipefail
root=src-tauri/gen/apple/build
# Printed before anything is asserted, so a failure here is
# diagnosable from the log alone rather than costing another run.
echo "--- what the build left behind"
find "$root" -maxdepth 3 -print 2>/dev/null | head -40 || echo "(no build directory)"
# Prefer the simulator export dir (build/arm64-sim, the `arch` of the
# aarch64-sim target); fall back to any .app so the error message can
# say what WAS built. The name is not hardcoded: it comes from
# productName via the CLI, and guessing it wrong is what failed here
# the first time.
app="$(find "$root" -type d -name '*.app' -path '*arm64-sim*' -print -quit 2>/dev/null || true)"
[ -n "$app" ] || app="$(find "$root" -type d -name '*.app' -print -quit 2>/dev/null || true)"
if [ -z "$app" ]; then
echo "::error::no .app was produced under $root"
exit 1
fi
echo "app: $app"
# The executable's name comes from the bundle itself for the same
# reason.
exe="$app/$(plutil -extract CFBundleExecutable raw "$app/Info.plist")"
test -f "$exe" || { echo "::error::$app has no executable"; ls -la "$app"; exit 1; }
file "$exe"
file "$exe" | grep -q 'arm64' || { echo "::error::the executable is not arm64"; exit 1; }
# iOS links the Rust in as a staticlib, so unlike Android there is no
# .so to look for -- the crate is INSIDE this binary, and its symbols
# are the only real evidence it got there. An .app that built without
# it would be the Swift template alone.
#
# Dumped to files rather than piped into `grep -q`: grep exits on the
# FIRST match, the dumper then dies of SIGPIPE, and `set -o pipefail`
# reports that as a failed pipeline -- so the check failed precisely
# BECAUSE it found what it was looking for. It cost a CI run.
nm -a "$exe" > "$RUNNER_TEMP/symbols.txt" 2>/dev/null || true
strings -a "$exe" > "$RUNNER_TEMP/strings.txt" 2>/dev/null || true
if grep -qi 'praisonai_mobile\|start_app' "$RUNNER_TEMP/symbols.txt" \
|| grep -qi 'praisonai_mobile' "$RUNNER_TEMP/strings.txt"; then
echo "the Rust crate is linked into the executable"
else
echo "::error::the Rust library is not linked into $exe"
exit 1
fi
# The frontend is embedded in that same binary by generate_context!,
# not copied into the bundle as a resource, so it is checked the same
# way rather than by looking for an index.html that is never there.
plutil -p "$app/Info.plist" | grep -E 'CFBundleIdentifier|CFBundleShortVersionString|MinimumOSVersion|UIDeviceFamily'
test -f "$app/Assets.car" || echo "note: no compiled asset catalog in the bundle"
du -sh "$app"
# ditto, not zip: it keeps the bundle's symlinks and modes, which is
# the difference between an .app the simulator installs and one it
# rejects as an invalid bundle.
ditto -c -k --keepParent "$app" "$RUNNER_TEMP/PraisonAI-ios-simulator.app.zip"
ls -la "$RUNNER_TEMP/PraisonAI-ios-simulator.app.zip"
- name: Upload the simulator app
uses: actions/upload-artifact@v4
with:
name: PraisonAI-ios-simulator-app
path: ${{ runner.temp }}/PraisonAI-ios-simulator.app.zip
if-no-files-found: error
retention-days: 30
- name: Package gen/apple for committing
# always(): the project is generated by `init`, so it is worth having
# even on a run whose BUILD failed -- that is exactly the run where
# someone needs to look at it.
if: always()
# Only what `git add src-tauri/gen/apple` would take: the template's
# own .gitignore excludes build/, xcuserdata/ and Externals/, and this
# respects it instead of re-listing them here. Nothing to upload when
# the project was already committed -- the guard above skipped init.
run: |
set -euo pipefail
files="$(git ls-files --others --exclude-standard src-tauri/gen/apple || true)"
if [ -z "$files" ]; then
echo "gen/apple is tracked already; nothing to package"
exit 0
fi
echo "$files" | tar -czf "$RUNNER_TEMP/gen-apple.tgz" -T -
echo "$files" | wc -l
ls -la "$RUNNER_TEMP/gen-apple.tgz"
- name: Upload gen/apple (only on the run that generated it)
if: always()
uses: actions/upload-artifact@v4
with:
name: gen-apple
path: ${{ runner.temp }}/gen-apple.tgz
if-no-files-found: ignore
retention-days: 30
android-apk:
name: android apk (debug, arm64)
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22.18'
cache: npm
cache-dependency-path: src/praisonai-mobile/package-lock.json
- uses: actions/setup-java@v4
with:
# 17, not the runner's default 21 or a laptop's 25: Gradle 8.14.3's
# Groovy front end rejects class files newer than it knows
# ("Unsupported class file major version 69" on JDK 25), and 17 is
# what AGP 8.11 documents.
distribution: temurin
java-version: '17'
- uses: android-actions/setup-android@v3
with:
# What gen/android/app/build.gradle.kts declares (compileSdk 36),
# plus the build-tools AGP pairs with it. Anything else AGP needs it
# downloads itself; the licences are accepted by this action.
packages: 'platform-tools platforms;android-36 build-tools;36.0.0'
- uses: nttld/setup-ndk@v1
id: ndk
with:
# r28c = 28.2.13676358, the NDK gen/android was generated and first
# built with.
ndk-version: r28c
add-to-path: false
- uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-linux-android
- uses: Swatinem/rust-cache@v2
with:
workspaces: src/praisonai-mobile/src-tauri
key: android-apk
- name: "Build praisonai-ts (the file: dependency)"
# praisonai-mobile depends on praisonai by `file:`, so `npm ci`
# here runs praisonai-ts's own `prepare` -> `rimraf dist`. On a
# fresh runner praisonai-ts has no node_modules, so rimraf is not
# installed and `npm ci` dies with code 127 before a single
# platform step runs. mobile.yml gives all four of its jobs this
# step; these two were written against the tree from before the
# dependency existed and never got it.
working-directory: src/praisonai-ts
run: |
npm install --no-audit --no-fund --legacy-peer-deps
npm run build
- run: npm ci
- name: Generate the Android project (only if gen/android is not committed)
env:
NDK_HOME: ${{ steps.ndk.outputs.ndk-path }}
run: |
set -euo pipefail
if [ -d src-tauri/gen/android ]; then
echo "src-tauri/gen/android is committed; not regenerating it"
exit 0
fi
npx tauri android init --ci --skip-targets-install
- name: Build the debug APK
env:
NDK_HOME: ${{ steps.ndk.outputs.ndk-path }}
# --target aarch64 only: every phone sold in the last several years is
# arm64-v8a, and one target keeps the Rust build to one link. The
# `universal` in the output path is Gradle's name for "the APK that is
# not split per ABI", not a claim about what is inside it.
run: npx tauri android build --debug --apk --target aarch64 --ci
- name: Is there an APK, and is the Rust inside it?
run: |
set -euo pipefail
root=src-tauri/gen/android/app/build/outputs/apk
apk="$(find "$root" -name '*.apk' -print -quit || true)"
if [ -z "$apk" ]; then
echo "::error::no .apk was produced under $root"
find "$root" -type f -print 2>/dev/null || echo "(the outputs directory does not exist)"
exit 1
fi
echo "apk: $apk"
ls -la "$apk"
unzip -l "$apk" | grep -q 'lib/arm64-v8a/libpraisonai_mobile_lib.so' \
|| { echo "::error::$apk does not contain lib/arm64-v8a/libpraisonai_mobile_lib.so"; unzip -l "$apk" | head -40; exit 1; }
unzip -l "$apk" | grep -E 'lib/|AndroidManifest|classes.dex' | head
cp "$apk" "$RUNNER_TEMP/PraisonAI-android-debug-arm64.apk"
- name: Upload the APK
uses: actions/upload-artifact@v4
with:
name: PraisonAI-android-debug-arm64-apk
path: ${{ runner.temp }}/PraisonAI-android-debug-arm64.apk
if-no-files-found: error
retention-days: 30
-164
View File
@@ -1,164 +0,0 @@
# The mobile package's WEB build: built, booted in a real browser, and
# deployed to GitHub Pages.
#
# mobile.yml proves the package's tests pass. This workflow proves the thing
# that ships to a browser actually ships: dist/ is built, served under the
# repository subpath and driven through first paint, install, and an offline
# reload by tools/web-boot.test.mjs, then published. A PR gets the build and
# the proof; only main deploys.
#
# One setting is outside this file's reach: the repository's Pages source must
# be "GitHub Actions" (Settings -> Pages -> Build and deployment -> Source).
# Until a human turns that on, the deploy job SKIPS with a notice rather than
# failing -- a red main every push because of a setting no workflow can set
# would train people to ignore the mobile checks, which is how the last silent
# breakage in this package survived.
name: mobile web
on:
push:
branches: [main]
paths: &watched
- 'src/praisonai-mobile/**'
- '.github/workflows/mobile-web.yml'
# `praisonai` is a `file:` link to ../praisonai-ts, and the web bundle is
# built FROM it -- so drift on that side changes what ships to a browser
# and must trigger this gate. mobile.yml watches these same paths for the
# same reason; a web gate that watched only praisonai-mobile would go
# quiet exactly when the dependency it bundles moved.
- 'src/praisonai-ts/src/**'
- 'src/praisonai-ts/scripts/**'
- 'src/praisonai-ts/package.json'
pull_request:
paths: *watched
permissions:
contents: read
defaults:
run:
working-directory: src/praisonai-mobile
jobs:
build:
name: build and boot
runs-on: ubuntu-22.04
concurrency:
group: mobile-web-build-${{ github.ref }}
cancel-in-progress: true
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22.18'
cache: npm
cache-dependency-path: src/praisonai-mobile/package-lock.json
# `praisonai` is a `file:` link to ../praisonai-ts, so its dist/ must
# exist before this package can even INSTALL: npm runs the linked
# package's prepare/prebuild, which fails without its own node_modules.
# `npm install`, not `npm ci`: praisonai-ts gitignores its lockfile.
# `--legacy-peer-deps` for the bedrock-agentcore -> @strands-agents/sdk
# peer conflict, exactly as mobile.yml and praisonai-ts-webview.yml do.
- name: "Build praisonai-ts (the file: dependency)"
working-directory: src/praisonai-ts
run: |
npm install --no-audit --no-fund --legacy-peer-deps
npm run build
- run: npm ci
- name: Cache the Playwright browser
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('src/praisonai-mobile/package-lock.json') }}
- name: Install Chromium for the boot proof
run: npx playwright install --with-deps chromium
- name: Build
run: npm run build
- name: Boot proof
# Serves dist/ under /PraisonAI/ in headless Chromium: composer and
# Send render, manifest served as application/manifest+json, service
# worker activates with the subpath as scope, an offline reload still
# renders, and an injected inline script is blocked by the CSP.
run: npm run test:web
- name: dist contents
run: ls -lR dist
- uses: actions/upload-artifact@v4
with:
name: mobile-web-dist
path: src/praisonai-mobile/dist
if-no-files-found: error
- uses: actions/upload-pages-artifact@v3
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
with:
path: src/praisonai-mobile/dist
# The readiness check is its OWN job, and deliberately declares no
# `environment:`. A job that names `environment: github-pages` cannot START
# until that environment exists, so when Pages is off the runner fails the
# job outright -- zero steps, about one second -- and every carefully
# written skip inside it is unreachable. Measured: run 33730322658 on main,
# "deploy to GitHub Pages" failed with steps: 0.
#
# Splitting the decision out means the check runs with no preconditions and
# the environment-bearing job is never scheduled unless it can succeed.
pages-ready:
name: is GitHub Pages enabled?
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: build
runs-on: ubuntu-22.04
permissions:
contents: read
# `gh api repos/<owner>/<repo>/pages` needs Pages read. Without it the
# request is rejected on a private repo, `2>/dev/null || echo '{}'`
# swallows the error to `{}`, build_type falls to "none", and the deploy
# SKIPS even though Pages is on -- the exact silent skip this file exists
# to prevent. `contents: read` alone leaves this permission at `none`.
pages: read
outputs:
ready: ${{ steps.check.outputs.ready }}
steps:
# Pages cannot be enabled from a workflow, so this reports the setting
# rather than demanding it. `ready` false SKIPS the deploy with a
# notice; it does not fail the run. deploy-pages would otherwise fail
# with a bare 404 that says nothing about which setting is missing.
- name: Is the Pages source "GitHub Actions"?
id: check
env:
GH_TOKEN: ${{ github.token }}
working-directory: .
run: |
site=$(gh api "repos/${{ github.repository }}/pages" 2>/dev/null || echo '{}')
build_type=$(echo "$site" | jq -r '.build_type // "none"')
cname=$(echo "$site" | jq -r '.cname // ""')
echo "build_type=$build_type cname=${cname:-<none>}"
if [ "$build_type" != "workflow" ]; then
echo "::notice::Skipping the Pages deploy: Pages is not set to build from GitHub Actions (build_type=$build_type). A repository admin must set it at https://github.com/${{ github.repository }}/settings/pages -> Build and deployment -> Source: GitHub Actions. The built site is attached to this run as the mobile-web-dist artifact."
echo "ready=false" >> "$GITHUB_OUTPUT"
exit 0
fi
if [ -n "$cname" ]; then
echo "::notice::Skipping the Pages deploy: a custom domain ($cname) is set, so the app would be served there rather than at the github.io URL."
echo "ready=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "ready=true" >> "$GITHUB_OUTPUT"
deploy:
name: deploy to GitHub Pages
needs: pages-ready
if: needs.pages-ready.outputs.ready == 'true'
runs-on: ubuntu-22.04
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
concurrency:
# Never cancel a deployment in flight; queue behind it instead.
group: pages
cancel-in-progress: false
steps:
- uses: actions/configure-pages@v5
- uses: actions/deploy-pages@v4
id: deployment
-263
View File
@@ -1,263 +0,0 @@
# The mobile package's tests.
#
# Written at the same time as the package, deliberately. praisonai-desktop's
# suite existed and ran nowhere for months because no workflow referenced it,
# and a package whose tests never run in CI is a package whose tests are
# decoration -- the 523 here would go stale silently.
#
# Four gates, and they catch different things:
#
# typecheck -- the boundary between erasable and non-erasable TypeScript.
# node:test strips types rather than compiling, so an enum or
# a constructor parameter property is a runtime parse error at
# import; `erasableSyntaxOnly` turns that into a typecheck
# failure at the file that caused it.
# boundaries -- the two seams. Only engines/src/praisonai-ts may import
# 'praisonai'; only adapters/src/tauri may import @tauri-apps.
# These are the whole reason the package is swappable, and a
# rule enforced only by review stops being enforced.
# test -- the suite itself.
# bundle gate -- webview compatibility. This is the one a normal test run
# CANNOT do: node:test runs in Node, where a `crypto` import
# and a top-level `process.env` both work fine, and both kill
# a webview bundle at import time.
name: mobile
on:
push:
branches: [main]
paths: &watched
- 'src/praisonai-mobile/**'
- '.github/workflows/mobile.yml'
# The `upstream parity` job below typechecks the REAL praisonai-ts Agent
# against the structural contract in engines/src/praisonai-ts/agent-api.ts.
# Watching only praisonai-mobile made that gate fire when the CONSUMER
# changed and stay silent when the thing it watches did -- so upstream
# drift landed on main unchecked. Verified on 71197fa80, which edited
# src/praisonai-ts/src/agent/simple.ts: no parity check ran on it.
- 'src/praisonai-ts/src/**'
# And the pieces of praisonai-ts that decide what `praisonai/mobile`
# RESOLVES to through the file: link -- the esm shim shapes dist/esm.
- 'src/praisonai-ts/scripts/**'
- 'src/praisonai-ts/package.json'
pull_request:
paths: *watched
concurrency:
group: mobile-${{ github.ref }}
cancel-in-progress: true
defaults:
run:
working-directory: src/praisonai-mobile
jobs:
check:
name: check (node ${{ matrix.node }})
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
# 22.18 is the floor declared in package.json -- tested because a
# declared floor nobody runs is a guess. 24 is the forward check:
# native type-stripping is the feature this package depends on most,
# and it is exactly the kind of thing a major bump changes.
node: ['22.18', '24']
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: npm
cache-dependency-path: src/praisonai-mobile/package-lock.json
# `praisonai` is a `file:` link to ../praisonai-ts (see praisonai-mobile's
# package.json for why), so its dist/ must exist before this package can
# resolve `praisonai/mobile`. `npm install`, not `npm ci`: praisonai-ts
# gitignores its lockfile. `--legacy-peer-deps` for the
# bedrock-agentcore -> @strands-agents/sdk peer conflict, exactly as
# praisonai-ts-webview.yml does.
- name: "Build praisonai-ts (the file: dependency)"
working-directory: src/praisonai-ts
run: |
npm install --no-audit --no-fund --legacy-peer-deps
npm run build
- run: npm ci
- name: Cache the Playwright browser
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('src/praisonai-mobile/package-lock.json') }}
- name: Install Chromium for the boot proof
# `npm test` includes tools/web-boot.test.mjs, which loads the built
# dist/ in a real browser. It fails rather than skips without one.
run: npx playwright install --with-deps chromium
- name: Typecheck
run: npm run typecheck
- name: Layer boundaries
run: npm run boundaries
- name: Tests
run: npm test
- name: Bundle gate fixtures
# Runs the gate's own suite, which proves each check can FAIL -- a
# build check that has only ever seen clean input is a check nobody
# knows the state of.
run: npm run test:bundle
upstream-parity:
name: upstream parity
# Its own job so drift from the real Agent is visible in the checks list
# rather than buried in another job's log.
#
# It needs only THIS package's install. praisonai-ts's own dependencies are
# not needed here (the `check` job installs them, with --legacy-peer-deps,
# to build the file: dependency): every member the check looks at is
# declared locally in simple.ts. That is verified rather than assumed:
# with praisonai-ts's node_modules moved aside, injected drift is still
# caught as TS2741.
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22.18'
cache: npm
cache-dependency-path: src/praisonai-mobile/package-lock.json
- name: "Build praisonai-ts (the file: dependency)"
working-directory: src/praisonai-ts
run: |
npm install --no-audit --no-fund --legacy-peer-deps
npm run build
- run: npm ci
- name: Does the real Agent still satisfy PraisonAgent?
# Skips loudly if the praisonai-ts checkout is missing, rather than
# passing quietly -- a check that silently stops covering anything is
# worse than no check.
run: npm run check:upstream
shell:
name: rust shell (${{ matrix.os }})
# The Rust crate had 14 tests and no job running them -- the same failure
# this workflow's header says it was created to prevent, one language over.
#
# Both platforms, because the crate is cfg-heavy: the back-gesture fallback
# is #[cfg(target_os = "android")] / ios / not(mobile), and a cfg mistake
# compiles perfectly on whichever host you happened to try.
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-22.04, macos-15]
defaults:
run:
working-directory: src/praisonai-mobile/src-tauri
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22.18'
cache: npm
cache-dependency-path: src/praisonai-mobile/package-lock.json
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@v2
with:
workspaces: src/praisonai-mobile/src-tauri
- name: Install the Linux build dependencies
if: runner.os == 'Linux'
working-directory: .
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libwebkit2gtk-4.1-dev librsvg2-dev libxdo-dev libssl-dev patchelf
# generate_context! expands frontendDist (../dist) at COMPILE time, and
# dist/ is gitignored -- absent on a fresh checkout. Without this the
# crate cannot compile and neither test nor clippy below ever runs.
# `praisonai` is a `file:` link to ../praisonai-ts (see praisonai-mobile's
# package.json for why), so its dist/ must exist before this package can
# resolve `praisonai/mobile`. `npm install`, not `npm ci`: praisonai-ts
# gitignores its lockfile. `--legacy-peer-deps` for the
# bedrock-agentcore -> @strands-agents/sdk peer conflict, exactly as
# praisonai-ts-webview.yml does.
- name: "Build praisonai-ts (the file: dependency)"
working-directory: src/praisonai-ts
run: |
npm install --no-audit --no-fund --legacy-peer-deps
npm run build
- name: Build the webview (creates ../dist for generate_context!)
working-directory: src/praisonai-mobile
run: |
npm ci
npm run build
# The pure-logic tests: back-gesture arbitration, the lifecycle mapping,
# and the event-name contract. None of them needs a phone.
- name: Test
run: cargo test
- name: Clippy
run: cargo clippy --all-targets -- -D warnings
# ------------------------------------------------------------------
# The mobile targets, which nothing compiled.
#
# `src/lib.rs` is `#[cfg_attr(mobile, tauri::mobile_entry_point)]`, and
# `tauri_build::build()` emits `cargo:rustc-cfg=mobile` only for android and
# ios targets. Both legs of `rust shell` above are HOST targets, so that
# attribute -- the one that generates the Android JNI symbol and the iOS
# start_app symbol -- expanded exactly zero times in CI, and neither did the
# `#[cfg(target_os = "android")]` / `ios` arms of `commands.rs`. The job
# above says it covers cfg mistakes; it covers the ones in `shell/`, not
# these.
#
# `cargo check`, not `build`: it type-checks and macro-expands under the
# mobile cfg, which is the thing that was never done, and needs no NDK and no
# Xcode project. Verified at 18s for the android target.
# ------------------------------------------------------------------
cross-check:
name: cross-check (${{ matrix.target }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- target: aarch64-linux-android
os: ubuntu-22.04
- target: aarch64-apple-ios
os: macos-15
defaults:
run:
working-directory: src/praisonai-mobile/src-tauri
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22.18'
cache: npm
cache-dependency-path: src/praisonai-mobile/package-lock.json
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
with:
workspaces: src/praisonai-mobile/src-tauri
key: ${{ matrix.target }}
# generate_context! expands frontendDist at COMPILE time and dist/ is
# gitignored, so the crate cannot be checked without this -- the same
# reason the job above needs it.
# `praisonai` is a `file:` link to ../praisonai-ts (see praisonai-mobile's
# package.json for why), so its dist/ must exist before this package can
# resolve `praisonai/mobile`. `npm install`, not `npm ci`: praisonai-ts
# gitignores its lockfile. `--legacy-peer-deps` for the
# bedrock-agentcore -> @strands-agents/sdk peer conflict, exactly as
# praisonai-ts-webview.yml does.
- name: "Build praisonai-ts (the file: dependency)"
working-directory: src/praisonai-ts
run: |
npm install --no-audit --no-fund --legacy-peer-deps
npm run build
- name: Build the webview (creates ../dist for generate_context!)
working-directory: src/praisonai-mobile
run: |
npm ci
npm run build
- name: Does the crate still expand and type-check for a phone?
run: cargo check --lib --target ${{ matrix.target }}
-106
View File
@@ -1,106 +0,0 @@
name: Parity Gate
# Validates Python <-> TypeScript SDK parity on pull requests and FAILS on drift.
#
# Two named checks, so each kind of drift is visible in the checks list:
# names - the exported-name tracker (PARITY.md / FEATURE_PARITY_TRACKER.json)
# must be regenerated in the PR when the public surface changes.
# behaviour - options accepted for parity but not yet acted on; a ratchet,
# so the count may fall and never rise.
# signatures - constructor/method parameters of the curated surfaces in
# src/praisonai/praisonai/_dev/parity/signatures/surface.yaml must
# match (after camelCase/alias/flattening rules) or be waived with a
# reason in waivers.yaml. Stale or expired waivers also fail.
#
# The regenerate-and-commit job stays in update-parity-tracker.yml (push to main).
# This workflow never commits; it only reports.
on:
pull_request:
paths:
- 'src/praisonai-agents/praisonaiagents/**/*.py'
- 'src/praisonai-ts/src/**/*.ts'
- 'src/praisonai-rust/praisonai/src/**/*.rs'
- 'src/praisonai/praisonai/_dev/parity/**'
- 'src/praisonai/praisonai/cli/features/**'
# The generator the `names` job runs. Editing it changed what the gate
# measured without the gate running.
- 'src/praisonai/scripts/**'
- 'src/praisonai-ts/PARITY.md'
- 'src/praisonai-ts/FEATURE_PARITY_TRACKER.json'
- 'src/praisonai-ts/SIGNATURE_PARITY.md'
- 'src/praisonai-ts/signature-parity.json'
# The behaviour baseline itself. These two carry the ratchet floor, so a
# pull request that edited only them raised the floor with no job running.
- 'src/praisonai-ts/BEHAVIOUR_PARITY.md'
- 'src/praisonai-ts/behaviour-parity.json'
- 'src/praisonai-rust/PARITY.md'
- 'src/praisonai-rust/FEATURE_PARITY_TRACKER.json'
- '.github/workflows/parity-gate.yml'
workflow_dispatch:
permissions:
contents: read
jobs:
names:
name: names (exported-symbol tracker up to date)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- run: pip install pyyaml
- name: Tracker files match the sources
run: |
if ! python3 src/praisonai/scripts/generate_parity_tracker.py --check; then
echo "::error::Parity tracker is out of date. Run: python3 src/praisonai/scripts/generate_parity_tracker.py and commit src/praisonai-ts/PARITY.md, FEATURE_PARITY_TRACKER.json and the Rust pair."
exit 1
fi
signatures:
name: signatures (constructor and method parameters)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- uses: actions/setup-node@v4
with:
node-version: '22'
- run: pip install pyyaml
- name: Install the TypeScript compiler only
# praisonai-ts's full dependency tree is not needed (and does not
# install cleanly); the extractor needs only the `typescript` package.
run: |
mkdir -p "$RUNNER_TEMP/parity-ts"
npm install --prefix "$RUNNER_TEMP/parity-ts" --no-audit --no-fund [email protected]
echo "PARITY_TS_NODE_MODULES=$RUNNER_TEMP/parity-ts/node_modules" >> "$GITHUB_ENV"
- name: Signatures match, or are waived with a reason
env:
PYTHONPATH: src/praisonai
run: |
if ! python3 -m praisonai._dev.parity.signatures --check; then
echo "::error::Signature parity failed. See the output above for the surface and parameter. Fix the TypeScript side, or add a waiver with a reason to src/praisonai/praisonai/_dev/parity/signatures/waivers.yaml, then run: PYTHONPATH=src/praisonai python3 -m praisonai._dev.parity.signatures --write"
exit 1
fi
behaviour:
name: behaviour (options accepted but not acted on)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- run: pip install pyyaml
- name: The count of ignored options has not grown
env:
PYTHONPATH: src/praisonai
run: |
if ! python3 -m praisonai._dev.parity.behaviour --check; then
echo "::error::An option accepted for Python parity is not acted on, and the total grew. The name and signature gates cannot see this: they check that a parameter EXISTS. Implement the behaviour and delete its entry from src/praisonai-ts/src/utils/parity-notice.ts, then run: PYTHONPATH=src/praisonai python3 -m praisonai._dev.parity.behaviour --write"
exit 1
fi
+1 -1
View File
@@ -1,6 +1,6 @@
name: Pipeline Status Sync
# Sync pipeline/* stage and blocker labels on open PRs (fork heads included; merge gate only for upstream branches).
# Sync pipeline/* stage and blocker labels on open internal PRs.
on:
schedule:
+2 -26
View File
@@ -16,6 +16,8 @@ jobs:
review:
runs-on: ubuntu-latest
if: >
secrets.PRAISONAI_APP_ID != '' &&
secrets.PRAISONAI_APP_PRIVATE_KEY != '' &&
(
(github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
github.event_name == 'workflow_dispatch' ||
@@ -30,28 +32,7 @@ jobs:
)
steps:
- name: Check reviewer configuration
id: config
env:
HAS_APP_CREDENTIALS: ${{ secrets.PRAISONAI_APP_ID != '' && secrets.PRAISONAI_APP_PRIVATE_KEY != '' }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
echo "configured=$HAS_APP_CREDENTIALS" >> "$GITHUB_OUTPUT"
openai_ready=true
if [ -z "$OPENAI_API_KEY" ] || [ "$OPENAI_API_KEY" = "not-needed" ]; then
openai_ready=false
if [ "$HAS_APP_CREDENTIALS" = "true" ]; then
echo "::notice::Skipping PraisonAI PR review: OPENAI_API_KEY is missing or a placeholder."
fi
fi
if [ "$HAS_APP_CREDENTIALS" = "true" ] && [ "$openai_ready" = "true" ]; then
echo "ready=true" >> "$GITHUB_OUTPUT"
else
echo "ready=false" >> "$GITHUB_OUTPUT"
fi
- name: Determine checkout ref
if: steps.config.outputs.ready == 'true'
id: dest
env:
ISSUE_NUMBER: ${{ github.event.issue.number }}
@@ -67,7 +48,6 @@ jobs:
fi
- name: Checkout Repository
if: steps.config.outputs.ready == 'true'
uses: actions/checkout@v4
with:
persist-credentials: false
@@ -75,7 +55,6 @@ jobs:
fetch-depth: 0
- name: Generate GitHub App Token
if: steps.config.outputs.ready == 'true'
id: generate_token
uses: tibdex/github-app-token@v2
with:
@@ -83,17 +62,14 @@ jobs:
private_key: ${{ secrets.PRAISONAI_APP_PRIVATE_KEY }}
- name: Set up Python
if: steps.config.outputs.ready == 'true'
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install PraisonAI
if: steps.config.outputs.ready == 'true'
run: pip install "praisonai[all]"
- name: Run PraisonAI PR Review
if: steps.config.outputs.ready == 'true'
env:
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
-70
View File
@@ -1,70 +0,0 @@
# Does praisonai-ts still pass its own tests?
#
# It has a suite -- 2,000+ tests -- and until now nothing ran it. The only
# workflow with `working-directory: src/praisonai-ts` builds the package and
# checks the webview bundle; the two other mentions of `npm test` in this
# directory are a comment and a line of prose telling a reviewer to run it by
# hand. So the suite was green the way an unread report is green.
#
# That mattered the moment the suite grew: PR #4755 added ~880 tests for the
# Python-parity port and not one of them would have run here. A test nobody
# executes is worse than no test, because it is counted as cover.
#
# Deliberately narrow, like its webview sibling: it runs only when
# praisonai-ts changes, installs only what it needs, and asserts two things --
# the package typechecks, and its tests pass.
name: praisonai-ts tests
on:
push:
branches: [main]
paths:
- 'src/praisonai-ts/src/**'
- 'src/praisonai-ts/tests/**'
- 'src/praisonai-ts/package.json'
- 'src/praisonai-ts/jest.config.ts'
- 'src/praisonai-ts/tsconfig*.json'
- '.github/workflows/praisonai-ts-tests.yml'
pull_request:
paths:
- 'src/praisonai-ts/src/**'
- 'src/praisonai-ts/tests/**'
- 'src/praisonai-ts/package.json'
- 'src/praisonai-ts/jest.config.ts'
- 'src/praisonai-ts/tsconfig*.json'
- '.github/workflows/praisonai-ts-tests.yml'
concurrency:
group: praisonai-ts-tests-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
tests:
name: unit tests
runs-on: ubuntu-22.04
defaults:
run:
working-directory: src/praisonai-ts
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22.18'
# `npm install --legacy-peer-deps`, matching praisonai-ts-webview.yml:
# the lockfile is gitignored so `npm ci` has nothing to read, and the
# dependency tree does not resolve without the flag (bedrock-agentcore
# wants a peer of @strands-agents/sdk that is not installed).
- name: Install
run: npm install --no-audit --no-fund --legacy-peer-deps
- name: Typecheck
run: npx tsc --noEmit -p .
# PRAISONAI_PARITY_SILENT quiets the one-line notices that options
# accepted for Python parity print when they are not yet honoured; they
# are deliberate output, not failures, and they drown the test log.
- name: Tests
run: npm test
env:
PRAISONAI_PARITY_SILENT: '1'
@@ -1,62 +0,0 @@
# Can praisonai-ts still be loaded in a webview?
#
# Issue #4437 was this failing: `crypto` and `events` were static imports on
# the Agent graph, so a webview bundle died at IMPORT time with no error
# boundary and a blank screen. PR #4438 fixed it, and nothing stopped it
# coming back.
#
# `npm test` cannot catch a regression here, structurally: it runs in Node,
# where every one of these imports works perfectly. One `import { x } from
# "fs"` added to a file the Agent happens to reach would silently un-ship the
# mobile app, and the first signal would be a blank screen on a device.
#
# Deliberately narrow. It runs only when praisonai-ts changes, installs only
# what it needs, and asserts one thing.
name: praisonai-ts webview
on:
push:
branches: [main]
paths:
- 'src/praisonai-ts/src/**'
- 'src/praisonai-ts/package.json'
- 'src/praisonai-ts/scripts/webview-gate.mjs'
- '.github/workflows/praisonai-ts-webview.yml'
pull_request:
paths:
- 'src/praisonai-ts/src/**'
- 'src/praisonai-ts/package.json'
- 'src/praisonai-ts/scripts/webview-gate.mjs'
- '.github/workflows/praisonai-ts-webview.yml'
concurrency:
group: praisonai-ts-webview-${{ github.ref }}
cancel-in-progress: true
jobs:
webview:
name: loadable in a webview
runs-on: ubuntu-22.04
defaults:
run:
working-directory: src/praisonai-ts
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22.18'
# `npm install`, not `npm ci`: praisonai-ts gitignores its lockfile
# (src/praisonai-ts/.gitignore), so `npm ci` has nothing to install from.
# `--legacy-peer-deps` because the dependency tree does not otherwise
# resolve -- bedrock-agentcore wants a peer of @strands-agents/sdk that
# is not installed. That is pre-existing and out of scope here; without
# the flag this job fails on something unrelated to what it checks.
- name: Install
run: npm install --no-audit --no-fund --legacy-peer-deps
# Build first: the gate must see what actually SHIPS. The esm-shim adds
# a CJS banner that does not exist in the TypeScript sources, and that
# banner is what broke the published entry while the sources passed.
- name: Build
run: npm run build
- name: Check
run: npm run check:webview
+1 -9
View File
@@ -23,9 +23,7 @@ jobs:
CLAUDE_APP_ID: ${{ secrets.CLAUDE_APP_ID }}
CLAUDE_APP_PRIVATE_KEY: ${{ secrets.CLAUDE_APP_PRIVATE_KEY }}
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
CLAUDE_CODE_OAUTH_TOKEN_B: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN_B }}
GH_TOKEN_VALUE: ${{ secrets.GH_TOKEN }}
DOCS_GITHUB_TOKEN: ${{ secrets.SECRETS_ADMIN_PAT }}
run: |
set -euo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
@@ -35,11 +33,5 @@ jobs:
echo -n "$CLAUDE_APP_ID" | gh secret set CLAUDE_APP_ID --repo "$TARGET"
echo -n "$CLAUDE_APP_PRIVATE_KEY" | gh secret set CLAUDE_APP_PRIVATE_KEY --repo "$TARGET"
echo -n "$CLAUDE_CODE_OAUTH_TOKEN" | gh secret set CLAUDE_CODE_OAUTH_TOKEN --repo "$TARGET"
if [ -n "${CLAUDE_CODE_OAUTH_TOKEN_B:-}" ]; then
echo -n "$CLAUDE_CODE_OAUTH_TOKEN_B" | gh secret set CLAUDE_CODE_OAUTH_TOKEN_B --repo "$TARGET"
fi
echo -n "$GH_TOKEN_VALUE" | gh secret set GH_TOKEN --repo "$TARGET"
if [ -n "${DOCS_GITHUB_TOKEN:-}" ]; then
echo -n "$DOCS_GITHUB_TOKEN" | gh secret set DOCS_GITHUB_TOKEN --repo "$TARGET"
fi
echo "Synced CLAUDE_APP_ID, CLAUDE_APP_PRIVATE_KEY, CLAUDE_CODE_OAUTH_TOKEN, CLAUDE_CODE_OAUTH_TOKEN_B (if set), GH_TOKEN → $TARGET"
echo "Synced CLAUDE_APP_ID, CLAUDE_APP_PRIVATE_KEY, CLAUDE_CODE_OAUTH_TOKEN, GH_TOKEN → $TARGET"
-106
View File
@@ -1,106 +0,0 @@
name: Sync to praisonai-js
# Canonical TS source: src/praisonai-ts/ in this monorepo.
# This workflow pushes the mirror to MervinPraison/praisonai-js (npm repo).
on:
workflow_dispatch:
inputs:
target_branch:
description: 'Target branch on MervinPraison/praisonai-js'
required: false
default: main
type: string
mode:
description: 'pr = open pull request (default); commit = push directly to target branch'
required: false
default: pr
type: choice
options:
- pr
- commit
permissions:
contents: read
jobs:
sync:
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
TARGET_REPO: MervinPraison/praisonai-js
SOURCE_PATH: src/praisonai-ts
TARGET_BRANCH: ${{ inputs.target_branch }}
steps:
- name: Validate token
run: |
if [ -z "${GH_TOKEN:-}" ]; then
echo "GH_TOKEN secret is required (PAT with repo write access to praisonai-js)."
exit 1
fi
- name: Checkout PraisonAI monorepo
uses: actions/checkout@v4
- name: Checkout praisonai-js
uses: actions/checkout@v4
with:
repository: ${{ env.TARGET_REPO }}
token: ${{ secrets.GH_TOKEN }}
path: praisonai-js-target
ref: ${{ env.TARGET_BRANCH }}
- name: Sync monorepo path into praisonai-js
run: |
set -euo pipefail
rsync -a --delete \
--exclude '.git/' \
--exclude 'node_modules/' \
--exclude 'dist/' \
--exclude 'coverage/' \
--exclude '.DS_Store' \
--exclude '*.tsbuildinfo' \
"${SOURCE_PATH}/" praisonai-js-target/
- name: Push changes
working-directory: praisonai-js-target
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
# Preserve praisonai-js automation (rsync source has no .github/)
if git diff --quiet; then
echo "No changes — praisonai-js already matches ${SOURCE_PATH} on monorepo main."
exit 0
fi
git add -A
msg="sync: update from monorepo ${SOURCE_PATH}"
if [ "${{ inputs.mode }}" = "commit" ]; then
git commit -m "$msg"
git push origin "HEAD:${TARGET_BRANCH}"
echo "Committed and pushed directly to ${TARGET_BRANCH} on praisonai-js."
exit 0
fi
branch="sync/monorepo-praisonai-ts-$(date -u +%Y%m%d-%H%M%S)"
git checkout -b "$branch"
git commit -m "$msg"
git push origin "$branch"
gh pr create \
--repo "$TARGET_REPO" \
--base "$TARGET_BRANCH" \
--head "$branch" \
--title "$msg" \
--body "$(cat <<EOF
Automated sync from [\`MervinPraison/PraisonAI\`](https://github.com/MervinPraison/PraisonAI) \`${SOURCE_PATH}/\` into the npm mirror repo.
Canonical development is in the monorepo; this PR updates the public \`praisonai-js\` checkout.
Excludes from rsync source: \`node_modules/\`, \`dist/\`, \`coverage/\`. \`.github/\` on praisonai-js is unchanged.
EOF
)"
echo "Opened PR on ${TARGET_REPO} (${branch} → ${TARGET_BRANCH})."
+2 -5
View File
@@ -21,10 +21,7 @@ jobs:
- uses: actions/setup-python@v5
with:
# harbor requires >=3.12; on 3.11 `pip install harbor` resolved to no
# candidate version at all and every scheduled run died in ~15s, so
# this gate has never produced a Terminal-Bench score.
python-version: "3.12"
python-version: "3.11"
- name: Install Harbor + PraisonAI
run: |
@@ -37,7 +34,7 @@ jobs:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
PYTHONPATH: .
run: |
harbor run -c examples/terminal_bench/job_code_smoke.yaml
harbor run -c examples/terminal_bench/job_code_smoke.yaml || true
- name: Upload Harbor results
if: always()
+1 -68
View File
@@ -31,13 +31,6 @@ env:
PRAISONAI_ALLOW_NETWORK: '0'
PRAISONAI_LIVE_TESTS: '0'
PRAISONAI_TEST_TIER: main
# litellm reads this at import and, without it, downloads its model-price
# table from raw.githubusercontent.com before a single test runs -- which
# made the suite depend on that host being reachable. It has to be set in
# the environment rather than a conftest, because pytest imports plugins
# (and litellm with them) before any conftest is loaded.
LITELLM_LOCAL_MODEL_COST_MAP: 'True'
HF_HUB_OFFLINE: '1'
jobs:
test-core-collect:
@@ -98,78 +91,18 @@ jobs:
extra_ignore: --ignore=tests/unit/cli/test_message_queue.py
- shard: bots-gateway
workdir: src/praisonai-bot
# tests/unit/test_bots_cli.py sat at the top level and was collected
# by no workflow at all, which is why a TypeError that made
# `praisonai bot slack` unstartable shipped unnoticed. Named
# explicitly rather than adding the whole top-level directory, which
# carries suites with pre-existing env-dependent failures.
# tests/unit/kanban/ was likewise collected by no workflow, which
# is how six declared KANBAN_TASK_* hook events shipped with no
# emitter at all.
paths: >-
tests/unit/bots/
tests/unit/gateway/
tests/unit/cli/
tests/unit/daemon/
tests/unit/kanban/
tests/unit/test_bots_cli.py
extra_ignore: ""
pythonpath: >-
${{ github.workspace }}/src/praisonai-agents:${{ github.workspace }}/src/praisonai-code
- shard: agents-core
workdir: src/praisonai-agents
# src/praisonai-agents/tests holds 727 files and 11,437 test
# functions, and until now NO workflow referenced the directory at
# all -- so the LLM layer, the provider adapters, the local-model
# package and the streaming paths were guarded by tests that never
# ran. Nearly every defect found in the recent local-model audits
# had a working test sitting unused beside it.
#
# The ten directories originally left out of this shard (knowledge,
# runtime, session, tools, bus, plugins, skills, agents, rag,
# streaming -- about 52 failures) have since been repaired, so they
# are all listed below: 1,567 tests in the seven added here alone.
# Leaving them out would have meant the repairs were never exercised.
#
# The whole tree in one invocation was hanging. The cause was NOT
# the pytest-rerunfailures run_server thread guessed at here
# earlier: it is test_botos_integration.py, where a supervised
# Bot.start() deliberately never returns and the mock adapter
# reports is_running=True forever (fixed on this branch). Naming
# directories explicitly is still the safer shape for a shard.
paths: >-
tests/unit/agent/ tests/unit/app/ tests/unit/approval/
tests/unit/auth/ tests/unit/background/ tests/unit/checkpoints/
tests/unit/cli/ tests/unit/compaction/ tests/unit/conditions/
tests/unit/config/ tests/unit/context/ tests/unit/embedding/
tests/unit/eval/ tests/unit/gateway/ tests/unit/goal/
tests/unit/hooks/ tests/unit/llm/ tests/unit/lsp/ tests/unit/mcp/
tests/unit/memory/ tests/unit/permissions/ tests/unit/policy/
tests/unit/process/ tests/unit/runs/ tests/unit/sandbox/
tests/unit/security/ tests/unit/server/ tests/unit/snapshot/
tests/unit/storage/ tests/unit/task/ tests/unit/telemetry/
tests/unit/trace/ tests/unit/utils/ tests/unit/workflows/
tests/unit/workspace/ tests/unit/knowledge/ tests/unit/session/
tests/unit/plugins/ tests/unit/runtime/ tests/unit/tools/
tests/unit/bus/ tests/unit/skills/ tests/unit/agents/
tests/unit/rag/ tests/unit/streaming/
tests/unit/test_adapter_registry.py
# Both tests previously deselected here now pass in combination.
# test_lazy_litellm_import was looking the loader module up by
# name and could get a different object than the one get_litellm()
# mutates; it reads the function's own __globals__ now.
# test_skills_discovery_uses_paths_module read the real home
# directory and asserted every discovered dir was named "skills",
# which is false once a remote skill has ever been cached; it runs
# against a temp HOME and asserts the paths module is used.
extra_ignore: ""
pythonpath: >-
${{ github.workspace }}/src/praisonai-agents
- shard: train
workdir: src/praisonai-train
# tests/unit/data/ (96 tests) was collected by no workflow at all.
paths: >-
tests/unit/train/ tests/unit/data/
tests/unit/train/
extra_ignore: ""
pythonpath: >-
${{ github.workspace }}/src/praisonai-agents:${{ github.workspace }}/src/praisonai-code
+2 -138
View File
@@ -42,7 +42,7 @@ jobs:
# ============================================
smoke:
runs-on: ubuntu-latest
timeout-minutes: 16
timeout-minutes: 12
steps:
- uses: actions/checkout@v4
with:
@@ -98,20 +98,6 @@ jobs:
--ignore=tests/unit/cli/test_message_queue.py \
--maxfail=5
- name: Run praisonai-agents subscription-auth smoke tests
env:
PRAISONAI_TEST_TIER: smoke
PRAISONAI_ALLOW_NETWORK: '0'
OPENAI_MODEL_NAME: gpt-4o-mini
OPENAI_API_KEY: 'test-key'
shell: bash
run: |
set -euo pipefail
cd src/praisonai-agents
python -m pytest tests/smoke/test_subscription_auth.py \
tests/unit/auth/test_agent_auth_wiring.py \
-q --tb=line --timeout=60 --maxfail=5
- name: Standalone praisonai-code import smoke
shell: bash
run: |
@@ -759,128 +745,6 @@ jobs:
if: steps.check-key.outputs.has_key == 'false'
run: echo "✅ Cohere tests skipped (no API key available)"
# ============================================
# OLLAMA LIVE TESTS - Real local model, no API key
# Nightly / extended only. Installs Ollama on the runner and pulls a small
# tools-capable model. Unlike the other live jobs this one needs no secret,
# so its required step has no "|| echo" fallback: a failure here is a real
# regression in local-model tool calling, not a missing credential.
# ============================================
extended-ollama:
if: github.event_name == 'schedule' || github.event.inputs.tier == 'extended' || github.event.inputs.tier == 'nightly'
runs-on: ubuntu-latest
timeout-minutes: 15
env:
# Bump deliberately: this is the version everything in this job was
# verified against. Latest at time of writing is v0.33.3.
# No leading "v": install.sh passes ?version=$OLLAMA_VERSION and ollama.com
# redirects to GitHub tag v$version (vv0.33.2 → 404 if set to v0.33.2).
OLLAMA_VERSION: "0.33.2"
# 522 MB, capabilities: completion, tools, thinking.
OLLAMA_TEST_MODEL: qwen3:0.6b
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Cache pip packages
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-ollama-${{ hashFiles('**/pyproject.toml') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Cache Ollama model blobs
uses: actions/cache@v4
with:
path: ~/.ollama/models
# No restore-keys on purpose: a partial hit from a different model would
# silently test something other than OLLAMA_TEST_MODEL.
key: ${{ runner.os }}-ollama-models-${{ env.OLLAMA_TEST_MODEL }}
- name: Install Ollama
shell: bash
run: |
set -euo pipefail
# The official installer, but VERSION-PINNED via OLLAMA_VERSION (which it
# honours) so this job cannot silently change what it tests. Fetching the
# release asset directly is not a workable alternative: since v0.33 the
# linux bundle ships as a 1.4 GB .tar.zst with GPU runtimes bundled in,
# and the installer already handles the zst/tgz split across versions.
sudo apt-get update -qq && sudo apt-get install -y -qq zstd
curl -fsSL https://ollama.com/install.sh | sh
ollama --version
- name: Start Ollama and pull model
shell: bash
run: |
set -euo pipefail
ollama serve > /tmp/ollama-serve.log 2>&1 &
for i in $(seq 1 30); do
if curl -sf http://127.0.0.1:11434/api/tags > /dev/null; then
echo "Ollama is up after ${i}s"
break
fi
sleep 1
done
curl -sf http://127.0.0.1:11434/api/tags > /dev/null
ollama pull "$OLLAMA_TEST_MODEL"
ollama list
- name: Install dependencies
run: |
pip install --upgrade pip
pip install pytest pytest-asyncio pytest-timeout
- uses: ./.github/actions/install-monorepo-packages
with:
agents-extras: ".[all]"
- name: Run Ollama minimum contract (required)
env:
PRAISONAI_TEST_TIER: extended
PRAISONAI_ALLOW_NETWORK: '1'
PRAISONAI_LIVE_TESTS: '1'
PRAISONAI_TEST_PROVIDERS: 'ollama'
PRAISONAI_TEST_OLLAMA: '1'
PRAISONAI_OLLAMA_TEST_MODEL: ${{ env.OLLAMA_TEST_MODEL }}
shell: bash
run: |
set -euo pipefail
cd src/praisonai
# No "|| echo" here: no secret is involved, so a failure is a real
# regression rather than a missing credential.
python -m pytest tests/integration/test_ollama_tool_calling_live.py::TestOllamaCIMinimum \
-v --tb=short \
--timeout=120
- name: Run remaining Ollama live tests (informational)
if: always()
env:
PRAISONAI_TEST_TIER: extended
PRAISONAI_ALLOW_NETWORK: '1'
PRAISONAI_LIVE_TESTS: '1'
PRAISONAI_TEST_PROVIDERS: 'ollama'
PRAISONAI_TEST_OLLAMA: '1'
PRAISONAI_OLLAMA_TEST_MODEL: ${{ env.OLLAMA_TEST_MODEL }}
run: |
cd src/praisonai
python -m pytest tests/integration/test_ollama_tool_calling_live.py \
--deselect tests/integration/test_ollama_tool_calling_live.py::TestOllamaCIMinimum \
-v --tb=short \
--timeout=180 \
|| echo "Ollama exploratory tests completed (small models are non-deterministic)"
- name: Ollama server log
if: failure()
run: tail -100 /tmp/ollama-serve.log
# ============================================
# LEGACY UNIT TESTS - Post-C6 re-enabled suite
# Nightly / extended tier only (~401 tests)
@@ -958,7 +822,7 @@ jobs:
# TEST SUMMARY
# ============================================
test-summary:
needs: [smoke, main, extended-ollama]
needs: [smoke, main]
if: always()
runs-on: ubuntu-latest
+1 -5
View File
@@ -6,11 +6,7 @@ on:
paths:
- 'src/praisonai-agents/praisonaiagents/**/*.py'
- 'src/praisonai-ts/src/**/*.ts'
# The crate lives at src/praisonai-rust/praisonai/src -- the old
# 'src/praisonai-rust/src/**' path does not exist, so a Rust change
# never triggered either tracker and the Rust numbers only ever
# refreshed incidentally, on an unrelated push.
- 'src/praisonai-rust/praisonai/src/**/*.rs'
- 'src/praisonai-rust/src/**/*.rs'
- 'src/praisonai/praisonai/_dev/parity/**/*.py'
- '.github/workflows/update-docs-parity.yml'
workflow_dispatch: # Allow manual trigger
+7 -50
View File
@@ -1,35 +1,21 @@
name: Update Feature Parity Tracker
# Regenerates the exported-name parity trackers on push to main and commits them.
# Validation on pull requests (fail on drift) lives in parity-gate.yml.
on:
push:
branches: [ "main" ]
paths:
- 'src/praisonai-agents/praisonaiagents/**/*.py'
- 'src/praisonai-ts/src/**/*.ts'
# The crate lives at src/praisonai-rust/praisonai/src -- the old
# 'src/praisonai-rust/src/**' path does not exist, so a Rust change
# never triggered either tracker and the Rust numbers only ever
# refreshed incidentally, on an unrelated push.
- 'src/praisonai-rust/praisonai/src/**/*.rs'
# The generator's own code and the wrapper features it counts. Change
# either and nothing regenerated until an unrelated push landed.
- 'src/praisonai/praisonai/_dev/parity/**/*.py'
- 'src/praisonai/praisonai/cli/features/**/*.py'
- 'src/praisonai-rust/src/**/*.rs'
- '.github/workflows/update-parity-tracker.yml'
permissions:
contents: write
jobs:
update-parity-tracker:
# Push only. On a PR the parity-gate.yml `names` job CHECKS instead --
# committing a regenerated tracker onto a contributor's branch is not this
# workflow's business, and would fail on a fork besides.
if: github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -46,45 +32,15 @@ jobs:
- name: Install dependencies
run: pip install pyyaml
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Install the TypeScript compiler only
# The signature extractor needs `typescript` and nothing else;
# praisonai-ts's own dependency tree does not install cleanly.
run: |
mkdir -p "$RUNNER_TEMP/parity-ts"
npm install --prefix "$RUNNER_TEMP/parity-ts" --no-audit --no-fund [email protected]
echo "PARITY_TS_NODE_MODULES=$RUNNER_TEMP/parity-ts/node_modules" >> "$GITHUB_ENV"
- name: Generate parity trackers
run: |
cd $GITHUB_WORKSPACE
python3 src/praisonai/scripts/generate_parity_tracker.py
- name: Regenerate the signature report
# Source locations in the report drift whenever a covered file is edited.
# The pull-request gate ignores line-number-only drift so it never reddens
# a build over it; refreshing here keeps the committed line numbers true.
env:
PYTHONPATH: src/praisonai
run: |
cd $GITHUB_WORKSPACE
python3 -m praisonai._dev.parity.signatures --write
- name: Regenerate the behaviour report
env:
PYTHONPATH: src/praisonai
run: |
cd $GITHUB_WORKSPACE
python3 -m praisonai._dev.parity.behaviour --write
- name: Check for changes
id: check_changes
run: |
if git diff --quiet src/praisonai-ts/FEATURE_PARITY_TRACKER.json src/praisonai-ts/PARITY.md src/praisonai-rust/FEATURE_PARITY_TRACKER.json src/praisonai-rust/PARITY.md src/praisonai-ts/SIGNATURE_PARITY.md src/praisonai-ts/signature-parity.json src/praisonai-ts/BEHAVIOUR_PARITY.md src/praisonai-ts/behaviour-parity.json 2>/dev/null; then
if git diff --quiet src/praisonai-ts/FEATURE_PARITY_TRACKER.json src/praisonai-ts/PARITY.md src/praisonai-rust/FEATURE_PARITY_TRACKER.json src/praisonai-rust/PARITY.md 2>/dev/null; then
echo "changes=false" >> $GITHUB_OUTPUT
echo "No changes to parity trackers"
else
@@ -98,7 +54,8 @@ jobs:
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "docs: auto-update feature parity trackers [skip ci]"
file_pattern: 'src/praisonai-ts/FEATURE_PARITY_TRACKER.json src/praisonai-ts/PARITY.md src/praisonai-rust/FEATURE_PARITY_TRACKER.json src/praisonai-rust/PARITY.md src/praisonai-ts/SIGNATURE_PARITY.md src/praisonai-ts/signature-parity.json src/praisonai-ts/BEHAVIOUR_PARITY.md src/praisonai-ts/behaviour-parity.json'
file_pattern: 'src/praisonai-ts/FEATURE_PARITY_TRACKER.json src/praisonai-ts/PARITY.md src/praisonai-rust/FEATURE_PARITY_TRACKER.json src/praisonai-rust/PARITY.md'
commit_user_name: "MervinPraison"
commit_user_email: "[email protected]"
commit_author: "MervinPraison <[email protected]>"
+1 -8
View File
@@ -26,9 +26,6 @@ output
!src/praisonai-code/praisonai_code/cli/output/
!src/praisonai-bot/praisonai_bot/cli/output/
!src/praisonai-ts/src/db/
# The bare `db` rule above also swallows the SQLite adapter's test
# directory, so the durability suite would silently never be committed.
!src/praisonai-ts/tests/unit/db/
!src/praisonai-ts/src/cli/output/
.files
@@ -118,8 +115,4 @@ Cargo.lock
src/praisonai/build*.egg-info/
src/praisonai/PraisonAI.egg-info/
jobs/*
*.egg-info
# Local Android SDK/NDK/JDK for building praisonai-mobile on a laptop without
# Android Studio (how: .github/workflows/mobile-platforms.yml). Several GB.
.android-sdk/
.worktrees/
*.egg-info
+2 -15
View File
@@ -7,22 +7,9 @@ You are working on the PraisonAI project.
- Be concise and helpful in responses
- Test implementation thoroughly
- Ensure backward compatibility with existing APIs
- Follow protocol-driven design across the nine Python PyPI packages plus the TypeScript SDK: core protocols in `praisonaiagents/`, agentic terminal CLI in `praisonai-code/`, bots/gateway in `praisonai-bot/`, LLM fine-tuning + agent training in `praisonai-train/`, browser automation in `praisonai-browser/`, MCP server host in `praisonai-mcp/`, sandbox backends in `praisonai-sandbox/`, deployment in `praisonai-deploy/`, integrations/serve/dashboard in the `praisonai/` wrapper, TypeScript/JavaScript SDK in `src/praisonai-ts/` (`npm: praisonai`)
- **TypeScript review routing:** For TS/JS issues and PRs, read [`src/praisonai-ts/AGENTS.md`](src/praisonai-ts/AGENTS.md) (not just this file). Canonical source is `src/praisonai-ts/` in this monorepo; [MervinPraison/praisonai-js](https://github.com/MervinPraison/praisonai-js) is the npm mirror only (sync: monorepo → praisonai-js via workflow **Sync to praisonai-js**). Do not implement TS fixes in praisonai-js for PraisonAI issues.
- Follow protocol-driven design across the nine packages: core protocols in `praisonaiagents/`, agentic terminal CLI in `praisonai-code/`, bots/gateway in `praisonai-bot/`, LLM fine-tuning + agent training in `praisonai-train/`, browser automation in `praisonai-browser/`, MCP server host in `praisonai-mcp/`, sandbox backends in `praisonai-sandbox/`, deployment in `praisonai-deploy/`, integrations/serve/dashboard in the `praisonai/` wrapper
- Preserve old `praisonai.*` import paths via shims when moving code between packages (see §2.3 in `src/praisonai-agents/AGENTS.md`; shim helpers in `src/praisonai/praisonai/cli/_shim.py`)
- Package boundaries and dependency rules: `ARCHITECTURE.md` §2 (Tier 2 packages must never PyPI-depend on the wrapper; cross-tier access goes through lazy `_*_bridge` modules)
- Boundary manifests: `src/praisonai/tests/PRAISONAI_BOT_MANIFEST.md` (C9), `src/praisonai/tests/PRAISONAI_TRAIN_MANIFEST.md` (C10), `src/praisonai/tests/PRAISONAI_BROWSER_MANIFEST.md` (C11), `src/praisonai/tests/PRAISONAI_MCP_MANIFEST.md` (C12), `src/praisonai/tests/PRAISONAI_SANDBOX_MANIFEST.md` (C13), `src/praisonai/tests/PRAISONAI_DEPLOY_MANIFEST.md` (C14)
- When reviewing a PR or an issue, evaluate whether the change addresses a framework concern or a user goal, and design its surface (params, naming, defaults) accordingly
- The aim of this package is to stay **lightweight and powerful**. Do a critical review at each stage — when triaging an issue, when planning a fix, and when reviewing/implementing a PR. Reject scope creep for the sake of adding features: if a capability already exists (e.g. via existing Agent params like `instructions`/`backstory`/`tools`/`hooks`/`memory`), prefer it over a new API surface. A change must genuinely strengthen the SDK (simpler, more robust, more user-friendly) — do not add knobs, params, modules, or exports that have no live consumer or that merely duplicate existing behaviour.
## Issue and PR routing
| Area | Where to implement | AGENTS.md to read | Tests |
|------|-------------------|-------------------|-------|
| Python core SDK | `src/praisonai-agents/praisonaiagents/` | `src/praisonai-agents/AGENTS.md` | `pytest` in `src/praisonai-agents/tests/` |
| Python wrapper / CLI | `src/praisonai/`, `src/praisonai-code/`, etc. | `src/praisonai-agents/AGENTS.md` | `pytest` under `src/praisonai/tests/` |
| **TypeScript / JavaScript SDK** | **`src/praisonai-ts/`** | **`src/praisonai-ts/AGENTS.md`** | `cd src/praisonai-ts && npm run build && npm test` |
| Agent-callable tools | [PraisonAI-Tools](https://github.com/MervinPraison/PraisonAI-Tools) | — | repo tests |
| Lifecycle plugins | [PraisonAI-Plugins](https://github.com/MervinPraison/PraisonAI-Plugins) | — | repo tests |
| Documentation | [PraisonAIDocs](https://github.com/MervinPraison/PraisonAIDocs) | — | `nav-check` |
| npm mirror (read-only for fixes) | [praisonai-js](https://github.com/MervinPraison/praisonai-js) | mirror of `src/praisonai-ts/AGENTS.md` | CI on praisonai-js |
- The aim of this package is to stay **lightweight and powerful**. Do a critical review at each stage — when triaging an issue, when planning a fix, and when reviewing/implementing a PR. Reject scope creep for the sake of adding features: if a capability already exists (e.g. via existing Agent params like `instructions`/`backstory`/`tools`/`hooks`/`memory`), prefer it over a new API surface. A change must genuinely strengthen the SDK (simpler, more robust, more user-friendly) — do not add knobs, params, modules, or exports that have no live consumer or that merely duplicate existing behaviour.
+12 -12
View File
@@ -222,15 +222,15 @@ The Python SDK is organised as three publishable tiers (see [§2](#2-python-thre
Core execution modules live in `praisonaiagents`:
- `src/praisonai-agents/praisonaiagents/agent/` — Agent class, handoff, autonomy
- `src/praisonai-agents/praisonaiagents/llm/` — Model runtime with provider routing, rate limiting, failover
- `src/praisonai-agents/praisonaiagents/tools/` — Tool runtime with sandbox, approval, retry
- `src/praisonai-agents/praisonaiagents/memory/` — Memory runtime (in-memory, SQLite, MongoDB, Mem0 adapters)
- `src/praisonai-agents/praisonaiagents/knowledge/` — Knowledge management (indexing, retrieval, chunking)
- `src/praisonai-agents/praisonaiagents/workflows/` — Workflow engine (YAML/SDK-based orchestration)
- `src/praisonai-agents/praisonaiagents/hooks/`, `src/praisonai-agents/praisonaiagents/bus/` — Hook system and event bus
- `src/praisonai-agents/praisonaiagents/mcp/` — MCP protocol support
- `src/praisonai-agents/praisonaiagents/ui/a2a/`, `src/praisonai-agents/praisonaiagents/ui/a2ui/` — Agent-to-agent and agent-to-UI protocols
- `agent/` — Agent class, handoff, autonomy
- `llm/` — Model runtime with provider routing, rate limiting, failover
- `tools/` — Tool runtime with sandbox, approval, retry
- `memory/` — Memory runtime (in-memory, SQLite, MongoDB, Mem0 adapters)
- `knowledge/` — Knowledge management (indexing, retrieval, chunking)
- `workflows/` — Workflow engine (YAML/SDK-based orchestration)
- `hooks/`, `bus/` — Hook system and event bus
- `mcp/` — MCP protocol support
- `a2a/`, `a2ui/` — Agent-to-agent and agent-to-UI protocols (wrapper/SDK)
Wrapper-only surfaces remain in `src/praisonai/`:
@@ -243,9 +243,9 @@ Wrapper-only surfaces remain in `src/praisonai/`:
- **Python Core SDK** — `src/praisonai-agents/` (`praisonaiagents`)
- **Python Terminal CLI** — `src/praisonai-code/` (`praisonai-code`)
- **Python Wrapper** — `src/praisonai/` (`praisonai`)
- **TypeScript SDK** — JS/TS runtime (`src/praisonai-ts/`)
- **Rust SDK** — High-performance Rust runtime (`src/praisonai-rust/`)
- **UI** — Web UI applications (`src/praisonai/praisonai/ui_chat/`, `src/praisonai/praisonai/ui_agents/`, `src/praisonai/praisonai/ui_bot/`, `src/praisonai/praisonai/ui_realtime/`); shared support code lives in `src/praisonai/praisonai/ui/`.
- **TypeScript SDK** — JS/TS runtime (`ts-sdk/`)
- **Rust SDK** — High-performance Rust runtime (`rust-sdk/`)
- **UI** — Web UI (`ui/`)
---
-12
View File
@@ -7,20 +7,8 @@ Thank you for your interest! Here's how to get started:
3. Make your changes
4. Submit a pull request
## Where to report issues
| Topic | Report here | Where fixes land |
|-------|-------------|------------------|
| Python SDK, CLI, wrapper | **This repo** | `src/praisonai-agents/`, `src/praisonai/` |
| TypeScript / JavaScript (`npm install praisonai`) | **This repo** | `src/praisonai-ts/` |
| Cross-language parity | **This repo** | Often both Python and TypeScript paths |
| Agent-callable tools | **This repo** | [PraisonAI-Tools](https://github.com/MervinPraison/PraisonAI-Tools) |
| Lifecycle plugins (tracing, hooks, guardrails) | **This repo** | [PraisonAI-Plugins](https://github.com/MervinPraison/PraisonAI-Plugins) |
## Guidelines
- Follow existing code style
- Add tests for new features
- Update documentation as needed
- TypeScript changes: implement in `src/praisonai-ts/` and run `npm test` there
- After merge, sync the npm mirror: `gh workflow run "Sync to praisonai-js" --repo MervinPraison/PraisonAI`
+17 -117
View File
@@ -12,6 +12,7 @@
<a href="https://github.com/MervinPraison/PraisonAI"><img src="https://static.pepy.tech/badge/PraisonAI" alt="Total Downloads" /></a>
<a href="https://github.com/MervinPraison/PraisonAI"><img src="https://img.shields.io/github/v/release/MervinPraison/PraisonAI" alt="Latest Stable Version" /></a>
<a href="https://github.com/MervinPraison/PraisonAI"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License" /></a>
<a href="https://registry.modelcontextprotocol.io/servers/io.github.MervinPraison/praisonai"><img src="https://img.shields.io/badge/MCP-Registry-blue" alt="MCP Registry" /></a>
</p>
<div align="center">
@@ -34,19 +35,6 @@ curl -fsSL https://praison.ai/install.sh | bash
<img src="https://img.shields.io/badge/Highlighted_by_Elon_Musk-000000?style=for-the-badge&logo=x&logoColor=white" alt="Highlighted by Elon Musk" />
</a>
<br>
<br>
<a href="https://github.com/MervinPraison/PraisonAI/releases/latest">
<img src="https://img.shields.io/badge/Download_for_macOS-000000?style=for-the-badge&logo=apple&logoColor=white" alt="Download for macOS" />
</a>
&nbsp;&nbsp;
<a href="https://github.com/MervinPraison/PraisonAI/releases/latest">
<img src="https://img.shields.io/badge/Download_for_Windows-0078D6?style=for-the-badge&logo=windows&logoColor=white" alt="Download for Windows" />
</a>
&nbsp;&nbsp;
<a href="https://github.com/MervinPraison/PraisonAI/releases/latest">
<img src="https://img.shields.io/badge/Download_for_Linux-FCC624?style=for-the-badge&logo=linux&logoColor=black" alt="Download for Linux" />
</a>
<br>
</div>
<p align="center">
@@ -142,7 +130,7 @@ Each layer wraps the one inside it. When an agent misbehaves, the layer tells yo
| **3 · Harness** | Can it act, and be checked? | `tools=`, `MCP()`, `guardrails=`, `approval=`, `hooks=`, `sandbox=` |
| **4 · Loop** | When do we stop? | `execution=ExecutionConfig(...)`, `reflection=`, `autonomy=`, doom-loop detection |
| **5 · Graph** | Who runs when, and who checks whom? | `AgentFlow`, `route()`, `parallel()`, `loop()`, `repeat()` |
| **⬡ Managed** | *Where does it actually run?* | `tools_run_on="docker"` — one shared sandbox for the tools, or `run_on="anthropic"` for the whole agent |
| **⬡ Managed** | *Where does it actually run?* | `backend=ManagedAgent(compute="e2b")` — remote sandbox, or a fully hosted loop |
### Layer 1 · Prompt — *Did I say it clearly?*
@@ -207,6 +195,7 @@ from praisonaiagents import Agent, ExecutionConfig
agent = Agent(
instructions="Fix the failing tests.",
execution=ExecutionConfig(max_iter=30, max_budget=0.50, on_budget_exceeded="stop"),
reflection=True, # completion check — the agent grades its own answer
autonomy=True, # required to drive the loop with run_autonomous()
)
result = agent.run_autonomous("Refactor the auth module", max_iterations=5)
@@ -247,85 +236,22 @@ The harness is commoditising; **where** the agent executes is the next multiplie
pip install praisonai
```
The simplest way in is `tools_run_on=` — one whole team or workflow shares **one** sandbox, so a file written by step 1 is there for step 2. Thinking stays on your machine:
```python
from praisonaiagents import Agent, AgentFlow
from praisonai import Agent, ManagedAgent, LocalManagedConfig
writer = Agent(name="Writer", instructions="You write files.")
reader = Agent(name="Reader", instructions="You read files.")
# A. Tools run in a remote sandbox; the agent loop stays local
sandboxed = ManagedAgent(
provider="local", compute="e2b", # or modal | daytona | flyio | docker | tenki
config=LocalManagedConfig(model="gpt-4o-mini", name="RemoteTools"),
)
agent = Agent(name="builder", backend=sandboxed)
flow = AgentFlow(tools_run_on="docker", steps=[writer, reader]) # or e2b | modal | daytona | flyio
flow.run("Write 'hello' to /workspace/note.txt, then read it back")
```
Same thing with no Python at all:
```yaml
name: remote-demo
tools_run_on: docker # every step shares one sandbox
agents:
writer: {role: Writer, goal: Write files}
reader: {role: Reader, goal: Read files}
steps:
- agent: writer
action: "Write 'hello' to /workspace/note.txt"
- agent: reader
action: "Read /workspace/note.txt"
```
For a single agent, two words cover it — and they answer different questions:
```python
from praisonaiagents import Agent
# A. Only the TOOLS move. Thinking stays on your machine.
agent = Agent(name="builder", instructions="You build things.",
tools_run_on="docker") # docker | e2b | modal | daytona | flyio
# tenki | sandlock | ssh | novita | subprocess
# B. The WHOLE agent moves — model calls, loop and tools
agent = Agent(name="teacher", instructions="You teach.", run_on="anthropic") # hosted
agent = Agent(name="builder", instructions="You build.", run_on="docker") # self-hosted
# B. The entire agent loop runs in the cloud (needs ANTHROPIC_API_KEY;
# with no key set, ManagedAgent() falls back to a local loop)
agent = Agent(name="teacher", backend=ManagedAgent())
agent.start("Write a Python script that prints the first 10 primes, then run it")
```
Ask any object where it runs, and it will tell you:
```python
>>> Agent(name="builder", instructions="x", tools_run_on="docker")
Agent(name='builder', thinks_on='this machine', tools_run_on='a Docker container')
>>> agent.where_does_it_run()
Thinking (the AI model calls) happens on this machine.
Tools run on a Docker container.
Your own tools (check_db) still run on this machine -- only shell, file and
code tools move. They read and write this machine's files.
```
Naming a place that cannot do the job is a typo, not a preference, so it says so:
```python
>>> Agent(name="x", instructions="i", run_on="e2b")
TypeError: Agent(run_on='e2b') is not valid: run_on= places the whole agent
-- model calls, loop and tools -- on a managed runtime, and 'e2b' runs
commands but cannot host an agent loop.
To run only the tools there: Agent(tools_run_on='e2b')
```
To run one block of code somewhere else, name the place on that call:
```python
agent.execute_code_sync("print(6 * 7)", run_in="sandlock") # kernel-enforced
```
See what is running and reclaim strays:
```bash
praisonai managed ps # list running sandboxes
praisonai managed stop --all # reclaim them
```
Sandboxes shut themselves down when idle (`auto_shutdown`, `idle_timeout_s`), and a post-setup snapshot is reused so the next run skips the image pull and dependency install. Commit a `.praisonai/environment.yaml` and the environment travels with the repo.
> 📖 [20 runnable examples](examples/python/managed-agents/) · manage sessions with `praisonai managed sessions list <agent-id>` or `praisonai managed sessions resume <session-id> "<prompt>"`
@@ -1111,39 +1037,13 @@ For other providers, see [Models docs](https://docs.praison.ai/docs/models).
<summary><strong>How do I use a local model (Ollama)?</strong></summary>
```bash
# Start Ollama and pull a model
# Start Ollama server first
ollama serve
ollama pull llama3.2
# Set environment variable
export OPENAI_BASE_URL=http://localhost:11434/v1
```
```python
from praisonaiagents import Agent
agent = Agent(instructions="You are a helpful assistant", llm="ollama/llama3.2")
agent.start("Why is the sky blue?")
```
The `ollama/` prefix is what selects Ollama's handling — tool-call repair,
tool-result formatting and the streaming rules small local models need. Or set
it by environment instead:
```bash
export OPENAI_MODEL_NAME=ollama/llama3.2
```
Setting only `OPENAI_BASE_URL` is not enough: with no model named, the OpenAI
default (`gpt-4o-mini`) is sent to Ollama, which answers
`404 model 'gpt-4o-mini' not found`. Always name the model.
Point at a non-default host with `base_url=` or `OLLAMA_HOST`:
```python
agent = Agent(instructions="...", llm="ollama/llama3.2", base_url="http://192.168.1.10:11434")
```
The same shape works for other local runtimes — `lm_studio/`, `vllm/` and
`hosted_vllm/` with their server's `base_url`.
See [Models docs](https://docs.praison.ai/docs/models) for more details.
</details>
+1 -8
View File
@@ -8,18 +8,11 @@ PraisonAI takes security seriously. We appreciate responsible disclosure from re
|---------|----------|-------------|
| `praisonaiagents` | [PyPI](https://pypi.org/project/praisonaiagents/) | `src/praisonai-agents/` |
| `praisonai` | [PyPI](https://pypi.org/project/praisonai/) | `src/praisonai/` |
| `praisonai-platform` | [PyPI](https://pypi.org/project/praisonai-platform/) | `src/praisonai-platform/` |
| `praisonai` | [npm](https://www.npmjs.com/package/praisonai) | `src/praisonai-ts/` |
Report issues against the **specific package** where the vulnerability exists. A platform-only issue does not necessarily affect the core SDK, and vice versa.
### Packages maintained in their own repositories
`praisonai-platform` now lives at
[MervinPraison/PraisonAI-Platform](https://github.com/MervinPraison/PraisonAI-Platform).
Report platform vulnerabilities through
[that repository's advisories](https://github.com/MervinPraison/PraisonAI-Platform/security/advisories/new),
not here — it is versioned and released independently of the SDK.
## In scope
- Authentication, authorisation, and multi-tenant isolation (e.g. workspace boundaries)
+94 -101
View File
@@ -7,7 +7,7 @@ Regenerate with: `praisonai docs api-md --write`
Types:
```python
from praisonaiagents import ContextPolicy, GuardrailResult, HandoffConfig, HandoffCycleError, HandoffDepthError, HandoffError, HandoffInputData, HandoffResult, HandoffTimeoutError, ReflectionOutput, StepResult, ToolResult, ToolValidationError, WorkflowContext
from praisonaiagents import ContextPolicy, GuardrailResult, HandoffConfig, HandoffCycleError, HandoffDepthError, HandoffError, HandoffInputData, HandoffResult, HandoffTimeoutError, ReflectionOutput, StepResult, TaskOutput, ToolResult, ToolValidationError, WorkflowContext
```
Methods:
@@ -16,6 +16,8 @@ Methods:
* <code title="class HandoffConfig">HandoffConfig.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">from_dict</a>(data: Dict[str, Any]) -> 'HandoffConfig'</code>
* <code title="class HandoffConfig">HandoffConfig.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class HandoffResult">HandoffResult.<a href="./src/praisonai-agents/praisonaiagents/agent/handoff.py">from_outcome</a>(outcome: AgentRunOutcome, target_agent: Optional[str] = None, source_agent: Optional[str] = None, handoff_depth: int = 0) -> 'HandoffResult'</code>
* <code title="class TaskOutput">TaskOutput.<a href="./src/praisonai-agents/praisonaiagents/main.py">json</a>() -> Optional[str]</code>
* <code title="class TaskOutput">TaskOutput.<a href="./src/praisonai-agents/praisonaiagents/main.py">to_dict</a>() -> dict</code>
* <code title="class ToolResult">ToolResult.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">is_multimodal</a>() -> bool</code>
* <code title="class ToolResult">ToolResult.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">to_dict</a>() -> Dict[str, Any]</code>
@@ -30,7 +32,6 @@ Methods:
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">aclose</a>() -> None</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">add_mcp_server</a>(name: str, mcp: Any) -> Any</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">add_verification</a>(hook: Any) -> None</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">agent_id</a>() -> str</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">analyze_prompt</a>(prompt: str) -> set</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">auto_memory</a>() -> Optional[bool]</code>
@@ -53,11 +54,9 @@ Methods:
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">get_recommended_stage</a>(prompt: str) -> str</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">get_rules_context</a>(file_path: Optional[str] = None, include_manual: Optional[List[str]] = None) -> str</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">get_skills_prompt</a>() -> str</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">guardrail_retry_count</a>() -> int</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">handoff_to</a>(target_agent: 'Agent', prompt: str, context: Optional[Dict[str, Any]] = None, config: Optional['HandoffConfig'] = None) -> 'HandoffResult'</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">handoff_to_async</a>(target_agent: 'Agent', prompt: str, context: Optional[Dict[str, Any]] = None, config: Optional['HandoffConfig'] = None) -> 'HandoffResult'</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">is_closed</a>() -> bool</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">last_guardrail_error</a>()</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">last_stop_reason</a>() -> str</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">list_mcp_servers</a>() -> List[str]</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">llm_instance</a>()</code>
@@ -70,12 +69,9 @@ Methods:
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">query</a>(question: str, **kwargs) -> 'RAGResult'</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">rag</a>() -> Optional[Any]</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">rag_query</a>(question: str, **kwargs) -> 'RAGResult'</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">reasoning_effort</a>() -> Optional[str]</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">reasoning_effort</a>(value: Optional[str]) -> None</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">redo</a>() -> bool</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">refresh_tools</a>() -> List[Any]</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">remove_mcp_server</a>(name: str) -> bool</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">remove_verification</a>(name: str) -> bool</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">retrieval_config</a>() -> Optional[Any]</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">retrieve</a>(query: str, **kwargs) -> 'ContextPack'</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">rules_manager</a>() -> Optional[Any]</code>
@@ -92,7 +88,6 @@ Methods:
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">thinking_budget</a>(value: Optional[int]) -> None</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">total_cost</a>() -> float</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">undo</a>() -> bool</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">where_does_it_run</a>() -> str</code>
* <code title="class AutoAgents">AutoAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/autoagents.py">astart</a>()</code>
* <code title="class AutoAgents">AutoAgents.<a href="./src/praisonai-agents/praisonaiagents/agents/autoagents.py">start</a>()</code>
* <code title="class AutoRagAgent">AutoRagAgent.<a href="./src/praisonai-agents/praisonaiagents/agents/auto_rag_agent.py">achat</a>(message: str, **kwargs) -> str</code>
@@ -195,7 +190,6 @@ Methods:
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">discover_single_file_plugins</a>() -> int</code>
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">get</a>(name: str) -> Optional[Union[BaseTool, Callable]]</code>
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">get_all</a>() -> Dict[str, Union[BaseTool, Callable]]</code>
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">get_tool_definition</a>(name: str) -> Optional[Dict[str, Any]]</code>
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">get_tool_definitions</a>(permission_resolver: Optional[Callable[[str], bool]] = None) -> List[Dict[str, Any]]</code>
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">get_trust_level</a>(name: str) -> Optional[str]</code>
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">list_available_tools</a>(context: Optional[Dict[str, Any]] = None, ttl_seconds: Optional[float] = None) -> List[Union[BaseTool, Callable]]</code>
@@ -245,12 +239,10 @@ from praisonaiagents import Memory, MemoryBackend, MemoryConfig
Methods:
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">acommit_memory_batch</a>(writes: List[tuple]) -> None</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">build_cache_optimized_context</a>(task_descr: str, user_id: Optional[str] = None, additional: str = '', max_items: int = 3, include_cache_boundary: bool = True, include_in_output: Optional[bool] = None) -> Dict[str, str]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">build_context_for_task</a>(task_descr: str, user_id: Optional[str] = None, additional: str = '', max_items: int = 3, include_in_output: Optional[bool] = None) -> str</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">calculate_quality_metrics</a>(output: str, expected_output: str, llm: Optional[str] = None, custom_prompt: Optional[str] = None) -> Dict[str, float]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">close_connections</a>()</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">commit_memory_batch</a>(writes: List[tuple]) -> None</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">compute_quality_score</a>(completeness: float, relevance: float, clarity: float, accuracy: float, weights: Dict[str, float] = None) -> float</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">delete_long_term</a>(memory_id: str) -> bool</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">delete_memories</a>(memory_ids: List[str]) -> int</code>
@@ -260,7 +252,6 @@ Methods:
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">finalize_task_output</a>(content: str, agent_name: str, quality_score: float, threshold: float = 0.7, metrics: Dict[str, Any] = None, task_id: str = None)</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">forget</a>(**kwargs) -> int</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">get_all_memories</a>() -> List[Dict[str, Any]]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">get_context</a>(query: Optional[str] = None, **kwargs) -> str</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">get_learn_context</a>() -> str</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">learn</a>()</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">recall</a>(query: str, **kwargs) -> List[Dict[str, Any]]</code>
@@ -272,14 +263,14 @@ Methods:
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">reset_user_memory</a>()</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search</a>(query: str, user_id: Optional[str] = None, agent_id: Optional[str] = None, run_id: Optional[str] = None, limit: int = 5, rerank: bool = False, **kwargs) -> List[Dict[str, Any]]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search_entity</a>(query: str, limit: int = 5) -> List[Dict[str, Any]]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search_long_term</a>(query: str, limit: int = 5, relevance_cutoff: float = 0.0, min_quality: float = 0.0, rerank: bool = False, metadata_filter: Optional[Dict[str, Any]] = None, user_id: Optional[str] = None, min_trust = None, **kwargs) -> List[Dict[str, Any]]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search_short_term</a>(query: str, limit: int = 5, min_quality: float = 0.0, relevance_cutoff: float = 0.0, rerank: bool = False, metadata_filter: Optional[Dict[str, Any]] = None, user_id: Optional[str] = None, min_trust = None, **kwargs) -> List[Dict[str, Any]]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search_long_term</a>(query: str, limit: int = 5, relevance_cutoff: float = 0.0, min_quality: float = 0.0, rerank: bool = False, metadata_filter: Optional[Dict[str, Any]] = None, user_id: Optional[str] = None, **kwargs) -> List[Dict[str, Any]]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search_short_term</a>(query: str, limit: int = 5, min_quality: float = 0.0, relevance_cutoff: float = 0.0, rerank: bool = False, metadata_filter: Optional[Dict[str, Any]] = None, user_id: Optional[str] = None, **kwargs) -> List[Dict[str, Any]]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search_user_memory</a>(user_id: str, query: str, limit: int = 5, rerank: bool = False, **kwargs) -> List[Dict[str, Any]]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">search_with_quality</a>(query: str, min_quality: float = 0.0, memory_type: Literal['short', 'long'] = 'long', limit: int = 5) -> List[Dict[str, Any]]</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">store_entity</a>(name: str, type_: str, desc: str, relations: str)</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">store_long_term</a>(text: str, metadata: Dict[str, Any] = None, completeness: float = None, relevance: float = None, clarity: float = None, accuracy: float = None, weights: Dict[str, float] = None, evaluator_quality: float = None, trust = None, origin: Optional[str] = None)</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">store_long_term</a>(text: str, metadata: Dict[str, Any] = None, completeness: float = None, relevance: float = None, clarity: float = None, accuracy: float = None, weights: Dict[str, float] = None, evaluator_quality: float = None)</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">store_quality</a>(text: str, quality_score: float, task_id: Optional[str] = None, iteration: Optional[int] = None, metrics: Optional[Dict[str, float]] = None, memory_type: Literal['short', 'long'] = 'long') -> None</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">store_short_term</a>(text: str, metadata: Dict[str, Any] = None, completeness: float = None, relevance: float = None, clarity: float = None, accuracy: float = None, weights: Dict[str, float] = None, evaluator_quality: float = None, trust = None, origin: Optional[str] = None)</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">store_short_term</a>(text: str, metadata: Dict[str, Any] = None, completeness: float = None, relevance: float = None, clarity: float = None, accuracy: float = None, weights: Dict[str, float] = None, evaluator_quality: float = None)</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">store_user_memory</a>(user_id: str, text: str, extra: Dict[str, Any] = None)</code>
* <code title="class MemoryConfig">MemoryConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
@@ -310,9 +301,9 @@ Methods:
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">markdown</a>()</code>
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">memory</a>()</code>
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">normalize_content</a>(content)</code>
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">reset</a>() -> bool</code>
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">reset</a>()</code>
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">search</a>(query, user_id = None, agent_id = None, run_id = None, rerank = None, **kwargs)</code>
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">store</a>(content, user_id = None, agent_id = None, run_id = None, metadata = None, is_content = False)</code>
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">store</a>(content, user_id = None, agent_id = None, run_id = None, metadata = None)</code>
* <code title="class Knowledge">Knowledge.<a href="./src/praisonai-agents/praisonaiagents/knowledge/knowledge.py">update</a>(memory_id, data)</code>
* <code title="class KnowledgeConfig">KnowledgeConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
@@ -390,7 +381,6 @@ Methods:
* <code title="class LLMGuardrail">LLMGuardrail.<a href="./src/praisonai-agents/praisonaiagents/guardrails/llm_guardrail.py">validate_input</a>(content: str, **kwargs) -> Tuple[bool, str]</code>
* <code title="class LLMGuardrail">LLMGuardrail.<a href="./src/praisonai-agents/praisonaiagents/guardrails/llm_guardrail.py">validate_output</a>(content: str, **kwargs) -> Tuple[bool, str]</code>
* <code title="class LLMGuardrail">LLMGuardrail.<a href="./src/praisonai-agents/praisonaiagents/guardrails/llm_guardrail.py">validate_tool_call</a>(tool_name: str, arguments: Dict[str, Any], **kwargs) -> Tuple[bool, Dict[str, Any]]</code>
* <code title="class LLMGuardrail">LLMGuardrail.<a href="./src/praisonai-agents/praisonaiagents/guardrails/llm_guardrail.py">validate_tool_result</a>(tool_name: str, result: Any, **kwargs) -> Tuple[bool, Any]</code>
# Planning
@@ -474,7 +464,7 @@ Methods:
Types:
```python
from praisonaiagents import SkillLoader, SkillsConfig
from praisonaiagents import SkillLoader, SkillManager, SkillMetadata, SkillProperties, SkillsConfig
```
Methods:
@@ -486,6 +476,55 @@ Methods:
* <code title="class SkillLoader">SkillLoader.<a href="./src/praisonai-agents/praisonaiagents/skills/loader.py">load_metadata</a>(skill_path: str) -> Optional[LoadedSkill]</code>
* <code title="class SkillLoader">SkillLoader.<a href="./src/praisonai-agents/praisonaiagents/skills/loader.py">load_references</a>(skill: LoadedSkill) -> dict</code>
* <code title="class SkillLoader">SkillLoader.<a href="./src/praisonai-agents/praisonaiagents/skills/loader.py">load_scripts</a>(skill: LoadedSkill) -> dict</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">activate</a>(skill: LoadedSkill) -> bool</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">activate_by_name</a>(name: str) -> bool</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">add_bundle</a>(manifest: 'BundleManifest') -> None</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">add_skill</a>(skill_path: str) -> Optional[LoadedSkill]</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">add_skill_by_name</a>(name: str, skill_dirs: Optional[List[str]] = None, include_defaults: bool = True) -> Optional[LoadedSkill]</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">approve</a>(identifier: str) -> dict</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">archive_skill</a>(name: str) -> dict</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">bundle_names</a>() -> List[str]</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">bundles</a>() -> List['BundleManifest']</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">clear</a>() -> None</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">create</a>(name: str, content: str, category: Optional[str] = None, propose: Optional[bool] = None) -> dict</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">create_skill</a>(name: str, content: str, category: str = None, agent_created: bool = True, propose: Optional[bool] = None, base_dir: Optional[str] = None) -> dict</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">delete</a>(name: str, propose: Optional[bool] = None) -> dict</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">delete_skill</a>(name: str, hard: bool = False, propose: Optional[bool] = None) -> dict</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">discover</a>(skill_dirs: Optional[List[str]] = None, include_defaults: bool = True) -> int</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">discover_bundles</a>(skill_dirs: Optional[List[str]] = None, include_defaults: bool = True) -> int</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">edit</a>(name: str, content: str, propose: Optional[bool] = None) -> dict</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">edit_skill</a>(name: str, content: str, propose: Optional[bool] = None) -> dict</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">get_allowed_tools</a>(name: str) -> List[str]</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">get_available_skills</a>() -> List[SkillMetadata]</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">get_available_skills_by_state</a>(state: SkillState) -> List[LoadedSkill]</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">get_bundle</a>(name: str) -> Optional['BundleManifest']</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">get_bundle_instructions</a>() -> List[str]</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">get_instructions</a>(name: str) -> Optional[str]</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">get_skill</a>(name: str) -> Optional[LoadedSkill]</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">get_skills_diagnostics</a>() -> Dict[str, ValidationResult]</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">get_user_invocable_skills</a>() -> List[LoadedSkill]</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">invoke</a>(name: str, raw_args: str = '', session_id: Optional[str] = None, shell_exec: bool = False) -> Optional[str]</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">list_archived_skills</a>() -> List[str]</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">list_pending</a>() -> List[dict]</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">load_resources</a>(name: str) -> bool</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">patch</a>(name: str, old_string: str, new_string: str, file_path: Optional[str] = None, replace_all: bool = False, propose: Optional[bool] = None) -> dict</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">patch_skill</a>(name: str, old_string: str, new_string: str, file_path: str = None, replace_all: bool = False, propose: Optional[bool] = None) -> dict</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">reject</a>(identifier: str) -> dict</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">reload</a>() -> Dict[str, List[str]]</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">remove_file</a>(name: str, file_path: str, propose: Optional[bool] = None) -> dict</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">remove_skill_file</a>(name: str, file_path: str, propose: Optional[bool] = None) -> dict</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">resolve</a>(selectors: List[str]) -> List[str]</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">restore_skill</a>(name: str, skill_dir: Optional[str] = None) -> dict</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">rollback_skill</a>(name: str) -> dict</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">skill_names</a>() -> List[str]</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">skills</a>() -> List[LoadedSkill]</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">to_prompt</a>() -> str</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">validate_skill_capabilities</a>(skill_name: str, force_refresh: bool = False) -> ValidationResult</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">write_file</a>(name: str, file_path: str, file_content: str, propose: Optional[bool] = None) -> dict</code>
* <code title="class SkillManager">SkillManager.<a href="./src/praisonai-agents/praisonaiagents/skills/manager.py">write_skill_file</a>(name: str, file_path: str, file_content: str, propose: Optional[bool] = None) -> dict</code>
* <code title="class SkillMetadata">SkillMetadata.<a href="./src/praisonai-agents/praisonaiagents/skills/models.py">from_properties</a>(props: SkillProperties) -> 'SkillMetadata'</code>
* <code title="class SkillProperties">SkillProperties.<a href="./src/praisonai-agents/praisonaiagents/skills/models.py">idle_days</a>() -> Optional[float]</code>
* <code title="class SkillProperties">SkillProperties.<a href="./src/praisonai-agents/praisonaiagents/skills/models.py">to_dict</a>() -> dict</code>
* <code title="class SkillsConfig">SkillsConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
# Session
@@ -714,14 +753,10 @@ Methods:
* <code title="class AgentFlow">AgentFlow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">reasoning</a>() -> bool</code>
* <code title="class AgentFlow">AgentFlow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">run</a>(input: str = '', llm: Optional[str] = None, verbose: bool = False, stream: bool = None) -> Dict[str, Any]</code>
* <code title="class AgentFlow">AgentFlow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">start</a>(input: str = '', **kwargs) -> Dict[str, Any]</code>
* <code title="class AgentFlow">AgentFlow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">state</a>()</code>
* <code title="class AgentFlow">AgentFlow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">stream</a>() -> bool</code>
* <code title="class AgentFlow">AgentFlow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class AgentFlow">AgentFlow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">to_mermaid</a>() -> str</code>
* <code title="class AgentFlow">AgentFlow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">validate_variables</a>() -> None</code>
* <code title="class AgentFlow">AgentFlow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">verbose</a>() -> bool</code>
* <code title="class AgentFlow">AgentFlow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">verbose</a>(value: bool)</code>
* <code title="class AgentFlow">AgentFlow.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">where_does_it_run</a>() -> str</code>
* <code title="class AgentOSProtocol">AgentOSProtocol.<a href="./src/praisonai-agents/praisonaiagents/app/protocols.py">get_app</a>() -> Any</code>
* <code title="class AgentOSProtocol">AgentOSProtocol.<a href="./src/praisonai-agents/praisonaiagents/app/protocols.py">serve</a>(host: Optional[str] = None, port: Optional[int] = None, reload: bool = False, **kwargs: Any) -> None</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">aannounce_completion</a>(agent_id: str, task_id: str, result: Any, success: bool = True, error: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None) -> None</code>
@@ -752,13 +787,13 @@ Methods:
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_spawned_agents</a>() -> List[SpawnedSubAgent]</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_state</a>(key: str, default: Any = None) -> Any</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_task_details</a>(task_id)</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_task_result</a>(task_id, tasks = None)</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_task_result</a>(task_id)</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_task_status</a>(task_id)</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_todo_markdown</a>() -> str</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">get_token_usage_summary</a>() -> Dict[str, Any]</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">has_state</a>(key: str) -> bool</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">increment_state</a>(key: str, amount: float = 1, default: float = 0) -> float</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">launch</a>(path: str = '/agents', port: int = 8000, host: str = '127.0.0.1', debug: bool = False, protocol: str = 'http')</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">launch</a>(path: str = '/agents', port: int = 8000, host: str = '0.0.0.0', debug: bool = False, protocol: str = 'http')</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">restore_session_state</a>(session_id: str) -> bool</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">run</a>(content = None, return_dict = False, **kwargs)</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">run_all_tasks</a>()</code>
@@ -774,9 +809,8 @@ Methods:
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">update_plan_step_status</a>(step_id: str, status: str) -> bool</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">update_state</a>(updates: Dict) -> None</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">wait_for_completions</a>(timeout: Optional[float] = None, agent_ids: Optional[List[str]] = None) -> List[SubAgentCompletionEvent]</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">where_does_it_run</a>() -> str</code>
* <code title="class AutoApproveBackend">AutoApproveBackend.<a href="./src/praisonai-agents/praisonaiagents/approval/backends.py">request_approval</a>(request: ApprovalRequest) -> ApprovalDecision</code>
* <code title="class AutoApproveBackend">AutoApproveBackend.<a href="./src/praisonai-agents/praisonaiagents/approval/backends.py">request_approval_sync</a>(request: ApprovalRequest, timeout: Optional[float] = None) -> ApprovalDecision</code>
* <code title="class AutoApproveBackend">AutoApproveBackend.<a href="./src/praisonai-agents/praisonaiagents/approval/backends.py">request_approval_sync</a>(request: ApprovalRequest) -> ApprovalDecision</code>
* <code title="class RunOutcome">RunOutcome.<a href="./src/praisonai-agents/praisonaiagents/agent/run_outcome.py">completed</a>(output: Optional[str] = None) -> 'RunOutcome'</code>
* <code title="class RunOutcome">RunOutcome.<a href="./src/praisonai-agents/praisonaiagents/agent/run_outcome.py">from_exception</a>(exc: BaseException, output: Optional[str] = None) -> 'RunOutcome'</code>
* <code title="class RunOutcome">RunOutcome.<a href="./src/praisonai-agents/praisonaiagents/agent/run_outcome.py">succeeded</a>() -> bool</code>
@@ -788,7 +822,7 @@ Methods:
Types:
```python
from praisonai import Agent, AgentApp, AgentOS, AnthropicManagedAgent, HostedAgent, HostedAgentConfig, LocalAgent, LocalAgentConfig, LocalManagedAgent, LocalManagedConfig, ManagedAgent, ManagedConfig, __version__, arun, run
from praisonai import Agent, AgentApp, AgentOS, AnthropicManagedAgent, DB, HostedAgent, HostedAgentConfig, LocalAgent, LocalAgentConfig, LocalManagedAgent, LocalManagedConfig, ManagedAgent, ManagedConfig, __version__, arun, run
```
# CLI
@@ -856,13 +890,11 @@ Methods:
* <code title="cli">praisonai managed list <a href="./src/praisonai/praisonai/cli/commands/managed.py">--help</a></code>
* <code title="cli">praisonai managed managed-callback <a href="./src/praisonai/praisonai/cli/commands/managed.py">--help</a></code>
* <code title="cli">praisonai managed multi <a href="./src/praisonai/praisonai/cli/commands/managed.py">--help</a></code>
* <code title="cli">praisonai managed ps <a href="./src/praisonai/praisonai/cli/commands/managed.py">--help</a></code>
* <code title="cli">praisonai managed restore <a href="./src/praisonai/praisonai/cli/commands/managed.py">--help</a></code>
* <code title="cli">praisonai managed resume <a href="./src/praisonai/praisonai/cli/commands/managed.py">--help</a></code>
* <code title="cli">praisonai managed run <a href="./src/praisonai/praisonai/cli/commands/managed.py">--help</a></code>
* <code title="cli">praisonai managed save <a href="./src/praisonai/praisonai/cli/commands/managed.py">--help</a></code>
* <code title="cli">praisonai managed show <a href="./src/praisonai/praisonai/cli/commands/managed.py">--help</a></code>
* <code title="cli">praisonai managed stop <a href="./src/praisonai/praisonai/cli/commands/managed.py">--help</a></code>
* <code title="cli">praisonai managed update <a href="./src/praisonai/praisonai/cli/commands/managed.py">--help</a></code>
* <code title="cli">praisonai n8n export <a href="./src/praisonai/praisonai/cli/commands/n8n.py">--help</a></code>
* <code title="cli">praisonai n8n import <a href="./src/praisonai/praisonai/cli/commands/n8n.py">--help</a></code>
@@ -892,7 +924,6 @@ Methods:
* <code title="cli">praisonai recipe list <a href="./src/praisonai/praisonai/cli/commands/recipe.py">--help</a></code>
* <code title="cli">praisonai recipe optimize <a href="./src/praisonai/praisonai/cli/commands/recipe.py">--help</a></code>
* <code title="cli">praisonai recipe run <a href="./src/praisonai/praisonai/cli/commands/recipe.py">--help</a></code>
* <code title="cli">praisonai recipe runs <a href="./src/praisonai/praisonai/cli/commands/recipe.py">--help</a></code>
* <code title="cli">praisonai recipe serve <a href="./src/praisonai/praisonai/cli/commands/recipe.py">--help</a></code>
* <code title="cli">praisonai replay cleanup <a href="./src/praisonai/praisonai/cli/commands/replay.py">--help</a></code>
* <code title="cli">praisonai replay context <a href="./src/praisonai/praisonai/cli/commands/replay.py">--help</a></code>
@@ -909,12 +940,7 @@ Methods:
* <code title="cli">praisonai schedule describe <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
* <code title="cli">praisonai schedule list <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
* <code title="cli">praisonai schedule logs <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
* <code title="cli">praisonai schedule pause <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
* <code title="cli">praisonai schedule remove <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
* <code title="cli">praisonai schedule restart <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
* <code title="cli">praisonai schedule resume <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
* <code title="cli">praisonai schedule run <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
* <code title="cli">praisonai schedule runs <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
* <code title="cli">praisonai schedule schedule-callback <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
* <code title="cli">praisonai schedule start <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
* <code title="cli">praisonai schedule stats <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
@@ -923,7 +949,6 @@ Methods:
* <code title="cli">praisonai schedule suggestion-dismiss <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
* <code title="cli">praisonai schedule suggestion-propose <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
* <code title="cli">praisonai schedule suggestions <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
* <code title="cli">praisonai schedule update <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
* <code title="cli">praisonai validate check <a href="./src/praisonai/praisonai/cli/commands/validate.py">--help</a></code>
* <code title="cli">praisonai validate schema <a href="./src/praisonai/praisonai/cli/commands/validate.py">--help</a></code>
* <code title="cli">praisonai validate validate <a href="./src/praisonai/praisonai/cli/commands/validate.py">--help</a></code>
@@ -932,24 +957,17 @@ Methods:
Types/Exports:
```ts
export { Agent, AgentTeam, Agents, CodeAgent, EmbeddingAgent, OCRAgent, PraisonAIAgents, RealtimeAgent, Router, TASK_STATUS, VideoAgent, VisionAgent, createCodeAgent, createEmbeddingAgent, createOCRAgent, createRealtimeAgent, createSubprocessExecutor, createVideoAgent, createVisionAgent } from "./agent";
export type { AgentChatCallOptions, AgentChatOptions, AgentEvent, AgentExecuteTask, AgentGuardrailEntry, AgentGuardrailFunction, AgentGuardrailInput, AgentHooksInput, AgentMemoryStore, AgentMessage, AgentRetryConfig, AgentStreamOptions, AgentTaskLike, AgentTeamConfig, AgentTeamProcess, AgentTeamStartDictOptions, AgentTeamStartOptions, AgentTeamStartOptionsInput, AgentWebConfig, PraisonAIAgentsConfig, SimpleAgentConfig, SimpleRouteConfig, SimpleRouterConfig, StopReason, TaskCallback, TaskGuardrail, TaskOnError } from "./agent";
export { Agent, AgentTeam, Agents, PraisonAIAgents, Router } from "./agent";
export type { AgentTeamConfig, PraisonAIAgentsConfig, SimpleAgentConfig, SimpleRouteConfig, SimpleRouterConfig } from "./agent";
export { AudioAgent, createAudioAgent } from "./agent/audio";
export type { AudioAgentConfig, AudioConfig, AudioProvider, AudioSpeakOptions, AudioSpeakResult, AudioTranscribeOptions, AudioTranscribeResult } from "./agent/audio";
export { ContextAgent, createContextAgent, create_context_agent } from "./agent/context";
export { ContextPolicy, DEFAULT_HANDOFF_TOOL_POLICY, Handoff, HandoffCycleError, HandoffDepthError, HandoffError, HandoffTimeoutError, HandoffToolPolicyMode, RECOMMENDED_PROMPT_PREFIX, handoff, handoffFilters, handoffToolName, handoff_filters, listAgentTools, parallelHandoffs, parallel_handoffs, promptWithHandoffInstructions, prompt_with_handoff_instructions, resolveHandoffToolPolicy } from "./agent/handoff";
export { Heartbeat, HeartbeatConfig, parseHeartbeatSchedule } from "./agent/heartbeat";
export type { HeartbeatAgent, HeartbeatOnError, HeartbeatOptions } from "./agent/heartbeat";
export type { AudioAgentConfig, AudioProvider, AudioSpeakOptions, AudioSpeakResult, AudioTranscribeOptions, AudioTranscribeResult } from "./agent/audio";
export { ContextAgent, createContextAgent } from "./agent/context";
export { ContextPolicy, Handoff, HandoffCycleError, HandoffDepthError, HandoffError, HandoffTimeoutError, RECOMMENDED_PROMPT_PREFIX, handoff, handoffFilters, promptWithHandoffInstructions } from "./agent/handoff";
export { ImageAgent, createImageAgent } from "./agent/image";
export { PromptExpanderAgent, createPromptExpanderAgent } from "./agent/prompt-expander";
export { QueryRewriterAgent, createQueryRewriterAgent } from "./agent/query-rewriter";
export { DeepResearchAgent, Provider, createDeepResearchAgent } from "./agent/research";
export { RetryBackoffConfig, interruptibleSleep, jitteredBackoff } from "./agent/retry-utils";
export type { JitteredBackoffOptions, RetryBackoffConfigOptions } from "./agent/retry-utils";
export { DeepResearchAgent, createDeepResearchAgent } from "./agent/research";
export { RouterAgent, createRouter, routeConditions } from "./agent/router";
export { AGENT_RUN_STATUSES, AgentRunOutcome, PROVIDER_BLOCK_REASONS, RunOutcome, TERMINAL_REASON_PRECEDENCE, TERMINATION_TO_RUN_STATUS, TerminationReason, classifyFinishReason, terminationToRunStatus, termination_to_run_status, validateDecisionString, validate_decision_string } from "./agent/run-outcome";
export type { AgentRunOutcomeFailureOptions, AgentRunOutcomeInit, AgentRunOutcomeOptions, AgentRunStatus, RunOutcomeInit, TerminalReason } from "./agent/run-outcome";
export { reviewTaskOutput } from "./agent/task-review";
export { // Agent loop
createAgentLoop, // DevTools
enableDevTools, // MCP
@@ -966,7 +984,6 @@ export { // Agent loop
defineTool, // UI Message (AI SDK v6 parity)
convertToModelMessages, AIAgentStep, AIEmbedManyResult, AIEmbedOptions, AIEmbedResult, AIFilePart, AIGenerateImageOptions, AIGenerateImageResult, AIGenerateObjectOptions, AIGenerateObjectResult, AIGenerateTextOptions, AIGenerateTextResult, AIImagePart, AIMiddleware, AIMiddlewareConfig, AIModelMessage, AISpan, AISpanKind, AISpanOptions, AISpanStatus, AIStreamObjectOptions, AIStreamObjectResult, AIStreamTextOptions, AIStreamTextResult, AITelemetryEvent, AITelemetrySettings, AITextPart, AIToolDefinition, AITracer, AgentLoop, DANGEROUS_PATTERNS, MCPClientType, MODEL_ALIASES, SPEECH_MODELS, TRANSCRIPTION_MODELS, ToolApprovalDeniedError, ToolApprovalTimeoutError, aiEmbed, aiEmbedMany, aiGenerateImage, aiGenerateObject, aiGenerateText, aiStreamObject, aiStreamText, applyMiddleware, autoEnableDevTools, base64ToUint8Array, clearAICache, clearEvents, closeAllMCPClients, closeMCPClient, convertToUIMessages, createAILoggingMiddleware, createAISpan, createApprovalResponse, createDangerousPatternChecker, createDevToolsMiddleware, createExpressHandler, createFastifyHandler, createFilePart, createHonoHandler, createMultimodalMessage, createNestHandler, createPagesHandler, createPdfPart, createSystemMessage, createTelemetryMiddleware, createTelemetrySettings, createTextMessage, createTextPart, createToolSet, disableAITelemetry, disableDevTools, enableAITelemetry, functionToTool, getAICacheStats, getApprovalManager, getDevToolsState, getDevToolsUrl, getEvents, getMCPClient, getModel, getTelemetrySettings, getToolsNeedingApproval, getTracer, hasModelAlias, hasPendingApprovals, initOpenTelemetry, isDangerous, isDataUrl, isDevToolsEnabled, isTelemetryEnabled, isUrl, listModelAliases, mcpToolsToAITools, parseModel, pipeUIMessageStreamToResponse, recordEvent, resolveModelAlias, safeValidateUIMessages, setApprovalManager, stopAfterSteps, stopWhen, stopWhenNoToolCalls, toMessageContent, toUIMessageStreamResponse, transcribe, uint8ArrayToBase64, validateUIMessages, withApproval, withSpan, wrapModel } from "./ai";
export { AutoAgents, AutoTaskConfig, createAutoAgents } from "./auto";
export { BotRunStatus } from "./bots/protocols";
export { BaseCache, FileCache, MemoryCache, createFileCache, createMemoryCache } from "./cache";
export { CLI_SPEC_VERSION, executeCommand, parseArgs } from "./cli";
export { // Autonomy Mode
@@ -985,31 +1002,26 @@ export { // Autonomy Mode
SandboxExecutor, // Scheduler
Scheduler, // Slash Commands
SlashCommandHandler, AiderAgent, ClaudeCodeAgent, CodexCliAgent, CommandValidator, CostTokenUsage, DEFAULT_BLOCKED_COMMANDS, DEFAULT_BLOCKED_PATHS, DEFAULT_IGNORE_PATTERNS, DiffViewer, FileCheckpointStorage, FileJobStorage, GeminiCliAgent, GenericExternalAgent, HistoryManager, MODEL_PRICING, MODE_POLICIES, MemoryCheckpointStorage, MemoryJobStorage, StatusDisplay, addLineRangeToFileMatch, cliApprovalPrompt, createAutonomyManager, createCheckpointManager, createCostTracker, createDiffViewer, createExternalAgent, createFastContext, createFileCheckpointStorage, createFileJobStorage, createFileMatch, createFlowDisplay, createGitManager, createHistoryManager, createInteractiveTUI, createJobQueue, createLineRange, createN8NIntegration, createRepoMap, createSandboxExecutor, createScheduler, createSlashCommandHandler, createStatusDisplay, cronExpressions, estimateTokens, executeSlashCommand, externalAgentAsTool, formatCost, getExternalAgentRegistry, getLineCount, getQuickContext, getRepoTree, getTotalLines, isSlashCommand, mergeRanges, parseSlashCommand, rangesOverlap, registerCommand, renderWorkflow, sandboxExec, triggerN8NWebhook } from "./cli/features";
export { ComputeError, DockerCompute, LocalCompute, listComputeProviders, registerComputeProvider, resolveComputeProvider } from "./compute";
export { // Classes
DictCondition, // Functions
evaluateCondition, // Types
type ConditionProtocol, ExpressionCondition, FunctionCondition, andConditions, createCondition, evaluate_condition, notCondition, orConditions } from "./conditions";
type ConditionProtocol, ExpressionCondition, FunctionCondition, andConditions, createCondition, notCondition, orConditions } from "./conditions";
export { // Enums
MemoryBackend, // Errors
ConfigValidationError, // Parse utilities
detect_url_scheme, // Presets
MEMORY_PRESETS, // Resolver functions
resolve, AUTONOMY_PRESETS, ArrayMode, CACHING_PRESETS, CONTEXT_PRESETS, ChunkingStrategy, EXECUTION_PRESETS, ExecutionPreset, FeatureMemoryConfig, GUARDRAIL_PRESETS, GuardrailAction, KNOWLEDGE_PRESETS, LearnBackend, LearnMode, LearnScope, MEMORY_URL_SCHEMES, MULTI_AGENT_EXECUTION_PRESETS, MULTI_AGENT_OUTPUT_PRESETS, OUTPUT_PRESETS, OutputPreset, PLANNING_PRESETS, PreCompactionMemoryFlushConfig, REFLECTION_PRESETS, RulesConfig, ToolSearchConfig, WEB_PRESETS, WebSearchProvider, apply_config_defaults, clean_triple_backticks, get_config, get_config_path, get_default, get_defaults_config, get_plugins_config, is_path_like, is_policy_string, parse_policy_string, resolve_autonomy, resolve_caching, resolve_context, resolve_execution, resolve_guardrails, resolve_hooks, resolve_knowledge, resolve_memory, resolve_output, resolve_planning, resolve_reflection, resolve_routing, resolve_skills, resolve_web, suggest_similar, validate_config } from "./config";
resolve, AUTONOMY_PRESETS, ArrayMode, CACHING_PRESETS, CONTEXT_PRESETS, ChunkingStrategy, EXECUTION_PRESETS, ExecutionPreset, FeatureMemoryConfig, GUARDRAIL_PRESETS, GuardrailAction, KNOWLEDGE_PRESETS, MEMORY_URL_SCHEMES, MULTI_AGENT_EXECUTION_PRESETS, MULTI_AGENT_OUTPUT_PRESETS, OUTPUT_PRESETS, OutputPreset, PLANNING_PRESETS, REFLECTION_PRESETS, WEB_PRESETS, WebSearchProvider, apply_config_defaults, clean_triple_backticks, get_config, get_config_path, get_default, get_defaults_config, get_plugins_config, is_path_like, is_policy_string, parse_policy_string, resolve_autonomy, resolve_caching, resolve_context, resolve_execution, resolve_guardrails, resolve_hooks, resolve_knowledge, resolve_memory, resolve_output, resolve_planning, resolve_reflection, resolve_routing, resolve_skills, resolve_web, suggest_similar, validate_config } from "./config";
export { createDbAdapter, db, getDefaultDbAdapter, setDefaultDbAdapter } from "./db";
export type { DbAdapter, DbConfig, DbMessage, DbRun, DbTrace } from "./db";
export { MemoryPostgresAdapter, NeonPostgresAdapter, PostgresSessionStorage, createMemoryPostgres, createNeonPostgres, createPostgresSessionStorage } from "./db/postgres";
export { MemoryRedisAdapter, UpstashRedisAdapter, createMemoryRedis, createUpstashRedis } from "./db/redis";
export { SQLiteAdapter, createSQLiteAdapter } from "./db/sqlite";
export { // Functions
// NB: the unaliased registerDisplayCallback name belongs to the per-type
// callback registry (callbacks module, // Types
type DisplayCallback, DisplayFlow, DisplayFlowConfig, asyncDisplayCallbacks, async_display_callbacks, clearDisplayCallbacks, clearErrorLogs, displayError, displayGenerating, displayInstruction, displayInteraction, displaySelfReflection, displayToolCall, display_error, display_generating, display_instruction, display_interaction, display_self_reflection, display_tool_call, errorLogs, error_logs, logError, registerDisplay, register_display_callback, syncDisplayCallbacks, sync_display_callbacks } from "./display";
export { // Types
type DisplayCallback, DisplayFlow, DisplayFlowConfig, asyncDisplayCallbacks, clearDisplayCallbacks, clearErrorLogs, displayError, displayGenerating, displayInstruction, displayInteraction, displaySelfReflection, displayToolCall, errorLogs, logError, registerDisplay, syncDisplayCallbacks } from "./display";
export { // Functions
embed, // Types
type EmbeddingResult, aembed, aembedding, aembeddings, cosineSimilarity, embedding, embeddings, euclideanDistance, getDimensions, get_dimensions, normalizeEmbedding, setEmbeddingConfig } from "./embeddings";
export { AGENT_ERROR_KINDS, FailoverDecision, IdleTimeoutBreaker, LEGACY_ERROR_CATEGORY_MAP, LLMError, NetworkError, PraisonAIConfigError, PraisonAIError, ToolExecutionError, ValidationError, isAgentErrorKind, isErrorContext, resolveErrorCategory } from "./errors";
export type { AgentErrorKind, ErrorContextProtocol, FailoverAction, FailoverDecisionOptions, LLMErrorOptions, LegacyErrorCategory, NetworkErrorOptions, PraisonAIConfigErrorOptions, PraisonAIErrorOptions, ToolExecutionErrorOptions, ValidationErrorOptions } from "./errors";
type EmbeddingResult, aembed, aembedding, aembeddings, cosineSimilarity, embedding, embeddings, euclideanDistance, getDimensions, normalizeEmbedding, setEmbeddingConfig } from "./embeddings";
export { // LLM-as-Judge
Judge, AccuracyJudge, CriteriaJudge, EvalResults, EvalSuite, Evaluator, RecipeJudge, accuracyEval, addJudge, addOptimizationRule, containsKeywordsCriterion, createDefaultEvaluator, createEvalResults, createEvaluator, getJudge, getOptimizationRule, lengthCriterion, listJudges, listOptimizationRules, noHarmfulContentCriterion, parseJudgeResponse, performanceEval, relevanceCriterion, reliabilityEval, removeJudge, removeOptimizationRule } from "./eval";
export { AgentEventBus, AgentEvents, EventEmitterPubSub, PubSub, createEventBus, createPubSub } from "./events";
@@ -1018,8 +1030,7 @@ export { // Bot types
FailoverManager, // Enums
SandboxStatus, // Gateway types
type GatewayConfig, // Other types
type ProviderStatus, // Session-projection reducer (Issue #5324)
SessionProjection, AutonomyLevel, GatewayEventType, RagRetrievalPolicy } from "./gateway";
type ProviderStatus, AutonomyLevel, RagRetrievalPolicy } from "./gateway";
export { LLMGuardrail, createLLMGuardrail } from "./guardrails/llm-guardrail";
export { DisplayTypes, HooksManager, WorkflowHooksExecutor, clearAllCallbacks, clearApprovalCallback, createHooksManager, createLoggingOperationHooks, createLoggingWorkflowHooks, createTimingWorkflowHooks, createValidationOperationHooks, createWorkflowHooks, executeCallback, executeSyncCallback, getRegisteredDisplayTypes, hasApprovalCallback, registerApprovalCallback, registerDisplayCallback, requestApproval, unregisterDisplayCallback } from "./hooks";
export { // Computer Use
@@ -1031,11 +1042,7 @@ export { // Slack
createSlackBot, SlackBot, parseSlackMessage, verifySlackSignature } from "./integrations/slack";
export { BaseVectorStore, ChromaVectorStore, MemoryVectorStore, PineconeVectorStore, QdrantVectorStore, VectorQueryResult, WeaviateVectorStore, createChromaStore, createMemoryVectorStore, createPineconeStore, createQdrantStore, createWeaviateStore } from "./integrations/vector";
export { BaseVoiceProvider, ElevenLabsVoiceProvider, OpenAIVoiceProvider, createElevenLabsVoice, createOpenAIVoice } from "./integrations/voice";
export type { KnowledgeRecord } from "./knowledge";
export { GraphRAG, GraphStore, createGraphRAG } from "./knowledge/graph-rag";
export { Knowledge } from "./knowledge/knowledge";
export type { KnowledgeIndexOptions, KnowledgeSearchCallOptions, KnowledgeStoreConfig, KnowledgeStoreOptions } from "./knowledge/knowledge";
export { KnowledgeBase, KnowledgeDocument, KnowledgeSearchResult, createKnowledgeBase } from "./knowledge/rag";
export { BaseReranker, CohereReranker, CrossEncoderReranker, LLMReranker, createCohereReranker, createCrossEncoderReranker, createLLMReranker } from "./knowledge/reranker";
export { // Provider classes
OpenAIProvider, // Provider factory and utilities
@@ -1043,22 +1050,14 @@ export { // Provider classes
ProviderRegistry, // Types
type LLMProvider, AnthropicProvider, BaseProvider, GoogleProvider, ProviderMessage, ProviderToolDefinition, createProviderRegistry, getAvailableProviders, getDefaultProvider, getDefaultRegistry, hasProvider, isProviderAvailable, listProviders, parseModelString, registerBuiltinProviders, registerProvider, unregisterProvider } from "./llm/providers";
export { ADAPTERS, AISDK_PROVIDERS, COMMUNITY_PROVIDERS, PROVIDER_ALIASES } from "./llm/providers/ai-sdk/types";
export { AgentMessageEvent, CustomToolUseEvent, ManagedEvent, SessionErrorEvent, SessionIdleEvent, SessionRunningEvent, ToolConfirmationEvent, ToolUseEvent, UsageEvent, isManagedBackend } from "./managed";
export type { AgentMessageEventInit, CustomToolUseEventInit, ManagedBackendKwargs, ManagedBackendProtocol, ManagedContentBlock, ManagedEventInit, ManagedEventType, ManagedStopReason, SessionErrorEventInit, SessionIdleEventInit, ToolConfirmationEventInit, ToolUseEventInit, UsageEventInit } from "./managed";
export { MCPClient, MCPSecurity, MCPServer, MCPSessionManager, createApiKeyPolicy, createMCPClient, createMCPSecurity, createMCPServer, createMCPSession, createRateLimitPolicy, getMCPTools } from "./mcp";
export { ChromaMemory, InMemoryAdapter, MEMORY_PROVIDER_ALIASES, MemoryAdapterRegistry, addMemoryAdapter, addMemoryFactory, add_memory_adapter, add_memory_factory, createChromaMemoryAdapter, createDakeraMemoryAdapter, createMem0MemoryAdapter, createMongodbMemoryAdapter, createSqliteMemoryAdapter, getDefaultMemoryRegistry, getFirstAvailableMemoryAdapter, getMemoryAdapter, get_memory_adapter, hasMemoryAdapter, has_memory_adapter, listMemoryAdapters, list_memory_adapters, registerMemoryAdapter, registerMemoryFactory, register_memory_adapter, register_memory_factory, resetDefaultMemoryRegistry, resolveMemoryAdapterName, sanitizeChromaMetadata } from "./memory/adapters";
export type { ChromaMemoryConfig, MaybePromise, MemoryEmbedder, MemoryProtocol, MemoryRecord } from "./memory/adapters";
export { AutoMemory, AutoMemoryKnowledgeBase, AutoMemoryVectorStore, DEFAULT_POLICIES, createAutoMemory, createLLMSummarizer } from "./memory/auto-memory";
export { DocsManager, createDocsManager } from "./memory/docs-manager";
export { FileMemory, createFileMemory } from "./memory/file-memory";
export { MemoryHooks, createEncryptionHooks, createLoggingHooks, createMemoryHooks, createValidationHooks } from "./memory/hooks";
export { BaseStore, DecisionStore, FeedbackStore, ImprovementStore, InsightStore, LearnBackendNotAvailableError, LearnEntry, LearnError, LearnManager, LearnManagerMode, MongoDBLearnBackend, PatternStore, PersonaStore, RedisLearnBackend, SQLiteLearnBackend, ThreadStore, getDataDir, getLearnDir, resolveLearnConfig, toLearnEntryDict } from "./memory/learn";
export type { AsyncLearnProtocol, BaseStoreOptions, LearnEntryConvertible, LearnEntryData, LearnEntryInit, LearnEntryLike, LearnExtractor, LearnManagerConfig, LearnManagerProtocol, LearnMessage, LearnProtocol, LearnStorageBackend, LearnStore, ProcessConversationResult, ResolvedLearnConfig } from "./memory/learn";
export { Memory, createMemory } from "./memory/memory";
export type { MemoryConfig, MemoryEntry } from "./memory/memory";
export { RulesManager, createRulesManager, createSafetyRules } from "./memory/rules-manager";
export { DEFAULT_PROFILE, HarnessProfile, ModelRequestBlocked, ScriptExhausted, ScriptedModel, allowModelRequests, listHarnessProfiles, modelRequestsAllowed, noModelRequests, registerProfile, register_profile, resetHarnessRegistry, resolveHarness, resolve_harness } from "./model-harness";
export type { HarnessProfileConfig, HarnessRegistryEntry, HarnessResolverProtocol } from "./model-harness";
export { // Adapters
NoopObservabilityAdapter, // Constants
OBSERVABILITY_TOOLS, // Global adapter management
@@ -1066,6 +1065,15 @@ export { // Adapters
type SpanKind, ConsoleObservabilityAdapter, MemoryObservabilityAdapter, clearAdapterCache, createConsoleAdapter, createMemoryAdapter, createObservabilityAdapter, getObservabilityAdapter, getObservabilityToolInfo, hasObservabilityToolEnvVar, listObservabilityTools, noopAdapter, resetObservabilityAdapter, trace } from "./observability";
export { AgentApp, AgentAppConfig, AgentAppProtocol, AgentOS, AgentOSConfig, AgentOSProtocol, DEFAULT_AGENTOS_CONFIG, mergeConfig } from "./os";
export type { AgentAppOptions, AgentOSOptions } from "./os";
export { // P0: Call Types (new)
type MCPCall, // P0: Specialized Agent Configs (new)
type AudioConfig, // P0: Specialized Agents (new)
CodeAgent, // P1: Workflow Patterns (new)
Chunking, // P2: Context Types (new - only items not already exported)
ContextManager, // P3: Display callbacks (snake_case for Python parity)
register_display_callback, // P3: Plugin functions
get_plugin_manager, // P3: Trace & condition functions
evaluate_condition, EmbeddingAgent, If, Knowledge, MCP, OCRAgent, Parallel, RealtimeAgent, Route, Session, VideoAgent, VisionAgent, async_display_callbacks, cleanup_telemetry_resources, create_context_agent, disable_performance_mode, disable_telemetry, discover_and_load_plugins, discover_plugins, display_error, display_generating, display_instruction, display_interaction, display_self_reflection, display_tool_call, enable_performance_mode, enable_telemetry, ensure_plugin_dir, error_logs, get_default_plugin_dirs, get_dimensions, get_plugin_template, get_telemetry, handoff_filters, load_plugin, parse_plugin_header, parse_plugin_header_from_file, prompt_with_handoff_instructions, resolve_guardrail_policies, sync_display_callbacks, trace_context, track_workflow, when } from "./parity";
export { // Core classes
Plan, // Python parity additions
ApprovalCallback, PlanStep, PlanStorage, PlanningAgent, READ_ONLY_TOOLS, RESEARCH_TOOLS, RESTRICTED_TOOLS, TaskAgent, TodoItem, TodoList, createApprovalCallback, createPlan, createPlanStorage, createPlanningAgent, createTaskAgent, createTodoList } from "./planning";
@@ -1073,51 +1081,36 @@ export { // Classes
Plugin, // Enums
PluginHook, // Functions
getPluginManager, // Interfaces
type PluginMetadata, FunctionPlugin, PluginManager, PluginParseError, PluginType, disablePlugins, discoverAndLoadPlugins, discoverPlugins, discover_and_load_plugins, discover_plugins, enablePlugins, ensurePluginDir, ensure_plugin_dir, getDefaultPluginDirs, getPluginTemplate, get_default_plugin_dirs, get_plugin_manager, get_plugin_template, isPluginEnabled, listPlugins, loadPlugin, load_plugin, parsePluginHeader, parsePluginHeaderFromFile, parse_plugin_header, parse_plugin_header_from_file } from "./plugins";
type PluginMetadata, FunctionPlugin, PluginManager, PluginParseError, PluginType, disablePlugins, discoverAndLoadPlugins, discoverPlugins, enablePlugins, ensurePluginDir, getDefaultPluginDirs, getPluginTemplate, isPluginEnabled, listPlugins, loadPlugin, parsePluginHeader, parsePluginHeaderFromFile } from "./plugins";
export { // A2A Protocol
A2ATaskState, // AGUI Protocol
AGUI, // AgentManager alias type
type AgentManager, // Global singletons
config, // Guardrail policies
type GuardrailPolicy, // Tools class
type ToolDefinition, A2A, A2ARole, AutoRagAgent, AutoRetrievalPolicy, DEFAULT_AUTO_KEYWORDS, GUARDRAIL_POLICY_PRESETS, Tools, memory, obs, resolveGuardrailPolicies, resolve_guardrail_policies, workflows } from "./protocols";
type ToolDefinition, A2A, A2ARole, AutoRagAgent, AutoRetrievalPolicy, DEFAULT_AUTO_KEYWORDS, GUARDRAIL_POLICY_PRESETS, Tools, memory, obs, resolveGuardrailPolicies, workflows } from "./protocols";
export { CitationsMode, DEFAULT_RAG_TEMPLATE, RAG, RAGCitation, RAGContextPack, RetrievalPolicy, RetrievalStrategy, createCitation, createContextPack, createRAG, createRAGConfig, createRAGResult, createRetrievalConfig, createSimpleRetrievalConfig, createSmartRetrievalConfig, formatAnswerWithCitations, formatCitation, formatContextPackForPrompt, hasCitations } from "./rag";
export { PraisonAIRuntime, RuntimeRegistry, RuntimeRegistryEntry, RuntimeRegistryError, addRuntimeAlias, getRuntimeRegistry, isAgentRuntime, isRuntimeAvailable, listRuntimes, list_runtimes, registerRuntime, register_runtime, resolveRuntime, resolve_runtime, unregisterRuntime } from "./runtime";
export type { AgentRuntimeProtocol, RunTurnOptions, RuntimeCapabilityMatrix, RuntimeConfig, RuntimeDelta, RuntimeDeltaType, RuntimeFactory, RuntimeMode, RuntimeRegistryEntryInit, RuntimeRegistryProtocol, RuntimeResult, TurnContextBuilderProtocol, TurnRuntimeProtocol } from "./runtime";
export type { RunStatus } from "./session";
export { Session } from "./session/session";
export { // Python parity additions
SkillLoader, EnforcementLevel, SkillManager, SkillParseError, SkillState, createSkillLoader, createSkillManager, createSkillProperties, discoverSkill, discoverSkills, discover_skills, findSkillMd, getDefaultSkillDirs, loadSkill, load_skill, parseFrontmatter, parseSkillFile, validate, validateMetadata, validateSkill, validate_metadata } from "./skills";
export type { RemoteSkillSource } from "./skills";
SkillLoader, SkillManager, createSkillLoader, createSkillManager, createSkillProperties, parseSkillFile } from "./skills";
export { AgentTask, AgentTaskConfig, BaseTask, createTaskOutput } from "./task";
export { // Python parity additions
MinimalTelemetry, AgentTelemetry, PerformanceMonitor, TelemetryCollector, TelemetryIntegration, cleanupTelemetry, cleanupTelemetryResources, cleanup_telemetry_resources, createAgentTelemetry, createConsoleSink, createHTTPSink, createPerformanceMonitor, createTelemetryIntegration, disablePerformanceMode, disableTelemetry, disable_performance_mode, disable_telemetry, enablePerformanceMode, enableTelemetry, enable_performance_mode, enable_telemetry, getMinimalTelemetry, getTelemetry, get_telemetry } from "./telemetry";
MinimalTelemetry, AgentTelemetry, PerformanceMonitor, TelemetryCollector, TelemetryIntegration, cleanupTelemetry, cleanupTelemetryResources, createAgentTelemetry, createConsoleSink, createHTTPSink, createPerformanceMonitor, createTelemetryIntegration, disablePerformanceMode, disableTelemetry, enablePerformanceMode, enableTelemetry, getMinimalTelemetry, getTelemetry } from "./telemetry";
export { // Subagent Tool (agent-as-tool pattern)
SubagentTool, BaseTool, FunctionTool, TOOL_TRUST_LEVELS, ToolRegistry, ToolResult, ToolValidationError, add_tool, createDelegator, createSubagentTool, createSubagentTools, createTool, getRegistry, getTool, getToolDefinitions, get_registry, get_tool, get_tool_definitions, hasTool, has_tool, listTools, list_tools, registerTool, register_tool, removeTool, remove_tool, tool, validateTool, validate_tool } from "./tools";
SubagentTool, BaseTool, FunctionTool, ToolRegistry, ToolResult, ToolValidationError, createDelegator, createSubagentTool, createSubagentTools, createTool, getRegistry, getTool, registerTool, tool, validateTool } from "./tools";
export { airweaveSearch, bedrockBrowserClick, bedrockBrowserFill, bedrockBrowserNavigate, bedrockCodeInterpreter, codeExecution, codeMode, createCustomTool, crwCrawl, crwScrape, exaSearch, firecrawlCrawl, firecrawlScrape, parallelSearch, perplexitySearch, registerCustomTool, registerLocalTool, registerNpmTool, superagentGuard, superagentRedact, superagentVerify, tavilyCrawl, tavilyExtract, tavilySearch, valyuBioSearch, valyuCompanyResearch, valyuEconomicsSearch, valyuFinanceSearch, valyuPaperSearch, valyuPatentSearch, valyuSecSearch, valyuWebSearch } from "./tools/builtins";
export { MCP, MCPToolTransportType } from "./tools/mcp";
export { BudgetExceededError, MissingDependencyError, MissingEnvVarError, ToolConstructionError, ToolNotRegisteredError, ToolsRegistry, composeMiddleware, createLoggingMiddleware, createRateLimitMiddleware, createRedactionMiddleware, createRetryMiddleware, createTimeoutMiddleware, createToolInstance, createToolsRegistry, createTracingMiddleware, createValidationMiddleware, getToolsRegistry, registerToolFactory, resetToolsRegistry, tryCreateToolInstance, validateToolInstall } from "./tools/registry";
export { BudgetExceededError, MissingDependencyError, MissingEnvVarError, ToolsRegistry, composeMiddleware, createLoggingMiddleware, createRateLimitMiddleware, createRedactionMiddleware, createRetryMiddleware, createTimeoutMiddleware, createToolsRegistry, createTracingMiddleware, createValidationMiddleware, getToolsRegistry, get_registry, get_tool, register_tool, resetToolsRegistry, validate_tool } from "./tools/registry";
export type { InstallHints, PraisonTool, RedactionHooks, RegisteredTool, ToolCapabilities, ToolExecutionContext, ToolExecutionResult, ToolFactory, ToolHooks, ToolInstallStatus, ToolLimits, ToolLogger, ToolMetadata, ToolMiddleware, ToolParameterProperty, ToolParameterSchema } from "./tools/registry";
export { registerBuiltinTools, tools } from "./tools/tools";
export { TOOLSET_TOOL_ID_MAP, ToolsetRegistry, ToolsetSpec, getToolset, getToolsetRegistry, get_toolset, get_toolset_registry, hasToolset, has_toolset, listToolsets, list_toolsets, registerToolset, register_toolset, resolveToolset, resolveToolsetBuiltinIds, resolveToolsets, resolveToolsetsForModel, resolve_toolset, resolve_toolsets, resolve_toolsets_for_model, toolsetToolId, unregisterToolset, unregister_toolset } from "./toolsets";
export type { ToolsetSpecConfig } from "./toolsets";
export { // Classes
TraceSink, // Enums
ContextEventType, // Functions
createContextEvent, // Types
type ContextEvent, ContextListSink, ContextNoOpSink, ContextTraceEmitter, ContextTraceSink, EventType, MessageType, TraceCtx, traceContext, trace_context, trackWorkflow, track_workflow } from "./trace";
export { A2UI, A2UINotInstalledError, A2UI_MIME_TYPE, createA2uiPart, getSchemaManager, isA2uiPart, parseA2uiResponse } from "./ui/a2ui";
export type { A2UIAdapter, A2UISystemPromptOptions, A2UIToolResultProtocol } from "./ui/a2ui";
export { AdapterBackendNotAvailableError, AdapterCreationError, AdapterRegistry, isBackendUnavailableError } from "./utils/adapter-registry";
export type { AdapterClass, AdapterFactory, AdapterKwargs } from "./utils/adapter-registry";
export { Logger, PraisonLogger, ROOT_LOGGER_NAME, StructuredFormatter, configureStructuredLogging, configure_structured_logging, getLogger, get_logger, normalizeLoggerName } from "./utils/logger";
export type { GetLoggerOptions, LogFormatter, LogLevelName, LogRecord, StructuredFormatterOptions } from "./utils/logger";
export { VERSION, __version__, getVersion } from "./version";
type ContextEvent, ContextListSink, ContextNoOpSink, ContextTraceEmitter, ContextTraceSink, EventType, MessageType, TraceCtx, traceContext, trackWorkflow } from "./trace";
export { // New: Python-parity Loop and Repeat classes
Loop, // Task class
Task, AgentFlow, DEFAULT_MAX_PARALLEL_WORKERS, If, Include, MAX_NESTING_DEPTH, NestingDepthError, PARALLEL_ON_FAILURE_MODES, Parallel, Pipeline, Repeat, Route, Workflow, WorkflowStepError, evaluateWorkflowCondition, getRecipeResolver, ifStep, if_, include, isControlFlowPattern, loop, loopPattern, parallel, parallelPattern, repeat, repeatPattern, route, routePattern, setRecipeResolver, substituteWorkflowVariables, when } from "./workflows";
export type { AgentLikeStep, FlowStep, IncludableWorkflow, LoopConfig, LoopResult, ParallelBranchError, ParallelOnFailure, RecipeResolver, RepeatConfig, RepeatContext, RepeatResult, StepContextConfig, StepExecutionConfig, StepOutputConfig, StepResult, StepRoutingConfig, TaskConfig, WorkflowContext } from "./workflows";
export { YAMLWorkflowParser, createWorkflowFromYAML, loadWorkflowFromFile, parseYAMLWorkflow, validateWorkflowDefinition } from "./workflows/yaml-parser";
Task, AgentFlow, Pipeline, Repeat, Workflow, loop, loopPattern, parallel, repeat, repeatPattern, route } from "./workflows";
export type { LoopConfig, LoopResult, RepeatConfig, RepeatContext, RepeatResult, StepContextConfig, StepExecutionConfig, StepOutputConfig, StepResult, StepRoutingConfig, TaskConfig, WorkflowContext } from "./workflows";
export { createWorkflowFromYAML, loadWorkflowFromFile, parseYAMLWorkflow, validateWorkflowDefinition } from "./workflows/yaml-parser";
```
# Optional Plugins
+10
View File
@@ -0,0 +1,10 @@
# Deployment infra has moved
Deployment assets are no longer at the monorepo root.
| Asset | New location |
|-------|----------------|
| Gateway Helm chart | [`src/praisonai-bot/infra/helm/praisonai-gateway/`](../src/praisonai-bot/infra/helm/praisonai-gateway/) |
| Agents API Helm, Compose, Starters | [`src/praisonai-deploy/infra/`](../src/praisonai-deploy/infra/) |
See [`src/praisonai-deploy/infra/README.md`](../src/praisonai-deploy/infra/README.md).
+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=4.7.12" \
"praisonai>=4.6.162" \
"praisonai[chat]" \
"embedchain[github,youtube]"
+1 -1
View File
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=4.7.12" \
"praisonai>=4.6.162" \
"praisonai[ui]" \
"praisonai[chat]" \
"praisonai[realtime]" \
+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=4.7.12" \
"praisonai>=4.6.162" \
"praisonai[ui]" \
"praisonai[crewai]"
+5 -5
View File
@@ -178,19 +178,19 @@ def example_url_parsing():
print("\n=== Example 9: URL Parsing ===")
# Memory supports URL connection strings
# Supported schemes: sqlite://, mongodb://, mongodb+srv://
# Supported: postgresql://, redis://, sqlite:///, mongodb://
agent = Agent(
instructions="You are a database-backed assistant",
memory="mongodb://user:password@localhost:27017/praisonai",
memory="postgresql://postgres:password@localhost:5432/praisonai",
)
print("Memory URL parsed: memory enabled")
# Also works with presets
agent2 = Agent(
instructions="You are a sqlite-backed assistant",
memory="sqlite", # Preset name
instructions="You are a redis-backed assistant",
memory="redis", # Preset name
)
print("Memory preset 'sqlite': configured")
print("Memory preset 'redis': configured")
def example_knowledge_sources():
+4 -4
View File
@@ -8,15 +8,15 @@ agent = Agent(
)
# With preset
agent_sqlite = Agent(
agent_redis = Agent(
instructions="You are a helpful assistant.",
memory="sqlite", # Uses sqlite preset
memory="redis", # Uses redis preset
)
# With URL
agent_mongo = Agent(
agent_postgres = Agent(
instructions="You are a helpful assistant.",
memory="mongodb://localhost/mydb", # URL auto-detected
memory="postgresql://localhost/mydb", # URL auto-detected
)
if __name__ == "__main__":
+1 -1
View File
@@ -140,4 +140,4 @@ Key fixes applied:
4. String-based LLM guardrails are also supported
The guardrail will automatically retry (up to max_retries times) if validation fails.
""")
""")
-43
View File
@@ -1,32 +1,5 @@
# MCP Server Examples
## Install (SHA-256)
Pin GitHub Release **v0.6.0** and verify `SHA256SUMS`. Website `install.sh` / `install.ps1` abort on mismatch.
https://github.com/LinespottingOrg/GrokBuildRemote-Agents/releases/tag/v0.6.0
https://github.com/LinespottingOrg/GrokBuildRemote-Agents/blob/main/docs/PINNED-INSTALL.md
```
96cef605d3e030ccef99d27ea6240e0d3b668dd045e6b5b9e585c9fd03c6ef23 gbr-agent-darwin-amd64
de7e065ef2cf6877b3b2cd04679a67b627f876337f529247e236204543e4062c gbr-agent-darwin-arm64
a50a5c41993e6531a3b477eb409ccc845212bf541384dc803061c80657f86719 gbr-agent-linux-amd64
5bfd22c7110234942c4c02ff8154b836d0af45a9422c178a4f52010187d40061 gbr-agent-linux-arm64
f773b89fd31310172b756e0593e0f3b2382b0a3440af2a7d0a8b3073b0c23e27 gbr-agent-windows-amd64.exe
8fb9efcbc7e2ac91c11964944bf0f45e31bb23f4356d9dcb4b305d7cb9b0fe8c gbr-agent-windows-arm64.exe
```
```bash
VER=v0.6.0
BASE=https://github.com/LinespottingOrg/GrokBuildRemote-Agents/releases/download/$VER
# swap darwin-arm64 for your OS/arch
curl -fsSL -o gbr-agent-darwin-arm64 "$BASE/gbr-agent-darwin-arm64"
curl -fsSL -o SHA256SUMS "$BASE/SHA256SUMS"
shasum -a 256 -c SHA256SUMS --ignore-missing
gbr-agent pair && gbr-agent run
```
Examples demonstrating the MCP Server v2 features per MCP Protocol Version 2025-11-25.
## Features Demonstrated
@@ -157,19 +130,3 @@ These examples comply with MCP Protocol Version 2025-11-25:
- `destructiveHint`: true
- `idempotentHint`: false
- `openWorldHint`: true
## Build Remote Agent (`gbr_pair.py`)
Pair a phone running [Build Remote Agent](https://grokbuildremote.com/) to a PraisonAI agent via MCP stdio `gbr-mcp` (protocol `gbr/1`). Requires `gbr-agent pair && gbr-agent run`. Phone is spectator. Never put mailbox keys in the example.
```bash
python gbr_pair.py
```
## What the phone sees
**Terminal windows** on this PC (machine-wide mailbox). Not headless OpenCode / CodeNomad sidecar / Electron. `:8788` in a sidecar is Bot API JSON, not a transcript.
https://github.com/LinespottingOrg/GrokBuildRemote-Agents/blob/main/docs/WHAT-THE-PHONE-SEES.md
https://grokbuildremote.com/integrations.html
-76
View File
@@ -1,76 +0,0 @@
"""
Build Remote Agent pairing example.
Pair a phone (Build Remote Agent) to this PraisonAI session through the
free MIT gbr-agent. Protocol gbr/1. Phone is spectator + veto.
Requirements:
- gbr-agent v0.6.0+ (`gbr-agent pair && gbr-agent run`)
- GrokBuildRemote-Agents cloned; `npm install` in mcp/gbr-mcp
- pip install praisonaiagents[mcp]
Usage:
python gbr_pair.py
Never put mailbox keys in this file. Phone Settings → Bot API is the
only place the relay key is copied.
Independent product by Linespotting AB. Not affiliated with xAI or SpaceX.
https://grokbuildremote.com/
"""
from praisonaiagents import Agent, MCP
# Clone https://github.com/LinespottingOrg/GrokBuildRemote-Agents.git
## Install (SHA-256)
Pin GitHub Release **v0.6.0** and verify `SHA256SUMS`. Website `install.sh` / `install.ps1` abort on mismatch.
https://github.com/LinespottingOrg/GrokBuildRemote-Agents/releases/tag/v0.6.0
https://github.com/LinespottingOrg/GrokBuildRemote-Agents/blob/main/docs/PINNED-INSTALL.md
```
96cef605d3e030ccef99d27ea6240e0d3b668dd045e6b5b9e585c9fd03c6ef23 gbr-agent-darwin-amd64
de7e065ef2cf6877b3b2cd04679a67b627f876337f529247e236204543e4062c gbr-agent-darwin-arm64
a50a5c41993e6531a3b477eb409ccc845212bf541384dc803061c80657f86719 gbr-agent-linux-amd64
5bfd22c7110234942c4c02ff8154b836d0af45a9422c178a4f52010187d40061 gbr-agent-linux-arm64
f773b89fd31310172b756e0593e0f3b2382b0a3440af2a7d0a8b3073b0c23e27 gbr-agent-windows-amd64.exe
8fb9efcbc7e2ac91c11964944bf0f45e31bb23f4356d9dcb4b305d7cb9b0fe8c gbr-agent-windows-arm64.exe
```
```bash
VER=v0.6.0
BASE=https://github.com/LinespottingOrg/GrokBuildRemote-Agents/releases/download/$VER
# swap darwin-arm64 for your OS/arch
curl -fsSL -o gbr-agent-darwin-arm64 "$BASE/gbr-agent-darwin-arm64"
curl -fsSL -o SHA256SUMS "$BASE/SHA256SUMS"
shasum -a 256 -c SHA256SUMS --ignore-missing
gbr-agent pair && gbr-agent run
```
# then point args at mcp/gbr-mcp/bin/gbr-mcp.js (loopback stdio).
gbr = Agent(
instructions=(
"You can attach a phone spectator via Build Remote Agent. "
"Use GBR tools only to diagnose or attach the local Bot API "
"at 127.0.0.1:8788. Do not request mailbox keys."
),
llm="gpt-4o-mini",
tools=MCP(
"node",
args=["GrokBuildRemote-Agents/mcp/gbr-mcp/bin/gbr-mcp.js"],
),
)
if __name__ == "__main__":
gbr.start(
"Check Build Remote Agent health on 127.0.0.1:8788 and list sessions."
)
## What the phone sees
**Terminal windows** on this PC (machine-wide mailbox). Not headless OpenCode / CodeNomad sidecar / Electron. `:8788` in a sidecar is Bot API JSON, not a transcript.
https://github.com/LinespottingOrg/GrokBuildRemote-Agents/blob/main/docs/WHAT-THE-PHONE-SEES.md
https://grokbuildremote.com/integrations.html
@@ -1,110 +0,0 @@
"""
HOL Guard Tool Policy Example - PraisonAI Agents
Demonstrates using HOL Guard (an optional, external runtime security engine)
at the existing `wrap_tool_call` middleware boundary to inspect command-bearing
tool calls *before* they execute.
HOL Guard is installed and versioned separately. Its side-effect-free CLI can
classify a shell command before execution:
hol-guard command test '<command>' --json
Fail-closed policy: the wrapped tool only runs when Guard returns an explicit
allow/benign classification. Review/block/unknown verdicts, malformed output,
timeouts, or CLI failures all short-circuit the call so the tool never runs.
pip install hol-guard # optional, external dependency
"""
import json
import shutil
import subprocess
from praisonaiagents import Agent, tool
from praisonaiagents.hooks import wrap_tool_call, ToolRequest, ToolResponse
COMMAND_BEARING_TOOLS = {"run_shell"}
GUARD_TIMEOUT_SECONDS = 10
@tool
def run_shell(command: str) -> str:
"""Run a shell command and return its output."""
result = subprocess.run(
command, shell=True, capture_output=True, text=True
)
return result.stdout or result.stderr
def _inspect_with_hol_guard(command: str) -> bool:
"""Return True only if HOL Guard explicitly allows the command.
Fails closed (returns False) on any ambiguity: missing CLI, non-zero exit,
timeout, malformed JSON, or a non-allow verdict.
"""
if not shutil.which("hol-guard"):
return False
try:
completed = subprocess.run(
["hol-guard", "command", "test", command, "--json"],
capture_output=True,
text=True,
timeout=GUARD_TIMEOUT_SECONDS,
)
except (subprocess.TimeoutExpired, OSError):
return False
if completed.returncode != 0:
return False
try:
verdict = json.loads(completed.stdout)
except (json.JSONDecodeError, ValueError):
return False
if not isinstance(verdict, dict):
return False
decision = str(verdict.get("decision", "")).lower()
return decision in ("allow", "benign")
@wrap_tool_call
def hol_guard_tool_policy(request: ToolRequest, call_next):
"""Gate command-bearing tools through HOL Guard before execution."""
if request.tool_name not in COMMAND_BEARING_TOOLS:
return call_next(request)
command = request.arguments.get("command", "")
if _inspect_with_hol_guard(command):
return call_next(request)
blocked_message = "Blocked by HOL Guard: command was not explicitly allowed."
# Surface the denial in `result` too: the agent tool executor returns
# `ToolResponse.result` to the model and discards `error`, so a denial with
# only `error` set would reach the model as a null result. Keeping `error`
# populated preserves the signal for after_tool hooks and logging.
return ToolResponse(
tool_name=request.tool_name,
result=blocked_message,
error=blocked_message,
context=request.context,
)
agent = Agent(
name="GuardedShellBot",
instructions="You run shell commands only when they are safe.",
tools=[run_shell],
hooks=[hol_guard_tool_policy],
)
if __name__ == "__main__":
print("HOL Guard tool policy registered on 'run_shell'.")
print("Commands are only executed when Guard returns an explicit allow.")
print("\n✓ HOL Guard example complete")
@@ -1,18 +1,16 @@
"""
Chroma Knowledge Store - Agent-First Example
Qdrant Knowledge Store - Agent-First Example
Demonstrates using Chroma (a shipped vector store) for knowledge-based RAG
with an Agent.
To use a provider that is not shipped (e.g. Qdrant), register an adapter
first with ``register_knowledge_adapter("qdrant", MyQdrantAdapter)`` and then
pass ``"provider": "qdrant"``.
Demonstrates using Qdrant for knowledge-based RAG with an Agent.
Requirements:
pip install "praisonai[tools]"
Docker Setup:
docker run -d --name praison-qdrant -p 6333:6333 qdrant/qdrant
Run:
python knowledge_chroma.py
python knowledge_qdrant.py
Expected Output:
Agent responds using knowledge from documents
@@ -46,16 +44,17 @@ doc_path = os.path.join(temp_dir, "guide.txt")
with open(doc_path, "w") as f:
f.write(sample_doc)
print("=== Chroma Knowledge Store Demo (Agent-First) ===")
print("=== Qdrant Knowledge Store Demo (Agent-First) ===")
# Agent-first approach: use knowledge parameter with a shipped vector store
# Agent-first approach: use knowledge parameter with Qdrant
agent = Agent(
name="KnowledgeAssistant",
instructions="You are a helpful assistant with access to technical documentation.",
knowledge={
"sources": [doc_path],
"vector_store": {
"provider": "chroma",
"provider": "qdrant",
"url": "http://localhost:6333",
},
}
)
@@ -71,4 +70,4 @@ print("\n=== Demo Complete ===")
# --- Advanced: Direct Store Usage ---
# from praisonai.persistence.factory import create_knowledge_store
# store = create_knowledge_store("chroma")
# store = create_knowledge_store("qdrant", url="http://localhost:6333")
+3 -2
View File
@@ -14,7 +14,7 @@ Expected Output:
Agent responds with state persisted to Redis
"""
from praisonaiagents import Agent, MemoryConfig
from praisonaiagents import Agent
from praisonaiagents.db import db
print("=== Redis State Store (Agent-First) ===")
@@ -28,7 +28,8 @@ my_db = db(
agent = Agent(
name="Assistant",
instructions="You are a helpful assistant.",
memory=MemoryConfig(db=my_db, session_id="redis-state-example"),
memory=my_db,
session_id="redis-state-example"
)
# Chat - state is automatically managed via Redis
@@ -1,73 +0,0 @@
"""
Meeting Notes Agent
An AI meeting-notes pipeline composed entirely from existing PraisonAI
primitives — no new SDK surface required:
1. AudioAgent.transcribe() -> Whisper speech-to-text
2. Agent (summary) -> structured summary + action items
3. Agent (knowledge=[...]) -> index transcript for cross-meeting Q&A
Usage:
python meeting-notes-agent.py path/to/recording.mp3
Requires: OPENAI_API_KEY (or any LiteLLM-supported STT/LLM provider).
"""
import sys
from praisonaiagents import Agent, AudioAgent
def transcribe_meeting(audio_path: str) -> str:
"""Speech-to-text using the built-in Whisper AudioAgent."""
stt = AudioAgent(llm="openai/whisper-1")
return stt.transcribe(audio_path)
def summarize_meeting(transcript: str) -> str:
"""Summarize decisions, topics, and action items."""
notetaker = Agent(
name="Meeting Notetaker",
instructions=(
"You are an expert meeting notetaker. Given a raw transcript, produce:\n"
"1. A concise summary (3-5 sentences).\n"
"2. Key decisions made.\n"
"3. Action items as a checklist with owners when mentioned.\n"
"Only include action items that were actually spoken."
),
)
return notetaker.start(f"Transcript:\n\n{transcript}")
def build_qa_agent(transcript_path: str) -> Agent:
"""Index the transcript for semantic search and cross-meeting Q&A."""
return Agent(
name="Meeting Q&A",
instructions="Answer questions using only the indexed meeting transcripts. Cite what was said.",
knowledge=[transcript_path],
)
def main() -> None:
if len(sys.argv) < 2:
print("Usage: python meeting-notes-agent.py <audio_file>")
sys.exit(1)
audio_path = sys.argv[1]
transcript = transcribe_meeting(audio_path)
transcript_path = "meeting_transcript.txt"
with open(transcript_path, "w", encoding="utf-8") as f:
f.write(transcript)
notes = summarize_meeting(transcript)
print("\n=== Meeting Notes ===\n")
print(notes)
qa = build_qa_agent(transcript_path)
print("\n=== Q&A ===\n")
print(qa.start("What were the main action items and who owns them?"))
if __name__ == "__main__":
main()
@@ -23,7 +23,6 @@ from praisonaiagents.hooks.verification import (
VerificationResult,
BaseVerificationHook,
CommandVerificationHook,
FileCheckHook,
)
@@ -101,19 +100,7 @@ def main():
print(f" Name: {cmd_hook.name}")
print(f" Command: {cmd_hook.command}")
print(f" Timeout: {cmd_hook.timeout_seconds}s")
# 6. FileCheckHook: declarative, no-shell completion gate
print("\n6. FileCheckHook (declarative completion gate):")
file_hook = FileCheckHook(
name="changelog", path="CHANGELOG.md", non_empty=True
)
file_result = file_hook.run()
print(f" {file_hook.name}: success={file_result.success} — {file_result.output}")
print(
" Gate wiring: when verification_hooks are configured, the autonomous "
"loop cannot finalise with a success outcome while a blocking hook fails."
)
print("\n" + "=" * 60)
print("✓ Verification hooks example completed successfully!")
print("=" * 60)
+2 -2
View File
@@ -7,12 +7,12 @@ allowed_dirs = [
"/path/to/other/allowed/dir"
]
# Pass the executable as the first token and the allowed directories as args
# Use a single string command with allowed directories
filesystem_agent = Agent(
instructions="""You are a helpful assistant that can interact with the filesystem.
Use the available tools when relevant to manage files and directories.""",
llm="gpt-4o-mini",
tools=MCP("npx", args=["-y", "@modelcontextprotocol/server-filesystem", *allowed_dirs])
tools=MCP("npx -y @modelcontextprotocol/server-filesystem", args=allowed_dirs)
)
filesystem_agent.start("List files in the allowed directories")
@@ -1,34 +0,0 @@
"""
Basic example of using EvoLink.AI with PraisonAI.
"""
import os
from praisonaiagents import Agent
api_key = os.getenv("EVOLINK_API_KEY")
if not api_key:
raise RuntimeError(
"EVOLINK_API_KEY is not set. Set it before running this example: "
"export EVOLINK_API_KEY=your_api_key"
)
agent = Agent(
instructions="You are a helpful assistant",
llm={
"model": "openai/gpt-5.2",
"api_key": api_key,
"base_url": "https://direct.evolink.ai/v1",
},
)
response = agent.start("Hello! Share one practical AI automation idea.")
print(response)
coding_task = """
Write a Python function that validates whether a string is a palindrome.
Include a short docstring and ignore spaces and capitalization.
"""
response = agent.start(coding_task)
print(response)
-28
View File
@@ -1,28 +0,0 @@
"""OpenRouter video generation via VideoAgent.
Requires OPENROUTER_API_KEY. Optionally set OPENROUTER_REFERER and
OPENROUTER_APP_TITLE for OpenRouter dashboard attribution.
The openrouter/ prefix is required so the request routes to OpenRouter.
Discover exact model ids at https://openrouter.ai/api/v1/videos/models.
"""
from praisonaiagents import VideoAgent
agent = VideoAgent(llm="openrouter/google/veo-3.1")
video = agent.generate(
"A serene mountain landscape at sunset, locked camera",
duration=6,
resolution="720p",
aspect_ratio="16:9",
generate_audio=False,
)
print(f"Video ID: {video.id}")
completed = agent.wait_for_completion(video.id)
if completed.status == "completed":
agent.download(video.id, "openrouter_veo.mp4")
print("Saved openrouter_veo.mp4")
else:
print(f"Generation ended with status: {completed.status}")
+1 -1
View File
@@ -15,4 +15,4 @@ if completed.status == "completed":
# List all videos
videos = agent.list()
# Models: openai/sora-2, gemini/veo-3.1-generate-preview, azure/sora-2
# Models: openai/sora-2, gemini/veo-3.0-generate-preview, azure/sora-2
+1 -1
View File
@@ -24,7 +24,7 @@ Examples for all PraisonAI server types and the unified endpoints CLI.
| `praisonai serve unified --port 8765` | Start unified server |
| `praisonai serve agents --file agents.yaml --port 8000` | Start agents API |
| `praisonai serve recipe --port 8765` | Start recipe server |
| `praisonai mcp serve --transport http-stream --port 8080` | Start MCP server |
| `praisonai serve mcp --transport http --port 8080` | Start MCP server |
| `praisonai serve tools --port 8081` | Start tools MCP server |
| `praisonai serve a2a --port 8082` | Start A2A server |
| `praisonai serve a2u --port 8083` | Start A2U server |
+1 -1
View File
@@ -13,7 +13,7 @@ Usage:
python mcp_http_server.py
# Or use CLI:
praisonai mcp serve --transport http-stream --port 8080
praisonai serve mcp --transport http --port 8080
# Test with curl:
curl http://localhost:8080/mcp/tools
+3 -3
View File
@@ -14,9 +14,9 @@ datasets:
# A small verified subset for a cheap smoke run. Verify against the registry
# with `harbor datasets list` before editing.
task_names:
- terminal-bench/build-pmars
- terminal-bench/bn-fit-modify
- terminal-bench/break-filter-js-from-html
- hello-world
- fix-permissions
- csv-to-parquet
agents:
- import_path: examples.terminal_bench.praisonai_code_agent:PraisonAICodeAgent
+11 -13
View File
@@ -16,13 +16,9 @@ Architecture:
Harbor Container → `praisonai code -p --output json "TASK" --dangerously-skip-approval`
Notes:
- `--dangerously-skip-approval` registers an always-approve backend on the
approval registry (plus PRAISON_APPROVAL_MODE=auto /
PRAISONAI_TOOL_SAFETY=off) so the assistant runs fully autonomously in the
container. Before that registry wiring existed the flag left every
critical-tool prompt in place and the non-TTY container auto-denied all of
them, so the agent could not run a single shell command while still
exiting 0 -- one cause of the 0.000 scores in #3932.
- `--dangerously-skip-approval` sets PRAISON_APPROVAL_MODE=auto +
PRAISONAI_TOOL_SAFETY=off so the assistant runs fully autonomously in the
container (no approval hang in a non-TTY session).
- The JSON envelope provides token, cost, session, and status metadata while
the real exit status still surfaces install/auth/startup failures.
- The base `praisonai` package is sufficient; heavy `code` extras are not
@@ -85,19 +81,21 @@ class PraisonAICodeAgent(BaseInstalledAgent):
model = self.model_name or "openai/gpt-4o-mini"
command = (
"log_file=$(mktemp /tmp/praisonai_code.XXXXXX); "
"chmod 600 \"$log_file\"; "
# Private per-run log (mktemp, 0600) instead of a predictable
# /tmp/praisonai_code.log path another process could pre-create as a
# symlink to redirect/clobber a file writable by the agent user.
"log=$(mktemp /tmp/praisonai_code.XXXXXX.log); "
f"praisonai code -p --output json {shlex.quote(instruction)} "
f"--dangerously-skip-approval "
f"--model {shlex.quote(model)} "
"> \"$log_file\" 2>&1; "
'> "$log" 2>&1; '
# Capture the real exit status so install/auth/startup failures still
# surface instead of being silently masked (Harbor otherwise records a
# completed attempt for a crashed agent).
"status=$?; "
# Echo the envelope so Harbor captures it for post-run accounting.
"cat \"$log_file\"; "
"rm -f \"$log_file\"; "
# Echo the envelope so Harbor captures it for post-run accounting,
# then remove the private log.
'cat "$log"; rm -f "$log"; '
# `praisonai code` normally exits 0 (Harbor grades by task
# verification), so a genuine benchmark miss won't fail here — but a
# nonzero status means the assistant itself failed to run.
@@ -34,7 +34,6 @@ except ImportError as e:
try:
from praisonaiagents import Agent
from praisonaiagents.approval import get_approval_registry, AutoApproveBackend
from praisonaiagents.config.feature_configs import ExecutionConfig
except ImportError as e:
raise ImportError(
f"PraisonAI not installed: {e}\n"
@@ -42,13 +41,6 @@ except ImportError as e:
) from e
TB_EXECUTION = ExecutionConfig(
max_tool_calls_per_turn=80,
max_iter=40,
max_steps=80,
)
class PraisonAIExternalAgent(BaseAgent):
"""External PraisonAI agent that drives a Harbor container environment."""
@@ -150,14 +142,26 @@ class PraisonAIExternalAgent(BaseAgent):
),
tools=[bash_tool],
llm=self.model_name or "openai/gpt-4o-mini",
execution=TB_EXECUTION,
)
# Execute one deliberately long coding turn. Harbor's hidden verifier
# is the only completion authority; model or pytest text is not.
# Execute the agent with outer loop to handle premature stopping
print(f"🚀 PraisonAI Agent starting task: {instruction[:100]}...")
result = await agent.achat(instruction)
print("✅ PraisonAI Agent completed task")
for _iter in range(19):
result_str = str(result)
# Stop if test passed
if any(sig in result_str.lower() for sig in [
" passed", "passed ", "test passed", "all tests pass", "1 passed"
]):
break
result = await agent.achat(
"The task is NOT complete yet — keep working. Run bash_tool commands now. "
"If you haven't already: (1) read all files in /app/, "
"(2) run the test to see the exact failure, "
"(3) implement a fix, (4) run the test again. "
"Repeat until the test passes. What is your next bash_tool command?"
)
print(f"✅ PraisonAI Agent completed task")
# Populate Harbor context with metadata
self._populate_context(agent, context, result)
@@ -199,10 +203,6 @@ class PraisonAIExternalAgent(BaseAgent):
"tools_used": ["bash_tool"],
"framework": "praisonai",
"version": self.version(),
"stop_reason": getattr(agent, "last_stop_reason", None),
"max_tool_calls_per_turn": TB_EXECUTION.max_tool_calls_per_turn,
"max_iter": TB_EXECUTION.max_iter,
"max_steps": TB_EXECUTION.max_steps,
}
except Exception as e:
@@ -218,4 +218,4 @@ if __name__ == "__main__":
print(" --model openai/gpt-4o-mini")
print()
print("Dependencies:")
print(" pip install harbor praisonaiagents")
print(" pip install harbor praisonaiagents")
@@ -1,39 +0,0 @@
"""Static regression checks for the in-tree Harbor adapter configuration."""
from pathlib import Path
import yaml
EXAMPLES = Path(__file__).parent
def test_external_adapter_sets_coding_execution_budget():
source = (EXAMPLES / "praisonai_external_agent.py").read_text(encoding="utf-8")
assert "ExecutionConfig" in source
assert "max_tool_calls_per_turn=80" in source
assert "max_iter=40" in source
assert "max_steps=80" in source
def test_external_adapter_does_not_guess_completion_from_pytest_text():
source = (EXAMPLES / "praisonai_external_agent.py").read_text(encoding="utf-8")
assert '"1 passed"' not in source
assert '"test passed"' not in source
def test_smoke_yaml_uses_terminal_bench_21_registry_ids():
job = yaml.safe_load(
(EXAMPLES / "job_code_smoke.yaml").read_text(encoding="utf-8")
)
names = job["datasets"][0]["task_names"]
assert names
assert all(name.startswith("terminal-bench/") for name in names)
assert names == [
"terminal-bench/build-pmars",
"terminal-bench/bn-fit-modify",
"terminal-bench/break-filter-js-from-html",
]
+10 -14
View File
@@ -1,25 +1,17 @@
"""Qdrant Vector Store - Agent-First Example
"""Qdrant Vector Store - Agent-First Example (requires Docker)
Qdrant is not a shipped adapter. Register one first, then use it via the
``knowledge`` parameter. If no adapter is registered this example skips
cleanly instead of crashing.
from praisonaiagents.knowledge.adapters import register_knowledge_adapter
register_knowledge_adapter("qdrant", MyQdrantAdapter)
Docker: docker run -d --name qdrant -p 6333:6333 qdrant/qdrant
"""
import sys
import os
from praisonaiagents import Agent
from praisonaiagents.knowledge.adapters import list_knowledge_adapters
if "qdrant" not in list_knowledge_adapters():
print("SKIPPED: Qdrant - no 'qdrant' adapter registered")
sys.exit(0)
# Agent-first approach: use knowledge parameter with Qdrant
url = os.getenv("QDRANT_URL", "http://localhost:6333")
# Agent-first approach: use knowledge parameter with a registered Qdrant adapter
agent = Agent(
name="Assistant",
instructions="You are a helpful assistant with access to documents.",
knowledge={"sources": ["./docs/guide.pdf"], "vector_store": {"provider": "qdrant"}}
knowledge={"sources": ["./docs/guide.pdf"], "vector_store": {"provider": "qdrant", "url": url}}
)
# Chat - agent uses knowledge for RAG
@@ -27,3 +19,7 @@ response = agent.chat("What information do you have?")
print(f"Response: {response}")
print("PASSED: Qdrant with Agent")
# --- Advanced: Direct Store Usage ---
# from praisonai.persistence import create_knowledge_store
# store = create_knowledge_store("qdrant", url=url)
+4 -3
View File
@@ -27,10 +27,11 @@ mcp:
enabled: true
time:
# Time and date utilities (published Python MCP server via uvx)
command: uvx
# Time and date utilities
command: npx
args:
- "mcp-server-time"
- "-y"
- "@modelcontextprotocol/server-time"
env: {}
enabled: true
@@ -1,45 +0,0 @@
# Run a whole workflow's shell/file tools inside ONE shared remote sandbox.
#
# praisonai examples/yaml/workflows/remote_sandbox_workflow.yaml
#
# `tools_run_on:` is the only line that differs from a normal local workflow.
# Every step shares the same sandbox and working directory, so the file the
# writer creates is there for the reader. Thinking and orchestration still run
# on your machine, and the sandbox is torn down when the run finishes.
#
# Places: docker | e2b | modal | daytona | flyio | tenki | local
# subprocess | sandlock | ssh | novita
# - docker needs a local Docker daemon and nothing else
# - e2b / modal / daytona / flyio need that provider's API key
# - sandlock is a kernel-enforced local boundary (Linux 6.12+)
# - subprocess is process separation only, NOT a security boundary
#
# `run_on:` is still accepted here as the old spelling for this key.
#
# See what is still running: praisonai managed ps
# Reclaim strays: praisonai managed stop --all
name: remote-sandbox-demo
tools_run_on: docker
agents:
writer:
role: File Writer
goal: Write files into the shared workspace
instructions: >
You have write_file and execute_command running on a remote Linux
sandbox. Use them; never say you cannot run commands.
reader:
role: File Reader
goal: Read files back out of the shared workspace
instructions: >
You have read_file and execute_command running on the same remote
sandbox the writer used. Use them.
steps:
- agent: writer
action: "Use write_file to put exactly 'hello from step 1' in /workspace/note.txt"
- agent: reader
action: "Use read_file on /workspace/note.txt and report its exact contents"
@@ -99,12 +99,13 @@ pip index versions praisonaiagents | head -1
pip index versions praisonai | head -1
```
13. **praisonai-platform** advisories are handled in a different repository:
[MervinPraison/PraisonAI-Platform](https://github.com/MervinPraison/PraisonAI-Platform).
It is not part of this monorepo and is not published from here. Triage, patch,
release and advisory publication all happen in that repo, on its own semver line.
Nothing in this repository imports it, so a platform advisory never requires an
SDK release.
13. Publish **praisonai-platform** (only when the advisory targets platform code):
```bash
cd /Users/praison/praisonai-package/src/praisonai-platform
praisonai publish pypi
# or: uv lock && uv build && uv publish --token $PYPI_TOKEN
```
Platform releases are **independent** of `praisonai` / `praisonaiagents` version numbers. A platform-only fix (e.g. `0.1.8`) does **not** require bumping the main SDK.
14. Publish **praisonai (npm / TypeScript)** (only when the advisory targets `src/praisonai-ts`):
```bash
@@ -112,8 +113,9 @@ cd /Users/praison/praisonai-package/src/praisonai-ts
npm version patch && npm publish
```
15. Verify npm when applicable:
15. Verify platform / npm when applicable:
```bash
pip index versions praisonai-platform | head -1
npm view praisonai version
```
@@ -137,7 +139,7 @@ gh api repos/MervinPraison/PraisonAI/security-advisories/<GHSA_ID> \
}
EOF
```
- `<PACKAGE_NAME>`: `praisonaiagents` (core SDK), `praisonai` (Python wrapper), or `praisonai` (npm ecosystem for TypeScript)
- `<PACKAGE_NAME>`: `praisonaiagents` (core SDK), `praisonai` (Python wrapper), `praisonai-platform` (platform layer), or `praisonai` (npm ecosystem for TypeScript)
- `<LAST_VULNERABLE_VERSION>`: version before the fix
- `<FIRST_PATCHED_VERSION>`: the version just published with the fix
- Always include credits for the reporter
@@ -188,8 +190,8 @@ After validating each advisory in source code, assign **one** label:
| Package | Version line | Notes |
|---|---|---|
| `praisonaiagents` + `praisonai` | Aligned patch (e.g. `1.6.62` / `4.6.62`) | Main SDK + wrapper; bump together via `bump_and_release.py` |
| `praisonai-platform` | Independent semver (e.g. `0.1.8`) | Platform-only fixes; users need **not** upgrade the main SDK |
| `praisonai` (npm) | Independent semver (e.g. `1.7.2`) | TypeScript SDK; publish from `src/praisonai-ts` |
| `praisonai-platform` | Independent semver, **separate repository** | Not released from this repo — see [PraisonAI-Platform](https://github.com/MervinPraison/PraisonAI-Platform) |
Always set the advisory `patched_versions` to the **package that actually contains the fix**. Do not imply users must upgrade all packages when only one changed.
@@ -215,11 +217,9 @@ Close (or publish advisory with no code change only if already mitigated) when:
|---|---|---|---|
| praisonaiagents | pip | src/praisonai-agents/praisonaiagents/ | tools/, mcp/, memory/, agent/, sandbox/ |
| praisonai | pip | src/praisonai/praisonai/ | cli/, capabilities/, mcp_server/, sandbox/, recipe/ |
| praisonai-platform | pip | src/praisonai-platform/praisonai_platform/ | api/routes/, services/, auth |
| praisonai | npm | src/praisonai-ts/src/ | agent/, mcp/, tools/, code-mode/ |
`praisonai-platform` is **not audited from this repository** — it lives in
[MervinPraison/PraisonAI-Platform](https://github.com/MervinPraison/PraisonAI-Platform).
## RESOURCES & DOCUMENTATION
- **Official Repository**: https://github.com/MervinPraison/PraisonAI — Source code, issue tracking, and contribution guidelines
@@ -234,7 +234,7 @@ Close (or publish advisory with no code change only if already mitigated) when:
- ALWAYS set patched_versions before publishing advisory
- ALWAYS publish to PyPI/npm BEFORE publishing advisories (so Dependabot can point users to a fix)
- Python SDK: `praisonai publish pypi` from `src/praisonai-agents`, then `bump_and_release.py` for the wrapper
- Platform-only: released from the separate [PraisonAI-Platform](https://github.com/MervinPraison/PraisonAI-Platform) repo, never from here
- Platform-only: `praisonai publish pypi` from `src/praisonai-platform` — no SDK bump required
- npm: `npm publish` from `src/praisonai-ts` when the TypeScript SDK is affected
- ALWAYS request CVE AFTER publishing advisory
- Use generic commit messages: "refactor: harden X" not "fix: SQL injection in Y"
@@ -0,0 +1,9 @@
{
"short_term_limit": 100,
"long_term_limit": 1000,
"episodic_retention_days": 30,
"enable_summarization": false,
"summarization_threshold": 50,
"importance_threshold": 0.7,
"auto_promote": true
}
@@ -0,0 +1,21 @@
{
"root_path": "/var/folders/0b/t9c4hwvx6lg6jk58gw8w_dp80000gn/T/tmp6_tn0h1t",
"total_files": 0,
"total_tokens": 11,
"levels": 3,
"built_at": "2026-02-02T17:47:19.241146",
"nodes": {
"/var/folders/0b/t9c4hwvx6lg6jk58gw8w_dp80000gn/T/tmp6_tn0h1t": {
"path": "/var/folders/0b/t9c4hwvx6lg6jk58gw8w_dp80000gn/T/tmp6_tn0h1t",
"level": 3,
"summary": "Project with 0 folders and 0 root files: ",
"children": [],
"token_count": 11,
"created_at": "2026-02-02T17:47:19.241224",
"metadata": {
"folder_count": 0,
"root_file_count": 0
}
}
}
}
@@ -0,0 +1,35 @@
{
"root_path": "/var/folders/0b/t9c4hwvx6lg6jk58gw8w_dp80000gn/T/tmp7wxr57wm",
"total_files": 1,
"total_tokens": 16,
"levels": 2,
"built_at": "2026-02-02T17:47:19.243732",
"nodes": {
"/var/folders/0b/t9c4hwvx6lg6jk58gw8w_dp80000gn/T/tmp7wxr57wm/src/app.py": {
"path": "/var/folders/0b/t9c4hwvx6lg6jk58gw8w_dp80000gn/T/tmp7wxr57wm/src/app.py",
"level": 1,
"summary": "# Application code",
"children": [],
"token_count": 5,
"created_at": "2026-02-02T17:47:19.243833",
"metadata": {
"filename": "app.py",
"relative_path": "src/app.py"
}
},
"/var/folders/0b/t9c4hwvx6lg6jk58gw8w_dp80000gn/T/tmp7wxr57wm/src": {
"path": "/var/folders/0b/t9c4hwvx6lg6jk58gw8w_dp80000gn/T/tmp7wxr57wm/src",
"level": 2,
"summary": "Folder contains 1 files: # Application code",
"children": [
"/var/folders/0b/t9c4hwvx6lg6jk58gw8w_dp80000gn/T/tmp7wxr57wm/src/app.py"
],
"token_count": 11,
"created_at": "2026-02-02T17:47:19.243852",
"metadata": {
"folder_name": "src",
"file_count": 1
}
}
}
}
@@ -0,0 +1,21 @@
{
"root_path": "/var/folders/0b/t9c4hwvx6lg6jk58gw8w_dp80000gn/T/tmp8qwfp4zl",
"total_files": 0,
"total_tokens": 11,
"levels": 3,
"built_at": "2026-02-02T17:49:37.074095",
"nodes": {
"/var/folders/0b/t9c4hwvx6lg6jk58gw8w_dp80000gn/T/tmp8qwfp4zl": {
"path": "/var/folders/0b/t9c4hwvx6lg6jk58gw8w_dp80000gn/T/tmp8qwfp4zl",
"level": 3,
"summary": "Project with 0 folders and 0 root files: ",
"children": [],
"token_count": 11,
"created_at": "2026-02-02T17:49:37.074201",
"metadata": {
"folder_count": 0,
"root_file_count": 0
}
}
}
}
@@ -0,0 +1,33 @@
{
"root_path": "/var/folders/0b/t9c4hwvx6lg6jk58gw8w_dp80000gn/T/tmp9nw2kfoj",
"total_files": 2,
"total_tokens": 14,
"levels": 1,
"built_at": "2026-02-02T17:47:19.242486",
"nodes": {
"/var/folders/0b/t9c4hwvx6lg6jk58gw8w_dp80000gn/T/tmp9nw2kfoj/utils.py": {
"path": "/var/folders/0b/t9c4hwvx6lg6jk58gw8w_dp80000gn/T/tmp9nw2kfoj/utils.py",
"level": 1,
"summary": "def helper(): return True",
"children": [],
"token_count": 7,
"created_at": "2026-02-02T17:47:19.242582",
"metadata": {
"filename": "utils.py",
"relative_path": "utils.py"
}
},
"/var/folders/0b/t9c4hwvx6lg6jk58gw8w_dp80000gn/T/tmp9nw2kfoj/main.py": {
"path": "/var/folders/0b/t9c4hwvx6lg6jk58gw8w_dp80000gn/T/tmp9nw2kfoj/main.py",
"level": 1,
"summary": "def main(): print('Hello')",
"children": [],
"token_count": 7,
"created_at": "2026-02-02T17:47:19.242630",
"metadata": {
"filename": "main.py",
"relative_path": "main.py"
}
}
}
}
@@ -0,0 +1,35 @@
{
"root_path": "/var/folders/0b/t9c4hwvx6lg6jk58gw8w_dp80000gn/T/tmp_dbottar",
"total_files": 1,
"total_tokens": 16,
"levels": 2,
"built_at": "2026-02-02T17:49:37.077857",
"nodes": {
"/var/folders/0b/t9c4hwvx6lg6jk58gw8w_dp80000gn/T/tmp_dbottar/src/app.py": {
"path": "/var/folders/0b/t9c4hwvx6lg6jk58gw8w_dp80000gn/T/tmp_dbottar/src/app.py",
"level": 1,
"summary": "# Application code",
"children": [],
"token_count": 5,
"created_at": "2026-02-02T17:49:37.077985",
"metadata": {
"filename": "app.py",
"relative_path": "src/app.py"
}
},
"/var/folders/0b/t9c4hwvx6lg6jk58gw8w_dp80000gn/T/tmp_dbottar/src": {
"path": "/var/folders/0b/t9c4hwvx6lg6jk58gw8w_dp80000gn/T/tmp_dbottar/src",
"level": 2,
"summary": "Folder contains 1 files: # Application code",
"children": [
"/var/folders/0b/t9c4hwvx6lg6jk58gw8w_dp80000gn/T/tmp_dbottar/src/app.py"
],
"token_count": 11,
"created_at": "2026-02-02T17:49:37.078011",
"metadata": {
"folder_name": "src",
"file_count": 1
}
}
}
}
@@ -0,0 +1,33 @@
{
"root_path": "/var/folders/0b/t9c4hwvx6lg6jk58gw8w_dp80000gn/T/tmpp5k48vv4",
"total_files": 2,
"total_tokens": 14,
"levels": 1,
"built_at": "2026-02-02T17:49:37.075925",
"nodes": {
"/var/folders/0b/t9c4hwvx6lg6jk58gw8w_dp80000gn/T/tmpp5k48vv4/utils.py": {
"path": "/var/folders/0b/t9c4hwvx6lg6jk58gw8w_dp80000gn/T/tmpp5k48vv4/utils.py",
"level": 1,
"summary": "def helper(): return True",
"children": [],
"token_count": 7,
"created_at": "2026-02-02T17:49:37.076036",
"metadata": {
"filename": "utils.py",
"relative_path": "utils.py"
}
},
"/var/folders/0b/t9c4hwvx6lg6jk58gw8w_dp80000gn/T/tmpp5k48vv4/main.py": {
"path": "/var/folders/0b/t9c4hwvx6lg6jk58gw8w_dp80000gn/T/tmpp5k48vv4/main.py",
"level": 1,
"summary": "def main(): print('Hello')",
"children": [],
"token_count": 7,
"created_at": "2026-02-02T17:49:37.076227",
"metadata": {
"filename": "main.py",
"relative_path": "main.py"
}
}
}
}
@@ -0,0 +1,33 @@
{
"root_path": "/var/folders/0b/t9c4hwvx6lg6jk58gw8w_dp80000gn/T/tmpwwb01831",
"total_files": 2,
"total_tokens": 16,
"levels": 1,
"built_at": "2026-02-02T17:49:37.079504",
"nodes": {
"/var/folders/0b/t9c4hwvx6lg6jk58gw8w_dp80000gn/T/tmpwwb01831/javascript.js": {
"path": "/var/folders/0b/t9c4hwvx6lg6jk58gw8w_dp80000gn/T/tmpwwb01831/javascript.js",
"level": 1,
"summary": "JavaScript web development",
"children": [],
"token_count": 7,
"created_at": "2026-02-02T17:49:37.079612",
"metadata": {
"filename": "javascript.js",
"relative_path": "javascript.js"
}
},
"/var/folders/0b/t9c4hwvx6lg6jk58gw8w_dp80000gn/T/tmpwwb01831/python.py": {
"path": "/var/folders/0b/t9c4hwvx6lg6jk58gw8w_dp80000gn/T/tmpwwb01831/python.py",
"level": 1,
"summary": "Python programming language code",
"children": [],
"token_count": 9,
"created_at": "2026-02-02T17:49:37.079658",
"metadata": {
"filename": "python.py",
"relative_path": "python.py"
}
}
}
}
@@ -0,0 +1,33 @@
{
"root_path": "/var/folders/0b/t9c4hwvx6lg6jk58gw8w_dp80000gn/T/tmpwzdvf3_7",
"total_files": 2,
"total_tokens": 16,
"levels": 1,
"built_at": "2026-02-02T17:47:19.245229",
"nodes": {
"/var/folders/0b/t9c4hwvx6lg6jk58gw8w_dp80000gn/T/tmpwzdvf3_7/javascript.js": {
"path": "/var/folders/0b/t9c4hwvx6lg6jk58gw8w_dp80000gn/T/tmpwzdvf3_7/javascript.js",
"level": 1,
"summary": "JavaScript web development",
"children": [],
"token_count": 7,
"created_at": "2026-02-02T17:47:19.245344",
"metadata": {
"filename": "javascript.js",
"relative_path": "javascript.js"
}
},
"/var/folders/0b/t9c4hwvx6lg6jk58gw8w_dp80000gn/T/tmpwzdvf3_7/python.py": {
"path": "/var/folders/0b/t9c4hwvx6lg6jk58gw8w_dp80000gn/T/tmpwzdvf3_7/python.py",
"level": 1,
"summary": "Python programming language code",
"children": [],
"token_count": 9,
"created_at": "2026-02-02T17:47:19.245395",
"metadata": {
"filename": "python.py",
"relative_path": "python.py"
}
}
}
}
+2 -23
View File
@@ -2,13 +2,6 @@
> **For AI Agents and Developers**: This document provides the complete context needed to work with the PraisonAI ecosystem, including design principles, architecture, repository structure, and implementation guidelines.
> **Active workstream — local model runtimes.** If your task touches local inference
> (Ollama, LM Studio, llama.cpp, vLLM, MLX, `transformers serve`), provider detection,
> `llm/adapters/`, or the `praisonaiagents/local/` package, read
> [`docs/local-model-layer/README.md`](docs/local-model-layer/README.md) **before editing**.
> It carries the measured ground truth, the invariants, and a file-ownership table that
> prevents concurrent work orders from corrupting each other.
---
## 1. What is PraisonAI?
@@ -214,7 +207,6 @@ and features (capabilities, bots, framework adapters) — use
| 2b | `praisonai-bot` | Bots, gateway, channel CLI, OS daemon |
| 2c | `praisonai-train` | LLM fine-tuning (Unsloth), agent training, `train` CLI |
| 3 | `praisonai` | `framework_adapters/`, capabilities, serve orchestration, dashboard |
| 1-TS | `praisonai-ts` | TypeScript/JavaScript SDK (`npm: praisonai`); canonical path `src/praisonai-ts/` |
**Cross-tier rule:** `praisonai-code` must not declare a PyPI dependency on `praisonai`. Use
[`praisonai_code._wrapper_bridge`](../praisonai-code/praisonai_code/_wrapper_bridge.py) for lazy wrapper access.
@@ -230,17 +222,6 @@ from `praisonai_train.cli.commands.*` (`train`, C10); `_WRAPPER_RESIDENT_COMMAND
Full boundary doc: [`src/praisonai/tests/C7.1_BOUNDARIES.md`](../praisonai/tests/C7.1_BOUNDARIES.md).
### 2.5 TypeScript SDK (`praisonai-ts`)
Python guidelines in this file do **not** replace the TypeScript SDK guide. For any issue, PR, or review touching `src/praisonai-ts/`:
1. Read [`src/praisonai-ts/AGENTS.md`](../praisonai-ts/AGENTS.md) — architecture, parity, triage, and PR review rules.
2. Implement fixes in **`src/praisonai-ts/`** in this monorepo (not in `praisonaiagents/`).
3. Verify with `cd src/praisonai-ts && npm install && npm run build && npm test`.
4. After merge, maintainers sync the npm mirror: `gh workflow run "Sync to praisonai-js" --repo MervinPraison/PraisonAI`.
[MervinPraison/praisonai-js](https://github.com/MervinPraison/praisonai-js) is the public npm checkout; direction is **monorepo → praisonai-js** only. Claude merge gate and `@claude` review TS changes under `src/praisonai-ts/` using the TS AGENTS.md criteria (lightweight, type-safe, Python parity where applicable).
---
## 3. Core SDK Architecture (praisonaiagents)
@@ -514,7 +495,7 @@ agent = Agent(
name="assistant",
llm="gpt-4o-mini",
memory=MemoryConfig(provider="chroma", use_long_term=True),
hooks=HooksConfig(on_tool_call=my_tool_logger, middleware=[my_validator]),
hooks=HooksConfig(before_tool=[my_validator]),
guardrail=my_guardrail
)
```
@@ -895,8 +876,6 @@ from praisonaiagents import Workflow, Route, Parallel, Loop
| CLI backends | `praisonai_code/cli_backends/` |
| Back-compat shims | `praisonai/cli/_shim.py`, `praisonai/cli/main.py` |
| C5 regression tests | `praisonai/tests/unit/test_c5_backward_compat.py` |
| TypeScript SDK | `src/praisonai-ts/` — see `src/praisonai-ts/AGENTS.md` |
| npm mirror (sync only) | [MervinPraison/praisonai-js](https://github.com/MervinPraison/praisonai-js) |
---
@@ -1095,7 +1074,7 @@ PraisonAI's reliability engineering rests on three pillars — **C**ontext, **H*
> **Evaluator status.** The `ContextEvaluator` / `HarnessEvaluator` / `LoopEvaluator` classes named in the rubric are **planned follow-ups** (PA-CHL-001–004), not yet in the repo. Until they land, the equivalent checks are covered by existing modules:
>
> - **Context** — `context/tokens.py` and `eval/tokens.py` (`estimate_tokens` / `count_tokens`) for budget; `compaction/compactor.py` (`ContextCompactor`) for compaction/retention.
> - **Harness** — `runtime/turn_context.py` for turn-context parity; `examples/runtime/example_harness.py` and `tests/` for harness/artifact checks.
> - **Harness** — `runtime/turn_context.py` for turn-context parity; `runtime/example_harness.py` and `tests/` for harness/artifact checks.
> - **Loop** — `eval/loop.py` (`EvaluationLoop`) and `agent/autonomy.py` for convergence and doom-loop guards.
>
> Keep terminology consistent with the `agent/context_agent.py` (`ContextAgent`) PRP methodology.
+56 -57
View File
@@ -17,79 +17,78 @@ This report compares **Python SDK feature categories** against **Python document
| Category | Features | Docs | Lines |
|----------|----------|------|-------|
| ✅ AGUI | 1 | 2 | 511 |
| ✅ Agent | 22 | 59 | 19076 |
| ✅ AGUI | 1 | 2 | 420 |
| ✅ Agent | 22 | 50 | 15809 |
| ✅ Agent-to-Agent (A2A) | 1 | 7 | 2996 |
| ✅ Agent-to-User (A2U) | 1 | 3 | 701 |
| ✅ Approval | 1 | 8 | 4028 |
| ✅ Agent-to-User (A2U) | 1 | 3 | 689 |
| ✅ Approval | 1 | 5 | 2537 |
| ✅ Audio | 2 | 12 | 1000 |
| ✅ Auto Generation | 5 | 12 | 3941 |
| ✅ Autonomy | 3 | 7 | 2805 |
| ✅ Bots | 7 | 38 | 15809 |
| ✅ Auto Generation | 5 | 10 | 3049 |
| ✅ Autonomy | 3 | 7 | 2744 |
| ✅ Bots | 7 | 31 | 13314 |
| ✅ Budget | 1 | 1 | 287 |
| ✅ CLI | 5 | 124 | 39199 |
| ✅ Chunking | 2 | 2 | 435 |
| ✅ CLI | 5 | 121 | 36021 |
| ✅ Chunking | 2 | 2 | 385 |
| ✅ Citations | 2 | 1 | 202 |
| ✅ Code Execution | 2 | 14 | 5022 |
| ✅ Conditions | 1 | 3 | 1386 |
| ✅ Configuration | 3 | 8 | 3341 |
| ✅ Context Management | 16 | 36 | 14384 |
| ✅ Deep Research | 8 | 2 | 591 |
| ✅ Display | 6 | 3 | 844 |
| ✅ Embeddings | 6 | 23 | 2051 |
| ✅ Evaluation | 1 | 9 | 3637 |
| ✅ Events | 1 | 2 | 782 |
| ✅ Execution | 3 | 4 | 1694 |
| ✅ Failover | 2 | 1 | 537 |
| ✅ Files | 2 | 7 | 2376 |
| ✅ Flow | 1 | 3 | 811 |
| ✅ Gateway | 7 | 97 | 37652 |
| ✅ Guardrails | 5 | 4 | 2642 |
| ✅ Handoffs | 11 | 6 | 2748 |
| ✅ Hooks | 2 | 9 | 5230 |
| ✅ Image | 1 | 11 | 1295 |
| ✅ Knowledge | 4 | 20 | 6260 |
| ✅ LLM | 3 | 15 | 5958 |
| ✅ Loops | 4 | 5 | 1478 |
| ✅ MCP | 1 | 64 | 15116 |
| ✅ Memory | 6 | 21 | 8198 |
| ✅ Code Execution | 2 | 14 | 4583 |
| ✅ Conditions | 1 | 3 | 1179 |
| ✅ Configuration | 3 | 8 | 3047 |
| ✅ Context Management | 16 | 34 | 13396 |
| ✅ Deep Research | 8 | 2 | 587 |
| ✅ Display | 6 | 3 | 741 |
| ✅ Embeddings | 6 | 23 | 2049 |
| ✅ Evaluation | 1 | 7 | 2905 |
| ✅ Events | 1 | 2 | 749 |
| ✅ Execution | 3 | 4 | 1562 |
| ✅ Failover | 2 | 1 | 432 |
| ✅ Files | 2 | 7 | 2273 |
| ✅ Flow | 1 | 3 | 788 |
| ✅ Gateway | 7 | 72 | 26391 |
| ✅ Guardrails | 4 | 4 | 2017 |
| ✅ Handoffs | 11 | 6 | 2563 |
| ✅ Hooks | 2 | 9 | 4317 |
| ✅ Image | 1 | 11 | 1255 |
| ✅ Knowledge | 4 | 13 | 4333 |
| ✅ LLM | 3 | 15 | 5192 |
| ✅ Loops | 4 | 5 | 1422 |
| ✅ MCP | 1 | 61 | 13847 |
| ✅ Memory | 6 | 18 | 7070 |
| ✅ OCR | 2 | 1 | 237 |
| ✅ Observability | 2 | 23 | 2859 |
| ✅ Observability | 2 | 23 | 2814 |
| ✅ Optimizer | 1 | 2 | 792 |
| ✅ Output | 3 | 5 | 1289 |
| ✅ Parallel Execution | 3 | 2 | 528 |
| ✅ Planning | 6 | 7 | 1827 |
| ✅ Plugins | 8 | 5 | 4112 |
| ✅ Prompts | 2 | 9 | 2043 |
| ✅ Providers | 1 | 58 | 8751 |
| ✅ Output | 3 | 5 | 1201 |
| ✅ Parallel Execution | 3 | 2 | 497 |
| ✅ Planning | 6 | 6 | 1549 |
| ✅ Plugins | 8 | 4 | 2850 |
| ✅ Prompts | 2 | 9 | 1965 |
| ✅ Providers | 1 | 56 | 7735 |
| ✅ Query | 1 | 2 | 736 |
| ✅ RAG | 5 | 15 | 3152 |
| ✅ RAG | 5 | 15 | 3003 |
| ✅ Realtime | 2 | 5 | 668 |
| ✅ Reflection | 3 | 3 | 732 |
| ✅ Retrieval | 2 | 5 | 1046 |
| ✅ Reflection | 3 | 3 | 680 |
| ✅ Retrieval | 2 | 5 | 1054 |
| ✅ Routing | 1 | 2 | 430 |
| ✅ Sandbox | 5 | 7 | 3720 |
| ✅ Security | 1 | 3 | 3642 |
| ✅ Sessions | 4 | 21 | 8388 |
| ✅ Skills | 6 | 16 | 6045 |
| ✅ Tasks | 2 | 8 | 3266 |
| ✅ Telemetry | 1 | 2 | 660 |
| ✅ Templates | 1 | 9 | 1859 |
| ✅ Tools | 12 | 147 | 45378 |
| ✅ Sandbox | 5 | 6 | 2918 |
| ✅ Security | 1 | 3 | 3206 |
| ✅ Sessions | 4 | 17 | 6963 |
| ✅ Skills | 6 | 15 | 5638 |
| ✅ Tasks | 2 | 6 | 2819 |
| ✅ Telemetry | 1 | 2 | 604 |
| ✅ Templates | 1 | 8 | 1663 |
| ✅ Tools | 12 | 141 | 41242 |
| ✅ Tracing | 3 | 2 | 139 |
| ✅ Vector Store | 1 | 12 | 1219 |
| ✅ Video | 2 | 10 | 1303 |
| ✅ Vector Store | 1 | 12 | 1211 |
| ✅ Video | 2 | 6 | 649 |
| ✅ Vision | 2 | 1 | 329 |
| ✅ Web | 3 | 10 | 2822 |
| ✅ Workflows | 5 | 21 | 8880 |
| ✅ Web | 3 | 9 | 2467 |
| ✅ Workflows | 5 | 16 | 6589 |
## Documentation Without Features
These docs exist but don't match any implemented feature category:
- ℹ️ Database (44 docs, 8292 lines)
- ℹ️ Documents (1 docs, 810 lines)
- ℹ️ Teams (1 docs, 214 lines)
- ℹ️ Database (42 docs, 6713 lines)
- ℹ️ Documents (1 docs, 787 lines)
---
@@ -6,13 +6,8 @@ Compares agent instantiation times across popular AI agent frameworks.
Usage:
python benchmarks/simple_benchmark.py
python benchmarks/simple_benchmark.py --skip-crewai --json
python benchmarks/simple_benchmark.py --json --fail-over-threshold
"""
import json
import os
import sys
import time
from typing import Literal
import argparse
@@ -21,14 +16,6 @@ import argparse
ITERATIONS = 100
WARMUP = 10
THRESHOLDS_PATH = os.path.join(os.path.dirname(__file__), 'thresholds.json')
def load_thresholds(path: str = THRESHOLDS_PATH) -> dict:
"""Load benchmark thresholds from thresholds.json."""
with open(path) as f:
return json.load(f)
def sample_tool(city: Literal['nyc', 'sf']):
"""Sample tool for benchmark testing."""
@@ -49,44 +36,35 @@ def measure_instantiation(create_fn, iterations=ITERATIONS, warmup=WARMUP):
return sum(times) / len(times)
def run_benchmark(skip_crewai: bool = False, quiet: bool = False,
iterations: int = ITERATIONS, warmup: int = WARMUP):
def run_benchmark():
"""Run the benchmark across all available frameworks (without tools)."""
results = {}
def log(msg: str = ''):
if not quiet:
print(msg)
def measure(create_fn):
return measure_instantiation(create_fn, iterations=iterations, warmup=warmup)
log('=' * 60)
log('PraisonAI Agents - Performance Benchmark')
log('=' * 60)
log(f'\nIterations: {iterations}')
log(f'Warmup: {warmup}')
log('Metric: Agent instantiation time (microseconds)\n')
print('=' * 60)
print('PraisonAI Agents - Performance Benchmark')
print('=' * 60)
print(f'\nIterations: {ITERATIONS}')
print('Metric: Agent instantiation time (microseconds)\n')
# PraisonAI (without tools)
log("Testing PraisonAI...")
print("Testing PraisonAI...")
from praisonaiagents import Agent as PraisonAgent
results['PraisonAI'] = measure(
results['PraisonAI'] = measure_instantiation(
lambda: PraisonAgent(name='Test', model='gpt-4o-mini', output="silent")
)
results['PraisonAI (LiteLLM)'] = measure(
results['PraisonAI (LiteLLM)'] = measure_instantiation(
lambda: PraisonAgent(name='Test', model='openai/gpt-4o-mini', output="silent")
)
# Other frameworks for comparison
log("Testing other frameworks...")
print("Testing other frameworks...")
try:
from agno.agent import Agent as AgnoAgent
from agno.models.openai import OpenAIChat
results['Agno'] = measure(
results['Agno'] = measure_instantiation(
lambda: AgnoAgent(model=OpenAIChat(id='gpt-4o-mini'))
)
except ImportError:
@@ -94,7 +72,7 @@ def run_benchmark(skip_crewai: bool = False, quiet: bool = False,
try:
from pydantic_ai import Agent as PydanticAgent
results['PydanticAI'] = measure(
results['PydanticAI'] = measure_instantiation(
lambda: PydanticAgent('openai:gpt-4o-mini')
)
except ImportError:
@@ -102,7 +80,7 @@ def run_benchmark(skip_crewai: bool = False, quiet: bool = False,
try:
from agents import Agent as OpenAIAgent
results['OpenAI Agents SDK'] = measure(
results['OpenAI Agents SDK'] = measure_instantiation(
lambda: OpenAIAgent(name='Test', model='gpt-4o-mini')
)
except ImportError:
@@ -118,50 +96,47 @@ def run_benchmark(skip_crewai: bool = False, quiet: bool = False,
"""Get weather info."""
return 'sunny' if city == 'sf' else 'cloudy'
results['LangGraph'] = measure(
results['LangGraph'] = measure_instantiation(
lambda: create_react_agent(model=ChatOpenAI(model='gpt-4o-mini'), tools=[get_weather_lg])
)
except ImportError:
pass
if skip_crewai:
log("Skipping CrewAI (--skip-crewai): optional framework import can hang on some platforms.")
else:
try:
from crewai.agent import Agent as CrewAgent
from crewai.tools import tool as crewai_tool
@crewai_tool("Weather Tool")
def get_weather_crew(city: Literal['nyc', 'sf']):
"""Get weather info."""
return 'sunny' if city == 'sf' else 'cloudy'
results['CrewAI'] = measure(
lambda: CrewAgent(
role='Weather Agent',
goal='Provide weather info',
backstory='A weather expert',
tools=[get_weather_crew],
verbose=False
)
try:
from crewai.agent import Agent as CrewAgent
from crewai.tools import tool as crewai_tool
@crewai_tool("Weather Tool")
def get_weather_crew(city: Literal['nyc', 'sf']):
"""Get weather info."""
return 'sunny' if city == 'sf' else 'cloudy'
results['CrewAI'] = measure_instantiation(
lambda: CrewAgent(
role='Weather Agent',
goal='Provide weather info',
backstory='A weather expert',
tools=[get_weather_crew],
verbose=False
)
except ImportError:
pass
)
except ImportError:
pass
# Print results
log('\n' + '=' * 60)
log('RESULTS')
log('=' * 60)
print('\n' + '=' * 60)
print('RESULTS')
print('=' * 60)
baseline = results.get('PraisonAI', 1)
log(f"\n{'Framework':<25} {'Avg Time (μs)':<15} {'Relative':<10}")
log('-' * 50)
print(f"\n{'Framework':<25} {'Avg Time (μs)':<15} {'Relative':<10}")
print('-' * 50)
for name, avg in sorted(results.items(), key=lambda x: x[1]):
ratio = avg / baseline
log(f'{name:<25} {avg:<15.2f} {ratio:.2f}x')
print(f'{name:<25} {avg:<15.2f} {ratio:.2f}x')
log('\n' + '=' * 60)
print('\n' + '=' * 60)
return results
@@ -188,13 +163,8 @@ def get_package_versions():
return versions
def save_results(results: dict, filename: str = 'BENCHMARK_RESULTS.md', *,
iterations: int = ITERATIONS, log=print):
"""Save benchmark results to a markdown file and update README.
Status messages are emitted via ``log`` (defaults to ``print``). Pass a
stderr logger when stdout must stay machine-readable (e.g. ``--json``).
"""
def save_results(results: dict, filename: str = 'BENCHMARK_RESULTS.md'):
"""Save benchmark results to a markdown file and update README."""
import os
import re
from datetime import datetime
@@ -221,7 +191,7 @@ def save_results(results: dict, filename: str = 'BENCHMARK_RESULTS.md', *,
with open(filepath, 'w') as f:
f.write('# PraisonAI Agents - Benchmark Results\n\n')
f.write(f'**Generated:** {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}\n')
f.write(f'**Iterations:** {iterations}\n')
f.write(f'**Iterations:** {ITERATIONS}\n')
f.write('**Test:** Agent instantiation (without tools)\n\n')
f.write('## Results\n\n')
f.write('| Framework | Avg Time (μs) | Relative |\n')
@@ -238,17 +208,17 @@ def save_results(results: dict, filename: str = 'BENCHMARK_RESULTS.md', *,
f.write('python benchmarks/simple_benchmark.py\n')
f.write('```\n')
log(f'\nResults saved to: {filepath}')
print(f'\nResults saved to: {filepath}')
# Also update the main README.md
readme_path = os.path.join(os.path.dirname(__file__), '..', '..', '..', 'README.md')
if os.path.exists(readme_path):
update_readme(readme_path, table_rows, log=log)
update_readme(readme_path, table_rows)
return filepath
def update_readme(readme_path: str, table_rows: list, *, log=print):
def update_readme(readme_path: str, table_rows: list):
"""Update the performance section in README.md with latest results."""
import re
@@ -269,54 +239,17 @@ def update_readme(readme_path: str, table_rows: list, *, log=print):
with open(readme_path, 'w') as f:
f.write(content)
log(f'README.md updated: {readme_path}')
def main():
parser = argparse.ArgumentParser(description='PraisonAI Agents - Performance Benchmark')
parser.add_argument('--save', action='store_true', help='Save results to file')
parser.add_argument('--skip-crewai', action='store_true',
help='Skip the optional CrewAI benchmark (its import can hang on some platforms)')
parser.add_argument('--json', action='store_true',
help='Emit results as JSON to stdout (for CI baselines/artifacts)')
parser.add_argument('--fail-over-threshold', action='store_true',
help='Exit non-zero if PraisonAI instantiation exceeds the budget in thresholds.json')
args = parser.parse_args()
thresholds = load_thresholds()
iterations = int(thresholds.get('iterations', ITERATIONS))
warmup = int(thresholds.get('warmup', WARMUP))
results = run_benchmark(
skip_crewai=args.skip_crewai,
quiet=args.json,
iterations=iterations,
warmup=warmup,
)
if args.save:
# Keep stdout machine-readable under --json by sending save status to stderr.
save_log = (lambda msg='': print(msg, file=sys.stderr)) if args.json else print
save_results(results, iterations=iterations, log=save_log)
elif not args.json:
print('\nResults not saved (use --save flag to save results to file)')
exit_code = 0
if args.fail_over_threshold:
max_us = thresholds['praisonai_instantiation_us_max']
actual_us = results.get('PraisonAI')
if actual_us is not None and actual_us > max_us:
print(
f"ERROR: PraisonAI instantiation {actual_us:.2f} us > threshold {max_us} us",
file=sys.stderr,
)
exit_code = 1
if args.json:
print(json.dumps({'results': results, 'iterations': iterations, 'warmup': warmup}))
return exit_code
print(f'README.md updated: {readme_path}')
if __name__ == '__main__':
sys.exit(main())
parser = argparse.ArgumentParser(description='PraisonAI Agents - Performance Benchmark')
parser.add_argument('--save', action='store_true', help='Save results to file')
args = parser.parse_args()
results = run_benchmark()
if args.save:
save_results(results)
else:
print('\nResults not saved (use --save flag to save results to file)')
@@ -1,5 +0,0 @@
{
"praisonai_instantiation_us_max": 450,
"iterations": 100,
"warmup": 10
}
-17
View File
@@ -1,17 +0,0 @@
"""Root conftest: environment that must be set before third-party imports.
pytest loads plugins registered through entry points before it loads
``tests/conftest.py``, and something in that chain imports litellm. litellm
reads LITELLM_LOCAL_MODEL_COST_MAP at *its* import and, without it, fetches
model_prices_and_context_window.json from raw.githubusercontent.com -- so the
suite made an outbound request before a single test ran, and stalled or failed
when offline. A rootdir conftest is loaded early enough to win that race.
Anything that merely needs to be set before the tests themselves belongs in
tests/conftest.py instead.
"""
import os
os.environ.setdefault("LITELLM_LOCAL_MODEL_COST_MAP", "True")
os.environ.setdefault("HF_HUB_OFFLINE", "1")
@@ -1,331 +0,0 @@
# Ground truth
Every claim here was measured, not estimated. Each carries **how** it was obtained so it can
be re-checked cheaply. Measured 2026-09-03 against `origin/main` at `2591aa405`, with a live
Ollama 0.33.2 (`b79067b0`) on `127.0.0.1:11434`.
If a work order contradicts something here, re-measure before acting. Numbers drift.
> **Capabilities are per-model, not per-server.** An early draft of this file attributed the
> 9B model's capability list to `qwen3:0.6b`. Re-verified 2026-09-03: `qwen3:0.6b` is
> `["completion","tools","thinking"]` — **no vision**. Always name the model when quoting a
> capability set.
---
## 1. Runtime facts, verified live
| Fact | Value | How to re-verify |
|---|---|---|
| Ollama liveness probe | `GET /` returns `200` with body `Ollama is running` | `curl -sI 127.0.0.1:11434/` |
| Ollama version probe | `{"version":"0.33.2"}` | `curl -s 127.0.0.1:11434/api/version` |
| Capability probe works | `qwen3:0.6b` → `["completion","tools","thinking"]` (the 9B tamil model, separately, → `[...,"vision"]`) | `curl -s :11434/api/show -d '{"model":"qwen3:0.6b"}' \| jq .capabilities` |
| Unknown `options` keys are silently dropped | `{"totally_bogus_key":123}` → HTTP 200, no warning | send it to `/api/chat`; response is a normal completion |
| Tool-param schema keywords are stripped | `minLength`, `format`, `default`, `additionalProperties` all absent from the rendered prompt | `/api/chat` with `_debug_render_only: true` and those keys set |
| `parameters.type` must be a bare string | `"type": ["object","null"]` → **HTTP 400** `cannot unmarshal array into Go struct field ToolFunctionParameters...type of type string` | send that tool definition to `/api/chat` |
| **`format` + `tools` are mutually destructive** | Tool call silently suppressed; model fabricates. Same request without `format` calls the tool correctly. | see §2 below |
### Installed on this machine
`ollama` (0.33.2), `llama-server` (b7620, Homebrew), `mlx_lm.server`, `hf` CLI.
**Not** installed: `lms` (LM Studio), `vllm`.
Ports open at time of measurement: `11434` only.
---
## 2. The reproduction that matters most
`format` (JSON schema) plus `tools`, sent to Ollama, produces a **confident fabrication** with
an HTTP 200 and no warning. The JSON grammar makes the tool-call tag unemittable, so the
model cannot call the tool and answers from nothing instead.
```
POST /api/chat model=qwen3:0.6b think=false
prompt: "What is the weather in Paris? Use the tool."
tools: [get_weather(city: string)]
WITH format + tools -> HTTP 200
message.tool_calls : null
message.content : {"answer": "The weather in Paris is sunny with a temperature of 15C."}
TOOLS ONLY (format removed) -> HTTP 200
message.tool_calls : [{"function": {"name": "get_weather",
"arguments": {"city": "Paris"}}}]
message.content : ""
```
Consequence for design: this combination must **raise**, not be silently repaired. A
fabricated answer that looks correct is worse than an error.
---
## 3. Codebase measurements
All obtained by script over `praisonaiagents/` at `2591aa405`.
### Scale
| Metric | Value |
|---|---|
| Python files / lines | 595 / 238,936 |
| Files over 2,000 lines | 13 |
| `agent/agent.py` | 7,701 |
| `llm/llm.py` | 7,160 |
| `agent/chat_mixin.py` | 6,151 |
| `llm/openai_client.py` | 2,691 |
| `llm/adapters/__init__.py` | 340 |
### `LLM` class cohesion
95 methods, 6,779 lines. Three largest methods total 3,087 lines — 43% of the file.
| Lines | Location | Method | Branch/loop/try nodes |
|---|---|---|---|
| 1,675 | `llm.py:2557` | `get_response` | **194** |
| 969 | `llm.py:4706` | `get_response_async` | 115 |
| 443 | `llm.py:4233` | `get_response_stream` | 56 |
| 249 | `llm.py:5966` | `_build_completion_params` | 52 |
| 199 | `llm.py:413` | `__init__` | 10 |
### Adapter call sites — the A1 finding
`DefaultAdapter` declares **17 methods** (an earlier draft said 18; the 18th item,
`add_provider_adapter`, is a module-level function). Eleven methods have **zero** call sites
anywhere, and with the three module functions that is 12 dead items. Note also that
`LLMProviderAdapterProtocol` declares only **16** — it omits `get_streaming_adapter`, so that
method is not part of the protocol it claims to implement.
```
supports_streaming 2
format_tool_result_message 1 (llm.py:1712)
get_default_settings 1 (llm.py:690)
should_summarize_tools 1
supports_prompt_caching 1
supports_streaming_with_tools 1
--- zero call sites ---
recover_tool_calls_from_text 0 (inline copy at llm.py:3468-3500)
parse_tool_calls 0
handle_empty_response_with_tools 0
post_tool_iteration 0
format_tools 0
get_streaming_adapter 0
get_max_iteration_threshold 0
should_skip_streaming_with_tools 0
supports_structured_output 0
inject_cache_control 0
extract_reasoning_tokens 0
add_provider_adapter 0 (public registration hook)
```
Ollama references (case-insensitive): `llm/llm.py` **145**, `llm/adapters/__init__.py` 19,
`llm/streaming_protocol.py` 11, `tools/call_executor.py` 6.
### Compensation coverage across the three response paths — the A2 finding
```
get_response _async _stream
1675 L 969 L 443 L
tool_result_mapping (value fix-up) 8 0 0
max_tool_repairs (repair loop) 2 0 0
force_tool_usage (prompt inject) 1 0 0
_validate_and_filter_ollama_arguments 1 1 0
_generate_ollama_tool_summary 1 1 0
_handle_ollama_sequential_logic 1 1 0
_format_ollama_tool_result_message 1 1 1* (indirect)
```
`get_response_stream` has none of the seven — except `_format_ollama_tool_result_message`,
which it reaches indirectly via `_create_tool_message` (`llm.py:6791`), so that row overstated
the gap. A fuller re-measurement found **16** divergent compensations, not 7; three of them run
*against* the sync path. The full matrix is in `06-adapter-revival.md` §2.6. `difflib` similarity of `get_response` against
`get_response_async` is **22.3%**, while the file's other four sync/async pairs are 76–91% —
so those two have diverged, not merely duplicated.
### Import graph — the I6 constraint
65 subpackages, 212 directed edges, **26 mutually-importing pairs**. Heaviest:
```
agent <-> tools 53 + 1 llm/ imports: tools 10, streaming 7,
agent <-> approval 28 + 1 agent 4, telemetry 2,
agent <-> config 25 + 1 auth 2, compaction 2,
agent <-> llm 19 + 4 context 1, thinking 1
llm <-> tools 10 + 1
context <-> llm 2 + 1 15 subpackages import llm/
```
### Reachability — the A3 finding
21 modules call a provider SDK directly, bypassing both clients: 23 `litellm.completion/acompletion`,
7 `litellm.embedding/aembedding`, 16 `chat.completions.create`, 3 bare `OpenAI()`.
These nine contain **zero** occurrences of `api_base` or `base_url`, so there is no parameter
to set — "run everything locally" is currently unachievable, not merely unconfigured:
```
memory/memory.py workflows/workflows.py eval/judge.py
eval/grader.py eval/accuracy.py context/compressor.py
context/optimizer.py lite/__init__.py telemetry/performance_monitor.py
```
Worst single case, `memory/memory.py:2186-2192`:
```python
from openai import OpenAI
client = OpenAI() # no base_url, no api_key
response = client.chat.completions.create(
model=llm or "gpt-4o-mini", ...
```
Hardcoded defaults in code (excluding docstrings): **105** `"gpt-4o-mini"`, **17**
`"text-embedding-3-small"`. Sixteen of the 105 are the identical line
`self.llm if hasattr(self,'llm') else "gpt-4o-mini"` in `agent/context_agent.py`.
### Stringly-typed provider identity — the A6 finding
**77 occurrences of 23 distinct string predicates** across `src/`. The same provider is
tested inconsistently, which is precisely how D3 and D4 arose:
```
9 startswith("anthropic/") gemini tested 3 ways: gpt tested 4 ways:
8 startswith("ollama/") startswith("gemini") startswith('gpt-')
7 "ollama" in startswith("gemini/") startswith("gpt-")
6 startswith("gemini") startswith('gemini-') startswith("gpt")
5 startswith("openai/") "gpt" in
5 startswith("claude")
3 ":11434" in
```
### Accretion history — why boundaries matter here
| File | First | Now | Commits | Ever shrank |
|---|---|---|---|---|
| `llm/llm.py` | 1,212 (Jan 2025) | 7,160 | 251 | never, in 18 sampled months |
| `agent/agent.py` | 1,251 (Jan 2025) | 7,701 | 472 | once — the split below |
| `agent/chat_mixin.py` | created Apr 2026 | 6,151 | 150 | — |
On **2026-04-01** `agent.py` was split 8,915 → 5,030 by extracting `chat_mixin.py`. Five
months later the two halves total **13,852 lines — 55% above the pre-split file**, growing
~33 lines/day. Moving code did not create a boundary. This is the argument for I6 being a
*test* rather than a convention.
---
## 4. Dependency facts
| Package | Version present | Status |
|---|---|---|
| `httpx` | 0.28.1 | **hard transitive dep** of required `openai>=2.0.0` (`httpx<1,>=0.23.0`) |
| `aiohttp` | 3.14.1 | direct required core dep |
| `openai` | 2.44.0 | direct required core dep |
| `litellm` | 1.90.2 | optional (`llm`, `memory` extras) |
`pyproject.toml` declares **17 optional extras** — `a2ui, api, all, auth, autonomy, crawl,
dakera, graph, knowledge, llm, mcp, memory, mongodb, os, sandbox, search, telemetry` — and
**none of them is local**. `import praisonaiagents` measures 0.04 s, so imports are lazy and
cheap; discovery latency is the only startup cost to manage.
**Conclusion (superseded).** This section originally concluded `local/` should use `httpx`.
That was reversed by the normative decision in `07-local-package-spec.md` §3: `httpx` is present
only as an **undeclared transitive** edge (`openai -> httpx`), so building the dependency sink on
it means a future `openai` release dropping `httpx` would silently break `local/`. The client is
therefore stdlib `urllib.request`, and the boundary test forbids `httpx`/`requests`/`aiohttp`.
The measurement above stands; only the recommendation drawn from it was wrong.
---
## 5. litellm prefix resolution vs ours
litellm 1.90.2 resolves all of these; PraisonAI's `_detect_provider` recognises only `ollama/`:
```
ollama/llama3.2 -> ollama OllamaAdapter repairs=2
ollama_chat/llama3.2 -> openai DefaultAdapter repairs=0 <- litellm's RECOMMENDED prefix
lm_studio/qwen -> openai DefaultAdapter repairs=0
vllm/x -> openai DefaultAdapter repairs=0
hosted_vllm/x -> openai DefaultAdapter repairs=0
huggingface/x -> openai DefaultAdapter repairs=0
```
---
## 6. Known-bad environment note
`mervinpraison/praisonai-qwen3.5-9b-tamil-en2ta:latest` does **not load** on Ollama 0.33.2:
`Failed to load CLIP model from ...sha256-11540ddc21b3...`, HTTP 500. The vision projector
blob is the failing piece. Use `qwen3:0.6b` for local verification instead. Unrelated to this
work, but it will waste an agent's time if not known.
---
## 7. The three response paths disagree — measured, not inferred
Live Ollama, `temperature=0`, identical prompt and tool, two trials, reproducible
(script in `06-adapter-revival.md` §2.7):
```
sync calls=2 'Paris: 21C sunny. Paris: 21C sunny.'
async calls=1 'Paris: 21C sunny'
stream calls=10 ''
```
> **Corrected 2026-09-03.** An earlier draft recorded the sync path returning
> `'{\n\n\n}'` with one tool call. That did not reproduce; the figures above did,
> across two trials. The finding is unchanged — the paths disagree and the stream
> path is broken — but the `'{\n\n\n}'` sample should not be cited.
Three facts follow, and they reframe the work:
1. **The best-compensated path produces the worst answer.** `get_response` carries 16 of the 18
compensations and calls the tool twice, returning a doubled answer;
`get_response_async` carries 8 and returns the correct answer once. More compensation is
not the goal — *the same* compensation is.
2. **The stream path is not under-compensated, it is broken.** It burns all 10 iterations of
`max_iter`, invokes the tool **ten times** for a one-tool question, and yields nothing. That
is a cost and side-effect amplifier, not a formatting defect.
3. Leading hypothesis for the sync/async split: `OLLAMA_FINAL_ANSWER_PROMPT` (`llm.py:5302`,
async only). **Still a hypothesis, not a measurement** — it was not settled during order 06,
because the corrected baseline changed what needs explaining.
## 8. Two test-infrastructure traps
- **`src/praisonai-agents/tests/` is almost entirely outside CI.** The only tests any workflow
runs from it are `tests/smoke/test_subscription_auth.py` and
`tests/unit/auth/test_agent_auth_wiring.py`. `tests/unit/test_adapter_registry.py` (38 tests)
and all 13 files in `tests/unit/llm/` run **nowhere**. Put new tests in
`src/praisonai/tests/unit/llm/`, which is run.
- **`test_ollama_tool_reliability.py` tests a copy, not the product.** Its `MockLLM`
re-implements `_is_ollama_provider`, `_apply_ollama_defaults`, `_validate_tool_call`,
`_should_force_tool_usage` and both prompt templates. It cannot fail in response to any change
in `llm.py`. Do not treat it as a safety net.
## 9. Test-gating facts (order 04)
- **`not network`, not `provider_ollama`, is what deselects the mocked base_url suite.** The gating
plugin implies `network` from *any* provider marker (`test_gating.py:235-238`) and every CI
expression contains `not network`. Editing the workflows' `-m` strings accomplishes nothing.
- `tests/unit/**` is **exempt** from provider auto-detection (`test_gating.py:215`,
`test_type != 'unit'`). That is why `tests/unit/llm/test_ollama_tool_reliability.py` runs in CI
while the integration file does not — and why live tests placed under `tests/unit/` need an
**explicit** marker.
- `-m provider_ollama` currently selects **32 tests across 5 files**; only 7 are Ollama tests.
- **4 of the 7 tests in `test_base_url_api_base_fix.py` fail when actually executed** —
`LLMResponseError: 'str' object has no attribute 'choices'`. The mock returns a dict; the
tool-calling loop needs an object with `.choices`.
- **`tests/test_double_api_fix.py` makes a real network call when forced to run.** `llm.py` routes
`gpt-4o-mini` through `litellm.responses` (`llm.py:6434`), not `litellm.completion`, so its patch
no longer intercepts (verified: 401 from `api.openai.com/v1/responses`). It is currently masked
by an unconditional auto-skip — **anyone who "fixes" the skip without fixing the mock gets live
billed calls.**
- **`network_guard.py` disables itself** for anything under `/integration/`, `/e2e/`, `/live/`
(lines 103-105) and for any `provider_*`-marked test (96-100). The suite's network blocker does
not cover the tests most likely to make network calls.
- **A live Ollama suite already exists** and has never run:
`tests/integration/test_ollama_tool_calling_live.py`, 230 lines, 7 tests, gated on
`PRAISONAI_TEST_OLLAMA`. It is broken against `main` — all 7 pass `verbose=True` to `Agent()`,
and two pass `force_tool_usage=` / `max_tool_repairs=`; all three now raise `TypeError`.
- **`qwen3:0.6b` is 522 MB** (not ~400 MB) and reports `completion, tools, thinking` — **no
vision**. Reliability through `Agent`: default 3/4; `force_tool_usage="always"` + `temperature=0`
**6/6**; multi-step arithmetic 0/2 (one hung past 90 s).
@@ -1,372 +0,0 @@
# 04 — D7: provider markers are assigned per file, so mocked tests are gated as if live
**Branch:** `fix/gating-per-test-provider-markers`
**Measured:** clean worktree at `origin/main` (`2591aa405`), working tree clean.
**Owns:** `src/praisonai/tests/_pytest_plugins/test_gating.py`, both `pytest.ini` files,
`src/praisonai/tests/conftest.py`, `tests/integration/test_base_url_api_base_fix.py`,
`tests/unit/cli/test_test_command.py`.
**Must not edit:** any `praisonaiagents/` source; `.github/workflows/**` (order 05).
> **The audit's stated root cause was wrong, and the correction changes the fix.**
> `provider_ollama` is a red herring. **`not network` deselects all 7 tests on its own**, because
> the plugin implies `network` from *any* provider marker and the file also matches
> `provider_openai`. **Removing `not provider_ollama` from the workflows would change nothing.**
> The fix must be marker-side, not workflow-side. This is why order 04 touches no YAML.
---
## 1. Corrections to the audit
| # | Claim | Verdict |
|---|---|---|
| 1 | 4 CI marker expressions carry `not provider_ollama` | **True but incomplete** — there is a 5th, non-CI location: `src/praisonai-code/praisonai_code/cli/commands/test.py:67` (`praisonai test --tier main`). |
| 2 | 7 tests, all deselected | **Exactly true** (`collected 7 items / 7 deselected / 0 selected`). **Stated cause is wrong** — see the callout above. |
| 3 | The file never inspects `litellm.completion` kwargs | **True** (only `mock_completion.assert_called()`). **And worse: 4 of the 7 tests fail outright when executed.** They are not merely toothless, they are broken. |
| 4 | `test_double_api_fix.py`: 0 asserts, 34 prints | **True. And worse:** all 3 tests are auto-`skip`ped in every configuration, and when forced to run its `patch('litellm.completion')` no longer intercepts — `llm.py` routes `gpt-4o-mini` through `litellm.responses`, so it makes a **real network call** (verified: 401 from `api.openai.com/v1/responses`). |
| 5 | `test_ollama_tool_reliability.py` asserts against MockLLM | **True.** 541 lines, MockLLM at 23–197, 28 tests, all pass. **Quantified:** 2 of the 8 re-implemented methods (`_apply_ollama_defaults`, `_try_parse_tool_call_json`) **do not exist on the real `LLM` at all.** |
| 6 | agents-side ollama tests gated only by `OLLAMA_HOST` | **True**, and `test_ollama_fix_openai` is gated only by `OPENAI_API_KEY` — a live **billed** call on a plain `pytest`. Mitigating: no CI workflow runs these two files. |
| 7 | No ollama live job | **True for CI**, but the audit **missed that the suite already exists**: `tests/integration/test_ollama_tool_calling_live.py`, 230 lines, 7 tests, gated on `PRAISONAI_TEST_OLLAMA=1`. It is **broken against current `main`**. See order 05. |
---
## 2. Reproduce before the fix
```bash
cd src/praisonai && export PYTHONPATH=$(cd ../praisonai-agents && pwd)
CI_M="not slow and not network and not local_service and not provider_anthropic and not provider_google and not provider_ollama and not provider_grok_xai and not provider_groq and not provider_cohere"
```
**R1 — 100% deselected:**
```bash
python -m pytest tests/integration/test_base_url_api_base_fix.py -m "$CI_M" --collect-only -q --disable-warnings
# collected 7 items / 7 deselected / 0 selected
```
**R2 — `provider_ollama` is a red herring.** Rerun R1 with `and not provider_ollama` **removed**:
still `7 deselected / 0 selected`. `not network` does it alone.
**R3 — the whole file is tagged.** With a marker-dump plugin, all seven items carry the identical
set `['integration', 'network', 'provider_ollama', 'provider_openai', 'skip']` — including the six
that never mention Ollama.
**R4 — 4 of 7 fail when actually executed:**
```bash
PRAISONAI_ALLOW_NETWORK=1 PRAISONAI_TEST_PROVIDERS=all OPENAI_API_KEY=sk-fake-offline \
python -m pytest tests/integration/test_base_url_api_base_fix.py -o addopts="" -q --tb=no -rA
# 4 failed, 3 passed
```
All four fail with `LLMResponseError: ... 'str' object has no attribute 'choices'`. The three that
pass are the three that never call `get_response()`.
**R5 — `-m provider_ollama` selects 4× more than it should:**
```
7 tests/integration/test_base_url_api_base_fix.py
7 tests/integration/test_ollama_tool_calling_live.py
11 tests/integration/test_rag_integration.py
4 tests/integration/test_setup_integration.py
3 tests/test_double_api_fix.py
```
**32 selected; only 7 are Ollama tests.** This is why order 04 is a hard prerequisite for order 05 —
`-m provider_ollama` is currently not a usable selector.
---
## 3. Root cause
`test_gating.py:153-166` scans the **entire file**:
```python
153 def _detect_providers_in_file(filepath: Path) -> Set[str]:
154 """Detect which providers are referenced in a test file."""
...
161 content = _get_file_content(filepath)
162 detected = set()
163 for marker, pattern in PROVIDER_PATTERNS.items():
164 if pattern.search(content):
165 detected.add(marker)
166 return detected
```
Line 28 is `'provider_ollama': re.compile(r'\b(ollama)\b', re.IGNORECASE)`. One occurrence inside
`test_ollama_environment_variable_compatibility` tags all seven items (applied at lines 213–230).
Note line 215: `if item.fspath and test_type != 'unit'` — **`tests/unit/**` is exempt**, which is
why `tests/unit/llm/test_ollama_tool_reliability.py` runs in CI while the integration file does not.
**The second mechanism, which the audit missed**, is lines 235–238:
```python
235 # 3. Add network marker if any provider marker is present
236 provider_markers = {m for m in existing_markers if m.startswith('provider_')}
237 if provider_markers and 'network' not in existing_markers:
238 item.add_marker(pytest.mark.network)
```
A mocked test that merely *mentions* `openai` gets `network`, and every CI expression contains
`not network`. Lines 262–267 then convert it into a runtime skip.
**Sub-defect 3.** Lines 59, 145, 177, 234 are all bare `mock_completion.assert_called()`. Nothing
reads `call_args`. Measured: `LLM(model='openai/mistral', base_url='http://localhost:4000',
api_key='sk-test').get_response("test", stream=False)` calls `litellm.completion` with
`base_url='http://localhost:4000'` and `api_base=None`. **So the file's title is stale** — the real
invariant is *`base_url` survives into the call*, not "maps to `api_base`".
---
## 4. The change
**Design decision: AST per-test marking, not file splitting.**
- *Split the file* — cost cheap, **benefit zero** (measured, R2): the six non-Ollama tests still
match `provider_openai`, still get `network`, still get deselected. It also leaves `-m
provider_ollama` selecting 32 tests and fixes nothing for the other 14 mixed-provider files.
**Rejected.**
- *AST per-test marking* — one change in one plugin, fixes the class of problem, and is the
prerequisite for order 05's selector to mean anything. **Recommended.**
### Sub-change 1 — per-test provider detection via AST
Keep `_detect_providers_in_file` (it now serves only the `network` marker) and add:
```python
def _detect_providers_in_text(text: str) -> Set[str]:
"""Return every provider marker whose pattern appears in ``text``."""
detected = set()
for marker, pattern in PROVIDER_PATTERNS.items():
if pattern.search(text):
detected.add(marker)
return detected
def _build_per_test_provider_map(filepath: Path) -> Optional[Dict[tuple, Set[str]]]:
"""Map ``(class_name, func_name)`` -> provider markers for one test file.
The text considered for a test is that test's own source segment plus its
decorators -- not the whole file. Returns ``None`` if the file cannot be
parsed, so callers fall back to whole-file rather than under-marking.
"""
key = str(filepath)
if key in _file_provider_cache:
return _file_provider_cache[key]
result: Optional[Dict[tuple, Set[str]]] = None
try:
source = _get_file_content(filepath)
tree = ast.parse(source)
result = {}
def _visit(node, class_name=None):
for child in getattr(node, 'body', ()):
if isinstance(child, ast.ClassDef):
_visit(child, child.name)
elif isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
segment = ast.get_source_segment(source, child) or ""
for decorator in child.decorator_list:
segment += "\n" + (ast.get_source_segment(source, decorator) or "")
result[(class_name, child.name)] = _detect_providers_in_text(segment)
_visit(tree)
except (SyntaxError, ValueError, RecursionError):
result = None
_file_provider_cache[key] = result
return result
def _detect_providers_for_item(item) -> Set[str]:
"""Provider markers for a single collected item (per test, not per file)."""
filepath = Path(item.fspath)
if _is_excluded_path(str(filepath)):
return set()
per_test = _build_per_test_provider_map(filepath)
if per_test is None:
return _detect_providers_in_file(filepath)
parts = item.nodeid.split("::")[1:]
if not parts:
return _detect_providers_in_file(filepath)
func = parts[-1].split("[")[0]
cls = parts[-2] if len(parts) >= 2 else None
if (cls, func) in per_test:
return set(per_test[(cls, func)])
if (None, func) in per_test:
return set(per_test[(None, func)])
# Dynamically generated item we cannot locate in the AST: stay conservative.
return _detect_providers_in_file(filepath)
```
Add `import ast`, a `_file_provider_cache: Dict[str, Optional[Dict[tuple, Set[str]]]] = {}` next to
`_file_content_cache` (line 46), and clear it in `pytest_configure` (186) and
`pytest_sessionfinish` (294).
### Sub-change 2 — `offline` opt-out, and pin `network` to the file
Replace lines 213–230 with a version that (a) honours an explicit `offline` marker, (b) calls
`_detect_providers_for_item`, and (c) **keeps `network` whole-file derived**:
```python
offline = 'offline' in existing_markers
if not offline:
detected_providers = _detect_providers_for_item(item)
for marker, pattern in PROVIDER_PATTERNS.items():
if pattern.search(item.nodeid):
detected_providers.add(marker)
for provider in detected_providers:
if provider not in existing_markers:
item.add_marker(getattr(pytest.mark, provider))
# The `network` marker stays WHOLE-FILE derived. Narrowing
# the provider markers must not, by itself, un-deselect a
# test that is really live -- only `offline` does that.
if _detect_providers_in_file(filepath) and 'network' not in existing_markers:
item.add_marker(pytest.mark.network)
```
**Why `network` stays file-derived — this is the whole backward-compatibility story and is not
optional.** Measured: if per-test narrowing were also allowed to narrow `network`, **48 currently-
deselected integration tests would newly select, 25 of them with no `skipif` — they would actually
execute**, across nine files including `test_rag_live.py` and `test_model_routing_real.py`, in a
suite where the network guard is disabled for everything under `/integration/`
(`network_guard.py:103-105`). Pinning `network` to the file makes sub-change 1 provably
**selection-neutral** under every existing `-m` expression, while still making `-m provider_ollama`
mean what it says.
Register the marker in both `pytest.ini` files and `tests/conftest.py`:
```
offline: Fully mocked - opt out of provider/network auto-marking
```
### Sub-change 3 — opt the file in, and make it guard its bug
Add `pytestmark = pytest.mark.offline` plus a response factory:
```python
def make_completion_response(text="Test response"):
"""A litellm.completion stand-in the LLM tool-calling loop can consume.
A bare dict is NOT sufficient: the loop reads ``.choices[0].message`` and a
dict raises ``'str' object has no attribute 'choices'``.
"""
resp = MagicMock()
resp.choices = [MagicMock()]
resp.choices[0].message.content = text
resp.choices[0].message.tool_calls = None
resp.choices[0].finish_reason = "stop"
return resp
```
For each of the four broken tests: replace the dict `return_value` with
`make_completion_response(...)`, pass `stream=False` to `get_response`, and replace
`mock_completion.assert_called()` with a real kwarg assertion:
```python
kwargs = mock_completion.call_args.kwargs
assert kwargs['base_url'] == 'http://localhost:4000'
assert kwargs['api_key'] == 'sk-test'
assert kwargs['model'] == 'openai/mistral'
```
- `test_koboldcpp_specific_scenario` → `kwargs['base_url'] == "http://127.0.0.1:5001/v1"`
- `test_litellm_documentation_example_compatibility` → `kwargs['base_url'] == "http://0.0.0.0:4000"`
- `test_ollama_environment_variable_compatibility` → `kwargs['model'] == 'ollama/llama2'` **and**
`'base_url' not in kwargs` (none was configured; the env path must not inject one)
All four were run standalone against `main` and pass. Leave the three currently-passing tests alone.
Update the stale module docstring ("maps base_url to api_base" → "base_url must reach litellm") but
**do not rename the test functions** — the nodeids are referenced in issue discussion, and renaming
would change what the nodeid scan at plugin lines 224–226 sees.
---
## 5. Backward-compatibility contract
Baseline, measured (`tests/integration/`, CI ignores applied): **290 collected, 187 selected,
103 deselected** under the CI marker expression.
> Caveat for the executing agent: a cold first invocation on a fresh interpreter has been observed
> to report `157/260`, apparently an optional-dependency import warming effect. **Run it twice**;
> `187/290/103` is stable from the second run on. Take your own baseline before touching anything.
**Tests whose selection status changes: exactly 7**, all in `test_base_url_api_base_fix.py`, all
deselected → selected. Post-fix expectation: **194/290 (96 deselected)**. Nothing else moves in
either direction — that is what the file-derived `network` guarantees.
Intended change under `-m provider_ollama` (32 → 9):
| file | before | after |
|---|---|---|
| `test_ollama_tool_calling_live.py` | 7 | 7 |
| `test_base_url_api_base_fix.py` | 7 | 0 (`offline`) |
| `test_rag_integration.py` | 11 | 1 |
| `test_setup_integration.py` | 4 | 0 |
| `test_double_api_fix.py` | 3 | 1 |
**The risk, stated plainly.** Un-deselecting tests turns CI red if any fail. **They do: 4 of the 7
fail today** (R4). This is not hypothetical.
**Mitigation — mandatory, not optional: land the gating fix and the four repairs in the same PR.**
Do not merge sub-changes 1+2 with the `pytestmark` but without the repairs. **Quarantining with
`xfail` is explicitly rejected** — the repairs are ~10 lines each, verified working, and an `xfail`
here would recreate the exact defect being fixed (a test that names a bug and does not guard it).
**Escape hatch.** If the 7 prove flaky in CI for a reason not reproducible locally, revert
**sub-change 3 only**. Selection returns to 187/290/103 and the plugin improvements stay.
---
## 6. Tests to add
In `tests/unit/cli/test_test_command.py` (the existing home of gating-plugin tests, and already in
`EXCLUDED_PATHS`, so these cannot be auto-marked by the thing they test), add
`class TestProviderMarkerGranularity` with:
- `test_ollama_does_not_leak_to_sibling_tests` — **THE meta-test.** If this fails, D7 has regressed.
- `test_openai_does_not_leak_to_sibling_tests`
- `test_plain_test_gets_no_provider_markers`
- `test_decorators_are_scanned` — a `@pytest.mark.parametrize("m", ["ollama/llama2"])` must mark it
- `test_unparseable_file_falls_back_to_whole_file` — conservative over-marking on a parse failure
- `test_network_marker_remains_whole_file_derived` — pins the decision that keeps this change
selection-neutral for the 96 still-deselected tests
- `test_offline_marker_is_registered` — both `pytest.ini` files, or every use warns
No new dependency: `ast`, `tmp_path` and `configparser` are stdlib/pytest builtins.
---
## 7. Verification and acceptance
```bash
# 1. The 7 are now selected
python -m pytest tests/integration/test_base_url_api_base_fix.py -m "$CI_M" --collect-only -q
# expect: 7 selected, 0 deselected
# 2. They pass offline, with no keys and no services
env -u OPENAI_API_KEY -u ANTHROPIC_API_KEY -u OLLAMA_HOST \
python -m pytest tests/integration/test_base_url_api_base_fix.py -m "$CI_M" -o addopts="" -q
# expect: 7 passed
# 3. Selection neutral everywhere else
python -m pytest tests/integration/ $IG -o addopts="" -q --collect-only -m "$CI_M" | tail -1
# expect: 194/290 tests collected (96 deselected)
# 4. `-m provider_ollama` now means Ollama -> 7 + 1 + 1, not 32
# 5. Meta-tests pass
# 6. The three CI shards reproduced verbatim do not regress
# 7. The two pytest.ini marker tables stay in sync
```
**Acceptance gate:** `tests/integration/` collects **194/290 (96 deselected)** — exactly seven more
than baseline, all seven from `test_base_url_api_base_fix.py` — **and passes with
`OPENAI_API_KEY`, `ANTHROPIC_API_KEY` and `OLLAMA_HOST` all unset and no Ollama server running.**
**PR title:** `fix(tests): mark provider tests per test function, not per file`
---
## 8. Filed for separate PRs
- **`tests/test_double_api_fix.py`'s mock no longer intercepts.** `llm.py` routes `gpt-4o-mini`
through `litellm.responses` (`llm.py:6434`), not `litellm.completion`. Forced to run, it makes a
**real OpenAI request** (verified: 401 for `https://api.openai.com/v1/responses`). Today it is
masked by an unconditional auto-`skip` — **anyone who "fixes" the skip without fixing the mock
gets live billed calls.**
- **`network_guard.py` disables itself for `/integration/`, `/e2e/`, `/live/`** (lines 103–105) and
for any `provider_*`-marked test (96–100). The suite's network blocker does not cover the tests
most likely to make network calls.
@@ -1,303 +0,0 @@
# 05 — Live local-model CI job
**Branch:** `feat/ci-ollama-live-job`
**Depends on:** order `04` must merge first. Before it, `-m provider_ollama` selects 32 tests
across 5 files (25 of which are not Ollama tests) and this job is unbuildable.
**Owns:** `.github/workflows/test-optimized.yml`,
`src/praisonai/tests/integration/test_ollama_tool_calling_live.py`.
**Must not edit:** `test_gating.py` (order 04); the `smoke` / `main` / `openai-live` jobs; anything
in `test-core.yml`; `src/praisonai-agents/tests/test_ollama_{fix,async_fix}.py`.
---
## 1. The suite already exists — and is broken
The audit concluded "no ollama live job, and no live suite". The first half is right; **the second
is wrong**. `src/praisonai/tests/integration/test_ollama_tool_calling_live.py` is 230 lines,
7 tests, gated at lines 19–22 on `PRAISONAI_TEST_OLLAMA`. It has never run anywhere.
Measured against `main`, **it cannot run** — three `Agent()` kwargs are now rejected:
```
$ python -c "from praisonaiagents import Agent; Agent(name='C', llm='ollama/qwen3:0.6b', verbose=True)"
TypeError: Agent.__init__() got unexpected keyword argument(s): verbose
verbose: verbosity moved into output=; use output='verbose' (or output=OutputConfig(verbose=True)).
$ ... Agent(..., force_tool_usage="always") -> TypeError: unexpected keyword argument(s): force_tool_usage
$ ... Agent(..., max_tool_repairs=3) -> TypeError: unexpected keyword argument(s): max_tool_repairs
```
All 7 tests use `verbose=True`; two also use the other two. **Every test errors on construction.**
The modern form, verified working:
```python
Agent(name="C",
llm={"model": "ollama/qwen3:0.6b", "force_tool_usage": "always", "max_tool_repairs": 3},
tools=[calculator])
# -> llm_instance.force_tool_usage == 'always', llm_instance.max_tool_repairs == 3
```
So this order is **"fix a dead test file and wire it up"**, not "write a live suite".
---
## 2. Model selection
Measured on a real Ollama 0.33.2 host:
```
$ ollama list
qwen3:0.6b 7df6b6e09427 522 MB
$ ollama show qwen3:0.6b
parameters 751.63M quantization Q4_K_M context length 40960
Capabilities: completion, tools, thinking
```
**Two corrections:** the blob is **522 MB**, not ~400 MB; and it is **not vision capable** —
`completion, tools, thinking` only. Tools + thinking is all this job needs.
**Reliability, measured end-to-end through `praisonaiagents.Agent`:**
| configuration | trials | tool invoked + correct answer | latency |
|---|---|---|---|
| default | 4 | **3/4** — one run returned `{}` with no tool call | 0.5–5.4 s |
| `force_tool_usage="always"`, `temperature=0` | 6 | **6/6** | 0.4–4.0 s |
| indirect prompt ("I know it's easy, but use the tool") | 1 | 0/1 — emitted `{"type": "calculator", ...}`, wrong key | 0.6 s |
| multi-step `(17+25)+(8+9)` | 2 | 1 hung past 90 s; 1 answered 42 instead of 59 | — |
**Conclusion: `qwen3:0.6b` is the right model, but only the forced, zero-temperature, single-call
prompt is CI-deterministic.**
Rejected alternatives: `qwen2.5:0.5b` (~400 MB, older tool calling, unmeasured — no reason to
prefer it over a verified model); `llama3.2:1b` (~1.3 GB, 2.5× the cache for no measured gain);
`smollm2:135m` (**no `tools` capability — disqualifying**); `olmo-3` (the file's current default,
~7B, far too large per run). Keep `olmo-3` as the *local* default so developer workflow is
unchanged; let CI override via env var.
**Cache:** `~/.ollama/models`, keyed `${{ runner.os }}-ollama-models-${{ env.OLLAMA_TEST_MODEL }}`.
**No `restore-keys`** — a partial restore of a *different* model's blobs is worse than a clean
pull, and a stale key would silently test the wrong model.
**Install:** pinned release tarball, not `curl https://ollama.com/install.sh | sh`. The install
script tracks latest, so the job would silently change what it tests, and it pulls GPU runtimes a
CPU-only runner never uses. Expect ~20–60 s tarball + ~30–90 s cold model pull, near-zero on a
cache hit — **the executing agent must confirm real timings on the first green run and write them
into a YAML comment.**
**Gating:** `if: github.event_name == 'schedule' || github.event.inputs.tier == 'extended' ||
github.event.inputs.tier == 'nightly'` — byte-identical to `extended-anthropic` (line 467),
`extended-google` (525), `extended-groq` (585), `extended-xai` (644), `extended-cohere` (703).
**It never runs on a pull request.**
---
## 3. One deliberate departure from the sibling jobs
Every existing live job ends `|| echo "... tests completed"`, so **it can never fail**. That is
defensible for key-dependent jobs — a rotated secret should not page anyone — but it is exactly
the "test that cannot fail" pattern this whole effort exists to remove, and **Ollama needs no
secret**: availability is fully under the job's control.
So the job has **two run steps**:
1. a **required** minimum-contract step with **no `|| echo`**;
2. an **informational** step for the measured-flaky remainder, keeping the sibling style.
**Do not merge them.**
---
## 4. Test-file changes
1. After `pytestmark`, add the model indirection:
```python
# CI overrides this with a small, fast model; local runs keep olmo-3.
OLLAMA_MODEL = "ollama/" + os.getenv("PRAISONAI_OLLAMA_TEST_MODEL", "olmo-3")
```
2. Replace all 7 hard-coded `llm="ollama/olmo-3"` with `llm=OLLAMA_MODEL`.
3. Remove `verbose=True` from all 7 `Agent(...)` calls. Where verbosity is wanted: `output="verbose"`.
4. Move `force_tool_usage="always"` and `max_tool_repairs=3` into an `llm` dict.
5. Add the CI-minimum test as a new first class — **the only assertion in the file with a measured
6/6 pass rate**:
```python
class TestOllamaCIMinimum:
"""The minimum contract a local model must satisfy: a tool call round-trips.
Deliberately narrow. Measured on qwen3:0.6b: this exact configuration
(forced tool usage, temperature 0, single arithmetic step) passed 6/6.
The default configuration passed 3/4 and multi-step prompts were worse --
those live in TestOllamaToolCallingLive, which CI runs informationally.
"""
def test_tool_call_round_trips(self, ollama_available):
from praisonaiagents import Agent
invocations = []
def add(a: int, b: int) -> int:
"""Add two integers together.
Args:
a: First number to add
b: Second number to add
"""
invocations.append((a, b))
return a + b
agent = Agent(
name="CI Calculator",
llm={"model": OLLAMA_MODEL, "force_tool_usage": "always", "temperature": 0},
tools=[add],
)
result = agent.chat("Compute 17 + 25. You MUST use the calculator tool.")
# 1. the model actually called our function, with the right arguments
assert invocations == [(17, 25)], f"tool was not invoked: {invocations!r}"
# 2. the tool's return value made it back into the model's answer
assert "42" in str(result), f"tool result did not round-trip: {result!r}"
```
Reuse the existing `ollama_available` fixture (lines 48–53) — it already probes
`http://localhost:11434/api/tags` and skips cleanly. Do not add a second probe.
### Why both assertions are needed
`qwen3:0.6b` answers `17 + 25 = 42` **correctly with no tools at all** (measured, 1.2 s,
`tools=[]`). So a bare `assert "42" in result` is satisfied by a model that ignores tools entirely.
The `invocations == [(17, 25)]` half is what proves the tool call was emitted, parsed and
dispatched with the right arguments; the `"42"` half proves the return value was fed back and
reached the final answer. Anything beyond this — multi-step chains, repair loops, resisting
distraction — is measured-flaky on a 0.6B model and belongs in the informational step.
---
## 5. The YAML
Insert after `extended-cohere`'s `Skip notice` step (ends line 760), before the `legacy-unit`
banner. Key fragments:
```yaml
extended-ollama:
if: github.event_name == 'schedule' || github.event.inputs.tier == 'extended' || github.event.inputs.tier == 'nightly'
runs-on: ubuntu-latest
timeout-minutes: 15
env:
# Bump deliberately; the cache key and what we test both follow this.
# No leading "v": install.sh passes ?version=$OLLAMA_VERSION and ollama.com
# redirects to GitHub tag v$version (vv0.33.2 → 404 if set to v0.33.2).
OLLAMA_VERSION: "0.33.2"
OLLAMA_TEST_MODEL: qwen3:0.6b
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Cache Ollama model blobs
uses: actions/cache@v4
with:
path: ~/.ollama/models
# No restore-keys: a partial hit from a different model would
# silently test something other than OLLAMA_TEST_MODEL.
key: ${{ runner.os }}-ollama-models-${{ env.OLLAMA_TEST_MODEL }}
- name: Install Ollama
shell: bash
run: |
set -euo pipefail
# The official installer, VERSION-PINNED via OLLAMA_VERSION (which it
# honours). Since v0.33 the linux bundle ships as a .tar.zst with GPU
# runtimes bundled in, so a direct release-asset download is not a
# workable alternative; the installer handles the zst/tgz split.
sudo apt-get update -qq && sudo apt-get install -y -qq zstd
curl -fsSL https://ollama.com/install.sh | sh
ollama --version
- name: Start Ollama and pull model
shell: bash
run: |
set -euo pipefail
ollama serve > /tmp/ollama-serve.log 2>&1 &
for i in $(seq 1 30); do
if curl -sf http://127.0.0.1:11434/api/tags > /dev/null; then
echo "Ollama is up after ${i}s"; break
fi
sleep 1
done
curl -sf http://127.0.0.1:11434/api/tags > /dev/null
ollama pull "$OLLAMA_TEST_MODEL"
ollama list
- name: Install dependencies
run: |
pip install --upgrade pip
pip install pytest pytest-asyncio pytest-timeout
- uses: ./.github/actions/install-monorepo-packages
with:
agents-extras: ".[all]"
- name: Run Ollama minimum contract (required)
env:
PRAISONAI_TEST_TIER: extended
PRAISONAI_ALLOW_NETWORK: '1'
PRAISONAI_LIVE_TESTS: '1'
PRAISONAI_TEST_PROVIDERS: 'ollama'
PRAISONAI_TEST_OLLAMA: '1'
PRAISONAI_OLLAMA_TEST_MODEL: ${{ env.OLLAMA_TEST_MODEL }}
shell: bash
run: |
set -euo pipefail
cd src/praisonai
# No "|| echo" here on purpose: no secret is involved, so a failure
# here is a real regression in local-model tool calling.
python -m pytest tests/integration/test_ollama_tool_calling_live.py::TestOllamaCIMinimum \
-m "provider_ollama" -v --tb=short --timeout=120
- name: Run remaining Ollama live tests (informational)
if: always()
env: { ...same... }
run: |
cd src/praisonai
python -m pytest tests/integration/test_ollama_tool_calling_live.py \
-m "provider_ollama" \
--deselect tests/integration/test_ollama_tool_calling_live.py::TestOllamaCIMinimum \
-v --tb=short --timeout=180 \
|| echo "Ollama exploratory tests completed (small models are non-deterministic)"
- name: Ollama server log
if: failure()
run: tail -100 /tmp/ollama-serve.log
```
And in `test-summary` (line 838): extend `needs: [smoke, main, extended-ollama]` and add the
summary rows. `test-summary` is already `if: always()`, so a skipped `extended-ollama` on PRs
reports `skipped` rather than blocking.
---
## 6. Verification and acceptance
```bash
ollama pull qwen3:0.6b
cd src/praisonai && export PYTHONPATH=$(cd ../praisonai-agents && pwd)
# Must be 6/6 before this ships.
for i in $(seq 1 6); do
PRAISONAI_TEST_OLLAMA=1 PRAISONAI_ALLOW_NETWORK=1 PRAISONAI_LIVE_TESTS=1 \
PRAISONAI_TEST_PROVIDERS=ollama PRAISONAI_OLLAMA_TEST_MODEL=qwen3:0.6b \
python -m pytest tests/integration/test_ollama_tool_calling_live.py::TestOllamaCIMinimum \
-o addopts="" -q --timeout=120 2>&1 | tail -1
done
# The gate still holds with the env var absent:
python -m pytest tests/integration/test_ollama_tool_calling_live.py -o addopts="" -q --collect-only
# expect: collected, all skipped by the module pytestmark
# Nothing leaked onto the PR path:
grep -n "extended-ollama" -A3 .github/workflows/test-optimized.yml | grep "if:"
python -c "import yaml; yaml.safe_load(open('.github/workflows/test-optimized.yml')); print('yaml ok')"
# Then: gh workflow run "Optimized Test Suite" -f tier=extended
```
**Acceptance gate:** a manual `workflow_dispatch` with `tier=extended` shows `extended-ollama`
**green**, with "Run Ollama minimum contract (required)" reporting `1 passed` — **and** the same
workflow triggered by a pull request shows `extended-ollama` **skipped**.
**PR title:** `ci: add nightly live-Ollama job exercising a real local model`
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -1,196 +0,0 @@
# Local Model Layer — implementation ledger
**Status:** every order below has an open PR. See §4.
**Owner workstream:** local model runtimes (Ollama, LM Studio, llama.cpp, vLLM, MLX, `transformers serve`)
**Created:** 2026-09-03
---
## 0. How to use this document
This directory is a **work-order ledger**. It exists so that any agent — including one
with no memory of the conversation that produced it — can pick up exactly one unit of
work and execute it correctly, in isolation, without re-deriving the analysis.
**If you are an agent starting work here, do this in order:**
1. Read [`00-ground-truth.md`](00-ground-truth.md). It records what was measured, how, and
when. Do not trust a claim in any work order that contradicts it; re-measure instead.
2. Read §2 (Invariants) and §3 (File ownership) of this file. §3 is what stops two agents
corrupting each other's work.
3. Open the single work order you were assigned. Execute only that one.
4. Update the status row in §4 of this file in the same commit as your change. That row is
the only shared file content; keep the edit to one line to avoid conflicts.
**Do not** expand scope, batch two work orders into one PR, or "fix while you're in there".
Each order is independently revertible by design and that property is worth more than
convenience.
> **Two corrections this ledger has already had to make.** Both were caught by measuring rather
> than reasoning, and both would have sent an agent down the wrong path.
>
> **The D7 root cause is not `provider_ollama`.** The audit blamed the per-file `provider_ollama`
> tag. Measurement (`04-test-gating.md` §2) shows `not network` deselects all seven tests **on its
> own**, because the plugin implies `network` from *any* provider marker and the file also matches
> `provider_openai`. **Removing `not provider_ollama` from the workflows would change nothing.**
> That is why order `04` touches no YAML at all — the fix is marker-side.
> Its first draft told an agent to "wire
> the twelve dead adapter methods". Measurement (`06-adapter-revival.md` §1) says the opposite:
> **12 of 20 items should be deleted, only 3 wired.** Six of the dead methods have no inline
> equivalent anywhere — they are speculative API, and an agent told to "wire them up" would
> invent an implementation that no test could ever have failed against. Read the decision table
> before touching `llm/adapters/`; do not act on the summary in §1 of this file.
---
## 1. Why this work exists
Every local runtime worth supporting already speaks `POST /v1/chat/completions`, and
`litellm` already handles that transport. So the problem is not calling local models — the
SDK can already do that. The problems are:
- **Discovery.** A user with a running daemon still hand-types a URL, a port and an exact
model tag; getting any one wrong produces `OPENAI_API_KEY environment variable is required`.
- **Capability.** "Does this model support tools?" is answerable by asking the server
(`/api/show`, `/props`, `/server_info`). The SDK asks nothing and guesses from a name prefix.
- **Quirks.** Local stacks fail silently with HTTP 200. The sharpest: sending `format` and
`tools` together to Ollama suppresses the tool call and the model fabricates an answer.
The layer being built is therefore a **resolver, not a transport**: it returns data about
what is running and what it will get wrong. Compensating behaviour stays in
`llm/adapters/`, which already declares methods for exactly this purpose.
---
## 2. Invariants — never break these
These come from `src/praisonai-agents/AGENTS.md` §4.6 plus the structural review in
`00-ground-truth.md`. Every work order inherits them; none may waive them.
| # | Invariant | Why it binds this work specifically |
|---|-----------|--------------------------------------|
| I1 | **Backward compatible.** Public API changes need a deprecation cycle. | Several fixes change which code path a given input takes. Each order carries an explicit list of inputs whose behaviour must not change. |
| I2 | **Lazy imports.** Optional deps never imported at module level. | Discovery must not import `litellm` or any runtime SDK at import time. |
| I3 | **Safe defaults.** New features are opt-in. | Discovery must not fire unless the model spec is `"local"` or unset. Never probe the network on a fully-specified config. |
| I4 | **Deterministic tests.** No dependence on timing or external state. | Every test must pass with **no server running**. Live-server tests go behind an explicit marker and never run in the default suite. |
| I5 | **Protocol-driven core.** No heavy implementations in core. | `local/` returns dataclasses. It performs no chat calls, no retry, no request mutation. |
| I6 | **`local/` is a dependency sink.** | It may import the **standard library only** — nothing from `praisonaiagents`, and no third-party distribution (`httpx`/`requests`/`aiohttp` are forbidden; use `urllib.request`). Enforced by a test. See §5 and `07` §3. |
| I7 | **No second abstraction.** | `llm/adapters/` is the extension point. Do not add a parallel quirk/adapter registry beside it. |
| I8 | **No new public knobs without a live consumer.** | Root `AGENTS.md`: do not add params, modules or exports that merely duplicate existing behaviour. |
### I6 in detail — the import rule
An AST pass over `praisonaiagents/` (595 files, 238,936 lines) found **65 subpackages, 212
directed edges and 26 mutually-importing subpackage pairs**. `llm/` already imports upward
into `agent/`, and 15 subpackages import `llm/` back. Consequently a shared helper placed in
`llm/` **cannot** be adopted by `memory/` or `eval/` without deepening the existing cycles.
`local/` is therefore specified as a leaf. That single property is what allows the 21
modules which currently bypass the LLM layer to adopt it later. Treat any import of
`praisonaiagents.*` from inside `local/` as a build break, not a style issue.
The HTTP client is stdlib `urllib.request`, **not** `httpx`. An earlier draft of this ledger
permitted `httpx` on the grounds that it is a transitive dependency of `openai>=2.0.0`. That
was reversed in `07` §3: the edge is undeclared in `pyproject.toml`, so a future `openai`
release that drops `httpx` would silently break the sink — precisely the class of failure this
package exists to catalogue. The boundary test in §5 hard-forbids `httpx`/`requests`/`aiohttp`
so the decision cannot drift.
---
## 3. File ownership — collision avoidance
Two work orders that touch the same file **must not run concurrently**. This table is the
authority. Before starting, check that no in-flight PR owns your files.
| Work order | Owns (may edit) | Must not edit |
|---|---|---|
| `01-d1-default-model` | `agent/agent.py` (branch chain only) | `llm/llm.py`, `llm/adapters/` |
| `02-d3-provider-detection` | `llm/llm.py` (`_detect_provider`, `_is_ollama_provider` only) | `agent/agent.py`, `llm/adapters/` |
| `03-d2-base-url-routing` | `agent/agent.py` (base_url branch), root `README.md` | `llm/llm.py`, `llm/adapters/` |
| `04-test-gating` | `tests/_pytest_plugins/test_gating.py`, both `pytest.ini`, `tests/conftest.py`, `tests/integration/test_base_url_api_base_fix.py`, `tests/unit/cli/test_test_command.py` | any `praisonaiagents/` source; **`.github/workflows/**` — the `-m` expressions are not the bug** |
| `05-live-ci-job` | `.github/workflows/test-optimized.yml`, `tests/integration/test_ollama_tool_calling_live.py` | any `praisonaiagents/` source; `test_gating.py` |
| `06-adapter-revival` | `llm/adapters/__init__.py`, `llm/llm.py` (3 response paths) | `agent/agent.py` |
| `07-local-package` | `praisonaiagents/local/**` (new), `src/praisonai/tests/unit/llm/local/**` (new), `pyproject.toml` extras | everything else |
| `08-reachability` | `memory/`, `eval/`, `context/`, `lite/`, `telemetry/` | `llm/`, `agent/` |
### Enforced serialisation
- **`01` then `03`** — both edit `agent/agent.py`. Land `01` first.
- **`02` then `06`** — both edit `llm/llm.py`. Land `02` first; it is far smaller.
- **`04` then `05`** — before `04`, `-m provider_ollama` selects 32 tests across 5 files (only 7
are Ollama tests), so `05` has no usable selector and is unbuildable.
- **`04` and `07` are independent** of orders 01/02/03/06 and may run in parallel with them.
- **`08` requires `07`** to have landed, because it consumes `local/`.
---
## 4. Status
Update your row in the same commit as your change. One line only.
Additional PRs not in the original order list, found while executing it:
D4 (local route prefixes) #4798, D5 (local embedding dimensions) #4802,
D6 (registry accepts local providers) #4800.
**Three claims in this ledger were corrected by executing it.** Each is marked
inline where it appears, but they are collected here because a reader who trusts
the summary would otherwise carry the wrong version:
1. *"Wire the twelve dead adapter methods."* Measurement said the opposite:
**12 of 20 items should be deleted, only 3 wired** (`06` §1). Six had no
inline equivalent at all — an agent told to wire them would have invented
behaviour no test could fail against.
2. *"`provider_ollama` deselects the mocked base_url suite."* It does not.
**`not network` deselects it on its own**, because the plugin implies
`network` from any provider marker (`04` §1). Editing the workflow `-m`
strings would have changed nothing.
3. *"Running fully locally is unachievable, not merely unconfigured."* Not so:
litellm and the openai SDK both honour `OPENAI_BASE_URL`, so the bypass sites
**do** reach a local server. The blocker is the hardcoded model name they then
ask for (`00` §5, `08`).
| Order | Title | Branch | PR | Status |
|---|---|---|---|---|
| 01 | D1 — provider-prefixed default model never reaches litellm | `fix/d1-default-provider-model` | #4795 | PR open |
| 02 | D3 — provider over-detection from base_url | `fix/d3-provider-detection` | #4797 | PR open |
| 03 | D2 — README recipe + local-endpoint diagnostic | `fix/d2-base-url-routing` | #4799 | PR open |
| 04 | D7 — per-file provider marker deselects unrelated tests | `fix/d7-per-test-provider-markers` | #4801 | PR open |
| 05 | Live local-model CI job | `feat/ci-ollama-live-job` | #4803 | PR open |
| 06 | A1/A2 — make the provider adapter load-bearing (6 sub-steps) | 6 branches, stacked | #4804 #4806 #4808 #4809 #4810 #4811 | **complete — KNOWN_DEAD empty** |
| 07 | `praisonaiagents/local/` — discovery and capability resolver | `feat/local-model-resolver` | #4807 | PR open |
| 08 | A3 — auxiliary-model fallback configurable everywhere | `fix/reachability-local-endpoint` | #4812 | PR open (model half; parameter half deferred) |
---
## 5. The two tests that keep this work honest
Both are cheap, both are anti-regression, and both should land with their respective orders.
**Import boundary (`07`).** Asserts `local/` imports nothing from `praisonaiagents`. Without
it, I6 decays silently and the leaf property — the whole reason the package can be adopted
by `memory/` and `eval/` — is lost.
**Adapter reachability (`06`).** Asserts every method on `DefaultAdapter` has at least one
call site. This is the test whose absence allowed 11 of 17 adapter methods to become dead code
while `llm.py` grew 145 hand-written Ollama references. Two limits, both real: it cannot detect
an *unreachable* call site (proven by `should_skip_streaming_with_tools`, which a caller would
have satisfied while the branch stayed dead), and **one call site satisfies it — so it does not
detect the three-path divergence at all.** A floor, not a ceiling.
---
## 6. Deliberately out of scope
Named here so that deferring them is a decision rather than an oversight. Do not let a work
order quietly grow into one of these.
| Deferred | Measured size | Why not now |
|---|---|---|
| Consolidating `LLM` and `OpenAIClient` behind one protocol | 7,160 + 2,691 lines, selected by one boolean at 6 sites | Neither is a superset of the other; the merge is larger than every order in this ledger combined. |
| Decomposing `LLM.get_response` | 1,675 lines, 194 branch/loop/try nodes | Highest-risk file in the package. Order `06` must edit it surgically without reflowing it. |
| Removing the sync/async divergence | ~1,628 duplicated lines across 10 pairs; `get_response` vs `_async` only 22.3% similar | Requires the consolidation above to be worth doing. |
The one obligation this ledger accepts toward them: **do not make them worse.** No order may
add a fifth copy of any compensation, and no order may add new logic inside `get_response`
that is not a delegation to an adapter.
@@ -92,6 +92,20 @@ from ._lazy import lazy_import, create_lazy_getattr_with_fallback
# Thread-safe cache for lazy-loaded values
_lazy_cache = {}
# Backward compatibility: _get_lazy_cache function for tests
import threading
_lazy_cache_local = threading.local()
def _get_lazy_cache():
"""Get thread-local lazy cache dict. Thread-safe for concurrent access.
Note: This is kept for backward compatibility with tests.
The main lazy loading now uses the centralized _lazy.py utility.
"""
if not hasattr(_lazy_cache_local, 'cache'):
_lazy_cache_local.cache = {}
return _lazy_cache_local.cache
# ============================================================================
# LAZY IMPORT MAPPING
# ============================================================================
@@ -133,23 +147,12 @@ _LAZY_IMPORTS = {
'resolve_harness': ('praisonaiagents.model_harness.profiles', 'resolve_harness'),
'register_profile': ('praisonaiagents.model_harness.profiles', 'register_profile'),
# Test doubles - drive an Agent offline and keep real requests out of a suite
'ScriptedModel': ('praisonaiagents.model_harness.scripted', 'ScriptedModel'),
'ScriptExhausted': ('praisonaiagents.model_harness.scripted', 'ScriptExhausted'),
'allow_model_requests': ('praisonaiagents.model_harness.guard', 'allow_model_requests'),
'no_model_requests': ('praisonaiagents.model_harness.guard', 'no_model_requests'),
'ModelRequestBlocked': ('praisonaiagents.model_harness.guard', 'ModelRequestBlocked'),
# Run outcomes - typed validation and agent execution results
'AgentRunOutcome': ('praisonaiagents.run_outcome', 'AgentRunOutcome'),
'RunStatus': ('praisonaiagents.run_outcome', 'RunStatus'),
'TerminationReason': ('praisonaiagents.run_outcome', 'TerminationReason'),
'termination_to_run_status': ('praisonaiagents.run_outcome', 'termination_to_run_status'),
'validate_decision_string': ('praisonaiagents.run_outcome', 'validate_decision_string'),
'RunTerminal': ('praisonaiagents.run_outcome', 'RunTerminal'),
'merge_run_terminal': ('praisonaiagents.run_outcome', 'merge_run_terminal'),
'is_sticky': ('praisonaiagents.run_outcome', 'is_sticky'),
'collapse': ('praisonaiagents.run_outcome', 'collapse'),
# Main display utilities (imports rich)
'TaskOutput': ('praisonaiagents.main', 'TaskOutput'),
@@ -231,7 +234,6 @@ _LAZY_IMPORTS = {
# Guardrails
'GuardrailResult': ('praisonaiagents.guardrails', 'GuardrailResult'),
'LLMGuardrail': ('praisonaiagents.guardrails', 'LLMGuardrail'),
'GuardrailRetry': ('praisonaiagents.guardrails', 'GuardrailRetry'),
# Approval (agent-centric approval backends)
'AutoApproveBackend': ('praisonaiagents.approval.backends', 'AutoApproveBackend'),
@@ -5,6 +5,7 @@ Provides feature flags and configuration options that can be controlled
via environment variables for performance tuning and behavior control.
Environment Variables:
PRAISONAI_LAZY_IMPORTS: Enable lazy imports (default: true)
PRAISONAI_TELEMETRY_ENABLED: Enable telemetry (default: false, opt-in)
PRAISONAI_TELEMETRY_DISABLED: Disable telemetry (takes precedence)
DO_NOT_TRACK: Standard opt-out flag (takes precedence)
@@ -27,6 +28,13 @@ def _str_to_bool(value: Optional[str], default: bool = False) -> bool:
# Feature Flags
# -------------
# Lazy imports: When True (default), heavy dependencies like litellm are
# only imported when actually needed, reducing import time significantly.
LAZY_IMPORTS = _str_to_bool(
os.environ.get('PRAISONAI_LAZY_IMPORTS'),
default=True
)
# Telemetry: Opt-in by default (False). Users must explicitly enable.
# Precedence (highest to lowest):
# 1. DO_NOT_TRACK=true -> disabled
@@ -99,3 +107,7 @@ def _get_plugins_list() -> list:
return []
# Parse comma-separated list
return [p.strip() for p in env_value.split(',') if p.strip()]
PLUGINS_ENABLED = _get_plugins_enabled()
PLUGINS_LIST = _get_plugins_list()
+3 -35
View File
@@ -37,22 +37,6 @@ _cache_lock = threading.RLock()
# Global module cache
_module_cache: Dict[str, Any] = {}
# Import roots of praisonaiagents' declared *core* runtime dependencies (see
# pyproject.toml ``[project].dependencies``). A lazy import that fails because
# one of these is missing/broken indicates a genuinely broken install, which
# must be surfaced rather than masked as a graceful ``None``. Optional
# integrations whose own optional SDKs are absent are NOT in this set, so they
# keep the documented ``None`` fallback.
_CORE_REQUIRED_DEPENDENCIES = frozenset({
"pydantic",
"rich",
"openai",
"posthog",
"aiohttp",
"yaml", # PyYAML — core dep; import root differs from the package name
"croniter",
})
def lazy_import(
module_path: str,
@@ -236,25 +220,9 @@ def create_lazy_getattr_with_fallback(
_cache[name] = None
return None
except ImportError as exc:
# Distinguish "optional integration not installed" from
# "a required *core* dependency is broken". A missing/broken core
# dependency (e.g. ``pydantic``) must be surfaced loudly instead
# of being masked as ``None`` (which resurfaces later as a
# confusing ``NoneType`` error). But an optional integration whose
# own optional SDK is absent must keep the documented graceful
# ``None`` fallback — the failing module name there belongs to
# that optional SDK, not to us. We therefore only escalate when
# the failing module is one of praisonaiagents' declared *core*
# runtime dependencies; everything else stays a soft None.
failing = getattr(exc, "name", None)
failing_root = failing.split(".")[0] if failing else None
if failing_root in _CORE_REQUIRED_DEPENDENCIES:
raise ImportError(
f"Failed to load {name!r} because importing {module_path!r} "
f"raised: {exc}. This usually means a required core dependency "
f"({failing_root!r}) is missing or broken, not that {name!r} is "
f"an optional feature you chose not to install."
) from exc
# Optional module not available: preserve the graceful None
# fallback, but record why so real import failures remain
# diagnosable instead of surfacing later as NoneType errors.
_logger.debug(
"Lazy import of %r (%s.%s) failed: %s",
name, module_path, attr_name, exc,
@@ -28,10 +28,8 @@ def _configure_environment():
"PYDANTIC_WARNINGS_ENABLED": "False",
}
# Use setdefault so a value the host exported before launch is preserved
# (and not leaked/overwritten into subprocesses spawned afterwards).
for key, value in env_vars.items():
os.environ.setdefault(key, value)
os.environ[key] = value
# ========================================================================
@@ -50,11 +48,7 @@ def _get_all_noisy_loggers() -> List[str]:
# Markdown
"markdown_it", "rich.markdown",
# System
# NOTE: "asyncio" is intentionally NOT suppressed here — it is the host
# application's own logger, not a PraisonAI dependency's. Forcing it to
# CRITICAL hides unrelated host errors (e.g. "Task exception was never
# retrieved").
"selector_events", "pydantic",
"asyncio", "selector_events", "pydantic",
"praisonaiagents.telemetry.telemetry",
]
@@ -27,7 +27,7 @@ SUPPRESSED_PATTERNS = [
"The `dict` method is deprecated; use `model_dump` instead",
"Pydantic serializer warnings",
"PydanticSerializationUnexpectedValue",
"fields but got", # litellm/pydantic "Expected N fields but got M"
"Expected", # Catches all "Expected N fields but got M" patterns
"Expected `StreamingChoices` but got `Choices`",
"serialized value may not be as expected",
"Mixing V1 models and V2 models",
@@ -215,14 +215,6 @@ def __getattr__(name):
from .memory_mixin import MemoryMixin
_lazy_cache[name] = MemoryMixin
return MemoryMixin
elif name == 'AttachmentError':
from .attachments import AttachmentError
_lazy_cache[name] = AttachmentError
return AttachmentError
elif name == 'build_attachment_parts':
from .attachments import build_attachment_parts
_lazy_cache[name] = build_attachment_parts
return build_attachment_parts
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -305,7 +297,4 @@ __all__ = [
'ChatMixin',
'ExecutionMixin',
'MemoryMixin',
# Attachments
'AttachmentError',
'build_attachment_parts',
]
File diff suppressed because it is too large. Load diff
Loaded 100 of 2283 files, more files were not shown because too many files have changed in this diff. Show more