Compare commits

..
Author SHA1 Message Date
praisonai-triage-agent[bot]andMervin Praison 9e0912499c style: restore PEP 8 blank line separation between test methods
Co-authored-by: Mervin Praison <[email protected]>
2026-07-02 21:43:02 +00:00
3735 changed files with 104834 additions and 574458 deletions

No files matched your search

-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. -->
@@ -222,13 +222,10 @@ runs:
STEP 2 — ARCHITECTURE VALIDATION & ROUTING (MANDATORY before writing code):
Before implementing anything, answer these questions:
- CORE vs WRAPPER vs TOOLS vs PLUGINS ROUTING:
- CORE vs WRAPPER vs TOOLS ROUTING:
1. Core SDK (praisonaiagents/): Only core protocols, base classes, decorators. No heavy implementations.
2. Wrapper (praisonai/): CLI, heavy implementations, optional dependencies.
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).
3. Tools (PraisonAI-Tools): Specific integrations, external tools (e.g. SurrealDB, Slack), and community extensions MUST go to the `MervinPraison/PraisonAI-Tools` repository. If the feature is a tool, clone `https://github.com/MervinPraison/PraisonAI-Tools`, implement it there, and create the PR using `gh pr create -R MervinPraison/PraisonAI-Tools`.
- 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 +285,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 +325,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,96 +0,0 @@
name: Install PraisonAI monorepo packages
description: Install praisonai-agents, praisonai-code, praisonai-bot, praisonai-train, praisonai-browser, praisonai-mcp, praisonai-sandbox, praisonai-deploy, and praisonai wrapper from local paths (mirrors PyPI nine-package install order).
inputs:
repo-root:
description: Path prefix to monorepo root (e.g. PraisonAI/ for cross-repo checkout)
required: false
default: ""
use-uv:
description: Use uv pip instead of pip
required: false
default: "false"
agents-extras:
description: Optional extras for praisonaiagents (e.g. .[all] or [all])
required: false
default: ""
wrapper-extras:
description: Optional extras for praisonai wrapper (e.g. .[chat,code] or [chat,code])
required: false
default: ""
runs:
using: composite
steps:
- name: Install monorepo packages
shell: bash
run: |
set -euo pipefail
WORKSPACE="${GITHUB_WORKSPACE:-$(pwd)}"
ROOT="${{ inputs.repo-root }}"
if [ -n "$ROOT" ]; then
BASE="${WORKSPACE%/}/${ROOT%/}/"
else
BASE="${WORKSPACE%/}/"
fi
AGENTS_PKG="${BASE}src/praisonai-agents"
CODE_PKG="${BASE}src/praisonai-code"
BOT_PKG="${BASE}src/praisonai-bot"
TRAIN_PKG="${BASE}src/praisonai-train"
BROWSER_PKG="${BASE}src/praisonai-browser"
MCP_PKG="${BASE}src/praisonai-mcp"
SANDBOX_PKG="${BASE}src/praisonai-sandbox"
DEPLOY_PKG="${BASE}src/praisonai-deploy"
WRAPPER_PKG="${BASE}src/praisonai"
normalise_extras() {
local value="${1#.}"
if [ -n "$value" ]; then
printf '.%s' "$value"
else
printf '.'
fi
}
AGENTS_SUFFIX="$(normalise_extras "${{ inputs.agents-extras }}")"
WRAPPER_SUFFIX="$(normalise_extras "${{ inputs.wrapper-extras }}")"
if [ "${{ inputs.use-uv }}" = "true" ]; then
cd "$AGENTS_PKG"
uv pip install --system -e "${AGENTS_SUFFIX}"
cd "$CODE_PKG"
uv pip install --system -e .
cd "$BOT_PKG"
uv pip install --system -e .
cd "$TRAIN_PKG"
uv pip install --system -e .
cd "$BROWSER_PKG"
uv pip install --system -e .
cd "$MCP_PKG"
uv pip install --system -e .
cd "$SANDBOX_PKG"
uv pip install --system -e .
cd "$DEPLOY_PKG"
uv pip install --system -e .
cd "$WRAPPER_PKG"
uv pip install --system "${WRAPPER_SUFFIX}"
else
cd "$AGENTS_PKG"
pip install -e "${AGENTS_SUFFIX}"
cd "$CODE_PKG"
pip install -e .
cd "$BOT_PKG"
pip install -e .
cd "$TRAIN_PKG"
pip install -e .
cd "$BROWSER_PKG"
pip install -e .
cd "$MCP_PKG"
pip install -e .
cd "$SANDBOX_PKG"
pip install -e .
cd "$DEPLOY_PKG"
pip install -e .
cd "$WRAPPER_PKG"
pip install -e "${WRAPPER_SUFFIX}"
fi
+2 -10
View File
@@ -33,20 +33,12 @@ 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.
Fifth, decide if this issue requires code changes:
Fourth, 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 SDK / Wrapper / Tools / 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) {
@@ -1,93 +0,0 @@
#!/usr/bin/env node
/**
* Run: node .github/scripts/ci-failure-claude-selftest.js
*/
const ciFix = require('./ci-failure-claude.js');
const mergeGate = require('./merge-gate.js');
let failed = 0;
function assert(name, cond) {
if (!cond) {
console.error('FAIL:', name);
failed += 1;
} else {
console.log('ok:', name);
}
}
const LOG_2767 = `
test-core (cli) UNKNOWN STEP 2026-07-07T17:21:49.8096670Z FAILED (0.0100s) tests/unit/cli/test_typer_cli.py::TestConfigCommand::test_config_list - ValueError: I/O operation on closed file.
test-core (cli) UNKNOWN STEP 2026-07-07T17:21:49.8097323Z ============= 1 failed, 1401 passed, 1 skipped in 88.86s (0:01:28) =============
test-core (cli) UNKNOWN STEP 2026-07-07T17:21:49.8693341Z ##[error]Process completed with exit code 1.
`;
const parsed = ciFix.parsePytestFailures(LOG_2767);
assert('parses pytest failure from log', parsed.length === 1);
assert(
'extracts test path',
parsed[0]?.testId === 'tests/unit/cli/test_typer_cli.py::TestConfigCommand::test_config_list'
);
assert(
'extracts error message',
parsed[0]?.error.includes('ValueError: I/O operation on closed file')
);
const comment = ciFix.buildCiFixComment({
headSha: 'dfb81bb0ae7f36bf8d66d8f45053464c18c005a3',
failedChecks: [
{
name: 'test-core (cli)',
workflow: 'Core Tests',
html_url: 'https://github.com/MervinPraison/PraisonAI/actions/runs/28885180173/job/85683605433',
},
],
failureSummaries: [{ jobName: 'test-core (cli)', failures: parsed }],
});
assert('comment includes @claude', comment.includes('@claude'));
assert('comment includes short sha', comment.includes('dfb81bb0'));
assert('comment includes test path', comment.includes('test_typer_cli.py::TestConfigCommand::test_config_list'));
assert('comment includes job url', comment.includes('85683605433'));
assert('comment requires critical review verdict', comment.includes('Critical review first'));
assert('comment forbids weakening tests', comment.includes('do not weaken, skip, or delete tests'));
assert('comment mentions SDK guardrails', comment.includes('SDK guardrails'));
assert('comment asks for legitimacy verdict', comment.includes('legitimate fix'));
const headSha = 'dfb81bb0ae7f36bf8d66d8f45053464c18c005a3';
const commentsWithFix = [
{
user: { login: 'MervinPraison' },
body: '@claude CI failed on HEAD `dfb81bb0`. Please fix...',
created_at: '2026-07-07T18:00:00Z',
},
];
assert('sha dedup detects existing comment', ciFix.hasCiFixCommentForSha(commentsWithFix, headSha));
const skipNoFinal = ciFix.shouldSkipCiFix({
comments: [],
headSha,
labels: [],
hasFinal: false,
claudeInProgress: false,
failedChecks: [{ name: 'test-core' }],
});
assert('skips when final not posted', skipNoFinal.skip && skipNoFinal.reason.includes('final'));
const skipPending = ciFix.shouldSkipCiFix({
comments: [],
headSha,
labels: [ciFix.CI_FIX_LABEL],
hasFinal: true,
claudeInProgress: false,
failedChecks: [{ name: 'test-core' }],
});
assert('skips when ci fix pending label', skipPending.skip && skipPending.reason.includes('pending'));
const failedRuns = mergeGate.listFailedChecksOnSha([
{ status: 'completed', conclusion: 'success', name: 'smoke' },
{ status: 'completed', conclusion: 'failure', name: 'test-core (cli)' },
{ status: 'in_progress', conclusion: null, name: 'pending-job' },
]);
assert('listFailedChecksOnSha finds failures only', failedRuns.length === 1);
assert('failed check name', failedRuns[0].name === 'test-core (cli)');
process.exit(failed ? 1 : 0);
-294
View File
@@ -1,294 +0,0 @@
/**
* Post @claude comments with CI failure details for internal PRs.
* @see .github/workflows/ci-failure-claude.yml
*/
const mergeGate = require('./merge-gate.js');
const CI_FIX_LABEL = 'claude-ci-fix-pending';
const COOLDOWN_MS = 12 * 60 * 60 * 1000;
const AUTO_ACTORS = mergeGate.AUTO_ACTORS;
const MAX_FAILURES = 15;
const MAX_JOBS_TO_FETCH = 5;
function shortSha(headSha) {
return (headSha || '').slice(0, 8);
}
function ciFixShaMarker(headSha) {
return `ci failed on head \`${shortSha(headSha)}`.toLowerCase();
}
function isCiFixComment(comment) {
const body = (comment.body || '').toLowerCase();
return body.includes('@claude') && body.includes('ci failed on head');
}
function hasCiFixCommentForSha(comments, headSha) {
const marker = shortSha(headSha).toLowerCase();
return comments.some((c) => {
if (!AUTO_ACTORS.includes(c.user.login)) return false;
if (!isCiFixComment(c)) return false;
return (c.body || '').toLowerCase().includes(marker);
});
}
function hasRecentCiFixComment(comments, headSha) {
const cutoff = Date.now() - COOLDOWN_MS;
const marker = shortSha(headSha).toLowerCase();
return comments.some((c) => {
if (!AUTO_ACTORS.includes(c.user.login)) return false;
if (!isCiFixComment(c)) return false;
const body = (c.body || '').toLowerCase();
if (body.includes(marker)) return true;
return new Date(c.created_at).getTime() > cutoff;
});
}
function shouldSkipCiFix({ comments, headSha, labels, hasFinal, claudeInProgress, failedChecks }) {
if (!failedChecks.length) return { skip: true, reason: 'no failed checks' };
if (labels.includes(CI_FIX_LABEL)) return { skip: true, reason: 'ci fix pending' };
if (!hasFinal) return { skip: true, reason: 'awaiting final claude review trigger' };
if (claudeInProgress) return { skip: true, reason: 'claude in progress' };
if (hasCiFixCommentForSha(comments, headSha)) {
return { skip: true, reason: 'already commented for this sha' };
}
if (hasRecentCiFixComment(comments, headSha)) {
return { skip: true, reason: 'recent ci fix comment cooldown' };
}
return { skip: false };
}
function parsePytestFailures(logText) {
if (!logText) return [];
const lines = logText.split('\n');
const failures = [];
const seen = new Set();
for (let i = lines.length - 1; i >= 0 && failures.length < MAX_FAILURES; i -= 1) {
const line = lines[i].trim();
if (!line) continue;
const pytestMatch = line.match(
/FAILED\s*(?:\([^)]+\))?\s+(tests\/[^\s]+(?:::[^\s-]+)*)\s*-\s*(.+)/i
);
if (pytestMatch) {
const testId = pytestMatch[1];
if (!seen.has(testId)) {
seen.add(testId);
failures.push({ testId, error: pytestMatch[2].trim() });
}
continue;
}
const errorMatch = line.match(/##\[error\](.+)/);
if (errorMatch && /tests\//.test(errorMatch[1])) {
const msg = errorMatch[1].trim();
if (!seen.has(msg)) {
seen.add(msg);
failures.push({ testId: msg, error: msg });
}
}
}
return failures;
}
async function fetchJobFailureSummary(github, owner, repo, jobId, jobName) {
try {
const response = await github.rest.actions.downloadJobLogsForWorkflowRun({
owner,
repo,
job_id: jobId,
});
let logText = typeof response.data === 'string' ? response.data : String(response.data || '');
if (logText.length > 100000) {
logText = logText.slice(-100000);
}
return { jobName, jobId, failures: parsePytestFailures(logText) };
} catch (err) {
return { jobName, jobId, failures: [], error: err.message };
}
}
function mapFailedChecks(runs) {
return runs.map((run) => ({
name: run.name,
id: run.id,
html_url: run.html_url || run.details_url,
workflow: run.app?.slug || run.check_suite?.app?.slug || run.name,
}));
}
function buildCiFixComment({ headSha, failedChecks, failureSummaries }) {
const parts = [
`@claude CI failed on HEAD \`${shortSha(headSha)}\`. Please fix the failures below and push to this branch.`,
'',
'## Failed checks',
];
for (const check of failedChecks) {
parts.push(`- **${check.workflow || check.name}** / \`${check.name}\` — ${check.html_url}`);
}
parts.push('', '## Failures (extracted)');
let idx = 0;
for (const summary of failureSummaries) {
for (const failure of summary.failures) {
idx += 1;
if (idx > MAX_FAILURES) break;
parts.push(`${idx}. \`${failure.testId}\` — \`${failure.error}\``);
parts.push(` - Job: \`${summary.jobName}\``);
}
if (idx >= MAX_FAILURES) break;
}
if (idx === 0) {
parts.push('_(Could not extract pytest details — see job logs above.)_');
}
const exampleTest = failureSummaries.find((s) => s.failures.length)?.failures[0]?.testId;
parts.push(
'',
'## Critical review first',
'Before changing code or tests, decide **which side is wrong**:',
'1. **Legitimate feature change** — the PR intent is correct but implementation or tests need updating. Preserve SDK guarantees; update tests only when behaviour intentionally changed and document why.',
'2. **Regression / bug in this PR** — the failure exposes a real breakage introduced here. Fix the implementation; **do not weaken, skip, or delete tests** just to go green.',
'3. **Pre-existing flake or unrelated failure** — say so explicitly; prefer fixing the root cause over masking it.',
'',
'**SDK guardrails (AGENTS.md):**',
'- Agent-callable tools → PraisonAI-Tools; lifecycle plugins → PraisonAI-Plugins; sandbox backends → PraisonAI-Plugins (`praisonai.sandbox`) — not praisonaiagents/.',
'- Do not disturb core SDK contracts to accommodate a wrapper/feature change.',
'- Tests must continue to guard backward compatibility and hot-path behaviour — passing CI by lowering test standards is not acceptable.',
'- If the feature does not genuinely add SDK value, recommend reverting or narrowing scope instead of patching around failures.',
'',
'## What to do',
'1. State your verdict: **legitimate fix**, **regression fix**, or **needs human review** — and why (1–3 sentences).',
'2. Fix root cause with **minimal changes**; never bloat the Agent class with extra params.',
exampleTest
? `3. Run failing tests locally, e.g. \`pytest ${exampleTest} -q\`, plus any related SDK tests touched by the PR.`
: '3. Run failing tests locally with targeted pytest, plus any related SDK tests touched by the PR.',
'4. Push to this branch and comment: files changed, review verdict, and why tests still protect SDK behaviour.',
);
return parts.join('\n');
}
async function maybeClearCiFixLabel(github, owner, repo, prNumber, labels, headSha, core) {
if (!labels.includes(CI_FIX_LABEL)) return false;
const ciGreen = await mergeGate.allChecksGreenOnSha(github, owner, repo, headSha, core);
if (!ciGreen) return false;
try {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: prNumber,
name: CI_FIX_LABEL,
});
core?.info?.(`Removed ${CI_FIX_LABEL} from PR #${prNumber}`);
} catch (err) {
if (err.status !== 404) throw err;
}
return true;
}
async function maybeTriggerCiFixClaude(github, owner, repo, prNumber, core, opts = {}) {
const baseRepo = `${owner}/${repo}`;
const ctx = await mergeGate.loadPrContext(github, owner, repo, prNumber);
const pr = ctx.pr;
if (pr.state !== 'open') return { skipped: true, reason: 'not open' };
if (pr.draft) return { skipped: true, reason: 'draft' };
const headRepo = pr.head.repo?.full_name;
if (headRepo && headRepo !== baseRepo && !pr.maintainer_can_modify) {
return { skipped: true, reason: 'fork without maintainer edits' };
}
const headSha = pr.head.sha;
const labels = ctx.labels;
if (await maybeClearCiFixLabel(github, owner, repo, prNumber, labels, headSha, core)) {
return { skipped: true, reason: 'ci green, label cleared' };
}
const ciGreen = await mergeGate.allChecksGreenOnSha(github, owner, repo, headSha, core);
if (ciGreen) return { skipped: true, reason: 'ci green' };
const runs = await mergeGate.listChecksOnSha(github, owner, repo, headSha);
const failedRuns = mergeGate.listFailedChecksOnSha(runs);
if (failedRuns.length === 0) {
return { skipped: true, reason: 'no failed checks yet (may be pending)' };
}
const hasFinal = mergeGate.hasFinalClaudeReviewTrigger(ctx.comments);
const claudeInProgress = await mergeGate.hasInProgressClaudeAssistant(
github, owner, repo, prNumber
);
const skip = shouldSkipCiFix({
comments: ctx.comments,
headSha,
labels,
hasFinal,
claudeInProgress,
failedChecks: failedRuns,
});
if (skip.skip) return { skipped: true, reason: skip.reason };
const failedChecks = mapFailedChecks(failedRuns);
const failureSummaries = [];
for (const check of failedChecks.slice(0, MAX_JOBS_TO_FETCH)) {
failureSummaries.push(
await fetchJobFailureSummary(github, owner, repo, check.id, check.name)
);
}
const body = buildCiFixComment({ headSha, failedChecks, failureSummaries });
await github.rest.issues.addLabels({
owner,
repo,
issue_number: prNumber,
labels: [CI_FIX_LABEL],
});
await github.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body,
});
core?.info?.(`Posted CI fix @claude on PR #${prNumber} (${shortSha(headSha)})`);
return { triggered: true, headSha: shortSha(headSha) };
}
async function processWorkflowRunFailure(github, owner, repo, workflowRun, core) {
if (workflowRun.conclusion !== 'failure') {
return { skipped: true, reason: `conclusion=${workflowRun.conclusion}` };
}
const allowed = ['Core Tests', 'Optimized Test Suite'];
if (!allowed.includes(workflowRun.name)) {
return { skipped: true, reason: `workflow ${workflowRun.name}` };
}
const prNumber = await mergeGate.resolvePrNumberFromWorkflowRun(
github, owner, repo, workflowRun
);
if (!prNumber) return { skipped: true, reason: 'no linked PR' };
return maybeTriggerCiFixClaude(github, owner, repo, prNumber, core);
}
module.exports = {
CI_FIX_LABEL,
COOLDOWN_MS,
ciFixShaMarker,
isCiFixComment,
hasCiFixCommentForSha,
hasRecentCiFixComment,
shouldSkipCiFix,
parsePytestFailures,
fetchJobFailureSummary,
buildCiFixComment,
maybeClearCiFixLabel,
maybeTriggerCiFixClaude,
processWorkflowRunFailure,
};
+7 -274
View File
@@ -27,109 +27,6 @@ const withRecovery = [
];
assert('not stale when @claude after head', !mg.isStaleFinalAfterPush(withRecovery, '2026-06-12T09:00:00Z'));
const withClaudeReply = [
...finals,
{
user: { login: 'praisonai-triage-agent[bot]' },
body: "**Claude finished @MervinPraison's task** —— [View job](https://github.com/)",
created_at: '2026-06-12T08:30:00Z',
},
];
assert('not stale when Claude replied after FINAL', !mg.isStaleFinalAfterPush(withClaudeReply, '2026-06-12T09:00:00Z'));
assert('claude final reply detected', mg.isClaudeFinalReplyComment(withClaudeReply[1]));
assert('cancelled detect-and-trigger does not block', mg.OPTIONAL_CANCELLED_CHECKS.has('detect-and-trigger'));
const coreGreenRuns = [
{ name: 'test-core', status: 'completed', conclusion: 'success' },
{ name: 'test-core (cli)', status: 'completed', conclusion: 'success' },
{ name: 'smoke', status: 'completed', conclusion: 'cancelled' },
{ name: 'test-windows', status: 'completed', conclusion: 'cancelled' },
];
assert('core green allows cancelled smoke', mg.isAcceptableCheckConclusion(
{ name: 'smoke', status: 'completed', conclusion: 'cancelled' },
coreGreenRuns
));
assert('core green allows cancelled test-windows', mg.isAcceptableCheckConclusion(
{ name: 'test-windows', status: 'completed', conclusion: 'cancelled' },
coreGreenRuns
));
assert('cancelled smoke blocks when core missing', !mg.isAcceptableCheckConclusion(
{ name: 'smoke', status: 'completed', conclusion: 'cancelled' },
[{ name: 'smoke', status: 'completed', conclusion: 'cancelled' }]
));
assert('cancelled smoke blocks when core failed', !mg.isAcceptableCheckConclusion(
{ name: 'smoke', status: 'completed', conclusion: 'cancelled' },
[
{ name: 'test-core', status: 'completed', conclusion: 'failure' },
{ name: 'smoke', status: 'completed', conclusion: 'cancelled' },
]
));
assert('bestRunsByName prefers success over cancelled', (() => {
const best = mg.bestRunsByName([
{ name: 'smoke', conclusion: 'cancelled' },
{ name: 'smoke', conclusion: 'success' },
]);
return best.length === 1 && best[0].conclusion === 'success';
})());
assert('bestRunsByName prefers pending over completed cancelled', (() => {
const best = mg.bestRunsByName([
{ name: 'smoke', status: 'completed', conclusion: 'cancelled' },
{ name: 'smoke', status: 'in_progress', conclusion: null },
]);
return best.length === 1 && best[0].status === 'in_progress';
})());
assert('bestRunsByName keeps pending regardless of order', (() => {
const best = mg.bestRunsByName([
{ name: 'smoke', status: 'in_progress', conclusion: null },
{ name: 'smoke', status: 'completed', conclusion: 'success' },
]);
return best.length === 1 && best[0].status === 'in_progress';
})());
assert('allChecksGreenOnSha style: pending re-run blocks despite core green', (() => {
const runs = mg.bestRunsByName([
{ name: 'test-core', status: 'completed', conclusion: 'success' },
{ name: 'smoke', status: 'completed', conclusion: 'cancelled' },
{ name: 'smoke', status: 'queued', conclusion: null },
]);
const smoke = runs.find((r) => r.name === 'smoke');
return smoke.status !== 'completed';
})());
// Stale-FINAL recovery guards (PR #2560 push loop)
const nowMs = Date.now();
const iso = (ms) => new Date(ms).toISOString();
const firstFinal = {
user: { login: 'MervinPraison' },
body: '@claude You are the FINAL architecture reviewer.',
created_at: iso(nowMs - 20 * 60 * 1000),
};
const pushSoonAfter = mg.shouldSkipStaleFinalRecovery(
[firstFinal],
iso(nowMs - 16 * 60 * 1000)
);
assert('debounce push soon after FINAL', pushSoonAfter.skip && pushSoonAfter.reason.includes('soon after'));
const secondFinal = {
user: { login: 'MervinPraison' },
body: '@claude You are the FINAL architecture reviewer.',
created_at: iso(nowMs - 20 * 60 * 1000),
};
const capped = mg.shouldSkipStaleFinalRecovery(
[firstFinal, secondFinal],
iso(nowMs - 2 * 60 * 1000)
);
assert('hourly cap blocks third FINAL in window', capped.skip && capped.reason.includes('capped'));
const botPush = mg.shouldSkipStaleFinalRecovery(
[firstFinal],
iso(nowMs - 16 * 60 * 1000),
'praisonai-triage-agent[bot]'
);
assert('skip when automation pushed head', botPush.skip && botPush.reason.includes('automation'));
assert('claude automation login', mg.isClaudeAutomationLogin('praisonai-triage-agent[bot]'));
// Bot CHANGES_REQUESTED then APPROVE
const reviews = [
{ user: { login: 'coderabbit[bot]', type: 'Bot' }, state: 'CHANGES_REQUESTED', submitted_at: '2026-06-12T08:00:00Z' },
@@ -151,95 +48,9 @@ assert('verdict after head accepted', mg.findMergeGateVerdict(
'2026-06-12T09:00:00Z'
) === 'APPROVE');
const fallbackApprove = [
{
body: 'MERGE_GATE_VERDICT: APPROVE\n\nAutomated fallback — Claude assess did not post a verdict comment.',
created_at: '2026-06-12T10:00:00Z',
},
];
assert(
'automated fallback APPROVE ignored when Opus required',
mg.findMergeGateVerdict(fallbackApprove, null, '2026-06-12T09:00:00Z', { excludeAutomatedFallback: true }) === null
);
assert(
'automated fallback APPROVE still visible without Opus-only filter',
mg.findMergeGateVerdict(fallbackApprove, null, '2026-06-12T09:00:00Z') === 'APPROVE'
);
const fallbackBlockNewFormat = [
{
body: 'MERGE_GATE_VERDICT: BLOCK\n\nAutomated fallback — Opus merge gate assessment did not complete.',
created_at: '2026-06-12T10:00:00Z',
},
];
assert(
'new-format fallback BLOCK ignored when Opus required',
mg.findMergeGateVerdict(fallbackBlockNewFormat, null, '2026-06-12T09:00:00Z', { excludeAutomatedFallback: true }) === null
);
assert(
'new-format fallback BLOCK detected by marker helper',
mg.isAutomatedFallbackVerdict(fallbackBlockNewFormat[0].body)
);
assert(
'findMergeGateVerdict null-safe with explicit null options',
mg.findMergeGateVerdict(fallbackApprove, null, '2026-06-12T09:00:00Z', null) === 'APPROVE'
);
const noise = [{ user: { login: 'MervinPraison' }, body: '**Merge gate scan** — wait for `@claude`', created_at: new Date().toISOString() }];
assert('diagnostic comment not a trigger', !mg.hasRecentClaudeTrigger(noise, 35));
// Cooldown skip requires an actual verdict on HEAD, not just a FINAL trigger comment
const recentFinalOnHead = [
{
user: { login: 'MervinPraison' },
body: '@claude You are the FINAL architecture reviewer.',
created_at: '2026-06-27T10:00:00Z',
},
];
assert(
'final trigger alone does not count as verdict on head',
mg.findMergeGateVerdict(recentFinalOnHead, null, '2026-06-27T09:55:00Z') === null
);
const recentVerdictOnHead = [
...recentFinalOnHead,
{
user: { login: 'github-actions[bot]' },
body: 'MERGE_GATE_VERDICT: APPROVE',
created_at: '2026-06-27T10:05:00Z',
},
];
assert(
'verdict on head skips cooldown gate',
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
);
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(
@@ -266,31 +77,24 @@ assert('issues event never blocks', !mg.hasBlockingClaudeRunForPr(
'my-branch'
));
// Conflict rebase clears after bot completion + FINAL on HEAD (within 12h cooldown)
const conflictNowMs = Date.now();
const conflictIso = (offsetMs) => new Date(conflictNowMs + offsetMs).toISOString();
// Conflict rebase clears after bot completion + FINAL on HEAD
const conflictTrigger = {
user: { login: 'MervinPraison' },
body: '@claude this PR has merge conflicts with main. Please rebase',
created_at: conflictIso(-30 * 60 * 1000),
created_at: '2026-06-26T10:30:38Z',
};
const rebaseDone = {
user: { login: 'praisonai-triage-agent[bot]' },
body: 'Rebase complete — PR #2308 onto latest main',
created_at: conflictIso(-29 * 60 * 1000),
created_at: '2026-06-26T10:30:55Z',
};
const finalAfterRebase = {
user: { login: 'MervinPraison' },
body: '@claude You are the FINAL architecture reviewer.',
created_at: conflictIso(-20 * 60 * 1000),
created_at: '2026-06-26T10:39:43Z',
};
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 headAfterRebase = '2026-06-26T10:39:25Z';
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(
@@ -304,79 +108,8 @@ 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);
assert('internal PR link accepted', mg.isInternalPullRequestLink(
{ base: { repo: { full_name: 'MervinPraison/PraisonAI' } } },
'MervinPraison',
'PraisonAI'
));
assert('fork sync PR link rejected', !mg.isInternalPullRequestLink(
{ number: 21, base: { repo: { full_name: 'Milkmange/PraisonAI' } } },
'MervinPraison',
'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);
+29 -475
View File
@@ -7,9 +7,7 @@ const CLAUDE_TRIGGER_LOGINS = ['MervinPraison', 'github-actions[bot]'];
const AUTO_ACTORS = CLAUDE_TRIGGER_LOGINS;
const CONFLICT_COOLDOWN_MS = 12 * 60 * 60 * 1000;
const CLAUDE_ACTIVE_MS = 35 * 60 * 1000;
const STALE_FINAL_RECOVERY_WINDOW_MS = 60 * 60 * 1000;
const STALE_FINAL_MAX_PER_WINDOW = 2;
const PUSH_AFTER_FINAL_DEBOUNCE_MS = 15 * 60 * 1000;
const POST_PUSH_BUFFER_MS = 5 * 60 * 1000;
const ALLOWED_MERGE_STATES = new Set(['CLEAN', 'UNSTABLE']);
const AGENT_PY_MAX_AUTO_LINES = 100;
const PR_MAX_AUTO_ADDITIONS = 800;
@@ -17,8 +15,7 @@ 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 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)\//,
@@ -27,17 +24,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}/,
@@ -52,11 +39,6 @@ const BLOCK_LABELS = new Set([
'no-auto-merge',
'auto-merged-by-gate',
]);
const MERGE_READY_LABEL = 'pipeline/merge-ready';
/** Superseded concurrency runs; must not block merge when real tests passed. */
const OPTIONAL_CANCELLED_CHECKS = new Set(['detect-and-trigger']);
/** Cancelled smoke/windows after timeout are non-blocking when core shards passed on HEAD. */
const OPTIONAL_CANCELLED_WHEN_CORE_GREEN = new Set(['smoke', 'test-windows']);
const BOT_REVIEWER_PATTERNS = [
'coderabbit',
'qodo',
@@ -83,16 +65,6 @@ function isClaudeTriggerNoise(c) {
return false;
}
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;
return (c.body || '').includes('Claude finished');
}
function hasRecentClaudeTrigger(comments, minutes = 35) {
const cutoff = Date.now() - minutes * 60 * 1000;
return comments.some((c) => {
@@ -111,47 +83,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;
@@ -233,11 +173,6 @@ function isStaleFinalAfterPush(comments, headPushedAt) {
);
const finalTime = new Date(latestFinal.created_at).getTime();
if (headTime <= finalTime + 60000) return false;
const claudeRepliedAfterFinal = comments.some((c) => {
if (!isClaudeFinalReplyComment(c)) return false;
return new Date(c.created_at).getTime() >= finalTime - 60000;
});
if (claudeRepliedAfterFinal) return false;
const claudeSinceHead = comments.some((c) => {
if (!CLAUDE_TRIGGER_LOGINS.includes(c.user.login)) return false;
if (isClaudeTriggerNoise(c)) return false;
@@ -260,75 +195,14 @@ function shouldSkipFinalRecovery(comments, headPushedAt) {
return hasRecentClaudeTrigger(comments, 35);
}
function countFinalTriggersSince(comments, sinceMs) {
return comments.filter(
(c) => isFinalClaudeTriggerComment(c) && new Date(c.created_at).getTime() > sinceMs
).length;
}
function isClaudeAutomationLogin(login) {
const lower = (login || '').toLowerCase();
return lower.includes('praisonai-triage') || lower === 'github-actions[bot]';
}
function isPushSoonAfterLatestFinal(comments, headPushedAt) {
if (!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();
const headTime = new Date(headPushedAt).getTime();
if (headTime <= finalTime) return false;
return headTime - finalTime < PUSH_AFTER_FINAL_DEBOUNCE_MS;
}
/** Returns { skip: true, reason } when stale-FINAL recovery should not post. */
function shouldSkipStaleFinalRecovery(comments, headPushedAt, headPusherLogin = null) {
if (!isStaleFinalAfterPush(comments, headPushedAt)) {
return { skip: true, reason: 'not stale' };
}
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,
reason: 'head pushed soon after FINAL (wait for CI / batched fixes)',
};
}
const windowStart = Date.now() - STALE_FINAL_RECOVERY_WINDOW_MS;
const finalsInWindow = countFinalTriggersSince(comments, windowStart);
if (finalsInWindow >= STALE_FINAL_MAX_PER_WINDOW) {
return {
skip: true,
reason: `stale-FINAL capped (${finalsInWindow} FINAL triggers in last hour)`,
};
}
return { skip: false, reason: '' };
}
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/\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.\n\n**Phase 2: FIX Valid Issues**\n5. For any VALID bugs or architectural flaws found by Gemini, CodeRabbit, Qodo, Copilot, or any other reviewer: implement the fix\n6. Push all code fixes directly to THIS branch (do NOT create a new PR)\n7. Comment a summary of exact files modified and what you skipped\n\n**Phase 3: Final Verdict**\n8. If all issues are resolved, approve the PR / close the Issue\n9. If blocking issues remain, request changes / leave clear action items';
async function getMergeState(github, owner, repo, prNumber) {
const query = `
@@ -369,87 +243,14 @@ async function getMergeState(github, owner, repo, prNumber) {
};
}
async function listChecksOnSha(github, owner, repo, sha) {
async function allChecksGreenOnSha(github, owner, repo, sha, core) {
const { data } = await github.rest.checks.listForRef({
owner,
repo,
ref: sha,
per_page: 100,
});
return (data.check_runs || []).filter((r) => r.head_sha === sha);
}
function listFailedChecksOnSha(runs) {
return (runs || []).filter((run) => {
if (run.status !== 'completed') return false;
return run.conclusion === 'failure';
});
}
function isCoreTestRun(run) {
const name = run?.name || '';
return name === 'test-core' || name.startsWith('test-core ') || name === 'test-core-collect';
}
function coreTestsGreenOnRuns(runs) {
const coreRuns = (runs || []).filter(isCoreTestRun);
if (coreRuns.length === 0) return false;
return coreRuns.every(
(run) =>
run.status === 'completed' &&
['success', 'neutral', 'skipped'].includes(run.conclusion)
);
}
function checkConclusionRank(conclusion) {
if (conclusion === 'success') return 4;
if (conclusion === 'neutral') return 3;
if (conclusion === 'skipped') return 2;
if (conclusion === 'cancelled') return 1;
return 0;
}
function isPendingRun(run) {
return !!(run && run.status && run.status !== 'completed');
}
function bestRunsByName(runs) {
const byName = new Map();
for (const run of runs || []) {
if (!run) continue;
const existing = byName.get(run.name);
if (!existing) {
byName.set(run.name, run);
continue;
}
const runPending = isPendingRun(run);
const existingPending = isPendingRun(existing);
if (existingPending) continue;
if (
runPending ||
checkConclusionRank(run.conclusion) > checkConclusionRank(existing.conclusion)
) {
byName.set(run.name, run);
}
}
return [...byName.values()];
}
function isAcceptableCheckConclusion(run, runs) {
if (['success', 'neutral', 'skipped'].includes(run.conclusion)) return true;
if (run.conclusion === 'cancelled' && OPTIONAL_CANCELLED_CHECKS.has(run.name)) return true;
if (
run.conclusion === 'cancelled' &&
OPTIONAL_CANCELLED_WHEN_CORE_GREEN.has(run.name) &&
coreTestsGreenOnRuns(runs)
) {
return true;
}
return false;
}
async function allChecksGreenOnSha(github, owner, repo, sha, core) {
const runs = bestRunsByName(await listChecksOnSha(github, owner, repo, sha));
const runs = (data.check_runs || []).filter((r) => r.head_sha === sha);
if (runs.length === 0) {
core?.info?.(`No check runs on ${sha.slice(0, 7)} — allowing (e.g. docs-only PR)`);
return true;
@@ -459,17 +260,11 @@ async function allChecksGreenOnSha(github, owner, repo, sha, core) {
core?.info?.(`Check pending: ${run.name} (${run.status})`);
return false;
}
if (!isAcceptableCheckConclusion(run, runs)) {
const ok = ['success', 'neutral', 'skipped'].includes(run.conclusion);
if (!ok) {
core?.info?.(`Check failed: ${run.name} (${run.conclusion})`);
return false;
}
if (
run.conclusion === 'cancelled' &&
OPTIONAL_CANCELLED_WHEN_CORE_GREEN.has(run.name) &&
coreTestsGreenOnRuns(runs)
) {
core?.info?.(`Ignoring cancelled ${run.name} — test-core green on HEAD`);
}
}
return true;
}
@@ -484,79 +279,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({
@@ -583,35 +305,11 @@ function isCiOnlyChange(files) {
return files.every((f) => CI_ONLY_PATH_PREFIXES.some((p) => f.filename.startsWith(p)));
}
function isInternalPullRequestLink(link, owner, repo) {
if (link?.base?.repo?.full_name === `${owner}/${repo}`) return true;
const baseUrl = link?.base?.repo?.url || '';
return baseUrl.endsWith(`/repos/${owner}/${repo}`);
}
async function resolvePrNumberFromLinkedPullRequests(github, owner, repo, linked) {
const repoFull = `${owner}/${repo}`;
const internal = (linked || []).filter((l) => isInternalPullRequestLink(l, owner, repo));
for (const link of internal) {
if (!link.number) continue;
try {
const { data } = await github.rest.pulls.get({
owner,
repo,
pull_number: link.number,
});
if (data.state === 'open' && data.base?.repo?.full_name === repoFull) {
return data.number;
}
} catch (err) {
if (err.status !== 404) throw err;
}
}
return null;
}
async function resolvePrNumberFromHeadBranch(github, owner, repo, branch) {
if (!branch || branch === 'main' || branch === 'master') return null;
async function resolvePrNumberFromWorkflowRun(github, owner, repo, workflowRun) {
const linked = workflowRun.pull_requests || [];
if (linked.length > 0 && linked[0].number) return linked[0].number;
const branch = workflowRun.head_branch;
if (!branch) return null;
const { data } = await github.rest.pulls.list({
owner,
repo,
@@ -622,20 +320,6 @@ async function resolvePrNumberFromHeadBranch(github, owner, repo, branch) {
return data[0]?.number || null;
}
async function resolvePrNumberFromWorkflowRun(github, owner, repo, workflowRun) {
const fromLinked = await resolvePrNumberFromLinkedPullRequests(
github, owner, repo, workflowRun.pull_requests
);
if (fromLinked) return fromLinked;
const fromBranch = await resolvePrNumberFromHeadBranch(
github, owner, repo, workflowRun.head_branch
);
if (fromBranch) return fromBranch;
return null;
}
async function listPullFiles(github, owner, repo, prNumber) {
if (typeof github.paginate === 'function') {
return github.paginate(github.rest.pulls.listFiles, {
@@ -720,35 +404,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;
}
@@ -898,16 +565,15 @@ async function evaluatePipelineQuiescent(github, owner, repo, prNumber, core, op
reasons.push('recent merge-conflict @claude');
}
if (!skipRecentClaudeCooldown && hasRecentClaudeTrigger(ctx.comments, 35)) {
const verdictOnHead = findMergeGateVerdict(ctx.comments, null, ctx.headPushedAt) !== null;
if (!verdictOnHead) reasons.push('recent @claude within 35min');
reasons.push('recent @claude within 35min');
}
if (!skipGlobalClaudeRunCheck && (await hasInProgressClaudeAssistant(github, owner, repo, prNumber))) {
reasons.push('claude.yml in progress');
}
const checksOk = await allChecksGreenOnSha(github, owner, repo, ctx.headSha, core);
if (!checksOk) reasons.push('CI not green on HEAD');
const headTime = new Date(ctx.headPushedAt).getTime();
if (Date.now() - headTime < POST_PUSH_BUFFER_MS) reasons.push('post-push buffer (<5min)');
if (!finalClaudeCompletedOnSha(ctx.comments, ctx.headPushedAt)) {
if (!hasFinalClaudeReviewTrigger(ctx.comments)) {
@@ -942,6 +608,9 @@ async function evaluatePipelineQuiescent(github, owner, repo, prNumber, core, op
const sdkCheckReason = await sdkTestChecksReason(github, owner, repo, ctx.headSha, pullFiles, core);
if (sdkCheckReason) reasons.push(sdkCheckReason);
const checksOk = await allChecksGreenOnSha(github, owner, repo, ctx.headSha, core);
if (!checksOk) reasons.push('CI not green on HEAD');
if (ctx.pr.mergeable === false) reasons.push('not mergeable');
const ready = reasons.length === 0;
@@ -953,67 +622,10 @@ async function evaluatePipelineQuiescent(github, owner, repo, prNumber, core, op
};
}
async function listPrNumbersForMergeGateScan(github, owner, repo, core) {
let mergeReadyIssues;
if (typeof github.paginate === 'function') {
mergeReadyIssues = await github.paginate(github.rest.issues.listForRepo, {
owner,
repo,
state: 'open',
labels: MERGE_READY_LABEL,
per_page: 100,
});
} else {
({ data: mergeReadyIssues } = await github.rest.issues.listForRepo({
owner,
repo,
state: 'open',
labels: MERGE_READY_LABEL,
per_page: 100,
}));
}
const mergeReady = mergeReadyIssues
.filter((issue) => issue.pull_request)
.map((issue) => issue.number)
.sort((a, b) => a - b);
if (mergeReady.length > 0) {
core?.info?.(
`Merge gate scan: ${mergeReady.length} ${MERGE_READY_LABEL} PR(s) (skipping unlabelled open PRs)`
);
return mergeReady;
}
core?.info?.(`No ${MERGE_READY_LABEL} PRs — scanning oldest open PRs`);
const prs =
typeof github.paginate === 'function'
? await github.paginate(github.rest.pulls.list, {
owner,
repo,
state: 'open',
per_page: 100,
sort: 'created',
direction: 'asc',
})
: (await github.rest.pulls.list({
owner,
repo,
state: 'open',
per_page: 100,
sort: 'created',
direction: 'asc',
})).data;
return prs.map((pr) => pr.number);
}
async function selectMergeGateCandidates(github, owner, repo, prNumbers, maxCandidates, core) {
const readyList = [];
const skipped = [];
for (const num of prNumbers) {
if (readyList.length >= maxCandidates) {
core?.info?.(`Found ${maxCandidates} candidate(s) — skipping remaining PR scans`);
break;
}
const result = await evaluatePipelineQuiescent(github, owner, repo, num, core);
if (result.ready) {
readyList.push({ pr_number: num, head_sha: result.headSha });
@@ -1028,35 +640,12 @@ 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 || {};
function findMergeGateVerdict(comments, minCreatedAt = null, headPushedAt = null) {
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 (excludeAutomatedFallback && isAutomatedFallbackVerdict(c.body)) return false;
if (!(c.body || '').includes('MERGE_GATE_VERDICT:')) return false;
const created = new Date(c.created_at).getTime();
if (minTime && created < minTime) return false;
if (headTime && created < headTime) return false;
@@ -1065,8 +654,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;
}
@@ -1075,35 +664,21 @@ module.exports = {
AUTO_ACTORS,
isFinalClaudeTriggerComment,
isClaudeTriggerNoise,
isClaudeFinalReplyComment,
hasRecentClaudeTrigger,
hasRecentConflictComment,
isConflictRebaseTriggerComment,
isConflictRebaseCompletionComment,
needsFinalReReviewAfterConflictRebase,
conflictRebaseQuiescent,
hasHumanChangesRequested,
hasAnyChangesRequested,
hasFinalClaudeReviewTrigger,
finalClaudeCompletedOnSha,
getMergeState,
OPTIONAL_CANCELLED_CHECKS,
OPTIONAL_CANCELLED_WHEN_CORE_GREEN,
isCoreTestRun,
coreTestsGreenOnRuns,
isPendingRun,
bestRunsByName,
isAcceptableCheckConclusion,
listChecksOnSha,
listFailedChecksOnSha,
allChecksGreenOnSha,
hasInProgressClaudeAssistant,
claudeRunBlocksPr,
hasBlockingClaudeRunForPr,
isCiOnlyChange,
isInternalPullRequestLink,
resolvePrNumberFromLinkedPullRequests,
resolvePrNumberFromHeadBranch,
resolvePrNumberFromWorkflowRun,
WORKFLOW_ONLY_LABEL,
getAgentPyChange,
@@ -1123,30 +698,9 @@ module.exports = {
isStaleFinalAfterPush,
needsStaleFinalRecovery,
shouldSkipFinalRecovery,
shouldSkipStaleFinalRecovery,
isClaudeAutomationLogin,
isPushSoonAfterLatestFinal,
countFinalTriggersSince,
STALE_FINAL_RECOVERY_WINDOW_MS,
STALE_FINAL_MAX_PER_WINDOW,
FINAL_CLAUDE_REVIEW_BODY,
loadPrContext,
evaluatePipelineQuiescent,
MERGE_READY_LABEL,
listPrNumbersForMergeGateScan,
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,
};
@@ -1,76 +0,0 @@
#!/usr/bin/env node
/**
* Run: node .github/scripts/pipeline-status-selftest.js
*/
const ps = require('./pipeline-status.js');
const mg = require('./merge-gate.js');
let failed = 0;
function assert(name, cond) {
if (!cond) {
console.error('FAIL:', name);
failed++;
} else {
console.log('ok:', name);
}
}
const kickComments = [
{ user: { login: 'MervinPraison' }, body: '@coderabbitai review' },
{ user: { login: 'MervinPraison' }, body: '/review' },
];
const finalComment = {
user: { login: 'MervinPraison' },
body: '@claude You are the FINAL architecture reviewer.',
created_at: '2026-06-26T10:00:00Z',
};
assert(
'reviews pending when not kicked',
ps.deriveStage([], { ready: false, reasons: ['no FINAL Claude review trigger'] })
=== 'pipeline/reviews-pending'
);
assert(
'final pending after kick',
ps.deriveStage(kickComments, { ready: false, reasons: ['no FINAL Claude review trigger'] })
=== 'pipeline/final-claude-pending'
);
assert(
'awaiting merge gate after final',
ps.deriveStage(
[...kickComments, finalComment],
{ ready: false, reasons: ['recent @claude within 35min'] }
) === 'pipeline/awaiting-merge-gate'
);
assert(
'merge ready',
ps.deriveStage([...kickComments, finalComment], { ready: true, reasons: [] })
=== 'pipeline/merge-ready'
);
const blockers = ps.deriveBlockerLabels({
ready: false,
reasons: ['CI not green on HEAD', 'SDK code added without test file changes — requires manual review'],
});
assert('maps ci blocker', blockers.includes('pipeline/blocked:ci'));
assert('maps manual blocker', blockers.includes('pipeline/blocked:manual-review'));
assert(
'internal link matches upstream base',
mg.isInternalPullRequestLink(
{ base: { repo: { full_name: 'MervinPraison/PraisonAI' } } },
'MervinPraison',
'PraisonAI'
)
);
assert(
'fork sync link rejected',
!mg.isInternalPullRequestLink(
{ number: 21, base: { repo: { full_name: 'Milkmange/PraisonAI' } } },
'MervinPraison',
'PraisonAI'
)
);
process.exit(failed ? 1 : 0);
-258
View File
@@ -1,258 +0,0 @@
/**
* Sync pipeline stage/blocker labels on open PRs.
* @see .github/workflows/pipeline-status-sync.yml
*/
const mergeGate = require('./merge-gate.js');
const chain = require('./bot-pr-review-chain.js');
const ciFix = require('./ci-failure-claude.js');
const PIPELINE_PREFIX = 'pipeline/';
const STAGE_LABELS = [
'pipeline/reviews-pending',
'pipeline/final-claude-pending',
'pipeline/awaiting-merge-gate',
'pipeline/merge-ready',
'pipeline/merged',
];
const BLOCKER_LABELS = [
'pipeline/blocked:ci',
'pipeline/blocked:conflict',
'pipeline/blocked:manual-review',
'pipeline/blocked:cooldown',
'pipeline/blocked:stale-final',
'pipeline/blocked:no-final',
];
const ALL_PIPELINE_LABELS = [...STAGE_LABELS, ...BLOCKER_LABELS];
const LABEL_SPECS = [
{ name: 'pipeline/reviews-pending', color: 'fbca04', description: 'Waiting for CodeRabbit/Qodo/Copilot reviews' },
{ name: 'pipeline/final-claude-pending', color: 'd4c5f9', description: 'Reviews done; waiting for FINAL @claude' },
{ name: 'pipeline/awaiting-merge-gate', color: 'c5def5', description: 'FINAL done; waiting for merge gate / CI' },
{ name: 'pipeline/merge-ready', color: '0e8a16', description: 'Eligible for merge gate auto-merge' },
{ name: 'pipeline/merged', color: '6f42c1', description: 'Merged via pipeline' },
{ name: 'pipeline/blocked:ci', color: 'd93f0b', description: 'Blocked: CI not green on HEAD' },
{ name: 'pipeline/blocked:conflict', color: 'b60205', description: 'Blocked: merge conflict or rebase pending' },
{ name: 'pipeline/blocked:manual-review', color: 'e99695', description: 'Blocked: requires manual review' },
{ 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' },
];
function deriveStage(comments, evalResult) {
if (evalResult.ready) return 'pipeline/merge-ready';
if (!chain.chainKickPosted(comments)) return 'pipeline/reviews-pending';
if (!mergeGate.hasFinalClaudeReviewTrigger(comments)) return 'pipeline/final-claude-pending';
return 'pipeline/awaiting-merge-gate';
}
function reasonToBlockerLabel(reason) {
const r = (reason || '').toLowerCase();
if (r.includes('ci not green') || r.includes('sdk code changed but no ci')) return 'pipeline/blocked:ci';
if (
r.includes('conflict') ||
r.includes('mergestate=dirty') ||
r.includes('not mergeable')
) return 'pipeline/blocked:conflict';
if (
r.includes('manual') ||
r.includes('requires manual') ||
r.includes('sensitive path') ||
r.includes('no-auto-merge') ||
r.includes('manual-only label') ||
r.includes('without test') ||
r.includes('files changed') ||
r.includes('possible secret') ||
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';
if (r.includes('no final claude')) return 'pipeline/blocked:no-final';
if (r.includes('final claude not complete')) return 'pipeline/blocked:stale-final';
if (r.includes('claude.yml in progress') || r.includes('claude-merge-gate-active')) {
return null;
}
return null;
}
function deriveBlockerLabels(evalResult) {
if (evalResult.ready) return [];
const labels = new Set();
for (const reason of evalResult.reasons || []) {
const mapped = reasonToBlockerLabel(reason);
if (mapped && mapped.startsWith('pipeline/blocked:')) labels.add(mapped);
}
return [...labels];
}
function computePipelineLabels(comments, evalResult) {
const stage = deriveStage(comments, evalResult);
const blockers = stage === 'pipeline/merge-ready' ? [] : deriveBlockerLabels(evalResult);
return { stage, blockers, all: [stage, ...blockers] };
}
async function ensurePipelineLabels(github, owner, repo, core) {
let existing;
if (typeof github.paginate === 'function') {
existing = await github.paginate(github.rest.issues.listLabelsForRepo, {
owner,
repo,
per_page: 100,
});
} else {
({ data: existing } = await github.rest.issues.listLabelsForRepo({
owner,
repo,
per_page: 100,
}));
}
const names = new Set(existing.map((l) => l.name));
for (const spec of LABEL_SPECS) {
if (names.has(spec.name)) continue;
try {
await github.rest.issues.createLabel({
owner,
repo,
name: spec.name,
color: spec.color,
description: spec.description,
});
core?.info?.(`Created label ${spec.name}`);
} catch (err) {
if (err.status !== 422) throw err;
core?.info?.(`Label ${spec.name} already exists`);
}
}
}
async function syncPipelineLabels(github, owner, repo, prNumber, core) {
const ctx = await mergeGate.loadPrContext(github, owner, repo, prNumber);
if (ctx.pr.state !== 'open') return { synced: false, reason: 'not_open' };
const evalResult = await mergeGate.evaluatePipelineQuiescent(
github, owner, repo, prNumber, core
);
const { stage, blockers, all } = computePipelineLabels(ctx.comments, evalResult);
const current = ctx.labels.filter((l) => l.startsWith(PIPELINE_PREFIX));
const desired = new Set(all);
const toRemove = current.filter((l) => !desired.has(l) && l !== 'pipeline/merged');
const toAdd = all.filter((l) => !current.includes(l));
for (const name of toRemove) {
try {
await github.rest.issues.removeLabel({ owner, repo, issue_number: prNumber, name });
} catch (err) {
if (err.status !== 404) throw err;
}
}
if (toAdd.length) {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: prNumber,
labels: toAdd,
});
}
core?.info?.(`PR #${prNumber}: stage=${stage} blockers=[${blockers.join(', ')}]`);
const ciNotGreen = (evalResult.reasons || []).some((r) =>
r.toLowerCase().includes('ci not green')
);
if (ciNotGreen || blockers.includes('pipeline/blocked:ci')) {
await ciFix.maybeTriggerCiFixClaude(github, owner, repo, prNumber, core);
} else {
await ciFix.maybeClearCiFixLabel(
github, owner, repo, prNumber, ctx.labels, ctx.headSha, core
);
}
return {
synced: true,
stage,
blockers,
ready: evalResult.ready,
reasons: evalResult.reasons,
labels: ctx.labels,
createdAt: new Date(ctx.pr.created_at).getTime(),
};
}
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)) {
continue;
}
await github.rest.repos.createDispatchEvent({
owner,
repo,
event_type: 'claude-merge-gate',
client_payload: { pr_number: cand.prNumber },
});
core?.info?.(`Dispatched merge gate for ready PR #${cand.prNumber}`);
return cand.prNumber;
}
return 0;
}
async function syncOpenPullRequests(github, owner, repo, options, core) {
const { maxPrs = 20, dispatchMergeGate = true } = options || {};
await ensurePipelineLabels(github, owner, repo, core);
let prs;
if (maxPrs <= 100) {
({ data: prs } = await github.rest.pulls.list({
owner,
repo,
state: 'open',
per_page: 100,
}));
} else {
prs = await github.paginate(github.rest.pulls.list, {
owner,
repo,
state: 'open',
per_page: 100,
});
}
let synced = 0;
const readyCandidates = [];
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) {
readyCandidates.push({
prNumber: pr.number,
createdAt: result.createdAt || new Date(pr.created_at).getTime(),
labels: result.labels || [],
});
}
synced += 1;
}
let dispatched = 0;
if (dispatchMergeGate && readyCandidates.length) {
dispatched = await dispatchMergeGateForOldestReady(
github, owner, repo, readyCandidates, core
);
}
core?.info?.(`Pipeline label sync complete (${synced} PR(s), dispatched=${dispatched || 'none'})`);
return synced;
}
module.exports = {
STAGE_LABELS,
BLOCKER_LABELS,
ALL_PIPELINE_LABELS,
deriveStage,
deriveBlockerLabels,
computePipelineLabels,
ensurePipelineLabels,
syncPipelineLabels,
syncOpenPullRequests,
dispatchMergeGateForOldestReady,
};
-111
View File
@@ -1,111 +0,0 @@
#!/usr/bin/env node
const chain = require('./pr-review-chain.js');
let failed = 0;
function assert(name, cond) {
if (!cond) {
console.error('FAIL:', name);
failed++;
} else {
console.log('ok:', name);
}
}
const coderabbit = {
user: { login: 'coderabbitai[bot]' },
body: '<!-- summarize by coderabbit.ai --> summary',
created_at: '2026-07-02T07:16:00Z',
};
const greptile = {
user: { login: 'greptile-apps[bot]' },
body: '<h3>Greptile Summary</h3><p>Looks good</p>',
created_at: '2026-07-02T07:19:00Z',
};
const copilotKick = {
user: { login: 'MervinPraison' },
body: '@copilot Do a thorough review',
created_at: '2026-07-02T07:20:00Z',
};
const copilotReply = {
user: { login: 'copilot-swe-agent' },
body: 'Copilot review here',
created_at: '2026-07-02T07:22:00Z',
};
assert('prior not ready without greptile', !chain.priorReviewersReady([coderabbit]).ready);
assert('prior ready with coderabbit + greptile', chain.priorReviewersReady([coderabbit, greptile]).ready);
assert('copilot not ready before trigger', !chain.copilotReviewReady([coderabbit, greptile]).ready);
assert(
'copilot ready after reply',
chain.copilotReviewReady([coderabbit, greptile, copilotKick, copilotReply]).ready
);
assert(
'claude not ready before copilot',
!chain.claudeFinalReady([coderabbit, greptile]).ready
);
assert(
'claude ready after full chain',
chain.claudeFinalReady([coderabbit, greptile, copilotKick, copilotReply]).ready
);
assert(
'claude timeout fallback when copilot triggered but silent',
chain.claudeFinalReady([coderabbit, greptile, copilotKick], [], { allowCopilotTimeout: true }).ready
);
assert(
'no timeout fallback without copilot trigger',
!chain.claudeFinalReady([coderabbit, greptile], [], { allowCopilotTimeout: true }).ready
);
assert(
'skipCopilot proceeds after prior reviewers',
chain.claudeFinalReady([coderabbit, greptile], [], { skipCopilot: true }).ready
);
const prev = process.env.REVIEW_CHAIN_SKIP_COPILOT;
process.env.REVIEW_CHAIN_SKIP_COPILOT = '1';
assert(
'REVIEW_CHAIN_SKIP_COPILOT env skips copilot',
chain.claudeFinalReady([coderabbit, greptile], [], {}).ready
);
process.env.REVIEW_CHAIN_SKIP_COPILOT = prev;
const geminiReview = {
user: { login: 'gemini-code-assist[bot]' },
body: '## Code Review\n\nThis pull request decomposes the large main.py file',
submitted_at: '2026-07-03T08:08:00Z',
};
assert(
'gemini pull review counts as prior reviewer',
chain.hasGeminiReview([], [geminiReview])
);
assert(
'prior ready with gemini review only (no gemini comment)',
chain.priorReviewersReady([coderabbit, greptile], [geminiReview], { optionalWaitMs: 0 }).ready
);
const oldPr = new Date(Date.now() - 31 * 60 * 1000).toISOString();
assert(
'prior not ready without coderabbit before timeout',
!chain.priorReviewersReady([greptile], [], { prCreatedAt: new Date().toISOString(), requiredWaitMs: 30 * 60 * 1000 }).ready
);
assert(
'prior timeout when coderabbit silent after 30m',
chain.priorReviewersReady([greptile], [], { prCreatedAt: oldPr, requiredWaitMs: 30 * 60 * 1000 }).ready
);
assert(
'claude FINAL timeout when required reviewer silent and no prior FINAL',
chain.claudeFinalReady([greptile], [], { prCreatedAt: oldPr, requiredWaitMs: 30 * 60 * 1000 }).ready
);
assert(
'claude FINAL timeout auto-skips copilot without explicit skipCopilot',
chain.claudeFinalReady([greptile], [], { prCreatedAt: oldPr, requiredWaitMs: 30 * 60 * 1000 }).copilotSkipped
);
const finalClaude = {
user: { login: 'github-actions[bot]' },
body: '@claude You are the FINAL architecture reviewer for this PR.',
created_at: '2026-07-02T07:25:00Z',
};
assert(
'claude FINAL not re-posted when already triggered',
!chain.claudeFinalReady([greptile, finalClaude], [], { prCreatedAt: oldPr, requiredWaitMs: 0 }).ready
);
process.exit(failed ? 1 : 0);
-322
View File
@@ -1,322 +0,0 @@
/**
* PR review pipeline: CodeRabbit / Greptile (+ optional Qodo/Gemini) → Claude (FINAL).
* Copilot step optional — set REVIEW_CHAIN_SKIP_COPILOT=1 or skipCopilot: true to bypass.
* @see .github/workflows/auto-pr-comment.yml
*/
function isSkipCopilot(options = {}) {
if (options.skipCopilot === true) return true;
if (options.skipCopilot === false) return false;
return process.env.REVIEW_CHAIN_SKIP_COPILOT === '1';
}
const COPILOT_TRIGGER_LOGINS = new Set(['MervinPraison', 'github-actions[bot]']);
const CLAUDE_TRIGGER_LOGINS = new Set(['MervinPraison', 'github-actions[bot]']);
const REQUIRED_PRIOR = ['coderabbit', 'greptile'];
const OPTIONAL_PRIOR = ['qodo', 'gemini'];
const OPTIONAL_PRIOR_WAIT_MS = 20 * 60 * 1000;
const REQUIRED_PRIOR_WAIT_MS = 30 * 60 * 1000;
function loginOf(item) {
return (item?.user?.login || '').toLowerCase();
}
function bodyOf(item) {
return item?.body || '';
}
function hasCoderabbitSummary(comments) {
return comments.some(
(c) =>
loginOf(c).includes('coderabbit') &&
bodyOf(c).toLowerCase().includes('summarize by coderabbit')
);
}
function hasGreptileReview(comments) {
return comments.some((c) => {
if (!loginOf(c).includes('greptile')) return false;
const body = bodyOf(c);
return body.includes('Greptile Summary') || body.includes('<h3>Greptile Summary</h3>') || body.length > 150;
});
}
function hasQodoReview(comments, reviews = []) {
if (comments.some((c) => loginOf(c).includes('qodo'))) return true;
return reviews.some((r) => loginOf(r).includes('qodo'));
}
function hasGeminiReview(comments, reviews = []) {
if (reviews.some((r) => loginOf(r).includes('gemini'))) return true;
return comments.some((c) => {
if (loginOf(c).includes('gemini')) return true;
const body = bodyOf(c);
return (
body.includes('Review completed by Gemini CLI') ||
body.includes('The fix is ready for review. Please test')
);
});
}
const PRIOR_CHECKS = {
coderabbit: (comments) => hasCoderabbitSummary(comments),
greptile: (comments) => hasGreptileReview(comments),
qodo: (comments, reviews) => hasQodoReview(comments, reviews),
gemini: (comments, reviews) => hasGeminiReview(comments, reviews),
};
function priorReviewerStatus(comments, reviews = []) {
const status = {};
for (const [id, check] of Object.entries(PRIOR_CHECKS)) {
status[id] = check(comments, reviews);
}
return status;
}
function priorReviewersReady(comments, reviews = [], options = {}) {
const waitMs = options.optionalWaitMs ?? OPTIONAL_PRIOR_WAIT_MS;
const requiredWaitMs = options.requiredWaitMs ?? REQUIRED_PRIOR_WAIT_MS;
const prCreatedAt = options.prCreatedAt || null;
const status = priorReviewerStatus(comments, reviews);
const missingRequired = REQUIRED_PRIOR.filter((id) => !status[id]);
if (missingRequired.length) {
if (prCreatedAt) {
const age = Date.now() - new Date(prCreatedAt).getTime();
if (age >= requiredWaitMs) {
return {
ready: true,
reason: `required reviewer timeout (${missingRequired.join(', ')} silent after ${Math.round(requiredWaitMs / 60000)}m)`,
status,
requiredTimeout: true,
};
}
}
return { ready: false, reason: `waiting for ${missingRequired.join(', ')}`, status };
}
const missingOptional = OPTIONAL_PRIOR.filter((id) => !status[id]);
if (missingOptional.length && prCreatedAt) {
const age = Date.now() - new Date(prCreatedAt).getTime();
if (age < waitMs) {
return {
ready: false,
reason: `waiting for optional ${missingOptional.join(', ')} (${Math.round((waitMs - age) / 60000)}m left)`,
status,
};
}
}
return { ready: true, reason: '', status };
}
function copilotTriggerComment(comments) {
return comments.find(
(c) =>
bodyOf(c).includes('@copilot') &&
COPILOT_TRIGGER_LOGINS.has(c.user?.login) &&
!bodyOf(c).toLowerCase().includes('@claude')
);
}
function copilotTriggered(comments) {
return Boolean(copilotTriggerComment(comments));
}
function copilotReviewReady(comments, reviews = []) {
const trigger = copilotTriggerComment(comments);
if (!trigger) {
return { ready: false, reason: 'copilot not triggered yet' };
}
const triggerTime = new Date(trigger.created_at).getTime();
const copilotComment = comments.some((c) => {
if (c === trigger) return false;
if (new Date(c.created_at).getTime() < triggerTime) return false;
const login = loginOf(c);
return (
login.includes('copilot') ||
c.user?.login === 'Copilot' ||
c.user?.login === 'copilot-swe-agent'
);
});
const copilotReview = reviews.some((r) => {
if (new Date(r.submitted_at).getTime() < triggerTime) return false;
return loginOf(r).includes('copilot');
});
if (copilotComment || copilotReview) {
return { ready: true, reason: '' };
}
return { ready: false, reason: 'awaiting copilot review' };
}
function isFinalClaudeTriggerComment(c) {
const body = bodyOf(c).toLowerCase();
if (!CLAUDE_TRIGGER_LOGINS.has(c.user?.login)) return false;
if (!body.includes('@claude')) return false;
if (body.includes('merge conflict')) return false;
return body.includes('final architecture reviewer') || body.includes('lead engineer');
}
function claudeFinalAlreadyTriggered(comments) {
return comments.some(isFinalClaudeTriggerComment);
}
function claudeFinalReady(comments, reviews = [], options = {}) {
if (claudeFinalAlreadyTriggered(comments)) {
return { ready: false, reason: 'claude FINAL already triggered', already: true };
}
const prior = priorReviewersReady(comments, reviews, options);
if (!prior.ready) {
return { ready: false, reason: prior.reason };
}
if (isSkipCopilot(options) || prior.requiredTimeout) {
return {
ready: true,
reason: prior.requiredTimeout ? prior.reason : 'copilot skipped',
copilotSkipped: true,
requiredTimeout: prior.requiredTimeout,
};
}
const copilot = copilotReviewReady(comments, reviews);
if (copilot.ready) {
return { ready: true, reason: '' };
}
if (options.allowCopilotTimeout && copilotTriggered(comments)) {
return { ready: true, reason: 'copilot timeout fallback', copilotSkipped: true };
}
return { ready: false, reason: copilot.reason };
}
/** Post @copilot when prior reviewers are ready, then @claude FINAL when Copilot done (or timeout). */
async function advanceReviewChain(github, owner, repo, prNumber, finalBody, core, options = {}) {
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
const chainOpts = {
prCreatedAt: pr.created_at,
optionalWaitMs: options.optionalWaitMs ?? 0,
};
const copilot = options.skipCopilot
? { triggered: false, reason: 'skipped' }
: await maybeTriggerCopilot(github, owner, repo, prNumber, core, chainOpts);
const claude = await maybeTriggerClaudeFinal(
github, owner, repo, prNumber, finalBody, core,
{ ...options, ...chainOpts }
);
return { copilot, claude };
}
async function maybeTriggerClaudeFinal(github, owner, repo, prNumber, finalBody, core, options = {}) {
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
const { comments, reviews } = await listCommentsAndReviews(github, owner, repo, prNumber);
const gate = claudeFinalReady(comments, reviews, {
...options,
prCreatedAt: pr.created_at,
optionalWaitMs: options.optionalWaitMs,
allowCopilotTimeout: options.allowCopilotTimeout !== false,
});
if (!gate.ready) {
core?.info?.(`PR #${prNumber}: skip Claude FINAL — ${gate.reason}`);
return { posted: false, reason: gate.reason };
}
await github.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body: finalBody,
});
const note = gate.requiredTimeout
? ' (required reviewer timeout fallback)'
: gate.copilotSkipped
? ' (Copilot timeout fallback)'
: '';
core?.info?.(`Posted Claude FINAL on PR #${prNumber}${note}`);
return { posted: true, reason: '' };
}
const COPILOT_REVIEW_BODY =
'@copilot Do a thorough review of this PR. Read ALL existing reviewer comments above from Qodo, CodeRabbit, Gemini, and Greptile first — incorporate their findings.\n\nReview areas:\n1. **Bloat check**: Are changes minimal and focused? Any unnecessary code or scope creep?\n2. **Security**: Any hardcoded secrets, unsafe eval/exec, missing input validation?\n3. **Performance**: Any module-level heavy imports? Hot-path regressions?\n4. **Tests**: Are tests included? Do they cover the changes adequately?\n5. **Backward compat**: Any public API changes without deprecation?\n6. **Code quality**: DRY violations, naming conventions, error handling?\n7. **Address reviewer feedback**: If Qodo, CodeRabbit, Gemini, or Greptile flagged valid issues, include them in your review\n8. Suggest specific improvements with code examples where possible';
async function listCommentsAndReviews(github, owner, repo, prNumber) {
let comments;
if (typeof github.paginate === 'function') {
comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number: prNumber,
per_page: 100,
});
} else {
const { data } = await github.rest.issues.listComments({
owner,
repo,
issue_number: prNumber,
per_page: 100,
});
comments = data;
}
const { data: reviews } = await github.rest.pulls.listReviews({
owner,
repo,
pull_number: prNumber,
per_page: 100,
});
return { comments, reviews };
}
async function maybeTriggerCopilot(github, owner, repo, prNumber, core, options = {}) {
const { comments, reviews } = await listCommentsAndReviews(github, owner, repo, prNumber);
if (copilotTriggered(comments)) {
core?.info?.(`Copilot already triggered on PR #${prNumber}`);
return { triggered: false, reason: 'already_triggered' };
}
const prior = priorReviewersReady(comments, reviews, options);
if (!prior.ready) {
core?.info?.(`PR #${prNumber}: not ready for Copilot — ${prior.reason}`);
return { triggered: false, reason: prior.reason };
}
await github.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body: COPILOT_REVIEW_BODY,
});
core?.info?.(`Posted @copilot on PR #${prNumber} after prior reviewers`);
return { triggered: true };
}
async function pollCopilotResponse(github, owner, repo, prNumber, options = {}) {
const maxAttempts = options.maxAttempts ?? 20;
const delayMs = options.delayMs ?? 30000;
for (let i = 0; i < maxAttempts; i += 1) {
const { comments, reviews } = await listCommentsAndReviews(github, owner, repo, prNumber);
const copilot = copilotReviewReady(comments, reviews);
if (copilot.ready) {
return { ready: true, reason: '' };
}
if (i < maxAttempts - 1) {
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
}
return { ready: false, reason: 'copilot timeout' };
}
module.exports = {
isSkipCopilot,
REQUIRED_PRIOR,
OPTIONAL_PRIOR,
REQUIRED_PRIOR_WAIT_MS,
COPILOT_REVIEW_BODY,
priorReviewerStatus,
priorReviewersReady,
copilotTriggered,
copilotReviewReady,
claudeFinalReady,
claudeFinalAlreadyTriggered,
isFinalClaudeTriggerComment,
maybeTriggerCopilot,
maybeTriggerClaudeFinal,
advanceReviewChain,
pollCopilotResponse,
hasGeminiReview,
listCommentsAndReviews,
};
-122
View File
@@ -1,122 +0,0 @@
const assert = require('assert');
const rg = require('./release-gate.js');
assert.strictEqual(rg.bumpPatch('1.6.80'), '1.6.81');
assert.strictEqual(rg.bumpPatch('4.6.80'), '4.6.81');
assert.strictEqual(rg.bumpPatch('0.0.2'), '0.0.3');
const versions = rg.readVersionsFromTree();
assert.ok(versions.currentAgents);
assert.ok(versions.currentCode);
assert.ok(versions.currentWrapper);
assert.ok(versions.targetCode);
assert.ok(rg.PACKAGE_PATHS.includes('src/praisonai-code'));
// Every released package must make the gate's path-change check eligible.
for (const p of [
'src/praisonai', 'src/praisonai-agents', 'src/praisonai-code',
'src/praisonai-bot', 'src/praisonai-train', 'src/praisonai-browser',
'src/praisonai-mcp', 'src/praisonai-sandbox', 'src/praisonai-deploy',
]) {
assert.ok(rg.PACKAGE_PATHS.includes(p), `PACKAGE_PATHS missing ${p}`);
}
const noonUtc = new Date('2026-07-08T12:00:00Z');
const dayStart = rg.utcDayStart(noonUtc);
assert.strictEqual(dayStart.toISOString(), '2026-07-08T00:00:00.000Z');
function mockGithub({ runs = [], releases = [] } = {}) {
return {
rest: {
actions: {
listWorkflowRuns: async () => ({ data: { workflow_runs: runs } }),
},
repos: {
listReleases: async () => ({ data: releases }),
},
},
};
}
(async () => {
assert.strictEqual(await rg.pypiVersionExists('praisonaiagents', '0.0.0'), false);
assert.strictEqual(rg.PATCH_RELEASE_INTERVAL_DAYS, 3);
// Dedupe is release-based: a v* release inside the window blocks…
const recentRelease = mockGithub({
releases: [{ tag_name: 'v4.6.159', published_at: '2026-07-08T09:00:00Z' }],
});
assert.strictEqual(
await rg.hasSuccessfulReleaseWithinDays(recentRelease, 'o', 'r', noonUtc),
true
);
const result = await rg.evaluateReleasePreflight(
recentRelease, 'o', 'r',
{ headSha: 'abc', isCiTrigger: false, bump: 'patch', now: noonUtc },
null
);
assert.ok(result.reasons.some((r) => r.includes('every 3 days')));
// …an old release does not…
const oldRelease = mockGithub({
releases: [{ tag_name: 'v4.6.150', published_at: '2026-07-04T09:00:00Z' }],
});
assert.strictEqual(
await rg.hasSuccessfulReleaseWithinDays(oldRelease, 'o', 'r', noonUtc),
false
);
// …and a dry-run "success" (workflow run, no release) does not consume the
// window: only real releases count.
const dryRunOnly = mockGithub({
runs: [{ conclusion: 'success', created_at: '2026-07-08T09:00:00Z', status: 'completed' }],
releases: [],
});
assert.strictEqual(
await rg.hasSuccessfulReleaseWithinDays(dryRunOnly, 'o', 'r', noonUtc),
false
);
// Active-run blocking: fresh waiting run blocks…
const freshWaiting = mockGithub({
runs: [{ status: 'waiting', conclusion: null, created_at: '2026-07-08T11:00:00Z', html_url: 'x' }],
});
assert.strictEqual(
await rg.hasActiveReleaseRun(freshWaiting, 'o', 'r', noonUtc, null),
true
);
// …a waiting run stuck longer than STALE_WAITING_HOURS no longer blocks…
const staleWaiting = mockGithub({
runs: [{ status: 'waiting', conclusion: null, created_at: '2026-07-08T01:00:00Z', html_url: 'x' }],
});
const warnings = [];
const fakeCore = { warning: (m) => warnings.push(m), info: () => {} };
assert.strictEqual(rg.STALE_WAITING_HOURS, 6);
assert.strictEqual(
await rg.hasActiveReleaseRun(staleWaiting, 'o', 'r', noonUtc, fakeCore),
false
);
assert.ok(warnings.length === 1 && warnings[0].includes('approve or cancel'));
// …but an in_progress run always blocks regardless of age.
const oldInProgress = mockGithub({
runs: [{ status: 'in_progress', conclusion: null, created_at: '2026-07-07T01:00:00Z', html_url: 'x' }],
});
assert.strictEqual(
await rg.hasActiveReleaseRun(oldInProgress, 'o', 'r', noonUtc, null),
true
);
console.log('ok: bumpPatch');
console.log('ok: readVersionsFromTree');
console.log('ok: PACKAGE_PATHS covers all 9 released packages');
console.log('ok: pypiVersionExists missing version');
console.log('ok: utcDayStart');
console.log('ok: release-based dedupe (recent blocks, old does not)');
console.log('ok: dry-run success does not consume the release window');
console.log('ok: 3-day dedupe blocks second release');
console.log('ok: fresh waiting run blocks; stale waiting run ignored with warning');
console.log('ok: in_progress always blocks');
})();
-315
View File
@@ -1,315 +0,0 @@
/**
* Release gate preflight — path changes, CI SHA, dedupe, PyPI version checks.
*/
const https = require('https');
/** Every directory whose changes should make a release eligible — all nine
* published packages (mirrors the skip_* inputs in pypi-release.yml). */
const PACKAGE_PATHS = [
'src/praisonai',
'src/praisonai-agents',
'src/praisonai-code',
'src/praisonai-bot',
'src/praisonai-train',
'src/praisonai-browser',
'src/praisonai-mcp',
'src/praisonai-sandbox',
'src/praisonai-deploy',
];
/** Minimum days between successful patch auto-releases. */
const PATCH_RELEASE_INTERVAL_DAYS = 3;
const ACTIVE_RELEASE_STATUSES = new Set([
'queued', 'in_progress', 'waiting', 'pending', 'requested',
]);
/** A run stuck in `waiting` (environment approval) longer than this no longer
* blocks the gate. GitHub only auto-fails unapproved deployments after 30
* days, so without a cutoff one forgotten manual dispatch stalls all
* auto-releases for up to a month. The run is warned about, never cancelled —
* if later approved, the pypi-release concurrency group serializes it and its
* pypi_exists checks no-op anything already published. */
const STALE_WAITING_HOURS = 6;
function bumpPatch(version) {
const parts = version.split('.');
if (parts.length !== 3) throw new Error(`Invalid version: ${version}`);
return `${parts[0]}.${parts[1]}.${Number(parts[2]) + 1}`;
}
function readVersionsFromTree(root = '.') {
const fs = require('fs');
const path = require('path');
const agentsToml = fs.readFileSync(
path.join(root, 'src/praisonai-agents/pyproject.toml'), 'utf8'
);
const agentsMatch = agentsToml.match(/^version\s*=\s*"([^"]+)"/m);
if (!agentsMatch) throw new Error('Could not read agents version');
const codeToml = fs.readFileSync(
path.join(root, 'src/praisonai-code/pyproject.toml'), 'utf8'
);
const codeMatch = codeToml.match(/^version\s*=\s*"([^"]+)"/m);
if (!codeMatch) throw new Error('Could not read praisonai-code version');
const wrapperPy = fs.readFileSync(
path.join(root, 'src/praisonai/praisonai/version.py'), 'utf8'
);
const wrapperMatch = wrapperPy.match(/__version__ = "([^"]+)"/);
if (!wrapperMatch) throw new Error('Could not read wrapper version');
return {
currentAgents: agentsMatch[1],
currentCode: codeMatch[1],
currentWrapper: wrapperMatch[1],
targetAgents: bumpPatch(agentsMatch[1]),
targetCode: bumpPatch(codeMatch[1]),
targetWrapper: bumpPatch(wrapperMatch[1]),
};
}
function pypiVersionExists(packageName, version) {
return new Promise((resolve) => {
const req = https.get(
`https://pypi.org/pypi/${packageName}/${version}/json`,
{ timeout: 15000 },
(res) => {
res.resume();
resolve(res.statusCode === 200);
}
);
req.on('error', () => resolve(false));
req.on('timeout', () => {
req.destroy();
resolve(false);
});
});
}
async function hasActiveReleaseRun(github, owner, repo, now = new Date(), core = null) {
const runs = await github.rest.actions.listWorkflowRuns({
owner,
repo,
workflow_id: 'pypi-release.yml',
per_page: 20,
});
const staleCutoff = now.getTime() - STALE_WAITING_HOURS * 60 * 60 * 1000;
return runs.data.workflow_runs.some((r) => {
if (!ACTIVE_RELEASE_STATUSES.has(r.status) || r.conclusion) return false;
if (r.status === 'waiting' && new Date(r.created_at).getTime() < staleCutoff) {
if (core) {
core.warning(
`Ignoring release run waiting >${STALE_WAITING_HOURS}h for environment approval: `
+ `${r.html_url} — approve or cancel it. It no longer blocks auto-releases.`
);
}
return false;
}
return true;
});
}
function utcDayStart(now = new Date()) {
return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
}
function releaseIntervalStart(now = new Date(), intervalDays = PATCH_RELEASE_INTERVAL_DAYS) {
return new Date(now.getTime() - intervalDays * 24 * 60 * 60 * 1000);
}
async function hasSuccessfulReleaseWithinDays(
github,
owner,
repo,
now = new Date(),
intervalDays = PATCH_RELEASE_INTERVAL_DAYS,
) {
const windowStart = releaseIntervalStart(now, intervalDays);
// Dedupe on GitHub releases (v* tags) rather than run conclusions: a
// dry_run=true dispatch concludes 'success' without publishing anything and
// must not consume the release window. Only a real full release creates a
// v* GitHub release (bump_and_release.py's `gh release create`).
const releases = await github.rest.repos.listReleases({
owner,
repo,
per_page: 10,
});
return releases.data.some(
(r) => r.tag_name && r.tag_name.startsWith('v')
&& new Date(r.published_at || r.created_at) >= windowStart
);
}
/** @deprecated Use hasSuccessfulReleaseWithinDays — kept for selftests. */
async function hasSuccessfulReleaseToday(github, owner, repo, now = new Date()) {
return hasSuccessfulReleaseWithinDays(github, owner, repo, now, 1);
}
async function lastGreenCoreTestsSha(github, owner, repo) {
const runs = await github.rest.actions.listWorkflowRuns({
owner,
repo,
workflow_id: 'test-core.yml',
branch: 'main',
status: 'completed',
per_page: 30,
});
const hit = runs.data.workflow_runs.find((r) => r.conclusion === 'success');
return hit ? hit.head_sha : '';
}
async function evaluateReleasePreflight(github, owner, repo, options, core) {
const {
headSha,
isCiTrigger = false,
bump = 'patch',
} = options;
const reasons = [];
const out = {
ready: false,
reasons,
headSha: headSha || '',
lastTag: '',
targetAgents: '',
targetCode: '',
targetWrapper: '',
};
if (bump !== 'patch') {
reasons.push('only patch auto-release supported');
return out;
}
const referenceTime = options.now instanceof Date ? options.now : new Date();
if (await hasActiveReleaseRun(github, owner, repo, referenceTime, core)) {
reasons.push('PyPI Release already in progress or awaiting approval');
return out;
}
if (await hasSuccessfulReleaseWithinDays(github, owner, repo, referenceTime)) {
reasons.push(
`already released within last ${PATCH_RELEASE_INTERVAL_DAYS} days; `
+ `max one patch release every ${PATCH_RELEASE_INTERVAL_DAYS} days`
);
return out;
}
let versions;
try {
versions = readVersionsFromTree();
} catch (err) {
reasons.push(err.message);
return out;
}
out.targetAgents = versions.targetAgents;
out.targetCode = versions.targetCode;
out.targetWrapper = versions.targetWrapper;
const agentsOnPypi = await pypiVersionExists('praisonaiagents', versions.targetAgents);
const codeOnPypi = await pypiVersionExists('praisonai-code', versions.targetCode);
const wrapperOnPypi = await pypiVersionExists('praisonai', versions.targetWrapper);
if (agentsOnPypi && codeOnPypi && wrapperOnPypi) {
reasons.push(
`already published: praisonaiagents==${versions.targetAgents}, `
+ `praisonai-code==${versions.targetCode}, praisonai==${versions.targetWrapper}`
);
return out;
}
const { execSync } = require('child_process');
let lastTag = '';
try {
lastTag = execSync('git describe --tags --match "v*" --abbrev=0', { encoding: 'utf8' }).trim();
} catch {
reasons.push('no v* tag found');
return out;
}
out.lastTag = lastTag;
let changed = '';
try {
changed = execSync(
`git diff --name-only ${lastTag} HEAD -- ${PACKAGE_PATHS.join(' ')}`,
{ encoding: 'utf8' }
).trim();
} catch {
reasons.push('git diff failed');
return out;
}
if (!changed) {
reasons.push(`no changes in ${PACKAGE_PATHS.join(' or ')} since ${lastTag}`);
return out;
}
if (isCiTrigger) {
let mainSha = '';
try {
mainSha = execSync('git rev-parse origin/main', { encoding: 'utf8' }).trim();
} catch {
reasons.push('could not resolve origin/main');
return out;
}
if (headSha !== mainSha) {
reasons.push(`superseded: green SHA ${headSha.slice(0, 7)} != main ${mainSha.slice(0, 7)}`);
return out;
}
out.headSha = headSha;
} else {
const evalSha = execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim();
out.headSha = evalSha;
const greenSha = await lastGreenCoreTestsSha(github, owner, repo);
if (greenSha !== evalSha) {
// Core Tests has path filters and honors [skip ci] (the release
// safety-net commit uses it), so the tip of main may legitimately have
// no Core Tests run of its own — e.g. a docs-only commit, or the
// "chore(release): … [skip ci]" commit right after a release. Accept a
// green ancestor when nothing under the released package paths changed
// after it; otherwise the cron path stalls until the next src/** push.
let greenAncestorOk = false;
if (greenSha && /^[0-9a-f]{40}$/.test(greenSha)) {
try {
execSync(`git merge-base --is-ancestor ${greenSha} ${evalSha}`);
const delta = execSync(
`git diff --name-only ${greenSha} ${evalSha} -- ${PACKAGE_PATHS.join(' ')}`,
{ encoding: 'utf8' }
).trim();
greenAncestorOk = delta === '';
} catch {
greenAncestorOk = false;
}
}
if (!greenAncestorOk) {
reasons.push(`CI not green on HEAD (last green: ${greenSha ? greenSha.slice(0, 7) : 'none'})`);
return out;
}
if (core) {
core.info(
`HEAD ${evalSha.slice(0, 7)} has no Core Tests run; accepting green ancestor `
+ `${greenSha.slice(0, 7)} (no package-path changes since).`
);
}
}
}
out.ready = true;
out.reasons = ['ready'];
if (core) core.info(`Release preflight passed for ${out.headSha || headSha}`);
return out;
}
module.exports = {
bumpPatch,
readVersionsFromTree,
pypiVersionExists,
PATCH_RELEASE_INTERVAL_DAYS,
STALE_WAITING_HOURS,
releaseIntervalStart,
hasActiveReleaseRun,
hasSuccessfulReleaseWithinDays,
hasSuccessfulReleaseToday,
utcDayStart,
evaluateReleasePreflight,
ACTIVE_RELEASE_STATUSES,
PACKAGE_PATHS,
};
+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. Classify this issue\n2. Search codebase for root cause\n3. Implement a minimal fix following AGENTS.md\n4. Create a PR with the changes'
});
+474 -141
View File
@@ -1,8 +1,11 @@
name: Auto PR Comment
# Review chain: CodeRabbit + Greptile (+ optional Qodo/Gemini) → Claude (FINAL)
# Copilot disabled (REVIEW_CHAIN_SKIP_COPILOT=1) — re-enable copilot-* jobs to restore.
# Fork PR jobs use pull_request_target so GH_TOKEN is available for comments.
# Review chain: CodeRabbit/Qodo/Gemini review first → Copilot → Claude (final)
# For human PRs: CodeRabbit/Qodo auto-review → this workflow triggers Copilot → then Claude
# For bot PRs: CodeRabbit/Qodo skip bots, so we trigger them explicitly first
#
# IMPORTANT: @copilot ignores bot comments. All @copilot mentions must use
# PAT_TOKEN so the comment posts as MervinPraison (a real user).
#
# Claude trigger runs IN-BAND (same workflow) to avoid GitHub Actions
# blocking bot-triggered workflows with action_required approval gates.
@@ -13,108 +16,484 @@ on:
types: [submitted]
pull_request:
types: [opened, synchronize]
pull_request_target:
types: [opened, synchronize]
schedule:
- cron: '30 3,9,15,21 * * *'
workflow_dispatch:
env:
REVIEW_CHAIN_SKIP_COPILOT: '1'
MAX_POLL_ATTEMPTS: 20
POLL_DELAY_MS: 30000
CLAUDE_TRIGGER_LOGINS: '["MervinPraison","github-actions[bot]"]'
jobs:
# ================================================================
# Claude FINAL after CodeRabbit / Greptile / optional Qodo / Gemini
# HUMAN PRs: Trigger @copilot AFTER CodeRabbit posts its summary
# ================================================================
claude-after-prior-reviewers:
copilot-after-coderabbit:
if: |
(github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
(
github.event.comment.user.login == 'coderabbitai[bot]' ||
github.event.comment.user.login == 'greptile-apps[bot]' ||
contains(github.event.comment.user.login, 'gemini-code-assist') ||
contains(github.event.comment.user.login, 'qodo-code-review')
)) ||
(github.event_name == 'pull_request_review' &&
contains(github.event.review.user.login, 'qodo-code-review'))
github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
github.event.comment.user.login == 'coderabbitai[bot]' &&
contains(github.event.comment.body, 'summarize by coderabbit')
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: read
pull-requests: write
outputs:
pr_number: ${{ steps.trigger.outputs.pr_number }}
triggered: ${{ steps.trigger.outputs.triggered }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Post Claude FINAL after prior reviewers
- name: Post Copilot review request (as user via PAT)
id: trigger
uses: actions/github-script@v7
with:
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'));
const mergeGate = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/merge-gate.js'));
const ps = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/pipeline-status.js'));
const prNumber =
context.payload.issue?.number ||
context.payload.pull_request?.number;
const owner = context.repo.owner;
const repo = context.repo.repo;
const { data: pr } = await github.rest.pulls.get({
owner, repo, pull_number: prNumber,
const comments = await github.rest.issues.listComments({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo
});
const result = await chain.maybeTriggerClaudeFinal(
github, owner, repo, prNumber,
mergeGate.FINAL_CLAUDE_REVIEW_BODY, core,
{ skipCopilot: true, optionalWaitMs: 0, prCreatedAt: pr.created_at }
// Check for duplicates — posted by MervinPraison (PAT) or github-actions[bot] (legacy)
const alreadyPosted = comments.data.some(c =>
c.body.includes('@copilot') &&
(c.user.login === 'MervinPraison' || c.user.login === 'github-actions[bot]')
);
if (result.posted) core.info(`Posted Claude FINAL on PR #${prNumber}`);
await ps.ensurePipelineLabels(github, owner, repo, core);
await ps.syncPipelineLabels(github, owner, repo, prNumber, core);
if (alreadyPosted) {
console.log('Copilot already triggered, skipping');
core.setOutput('triggered', 'false');
core.setOutput('pr_number', context.issue.number.toString());
return;
}
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '@copilot Do a thorough review of this PR. Read ALL existing reviewer comments above from Qodo, Coderabbit, and Gemini first — incorporate their findings.\n\nReview areas:\n1. **Bloat check**: Are changes minimal and focused? Any unnecessary code or scope creep?\n2. **Security**: Any hardcoded secrets, unsafe eval/exec, missing input validation?\n3. **Performance**: Any module-level heavy imports? Hot-path regressions?\n4. **Tests**: Are tests included? Do they cover the changes adequately?\n5. **Backward compat**: Any public API changes without deprecation?\n6. **Code quality**: DRY violations, naming conventions, error handling?\n7. **Address reviewer feedback**: If Qodo, Coderabbit, or Gemini flagged valid issues, include them in your review\n8. Suggest specific improvements with code examples where possible'
});
core.setOutput('triggered', 'true');
core.setOutput('pr_number', context.issue.number.toString());
# Fallback: Trigger @copilot after Qodo review (if Coderabbit doesn't fire)
copilot-after-qodo:
if: |
github.event_name == 'pull_request_review' &&
github.event.review.user.login == 'qodo-code-review[bot]'
runs-on: ubuntu-latest
permissions:
pull-requests: write
outputs:
pr_number: ${{ steps.trigger.outputs.pr_number }}
triggered: ${{ steps.trigger.outputs.triggered }}
steps:
- name: Post Copilot review request (as user via PAT)
id: trigger
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const comments = await github.rest.issues.listComments({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo
});
const alreadyPosted = comments.data.some(c =>
c.body.includes('@copilot') &&
(c.user.login === 'MervinPraison' || c.user.login === 'github-actions[bot]')
);
if (alreadyPosted) {
console.log('Copilot already triggered, skipping');
core.setOutput('triggered', 'false');
core.setOutput('pr_number', context.issue.number.toString());
return;
}
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '@copilot Do a thorough review of this PR. Read ALL existing reviewer comments above first.\n\nReview areas:\n1. **Bloat check**: Are changes minimal and focused?\n2. **Security**: Any hardcoded secrets, unsafe eval/exec, missing input validation?\n3. **Performance**: Any module-level heavy imports? Hot-path regressions?\n4. **Tests**: Are tests included? Do they cover the changes adequately?\n5. **Backward compat**: Any public API changes without deprecation?\n6. **Code quality**: DRY violations, naming conventions, error handling?\n7. Suggest specific improvements with code examples where possible'
});
core.setOutput('triggered', 'true');
core.setOutput('pr_number', context.issue.number.toString());
# ================================================================
# FALLBACK: Claude FINAL after PR open if reviewers are slow
# FALLBACK: Claude trigger for cases where Copilot chain fails/skips
# Ensures Claude always runs after sufficient review time
# ================================================================
claude-fallback-timeout:
if: |
github.event_name == 'pull_request_target' &&
github.event_name == 'pull_request' &&
github.event.action == 'opened'
runs-on: ubuntu-latest
timeout-minutes: 45
timeout-minutes: 20
permissions:
issues: write
pull-requests: read
pull-requests: write
steps:
- uses: actions/checkout@v4
- name: Wait for prior reviewers (30 min)
run: sleep 1800
- name: Post Claude FINAL (skip Copilot)
- name: Wait for review window (15 min)
run: sleep 900
- name: Check if Claude already triggered
id: check_claude
uses: actions/github-script@v7
env:
TRIGGER_LOGINS: ${{ env.CLAUDE_TRIGGER_LOGINS }}
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const triggerLogins = JSON.parse(process.env.TRIGGER_LOGINS);
const comments = await github.rest.issues.listComments({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo
});
const alreadyPosted = comments.data.some(c => {
const body = (c.body || '').toLowerCase();
if (!body.includes('@claude') || !triggerLogins.includes(c.user.login)) return false;
if (body.includes('merge conflict')) return false;
return body.includes('final architecture reviewer') || body.includes('lead engineer');
});
core.setOutput('already_triggered', alreadyPosted ? 'true' : 'false');
core.setOutput('comments_count', comments.data.length.toString());
// Collect all bot reviewer comments for context
const botReviews = comments.data.filter(c =>
['coderabbitai[bot]', 'qodo-code-review[bot]', 'gemini-code-assist[bot]',
'copilot-swe-agent', 'Copilot', 'greptile-apps[bot]'].some(bot =>
c.user.login.toLowerCase().includes(bot.toLowerCase())
)
);
core.setOutput('review_count', botReviews.length.toString());
- name: Aggregate reviews and trigger Claude
if: steps.check_claude.outputs.already_triggered != 'true'
uses: actions/github-script@v7
with:
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'));
const mergeGate = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/merge-gate.js'));
const pr = context.payload.pull_request.number;
await chain.maybeTriggerClaudeFinal(
github, context.repo.owner, context.repo.repo, pr,
mergeGate.FINAL_CLAUDE_REVIEW_BODY, core,
{ skipCopilot: true, optionalWaitMs: 0 }
);
const comments = await github.rest.issues.listComments({issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, per_page: 100});
const reviews = {coderabbit: [], qodo: [], gemini: [], copilot: [], greptile: [], others: []};
comments.data.forEach(c => {
const login = c.user.login.toLowerCase();
const body = c.body.substring(0, 500);
if (login.includes('coderabbit')) reviews.coderabbit.push(body);
else if (login.includes('qodo')) reviews.qodo.push(body);
else if (login.includes('gemini')) reviews.gemini.push(body);
else if (login.includes('copilot')) reviews.copilot.push(body);
else if (login.includes('greptile')) reviews.greptile.push(body);
else if (c.user.type === 'Bot') reviews.others.push(body);
});
const summaryParts = [];
if (reviews.coderabbit.length) summaryParts.push('CodeRabbit: ' + reviews.coderabbit.length + ' comments');
if (reviews.qodo.length) summaryParts.push('Qodo: ' + reviews.qodo.length + ' comments');
if (reviews.gemini.length) summaryParts.push('Gemini: ' + reviews.gemini.length + ' comments');
if (reviews.copilot.length) summaryParts.push('Copilot: ' + reviews.copilot.length + ' comments');
if (reviews.greptile.length) summaryParts.push('Greptile: ' + reviews.greptile.length + ' comments');
const reviewSummary = summaryParts.length ? summaryParts.join(' | ') : 'No bot reviews detected';
const claudeBody = '@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. ' + reviewSummary + '.\n\n' +
'Review Context: ' + comments.data.length + ' total comments, ' +
(reviews.coderabbit.length + reviews.qodo.length + reviews.gemini.length + reviews.copilot.length + reviews.greptile.length) + ' bot reviews.\n\n' +
'Phase 1: Review per AGENTS.md principles (protocol-driven, backward compatible, no perf impact, SDK value gate — no feature bloat).\n' +
'Do not bloat the Agent class with additional params — only if absolutely required; we already support many params.\n' +
'Phase 2: FIX valid issues found by prior reviewers.\n' +
'Phase 3: Provide final verdict (approve or request changes).';
await github.rest.issues.createComment({issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: claudeBody});
console.log('Claude fallback triggered on PR #' + context.issue.number + ' with ' + reviewSummary);
# ================================================================
# Recovery: post FINAL @claude after push or when chain missed
# Claude FINAL review — polls for Copilot's response, then triggers
# This runs IN-BAND under the PAT context (MervinPraison), avoiding
# the action_required approval gate that blocks bot-triggered workflows.
# ================================================================
claude-after-copilot:
needs: [copilot-after-coderabbit]
if: |
always() &&
needs.copilot-after-coderabbit.result == 'success' &&
needs.copilot-after-coderabbit.outputs.triggered == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
issues: write
pull-requests: write
steps:
- name: Wait for Copilot to respond (poll)
id: poll
uses: actions/github-script@v7
env:
PR_NUMBER: ${{ needs.copilot-after-coderabbit.outputs.pr_number }}
MAX_POLL_ATTEMPTS: ${{ env.MAX_POLL_ATTEMPTS }}
POLL_DELAY_MS: ${{ env.POLL_DELAY_MS }}
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const pr = parseInt(process.env.PR_NUMBER, 10);
const owner = context.repo.owner;
const repo = context.repo.repo;
// Poll for Copilot's response — check every 30s for up to 10 minutes
const maxAttempts = parseInt(process.env.MAX_POLL_ATTEMPTS, 10);
const delayMs = parseInt(process.env.POLL_DELAY_MS, 10);
for (let i = 0; i < maxAttempts; i++) {
console.log(`Poll attempt ${i + 1}/${maxAttempts}...`);
const comments = await github.rest.issues.listComments({
issue_number: pr,
owner,
repo,
per_page: 50,
sort: 'created',
direction: 'desc'
});
// Look for Copilot's response (copilot-swe-agent or Copilot)
const copilotResponse = comments.data.find(c =>
(c.user.login === 'copilot-swe-agent' ||
c.user.login === 'Copilot' ||
c.user.login.toLowerCase().includes('copilot')) &&
c.user.type === 'Bot'
);
if (copilotResponse) {
console.log(`Copilot responded at ${copilotResponse.created_at}`);
core.setOutput('copilot_responded', 'true');
return;
}
// Also check PR reviews for Copilot
const reviews = await github.rest.pulls.listReviews({
pull_number: pr,
owner,
repo
});
const copilotReview = reviews.data.find(r =>
r.user.login.toLowerCase().includes('copilot')
);
if (copilotReview) {
console.log(`Copilot review submitted at ${copilotReview.submitted_at}`);
core.setOutput('copilot_responded', 'true');
return;
}
if (i < maxAttempts - 1) {
console.log(`Copilot hasn't responded yet, waiting ${delayMs/1000}s...`);
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
console.log('Copilot did not respond within timeout, triggering Claude anyway');
core.setOutput('copilot_responded', 'timeout');
- name: Check if Claude already triggered
id: check_claude
uses: actions/github-script@v7
env:
PR_NUMBER: ${{ needs.copilot-after-coderabbit.outputs.pr_number }}
TRIGGER_LOGINS: ${{ env.CLAUDE_TRIGGER_LOGINS }}
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const pr = parseInt(process.env.PR_NUMBER, 10);
const triggerLogins = JSON.parse(process.env.TRIGGER_LOGINS);
const comments = await github.rest.issues.listComments({
issue_number: pr,
owner: context.repo.owner,
repo: context.repo.repo
});
const alreadyPosted = comments.data.some(c => {
const body = (c.body || '').toLowerCase();
if (!body.includes('@claude') || !triggerLogins.includes(c.user.login)) return false;
if (body.includes('merge conflict')) return false;
return body.includes('final architecture reviewer') || body.includes('lead engineer');
});
core.setOutput('already_triggered', alreadyPosted ? 'true' : 'false');
- name: Trigger Claude final review
if: steps.check_claude.outputs.already_triggered != 'true'
uses: actions/github-script@v7
env:
PR_NUMBER: ${{ needs.copilot-after-coderabbit.outputs.pr_number }}
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const pr = parseInt(process.env.PR_NUMBER, 10);
await github.rest.issues.createComment({
issue_number: pr,
owner: context.repo.owner,
repo: context.repo.repo,
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: 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.\n\n**Phase 2: FIX Valid Issues**\n5. For any VALID bugs or architectural flaws found by Gemini, CodeRabbit, Qodo, Copilot, or any other reviewer: implement the fix\n6. Push all code fixes directly to THIS branch (do NOT create a new PR)\n7. Comment a summary of exact files modified and what you skipped\n\n**Phase 3: Final Verdict**\n8. If all issues are resolved, approve the PR / close the Issue\n9. If blocking issues remain, request changes / leave clear action items'
});
console.log(`Claude triggered on PR #${pr}`);
# ================================================================
# Claude after Qodo fallback path (same polling approach)
# ================================================================
claude-after-copilot-qodo:
needs: [copilot-after-qodo]
if: |
always() &&
needs.copilot-after-qodo.result == 'success' &&
needs.copilot-after-qodo.outputs.triggered == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
issues: write
pull-requests: write
steps:
- name: Wait for Copilot to respond (poll)
id: poll
uses: actions/github-script@v7
env:
PR_NUMBER: ${{ needs.copilot-after-qodo.outputs.pr_number }}
MAX_POLL_ATTEMPTS: ${{ env.MAX_POLL_ATTEMPTS }}
POLL_DELAY_MS: ${{ env.POLL_DELAY_MS }}
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const pr = parseInt(process.env.PR_NUMBER, 10);
const owner = context.repo.owner;
const repo = context.repo.repo;
const maxAttempts = parseInt(process.env.MAX_POLL_ATTEMPTS, 10);
const delayMs = parseInt(process.env.POLL_DELAY_MS, 10);
for (let i = 0; i < maxAttempts; i++) {
console.log(`Poll attempt ${i + 1}/${maxAttempts}...`);
const comments = await github.rest.issues.listComments({
issue_number: pr, owner, repo,
per_page: 50, sort: 'created', direction: 'desc'
});
const copilotResponse = comments.data.find(c =>
(c.user.login === 'copilot-swe-agent' ||
c.user.login === 'Copilot' ||
c.user.login.toLowerCase().includes('copilot')) &&
c.user.type === 'Bot'
);
if (copilotResponse) {
console.log(`Copilot responded at ${copilotResponse.created_at}`);
core.setOutput('copilot_responded', 'true');
return;
}
const reviews = await github.rest.pulls.listReviews({
pull_number: pr, owner, repo
});
const copilotReview = reviews.data.find(r =>
r.user.login.toLowerCase().includes('copilot')
);
if (copilotReview) {
console.log(`Copilot review submitted at ${copilotReview.submitted_at}`);
core.setOutput('copilot_responded', 'true');
return;
}
if (i < maxAttempts - 1) {
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
console.log('Copilot did not respond within timeout, triggering Claude anyway');
core.setOutput('copilot_responded', 'timeout');
- name: Check if Claude already triggered
id: check_claude
uses: actions/github-script@v7
env:
PR_NUMBER: ${{ needs.copilot-after-qodo.outputs.pr_number }}
TRIGGER_LOGINS: ${{ env.CLAUDE_TRIGGER_LOGINS }}
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const pr = parseInt(process.env.PR_NUMBER, 10);
const triggerLogins = JSON.parse(process.env.TRIGGER_LOGINS);
const comments = await github.rest.issues.listComments({
issue_number: pr,
owner: context.repo.owner,
repo: context.repo.repo
});
const alreadyPosted = comments.data.some(c => {
const body = (c.body || '').toLowerCase();
if (!body.includes('@claude') || !triggerLogins.includes(c.user.login)) return false;
if (body.includes('merge conflict')) return false;
return body.includes('final architecture reviewer') || body.includes('lead engineer');
});
core.setOutput('already_triggered', alreadyPosted ? 'true' : 'false');
- name: Trigger Claude final review
if: steps.check_claude.outputs.already_triggered != 'true'
uses: actions/github-script@v7
env:
PR_NUMBER: ${{ needs.copilot-after-qodo.outputs.pr_number }}
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const pr = parseInt(process.env.PR_NUMBER, 10);
await github.rest.issues.createComment({
issue_number: pr,
owner: context.repo.owner,
repo: context.repo.repo,
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: 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.\n\n**Phase 2: FIX Valid Issues**\n5. For any VALID bugs or architectural flaws found by Gemini, CodeRabbit, Qodo, Copilot, or any other reviewer: implement the fix\n6. Push all code fixes directly to THIS branch (do NOT create a new PR)\n7. Comment a summary of exact files modified and what you skipped\n\n**Phase 3: Final Verdict**\n8. If all issues are resolved, approve the PR / close the Issue\n9. If blocking issues remain, request changes / leave clear action items'
});
console.log(`Claude triggered on PR #${pr}`);
# ================================================================
# Claude after Gemini — restores the trigger lost from the deleted
# chain-claude-after-copilot.yml workflow.
# Fires when Gemini posts a review comment on a PR or issue.
# ================================================================
claude-after-gemini:
if: |
github.event_name == 'issue_comment' &&
(
github.event.comment.user.login == 'gemini-code-assist[bot]' ||
contains(github.event.comment.body, 'Review completed by Gemini CLI') ||
contains(github.event.comment.body, 'The fix is ready for review. Please test')
)
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- name: Check if Claude already triggered
id: check_claude
uses: actions/github-script@v7
env:
TRIGGER_LOGINS: ${{ env.CLAUDE_TRIGGER_LOGINS }}
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const triggerLogins = JSON.parse(process.env.TRIGGER_LOGINS);
const comments = await github.rest.issues.listComments({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo
});
const alreadyPosted = comments.data.some(c => {
const body = (c.body || '').toLowerCase();
if (!body.includes('@claude') || !triggerLogins.includes(c.user.login)) return false;
if (body.includes('merge conflict')) return false;
return body.includes('final architecture reviewer') || body.includes('lead engineer');
});
core.setOutput('already_triggered', alreadyPosted ? 'true' : 'false');
- name: Post Claude analysis request (Lead Engineer)
if: steps.check_claude.outputs.already_triggered != 'true'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '@claude You are the Lead Engineer. 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 analysis and reviews above carefully (Gemini, CodeRabbit, Qodo, Copilot, etc).\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.\n\n**Phase 2: FIX Valid Issues**\n5. For any VALID bugs or architectural flaws found by Gemini, CodeRabbit, Qodo, Copilot, or any other reviewer: implement the fix\n6. Push all code fixes directly to THIS branch (do NOT create a new PR)\n7. Comment a summary of exact files modified and what you skipped\n\n**Phase 3: Final Verdict**\n8. If all issues are resolved, approve the PR / close the Issue\n9. If blocking issues remain, request changes / leave clear action items'
});
# ================================================================
# Recovery: post FINAL @claude if merge-conflict comment blocked the chain
# (e.g. conflict fired before claude-after-copilot could post review prompt)
# ================================================================
claude-review-recovery:
if: |
github.event_name == 'pull_request_target' &&
github.event_name == 'pull_request' &&
github.event.action == 'synchronize'
runs-on: ubuntu-latest
permissions:
@@ -130,7 +509,6 @@ jobs:
script: |
const path = require('path');
const mergeGate = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/merge-gate.js'));
const chain = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/pr-review-chain.js'));
const pr = context.payload.pull_request.number;
const owner = context.repo.owner;
const repo = context.repo.repo;
@@ -138,7 +516,6 @@ jobs:
const comments = await mergeGate.listAllComments(github, owner, repo, pr);
const headPushedAt = (await mergeGate.getHeadCommitDate(github, owner, repo, pr))
|| context.payload.pull_request.updated_at;
const headPusher = context.payload.sender?.login || null;
const hasFinal = mergeGate.hasFinalClaudeReviewTrigger(comments);
const isStale = mergeGate.isStaleFinalAfterPush(comments, headPushedAt);
@@ -148,24 +525,11 @@ jobs:
return;
}
if (await mergeGate.hasInProgressClaudeAssistant(github, owner, repo, pr)) {
core.info('Claude workflow in progress — skip recovery');
return;
}
const finalBody = mergeGate.FINAL_CLAUDE_REVIEW_BODY;
const opts = { skipCopilot: true, optionalWaitMs: 0 };
if (hasFinal && isStale) {
const gate = mergeGate.shouldSkipStaleFinalRecovery(comments, headPushedAt, headPusher);
if (gate.skip) {
core.info(`Skip stale-FINAL recovery: ${gate.reason}`);
return;
}
const result = await chain.maybeTriggerClaudeFinal(
github, owner, repo, pr, finalBody, core, opts
);
if (result.posted) core.info(`Posted stale-FINAL recovery @claude on PR #${pr}`);
await github.rest.issues.createComment({ issue_number: pr, owner, repo, body: finalBody });
core.info(`Posted stale-FINAL recovery @claude on PR #${pr}`);
return;
}
@@ -174,24 +538,21 @@ jobs:
return;
}
const result = await chain.maybeTriggerClaudeFinal(
github, owner, repo, pr, finalBody, core, opts
);
if (result.posted) core.info(`Posted missing FINAL Claude review on PR #${pr}`);
const hasReviewerActivity = comments.some(c => {
const login = c.user.login.toLowerCase();
return login.includes('coderabbit') || login.includes('copilot') ||
login.includes('greptile') || login.includes('qodo') || login.includes('gemini');
});
if (!hasReviewerActivity) {
core.info('No bot reviewer activity yet, skipping recovery');
return;
}
- name: Sync pipeline status labels
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const path = require('path');
const ps = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/pipeline-status.js'));
const pr = context.payload.pull_request.number;
await ps.ensurePipelineLabels(github, context.repo.owner, context.repo.repo, core);
await ps.syncPipelineLabels(github, context.repo.owner, context.repo.repo, pr, core);
await github.rest.issues.createComment({ issue_number: pr, owner, repo, body: finalBody });
core.info(`Posted missing FINAL Claude review on PR #${pr}`);
# Scheduled + manual: post missing or stale FINAL @claude on all open PRs (incl. forks)
post-missing-claude-final:
# Scheduled: re-post @claude when FINAL is stale (Claude pushed after last FINAL trigger)
stale-final-recovery-scheduled:
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
permissions:
@@ -200,14 +561,13 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Post missing/stale FINAL @claude on open PRs
- name: Re-post stale FINAL @claude on open internal PRs
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const path = require('path');
const mergeGate = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/merge-gate.js'));
const chain = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/pr-review-chain.js'));
const owner = context.repo.owner;
const repo = context.repo.repo;
const prs = await github.paginate(github.rest.pulls.list, {
@@ -216,43 +576,28 @@ jobs:
let posted = 0;
for (const pr of prs) {
if (pr.draft) continue;
if (pr.head?.repo?.full_name && pr.head.repo.full_name !== `${owner}/${repo}`) continue;
const comments = await mergeGate.listAllComments(github, owner, repo, pr.number);
const headPushedAt = (await mergeGate.getHeadCommitDate(github, owner, repo, pr.number)) || pr.updated_at;
const hasFinal = mergeGate.hasFinalClaudeReviewTrigger(comments);
const isStale = mergeGate.isStaleFinalAfterPush(comments, headPushedAt);
if (hasFinal && !isStale) continue;
if (!mergeGate.needsStaleFinalRecovery(comments, headPushedAt)) continue;
if (mergeGate.hasRecentClaudeTrigger(comments, 10)) {
core.info(`PR #${pr.number}: @claude within 10min — skip`);
core.info(`PR #${pr.number}: stale FINAL but @claude within 10min — skip`);
continue;
}
if (await mergeGate.hasInProgressClaudeAssistant(github, owner, repo, pr.number)) {
core.info(`PR #${pr.number}: Claude in progress — skip`);
continue;
}
if (hasFinal && isStale) {
const gate = mergeGate.shouldSkipStaleFinalRecovery(comments, headPushedAt);
if (gate.skip) {
core.info(`PR #${pr.number}: skip stale — ${gate.reason}`);
continue;
}
}
const result = await chain.maybeTriggerClaudeFinal(
github, owner, repo, pr.number,
mergeGate.FINAL_CLAUDE_REVIEW_BODY, core,
{ skipCopilot: true, optionalWaitMs: 0 }
);
if (result.posted) {
posted++;
core.info(`Posted Claude FINAL on PR #${pr.number}`);
}
if (posted >= 10) break;
await github.rest.issues.createComment({
issue_number: pr.number, owner, repo,
body: mergeGate.FINAL_CLAUDE_REVIEW_BODY,
});
posted++;
core.info(`Posted scheduled stale-FINAL recovery on PR #${pr.number}`);
if (posted >= 5) break;
}
core.info(`Missing/stale Claude FINAL sweep complete (${posted} posted)`);
core.info(`Scheduled stale-FINAL recovery complete (${posted} posted)`);
# ================================================================
# BOT PRs: kick CodeRabbit + Qodo (Greptile may follow on its own)
# BOT PRs: CodeRabbit/Qodo skip bot-authored PRs by default.
# Trigger them explicitly. Do NOT trigger Copilot here — it will
# be triggered by copilot-after-coderabbit/qodo when they finish.
# ================================================================
bot-pr-trigger-reviews:
if: |
@@ -266,7 +611,6 @@ jobs:
runs-on: ubuntu-latest
permissions:
pull-requests: write
issues: write
steps:
- uses: actions/checkout@v4
with:
@@ -280,15 +624,4 @@ jobs:
const path = require('path');
const chain = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/bot-pr-review-chain.js'));
const pr = context.payload.pull_request.number;
await chain.kickReviewChain(github, context.repo.owner, context.repo.repo, pr, core);
- name: Sync pipeline status labels
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const path = require('path');
const ps = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/pipeline-status.js'));
const pr = context.payload.pull_request.number;
await ps.ensurePipelineLabels(github, context.repo.owner, context.repo.repo, core);
await ps.syncPipelineLabels(github, context.repo.owner, context.repo.repo, pr, core);
await chain.kickReviewChain(github, context.repo.owner, context.repo.repo, pr, core);
-102
View File
@@ -1,102 +0,0 @@
name: Auto CI Failure → Claude
# Detects internal PRs with failing CI and posts @claude with extracted failure details.
on:
workflow_run:
workflows: [Core Tests, Optimized Test Suite]
types: [completed]
pull_request:
types: [opened, synchronize, reopened]
workflow_dispatch:
inputs:
pr_number:
description: 'Optional PR number to process (default: scan blocked PRs)'
required: false
type: string
schedule:
- cron: '15 */6 * * *'
concurrency:
group: ci-failure-scan-${{ github.repository }}-${{ github.event.pull_request.number || github.event.workflow_run.id || github.run_id }}
cancel-in-progress: true
env:
# PAT posts as MervinPraison so issue_comment triggers claude.yml (GITHUB_TOKEN cannot chain workflows)
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
permissions:
pull-requests: read
issues: write
actions: read
jobs:
detect-and-trigger:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Detect CI failures and trigger Claude
uses: actions/github-script@v7
with:
github-token: ${{ env.GH_TOKEN }}
script: |
const path = require('path');
const ciFix = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/ci-failure-claude.js'));
const owner = context.repo.owner;
const repo = context.repo.repo;
const baseRepo = `${owner}/${repo}`;
async function processPR(prNumber) {
return ciFix.maybeTriggerCiFixClaude(github, owner, repo, prNumber, core);
}
let results = [];
if (context.eventName === 'workflow_run') {
const wr = context.payload.workflow_run;
const result = await ciFix.processWorkflowRunFailure(
github, owner, repo, wr, core
);
results.push({ event: 'workflow_run', workflow: wr.name, ...result });
} else if (context.eventName === 'pull_request') {
results.push({
pr: context.payload.pull_request.number,
...(await processPR(context.payload.pull_request.number)),
});
} else {
let prNumbers = [];
if (context.eventName === 'workflow_dispatch' && context.payload.inputs?.pr_number) {
prNumbers = [Number(context.payload.inputs.pr_number)];
} else {
const prs = await github.paginate(github.rest.pulls.list, {
owner,
repo,
state: 'open',
per_page: 100,
});
for (const pr of prs) {
if (pr.draft) continue;
const headRepo = pr.head?.repo?.full_name;
if (headRepo && headRepo !== baseRepo) continue;
const { data: issue } = await github.rest.issues.get({
owner,
repo,
issue_number: pr.number,
});
const labels = issue.labels.map((l) => l.name);
if (labels.includes('pipeline/blocked:ci') || labels.includes(ciFix.CI_FIX_LABEL)) {
prNumbers.push(pr.number);
}
}
}
for (const num of prNumbers) {
results.push({ pr: num, ...(await processPR(num)) });
}
}
core.summary.addRaw('```json\n' + JSON.stringify(results, null, 2) + '\n```');
await core.summary.write();
+55 -211
View File
@@ -5,8 +5,7 @@ name: Claude PR Merge Gate
on:
schedule:
# Scan for merge-ready PRs every 10 minutes (assess runs only when candidates exist).
- cron: '*/10 * * * *'
- cron: '0 2,8,14,20 * * *'
workflow_dispatch:
inputs:
pr_number:
@@ -33,13 +32,12 @@ 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' }}
cancel-in-progress: false
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: true
env:
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
MAX_CANDIDATES: 3
MAX_CANDIDATES: 5
DISPATCH_PR_NUMBER: ${{ github.event.inputs.pr_number || github.event.client_payload.pr_number || '' }}
permissions:
@@ -57,22 +55,15 @@ jobs:
pull-requests: read
issues: read
actions: write
id-token: write
steps:
- uses: actions/checkout@v4
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.CLAUDE_APP_ID }}
private-key: ${{ secrets.CLAUDE_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
- name: Dispatch merge gate when PR pipeline is ready
uses: actions/github-script@v7
env:
PAT: ${{ secrets.GH_TOKEN }}
with:
github-token: ${{ steps.app-token.outputs.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'));
@@ -80,6 +71,11 @@ jobs:
const owner = context.repo.owner;
const repo = context.repo.repo;
if (!process.env.PAT) {
core.setFailed('GH_TOKEN secret is required for merge gate auto-dispatch (needs repo + workflow scope).');
return;
}
if (wr.conclusion !== 'success') {
core.info(`Skip dispatch: ${wr.name} conclusion=${wr.conclusion}`);
return;
@@ -108,10 +104,6 @@ jobs:
return;
}
if (await mergeGate.shouldSkipMergeGateDispatch(github, owner, repo, prNumber, core)) {
return;
}
await github.rest.repos.createDispatchEvent({
owner,
repo,
@@ -128,30 +120,20 @@ jobs:
pull-requests: read
issues: write
actions: read
id-token: write
outputs:
matrix: ${{ steps.scan.outputs.matrix }}
has_candidates: ${{ steps.scan.outputs.has_candidates }}
steps:
- uses: actions/checkout@v4
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.CLAUDE_APP_ID }}
private-key: ${{ secrets.CLAUDE_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
- name: Scan open PRs for merge gate eligibility
id: scan
uses: actions/github-script@v7
with:
github-token: ${{ steps.app-token.outputs.token }}
github-token: ${{ env.GH_TOKEN }}
script: |
const path = require('path');
const mergeGate = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/merge-gate.js'));
const ps = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/pipeline-status.js'));
const owner = context.repo.owner;
const repo = context.repo.repo;
const maxCandidates = parseInt(process.env.MAX_CANDIDATES || '5', 10);
@@ -172,9 +154,10 @@ jobs:
skipped.push({ pr: n, reasons: result.reasons });
}
} else {
const prNumbers = await mergeGate.listPrNumbersForMergeGateScan(
github, owner, repo, core
);
const prs = await github.paginate(github.rest.pulls.list, {
owner, repo, state: 'open', per_page: 100, sort: 'created', direction: 'asc',
});
const prNumbers = prs.map((p) => p.number);
const selected = await mergeGate.selectMergeGateCandidates(
github, owner, repo, prNumbers, maxCandidates, core
);
@@ -201,27 +184,16 @@ 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
);
claude-assess:
needs: scan-candidates
if: needs.scan-candidates.outputs.has_candidates == 'true'
@@ -254,7 +226,7 @@ jobs:
id: mark_active
uses: actions/github-script@v7
with:
github-token: ${{ steps.app-token.outputs.token }}
github-token: ${{ env.GH_TOKEN }}
script: |
core.setOutput('started_at', new Date().toISOString());
await github.rest.issues.addLabels({
@@ -303,13 +275,12 @@ 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.
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).
7. BLOCK if labels security/breaking-change/needs-manual-review/release, sensitive paths (.github/workflows, auth, pyproject.toml), secrets in diff, PR >800 lines or >30 files, or SDK changes without tests. Label `merge-gate-ci-only` exempts CI-only changes under `.github/workflows/`, `.github/actions/`, or merge-gate scripts.
8. Confirm no CHANGES_REQUESTED reviews and CI green on HEAD ${{ matrix.head_sha }}.
9. If ready to merge: MERGE_GATE_VERDICT: APPROVE. Otherwise: MERGE_GATE_VERDICT: BLOCK with reasons.
3. Confirm SDK value gate: change strengthens praisonaiagents (no scope creep).
4. Do not bloat the Agent class with additional params — only if absolutely required; we already support many params.
5. If agent.py adds >100 lines or new Agent __init__ params, respond MERGE_GATE_VERDICT: BLOCK (manual review required).
6. BLOCK if labels security/breaking-change/needs-manual-review/release, sensitive paths (.github/workflows, auth, pyproject.toml), secrets in diff, PR >800 lines or >30 files, or SDK changes without tests. Label `merge-gate-ci-only` exempts CI-only changes under `.github/workflows/`, `.github/actions/`, or merge-gate scripts.
7. Confirm no CHANGES_REQUESTED reviews and CI green on HEAD ${{ matrix.head_sha }}.
8. If ready to merge: MERGE_GATE_VERDICT: APPROVE. Otherwise: MERGE_GATE_VERDICT: BLOCK with reasons.
Do not approve or merge via GitHub UI/API — only post the verdict comment.
disallowed_tools: |
@@ -318,100 +289,17 @@ 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 verdict if Claude did not comment
id: fallback_verdict
if: always() && steps.assess.conclusion != 'skipped'
uses: actions/github-script@v7
with:
github-token: ${{ steps.app-token.outputs.token }}
github-token: ${{ env.GH_TOKEN }}
script: |
const mergeGate = require('/tmp/merge-gate.js');
const owner = context.repo.owner;
@@ -420,11 +308,10 @@ jobs:
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 }
);
const existing = mergeGate.findMergeGateVerdict(ctx.comments, minCreatedAt, ctx.headPushedAt);
if (existing) {
core.info(`Verdict already posted: ${existing}`);
core.setOutput('verdict', existing);
return;
}
@@ -435,7 +322,7 @@ jobs:
const lines = [
`MERGE_GATE_VERDICT: ${verdict}`,
'',
'Automated fallback — Opus merge gate did not post a PR comment.',
'Automated fallback — Claude assess did not post a verdict comment (e.g. GitHub MCP unavailable).',
];
if (check.ready) {
lines.push('All deterministic merge gates passed.');
@@ -447,12 +334,13 @@ jobs:
body: lines.join('\n'),
});
core.info(`Posted fallback verdict: ${verdict}`);
core.setOutput('verdict', verdict);
- name: Capture Opus MERGE_GATE_VERDICT
id: capture_verdict
- name: Verify verdict posted
id: verdict
uses: actions/github-script@v7
with:
github-token: ${{ steps.app-token.outputs.token }}
github-token: ${{ env.GH_TOKEN }}
script: |
const mergeGate = require('/tmp/merge-gate.js');
const comments = await mergeGate.listAllComments(
@@ -462,23 +350,15 @@ jobs:
github, context.repo.owner, context.repo.repo, ${{ matrix.pr_number }}
);
const minCreatedAt = new Date(Date.now() - 25 * 60 * 1000).toISOString();
const verdict = mergeGate.findMergeGateVerdict(
comments, minCreatedAt, ctx.headPushedAt, { excludeAutomatedFallback: true }
);
const verdict = mergeGate.findMergeGateVerdict(comments, minCreatedAt, ctx.headPushedAt);
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.'
);
} else {
core.info(`Opus merge gate verdict: ${verdict}`);
}
if (!verdict) core.warning('No recent MERGE_GATE_VERDICT comment found after assessment');
- name: Remove merge gate active label
if: always()
uses: actions/github-script@v7
with:
github-token: ${{ steps.app-token.outputs.token }}
github-token: ${{ env.GH_TOKEN }}
script: |
const prNumber = ${{ matrix.pr_number }};
try {
@@ -493,6 +373,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: |
@@ -508,25 +392,16 @@ jobs:
pull-requests: write
issues: write
actions: read
id-token: write
steps:
- uses: actions/checkout@v4
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.CLAUDE_APP_ID }}
private-key: ${{ secrets.CLAUDE_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
- name: Re-check gates and merge
uses: actions/github-script@v7
env:
MERGE_METHOD: ${{ github.event.inputs.merge_method || 'merge' }}
SCAN_HEAD_SHA: ${{ matrix.head_sha }}
with:
github-token: ${{ steps.app-token.outputs.token }}
github-token: ${{ env.GH_TOKEN }}
script: |
const path = require('path');
const mergeGate = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/merge-gate.js'));
@@ -549,9 +424,7 @@ jobs:
const ctx = await mergeGate.loadPrContext(github, owner, repo, prNumber);
const comments = ctx.comments;
const verdictMin = new Date(Date.now() - 25 * 60 * 1000).toISOString();
const verdict = mergeGate.findMergeGateVerdict(
comments, verdictMin, ctx.headPushedAt, { excludeAutomatedFallback: true }
);
const verdict = mergeGate.findMergeGateVerdict(comments, verdictMin, ctx.headPushedAt);
if (verdict !== 'APPROVE') {
core.info(`Skip merge: verdict=${verdict || 'NONE'}`);
await removeLabels();
@@ -574,38 +447,9 @@ jobs:
}
const method = process.env.MERGE_METHOD || 'merge';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
let merged = false;
for (let attempt = 1; attempt <= 3; attempt++) {
try {
await github.rest.pulls.merge({
owner, repo, pull_number: prNumber, merge_method: method,
});
merged = true;
break;
} catch (e) {
const msg = (e.message || '').toLowerCase();
if (attempt < 3 && msg.includes('base branch was modified')) {
core.info(`Merge attempt ${attempt} failed (base moved), retrying in 8s...`);
await sleep(8000);
const recheck = await mergeGate.evaluatePipelineQuiescent(
github, owner, repo, prNumber, core, { forMergeStep: true }
);
if (!recheck.ready) {
core.info(`Skip merge retry: ${recheck.reasons.join(', ')}`);
await removeLabels();
return;
}
continue;
}
throw e;
}
}
if (!merged) {
core.info('Skip merge: exhausted retries');
await removeLabels();
return;
}
await github.rest.pulls.merge({
owner, repo, pull_number: prNumber, merge_method: method,
});
await github.rest.issues.addLabels({
owner, repo, issue_number: prNumber, labels: ['auto-merged-by-gate'],
+17 -93
View File
@@ -49,14 +49,12 @@ jobs:
// --- Label triage ---
const labels = [];
// Kind labels (bug before "how to" — bug reports often include "## How to fix")
// Kind labels
if (content.includes('feature request') || content.includes('[feature')) {
labels.push('enhancement');
} else if (!title.includes('[question]') && (content.includes('bug') || content.includes('error') || content.includes('crash') || content.includes('traceback'))) {
} else if (content.includes('bug') || content.includes('error') || content.includes('crash') || content.includes('traceback')) {
labels.push('bug');
} else if (title.includes('[question]') || content.includes('how do i') || (content.includes('how to') && !content.includes('how to fix'))) {
labels.push('question');
} else if (content.includes('question')) {
} else if (content.includes('question') || content.includes('how do i') || content.includes('how to')) {
labels.push('question');
}
@@ -98,22 +96,13 @@ jobs:
'- Check [existing issues](https://github.com/MervinPraison/PraisonAI/issues) for duplicates',
'- Review the [documentation](https://docs.praison.ai) for related guides',
'',
'_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.'
'_A maintainer can trigger deeper analysis by commenting `@claude` on this issue._'
].join('\n')
});
}
// --- Auto-trigger Claude for 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);
if (autoClaude) {
// --- For owner: add 'claude' label to trigger the code-fix job ---
if (isOwner) {
await github.rest.issues.addLabels({
issue_number: issue.number,
owner: context.repo.owner,
@@ -123,43 +112,27 @@ jobs:
}
# ================================================================
# Job 2: Full Claude code-fix
# Job 2: Full Claude code-fix (owner/collaborator only)
# Triggers on: labeled 'claude', @claude comments, assignments
# ================================================================
claude-response:
# Allow: human users, github-actions[bot] (FINAL @claude / CI-fix comments)
# Allow: praisonai-triage-agent[bot] only for issues labeled 'claude' (auto-triage)
# Block: praisonai-triage-agent on issue_comment (merge-gate scan mentions @claude)
# Allow: human users, github-actions[bot] (auto-comments/labels)
# Block: dependabot, cursor, renovate, other bots
# Skip 'opened' events — triage job handles those above
if: |
github.event.action != 'opened' &&
(github.event.action != 'labeled' || github.event.label.name == 'claude') &&
(github.event_name != 'issue_comment' || (
contains(github.event.comment.body, '@claude') &&
!contains(github.event.comment.body, 'Merge gate scan') &&
!contains(github.event.comment.body, 'MERGE_GATE_VERDICT') &&
!contains(github.event.comment.body, 'Merged by **Claude PR merge gate**') &&
(
!contains(github.actor, '[bot]') ||
contains(github.event.comment.body, 'FINAL architecture reviewer') ||
contains(github.event.comment.body, 'lead engineer') ||
contains(github.event.comment.body, 'merge conflict') ||
contains(github.event.comment.body, 'CI failed on HEAD')
)
)) &&
(github.event_name != 'issue_comment' || contains(github.event.comment.body, '@claude')) &&
(github.event_name != 'pull_request_review' || contains(github.event.review.body, '@claude')) &&
(github.event_name != 'pull_request_review_comment' || contains(github.event.comment.body, '@claude')) &&
(
!contains(github.actor, '[bot]') ||
github.actor == 'github-actions[bot]' ||
(github.event_name == 'issues' && github.event.action == 'labeled' && github.event.label.name == 'claude' && github.actor == 'praisonai-triage-agent[bot]')
github.actor == 'praisonai-triage-agent[bot]'
) &&
github.actor != 'dependabot[bot]' &&
github.actor != 'cursor[bot]' &&
github.actor != 'renovate[bot]'
concurrency:
group: claude-response-${{ github.repository }}-${{ github.event.pull_request.number || github.event.issue.number || github.run_id }}
cancel-in-progress: false
runs-on: ubuntu-latest
permissions:
contents: write
@@ -294,21 +267,18 @@ 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.
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.
SCOPE: Focus ONLY on Python packages (praisonaiagents, praisonai). Do NOT modify or touch praisonai-rust or praisonai-ts packages.
STEP 2 — ARCHITECTURE VALIDATION & ROUTING (MANDATORY before writing code):
Before implementing anything, answer these questions:
- CORE vs WRAPPER vs TOOLS vs DOCS vs PLUGINS ROUTING:
1. Core SDK (praisonaiagents/): Only core protocols, base classes, decorators. No heavy implementations.
2. Wrapper (praisonai/): CLI, heavy implementations, optional dependencies.
3. Tools → MervinPraison/PraisonAI-Tools: agent-callable integrations at runtime (Composio, Slack, API/DB connectors). Do NOT implement in praisonaiagents/ — use STEP 3-ALT.
3. Tools (PraisonAI-Tools): Capabilities Agents actively call during execution (e.g. SurrealDB tool, Slack tool).
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/).
5. Plugins (PraisonAI-Plugins): Framework lifecycle extensions replacing systemic behavior (e.g. tracing, logging, metrics, hooks, guardrails).
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.
- 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.
- Does it add new dependencies? Only optional deps allowed, must be lazy-imported.
@@ -338,11 +308,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
@@ -355,8 +323,6 @@ jobs:
Bash(conda:*)
Bash(pytest:*)
Bash(gh:*)
Bash(npm:*)
Bash(node:*)
Bash(python -m pytest:*)
Bash(python -m pip:*)
Bash(poetry:*)
@@ -376,10 +342,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'));
@@ -387,44 +352,3 @@ jobs:
await chain.kickReviewChainForIssue(
github, context.repo.owner, context.repo.repo, issueNumber, core
);
- name: Sync pipeline status labels for PR
if: steps.check_fork.outputs.is_pr == 'true'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const path = require('path');
const ps = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/pipeline-status.js'));
const prNumber = parseInt('${{ steps.check_fork.outputs.pr_number }}', 10);
await ps.ensurePipelineLabels(github, context.repo.owner, context.repo.repo, core);
await ps.syncPipelineLabels(github, context.repo.owner, context.repo.repo, prNumber, core);
- name: Dispatch merge gate when PR pipeline ready
if: steps.check_fork.outputs.is_pr == 'true'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const path = require('path');
const mergeGate = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/merge-gate.js'));
const owner = context.repo.owner;
const repo = context.repo.repo;
const prNumber = parseInt('${{ steps.check_fork.outputs.pr_number }}', 10);
const check = await mergeGate.evaluatePipelineQuiescent(
github, owner, repo, prNumber, core, {
skipGlobalClaudeRunCheck: true,
skipRecentClaudeCooldown: true,
}
);
if (!check.ready) {
core.info(`PR #${prNumber} not ready for merge gate: ${check.reasons.join(', ')}`);
return;
}
await github.rest.repos.createDispatchEvent({
owner,
repo,
event_type: 'claude-merge-gate',
client_payload: { pr_number: prNumber },
});
core.info(`Dispatched merge gate for PR #${prNumber}`);
+22 -47
View File
@@ -1,7 +1,7 @@
name: Deploy install scripts to praison.ai
# Production praison.ai runs on Hestia (migration-target), not Azure AKS.
# Uses scoped deploy user (praison-deploy) — not root.
# When install.sh or install.ps1 change on main, trigger praison.ai CI which
# syncs scripts at build time and deploys to Azure AKS (existing azure-pipeline.yaml).
on:
push:
@@ -19,59 +19,34 @@ permissions:
contents: read
jobs:
deploy-to-hestia:
trigger-website-deploy:
runs-on: ubuntu-latest
environment: production-hestia
steps:
- uses: actions/checkout@v4
with:
ref: main
persist-credentials: false
- name: Sync install scripts to Hestia
- name: Trigger praison.ai CI
env:
SSH_PRIVATE_KEY: ${{ secrets.MIGRATION_TARGET_SSH_KEY }}
HESTIA_HOST: ${{ secrets.MIGRATION_TARGET_HOST }}
HESTIA_USER: ${{ secrets.MIGRATION_TARGET_USER }}
HESTIA_KNOWN_HOSTS: ${{ secrets.MIGRATION_TARGET_KNOWN_HOSTS }}
GH_TOKEN: ${{ secrets.SECRETS_ADMIN_PAT }}
SOURCE_SHA: ${{ github.sha }}
run: |
set -euo pipefail
for var in SSH_PRIVATE_KEY HESTIA_HOST HESTIA_USER; do
if [ -z "${!var:-}" ]; then
echo "Missing secret for deploy (need MIGRATION_TARGET_SSH_KEY, MIGRATION_TARGET_HOST, MIGRATION_TARGET_USER)"
exit 1
fi
done
REMOTE_WEB="/home/hestiaadmin/web/praison.ai/pa/web"
SSH_OPTS="-o BatchMode=yes -o ConnectTimeout=15 -o StrictHostKeyChecking=yes"
mkdir -p ~/.ssh
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
trap 'rm -f ~/.ssh/id_ed25519' EXIT
# Pin host identity via an out-of-band secret (no TOFU/ssh-keyscan).
if [ -z "${HESTIA_KNOWN_HOSTS:-}" ]; then
echo "Missing secret MIGRATION_TARGET_KNOWN_HOSTS (pinned host key). Refusing to deploy without a verified host identity."
if [ -z "${GH_TOKEN:-}" ]; then
echo "SECRETS_ADMIN_PAT is not configured in this repository"
exit 1
fi
echo "$HESTIA_KNOWN_HOSTS" > ~/.ssh/known_hosts
chmod 644 ~/.ssh/known_hosts
# Upload to temp files, then atomically move both into place in a
# single remote step to avoid serving truncated or mixed-commit files.
for f in install.sh install.ps1; do
echo "Syncing $f..."
scp $SSH_OPTS "src/praisonai/scripts/$f" "${HESTIA_USER}@${HESTIA_HOST}:${REMOTE_WEB}/.${f}.tmp"
done
gh workflow run CI \
--repo MervinPraison/praison.ai \
--ref master
ssh $SSH_OPTS "${HESTIA_USER}@${HESTIA_HOST}" \
"mv -f ${REMOTE_WEB}/.install.sh.tmp ${REMOTE_WEB}/install.sh && \
mv -f ${REMOTE_WEB}/.install.ps1.tmp ${REMOTE_WEB}/install.ps1 && \
sudo chown hestiaadmin:hestiaadmin ${REMOTE_WEB}/install.sh ${REMOTE_WEB}/install.ps1 && \
sudo chmod 644 ${REMOTE_WEB}/install.sh ${REMOTE_WEB}/install.ps1"
echo "Triggered praison.ai CI for PraisonAI commit ${SOURCE_SHA}"
echo "Monitor: https://github.com/MervinPraison/praison.ai/actions/workflows/azure-pipeline.yaml"
echo "Deployed install scripts for commit ${SOURCE_SHA}"
echo "Verify: curl -fsSL https://praison.ai/install.ps1 | head -5"
sleep 10
RUN_URL=$(gh run list \
--repo MervinPraison/praison.ai \
--workflow CI \
--limit 1 \
--json url,status,createdAt \
--jq '.[0].url' 2>/dev/null || true)
if [ -n "${RUN_URL}" ]; then
echo "Latest praison.ai CI run: ${RUN_URL}"
fi
-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
+14 -21
View File
@@ -7,18 +7,12 @@ on:
- 'src/praisonai/praisonai/integration/**'
- 'src/praisonai/tests/integration/test_aiui_*'
- 'src/praisonai/pyproject.toml'
- 'src/praisonai-code/**'
- '.github/workflows/integration-cross-repo.yml'
- '.github/actions/install-monorepo-packages/**'
pull_request:
branches: [ main ]
paths:
- 'src/praisonai/praisonai/integration/**'
- 'src/praisonai/tests/integration/test_aiui_*'
- 'src/praisonai/pyproject.toml'
- 'src/praisonai-code/**'
- '.github/workflows/integration-cross-repo.yml'
- '.github/actions/install-monorepo-packages/**'
workflow_dispatch:
jobs:
@@ -46,16 +40,16 @@ jobs:
python-version: '3.11'
- name: Install dependencies
run: python -m pip install --upgrade pip
- uses: PraisonAI/.github/actions/install-monorepo-packages
with:
repo-root: PraisonAI/
wrapper-extras: ".[ui,dev]"
- name: Install PraisonAIUI
run: |
cd PraisonAIUI
python -m pip install --upgrade pip
# Install PraisonAI packages in development mode
cd PraisonAI/src/praisonai-agents
pip install -e .
cd ../praisonai
pip install -e ".[ui,dev]"
# Install PraisonAIUI in development mode
cd ../../../PraisonAIUI
pip install -e .
pip install pytest pytest-cov
- name: Verify versions
run: |
@@ -143,14 +137,13 @@ jobs:
python-version: '3.11'
- name: Install dependencies
run: python -m pip install --upgrade pip
- uses: PraisonAI/.github/actions/install-monorepo-packages
with:
repo-root: PraisonAI/
wrapper-extras: ".[ui,dev]"
- name: Install PraisonAIUI
run: |
cd PraisonAIUI
python -m pip install --upgrade pip
cd PraisonAI/src/praisonai-agents
pip install -e .
cd ../praisonai
pip install -e ".[ui,dev]"
cd ../../../PraisonAIUI
pip install -e .
- name: Run agentic integration test
+1 -2
View File
@@ -12,7 +12,7 @@ on:
- cron: '0 */6 * * *'
concurrency:
group: merge-conflict-scan-${{ github.repository }}-${{ github.event.pull_request.number || github.run_id }}
group: merge-conflict-scan-${{ github.repository }}
cancel-in-progress: true
env:
@@ -44,7 +44,6 @@ jobs:
'run targeted tests, and force-push with `--force-with-lease`.',
'Comment which files you resolved.',
'Do not bloat the Agent class with additional params — only if absolutely required; we already support many params.',
'Keep the resolution minimal — the aim is a lightweight and powerful package; do not introduce scope creep while resolving conflicts.',
].join(' ');
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
-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 }}
+73 -91
View File
@@ -1,12 +1,11 @@
name: Nightly Release Gate
# Preflight before PyPI Release (patch bump).
# Triggers: Core Tests success on main (workflow_run) or cron every 3 days at 00:00 UTC.
# Dedupe: max one successful patch release every 3 days (see release-gate.js).
# Preflight checks before dispatching PyPI Release (patch bump).
# Runs nightly at 00:00 UTC (midnight).
on:
schedule:
- cron: '0 0 */3 * *'
- cron: '0 0 * * *'
workflow_dispatch:
inputs:
dry_run:
@@ -14,13 +13,6 @@ on:
required: false
default: false
type: boolean
workflow_run:
workflows:
- Core Tests
types:
- completed
branches:
- main
permissions:
contents: read
@@ -32,91 +24,83 @@ concurrency:
jobs:
preflight:
if: |
github.event_name == 'workflow_dispatch' ||
github.event_name == 'schedule' ||
(
github.event_name == 'workflow_run' &&
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.head_branch == 'main' &&
github.event.workflow_run.event == 'push' &&
!startsWith(github.event.workflow_run.head_commit.message, 'Release v') &&
!startsWith(github.event.workflow_run.head_commit.message, 'chore(release):')
)
if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
steps:
- name: Resolve trigger context
id: ctx
run: |
if [ "${{ github.event_name }}" = "workflow_run" ]; then
echo "checkout_sha=${{ github.event.workflow_run.head_sha }}" >> "$GITHUB_OUTPUT"
echo "source=ci" >> "$GITHUB_OUTPUT"
echo "is_ci=true" >> "$GITHUB_OUTPUT"
else
echo "checkout_sha=main" >> "$GITHUB_OUTPUT"
echo "source=nightly" >> "$GITHUB_OUTPUT"
echo "is_ci=false" >> "$GITHUB_OUTPUT"
fi
- name: Checkout
- name: Checkout main
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ steps.ctx.outputs.checkout_sha }}
ref: main
fetch-depth: 0
- name: Fetch main and tags
run: git fetch --tags origin main
# Guards the gate logic itself — no workflow ran these selftests before.
- name: Selftest release-gate script
run: node .github/scripts/release-gate-selftest.js
- name: Preflight — path changes, CI SHA, dedupe
- name: Preflight — path changes and CI green
id: preflight
uses: actions/github-script@v7
env:
HEAD_SHA: ${{ steps.ctx.outputs.checkout_sha }}
IS_CI: ${{ steps.ctx.outputs.is_ci }}
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const path = require('path');
const rg = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/release-gate.js'));
const owner = context.repo.owner;
const repo = context.repo.repo;
const headSha = process.env.HEAD_SHA;
const isCi = process.env.IS_CI === 'true';
run: |
set -euo pipefail
const result = await rg.evaluateReleasePreflight(
github, owner, repo,
{ headSha, isCiTrigger: isCi, bump: 'patch' },
core
);
git fetch --tags origin
core.setOutput('should_release', result.ready ? 'true' : 'false');
core.setOutput('reason', result.reasons.join('; '));
core.setOutput('head_sha', result.headSha || headSha);
core.setOutput('last_tag', result.lastTag || '');
core.setOutput('target_agents', result.targetAgents || '');
core.setOutput('target_code', result.targetCode || '');
core.setOutput('target_wrapper', result.targetWrapper || '');
LAST_TAG=$(git describe --tags --match 'v*' --abbrev=0 2>/dev/null || true)
if [ -z "$LAST_TAG" ]; then
echo "No v* tag found — skip automated release (first release must be manual)"
echo "should_release=false" >> "$GITHUB_OUTPUT"
echo "reason=no_tag" >> "$GITHUB_OUTPUT"
exit 0
fi
core.summary.addRaw('## Release gate preflight\n');
core.summary.addRaw(`Ready: ${result.ready}\n`);
core.summary.addRaw(`Reason: ${result.reasons.join('; ')}\n`);
if (result.targetAgents) {
core.summary.addRaw(
`Targets: praisonaiagents==${result.targetAgents}, `
+ `praisonai-code==${result.targetCode}, praisonai==${result.targetWrapper}\n`
);
}
await core.summary.write();
CHANGED=$(git diff --name-only "${LAST_TAG}" HEAD -- \
src/praisonai \
src/praisonai-agents || true)
if (!result.ready) {
core.info(`Skip release: ${result.reasons.join('; ')}`);
}
if [ -z "$CHANGED" ]; then
echo "No changes in src/praisonai or src/praisonai-agents since ${LAST_TAG} — skip"
echo "should_release=false" >> "$GITHUB_OUTPUT"
echo "reason=no_path_changes" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "Changes since ${LAST_TAG}:"
echo "$CHANGED"
HEAD_SHA=$(git rev-parse HEAD)
echo "main HEAD: ${HEAD_SHA}"
GREEN_SHA=$(gh run list \
--repo "${GITHUB_REPOSITORY}" \
--workflow "Core Tests" \
--branch main \
--status success \
--json headSha \
--jq '.[0].headSha // empty')
if [ "$GREEN_SHA" != "$HEAD_SHA" ]; then
echo "main@${HEAD_SHA} is not covered by a green Core Tests run (last green: ${GREEN_SHA:-none}) — skip"
echo "should_release=false" >> "$GITHUB_OUTPUT"
echo "reason=ci_not_green" >> "$GITHUB_OUTPUT"
exit 0
fi
IN_PROGRESS=$(gh run list \
--repo "${GITHUB_REPOSITORY}" \
--workflow "Core Tests" \
--branch main \
--json headSha,status \
--jq "[.[] | select(.headSha == \"${HEAD_SHA}\" and .status == \"in_progress\")] | length")
if [ "${IN_PROGRESS:-0}" != "0" ]; then
echo "Core Tests still in progress on ${HEAD_SHA} — skip"
echo "should_release=false" >> "$GITHUB_OUTPUT"
echo "reason=ci_in_progress" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "Preflight passed — ready to dispatch patch release"
echo "should_release=true" >> "$GITHUB_OUTPUT"
echo "reason=ready" >> "$GITHUB_OUTPUT"
echo "last_tag=${LAST_TAG}" >> "$GITHUB_OUTPUT"
echo "head_sha=${HEAD_SHA}" >> "$GITHUB_OUTPUT"
- name: Dry run summary
if: inputs.dry_run == true
@@ -124,6 +108,9 @@ jobs:
echo "Dry run — preflight only, no pypi-release dispatch."
echo "should_release=${{ steps.preflight.outputs.should_release }}"
echo "reason=${{ steps.preflight.outputs.reason }}"
if [ "${{ steps.preflight.outputs.should_release }}" = "true" ]; then
echo "Would dispatch: pypi-release.yml bump=patch source=nightly"
fi
- name: Dispatch PyPI Release (patch)
if: steps.preflight.outputs.should_release == 'true' && inputs.dry_run != true
@@ -133,12 +120,7 @@ jobs:
--repo "${GITHUB_REPOSITORY}" \
--ref main \
-f bump=patch \
-f source=${{ steps.ctx.outputs.source }} \
-f trigger_sha=${{ steps.preflight.outputs.head_sha }} \
-f source=nightly \
-f dry_run=false
echo "Dispatched pypi-release.yml (patch, source=${{ steps.ctx.outputs.source }})"
if [ "${{ steps.ctx.outputs.source }}" = "ci" ]; then
echo "Uses pypi-auto environment (no manual approval)."
else
echo "Uses pypi-auto environment for nightly patch releases."
fi
echo "Dispatched pypi-release.yml (patch, source=nightly)"
echo "Approve the pypi environment when prompted."
-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,42 +0,0 @@
name: Pipeline Status Sync
# Sync pipeline/* stage and blocker labels on open internal PRs.
on:
schedule:
- cron: '*/10 * * * *'
workflow_dispatch:
permissions:
pull-requests: read
issues: write
actions: write
id-token: write
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.CLAUDE_APP_ID }}
private-key: ${{ secrets.CLAUDE_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
- name: Sync pipeline labels on open PRs
uses: actions/github-script@v7
with:
github-token: ${{ steps.app-token.outputs.token }}
script: |
const path = require('path');
const ps = require(path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/pipeline-status.js'));
await ps.syncOpenPullRequests(
github, context.repo.owner, context.repo.repo,
{ maxPrs: 30, dispatchMergeGate: true }, core
);
+4 -6
View File
@@ -49,12 +49,10 @@ jobs:
git config --local user.name "PraisonAI Bot"
- name: Install PraisonAI
run: pip install uv
- uses: ./.github/actions/install-monorepo-packages
with:
use-uv: "true"
agents-extras: ".[all]"
wrapper-extras: ".[all]"
run: |
pip install uv
cd src/praisonai-agents && uv pip install --system -e ".[all]"
cd ../praisonai && uv pip install --system -e ".[all]"
- name: Run PraisonAI Issue Triage
env:
+10 -14
View File
@@ -16,20 +16,16 @@ 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' ||
(github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
contains(github.event.comment.body, '@praisonai') &&
github.actor != 'github-actions[bot]' &&
github.event.comment.user.type != 'Bot' &&
(github.event.comment.author_association == 'OWNER' ||
github.event.comment.author_association == 'MEMBER' ||
github.event.comment.author_association == 'COLLABORATOR'))
)
(github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
contains(github.event.comment.body, '@praisonai') &&
github.actor != 'github-actions[bot]' &&
github.event.comment.user.type != 'Bot' &&
(github.event.comment.author_association == 'OWNER' ||
github.event.comment.author_association == 'MEMBER' ||
github.event.comment.author_association == 'COLLABORATOR'))
steps:
- name: Determine checkout ref
-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
File diff suppressed because it is too large. Load diff
+3 -5
View File
@@ -30,12 +30,10 @@ jobs:
echo "PYTHONPATH=${{ github.workspace }}/src/praisonai-agents:$PYTHONPATH" >> $GITHUB_ENV
- name: Install dependencies
run: python -m pip install --upgrade pip
- uses: ./.github/actions/install-monorepo-packages
with:
wrapper-extras: ".[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
- name: Install test tools
run: |
cd src/praisonai
python -m pip install --upgrade pip
python -m pip install ."[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
python -m pip install duckduckgo_search pytest pytest-asyncio pytest-cov
python -m pip install "praisonaiagents[knowledge]"
+3 -13
View File
@@ -1,13 +1,7 @@
name: Sync Claude Secrets
name: Sync Claude Secrets to PraisonAIUI
on:
workflow_dispatch:
inputs:
target_repo:
description: 'Destination repo (owner/name)'
required: true
default: 'MervinPraison/PraisonAIUI'
type: string
permissions:
contents: read
@@ -16,15 +10,14 @@ jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: Sync Claude secrets
- name: Sync secrets to PraisonAIUI
env:
GH_TOKEN: ${{ secrets.SECRETS_ADMIN_PAT }}
TARGET: ${{ inputs.target_repo }}
TARGET: MervinPraison/PraisonAIUI
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 }}
GH_TOKEN_VALUE: ${{ secrets.GH_TOKEN }}
DOCS_GITHUB_TOKEN: ${{ secrets.SECRETS_ADMIN_PAT }}
run: |
set -euo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
@@ -35,7 +28,4 @@ jobs:
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"
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, 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})."
@@ -1,47 +0,0 @@
name: Terminal-Bench Smoke
# Manual (and optional weekly) smoke run of the `praisonai code` assistant on a
# small verified Terminal-Bench 2.1 subset via the Harbor harness.
# Never runs on PRs — it costs money (LLM calls) and is inherently flaky.
on:
workflow_dispatch:
schedule:
- cron: "0 6 * * 1" # Mondays 06:00 UTC
permissions:
contents: read
jobs:
smoke:
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install Harbor + PraisonAI
run: |
python -m pip install --upgrade pip
pip install harbor
pip install -e src/praisonai-agents -e src/praisonai
- name: Run Terminal-Bench 2.1 smoke subset
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
PYTHONPATH: .
run: |
harbor run -c examples/terminal_bench/job_code_smoke.yaml || true
- name: Upload Harbor results
if: always()
uses: actions/upload-artifact@v4
with:
name: harbor-results
path: |
runs/
**/results.json
if-no-files-found: warn
+3 -4
View File
@@ -53,13 +53,12 @@ jobs:
- name: Install dependencies
run: |
cd src/praisonai
uv pip install --system ."[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
uv pip install --system duckduckgo_search
uv pip install --system pytest pytest-asyncio pytest-cov pytest-benchmark pytest-timeout
# Install knowledge dependencies from praisonai-agents
uv pip install --system "praisonaiagents[knowledge]"
- uses: ./.github/actions/install-monorepo-packages
with:
use-uv: "true"
wrapper-extras: ".[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
- name: Verify environment variables
run: |
+52 -303
View File
@@ -5,321 +5,70 @@ on:
branches: [ main, develop ]
paths:
- 'src/**'
- 'src/praisonai-deploy/infra/**'
- 'src/praisonai-bot/infra/**'
- 'scripts/check_helm_charts.sh'
- 'pyproject.toml'
- '.github/workflows/test-core.yml'
- '.github/actions/install-monorepo-packages/**'
pull_request:
branches: [ main, develop ]
paths:
- 'src/**'
- 'src/praisonai-deploy/infra/**'
- 'src/praisonai-bot/infra/**'
- 'scripts/check_helm_charts.sh'
- 'pyproject.toml'
- '.github/workflows/test-core.yml'
- '.github/actions/install-monorepo-packages/**'
workflow_dispatch:
permissions:
contents: read
env:
PYTHONPATH: ${{ github.workspace }}/src/praisonai-agents
PRAISONAI_ALLOW_NETWORK: '0'
PRAISONAI_LIVE_TESTS: '0'
PRAISONAI_TEST_TIER: main
jobs:
test-core-collect:
name: test-core-collect
runs-on: ubuntu-latest
timeout-minutes: 5
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: Install UV
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Install dependencies
run: |
cd src/praisonai
uv pip install --system duckduckgo_search
uv pip install --system pytest pytest-asyncio pytest-timeout pytest-xdist
uv pip install --system "praisonaiagents[knowledge]"
- uses: ./.github/actions/install-monorepo-packages
with:
use-uv: "true"
wrapper-extras: ".[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
- name: Set environment variables
run: |
echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY || 'sk-test-key-for-github-actions-testing-only-not-real' }}" >> $GITHUB_ENV
echo "OPENAI_API_BASE=${{ secrets.OPENAI_API_BASE || 'https://api.openai.com/v1' }}" >> $GITHUB_ENV
echo "OPENAI_MODEL_NAME=gpt-4o-mini" >> $GITHUB_ENV
echo "PYTHONPATH=${{ github.workspace }}/src/praisonai-agents:$PYTHONPATH" >> $GITHUB_ENV
- name: Verify unit test collection
shell: bash
run: |
set -euo pipefail
cd src/praisonai
python -m pytest tests/unit/ \
-m "not provider_anthropic and not provider_google and not provider_ollama and not provider_grok_xai and not provider_groq and not provider_cohere" \
--ignore=tests/fixtures \
--collect-only -q --disable-warnings
test-core-unit:
name: test-core (${{ matrix.shard }})
needs: test-core-collect
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
include:
- shard: cli
paths: >-
tests/unit/cli/
tests/unit/tui/
extra_ignore: --ignore=tests/unit/cli/test_message_queue.py
- shard: bots-gateway
workdir: src/praisonai-bot
paths: >-
tests/unit/bots/
tests/unit/gateway/
tests/unit/cli/
tests/unit/daemon/
extra_ignore: ""
pythonpath: >-
${{ github.workspace }}/src/praisonai-agents:${{ github.workspace }}/src/praisonai-code
- 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/
extra_ignore: ""
pythonpath: >-
${{ github.workspace }}/src/praisonai-agents:${{ github.workspace }}/src/praisonai-code
- shard: mcp
workdir: src/praisonai-mcp
paths: >-
tests/mcp_server/
extra_ignore: ""
pythonpath: >-
${{ github.workspace }}/src/praisonai-agents
- shard: sandbox
workdir: src/praisonai-sandbox
paths: >-
tests/
extra_ignore: ""
pythonpath: >-
${{ github.workspace }}/src/praisonai-agents
- shard: deploy
workdir: src/praisonai-deploy
paths: >-
tests/
extra_ignore: ""
pythonpath: >-
${{ github.workspace }}/src/praisonai-agents
- shard: subdirs
paths: >-
tests/unit/scheduler/
tests/unit/integrations/
tests/unit/mcp/
tests/unit/knowledge/
tests/unit/llm/
tests/unit/doctor/
tests/unit/code/
tests/unit/acp/
tests/unit/agent/
tests/unit/tools/
tests/unit/persistence/
tests/unit/security/
tests/unit/recipe/
tests/unit/storage/
tests/unit/rag/
tests/unit/replay/
tests/unit/escalation/
tests/unit/orchestration/
tests/unit/dev/
tests/unit/_dev/
tests/unit/context/
tests/unit/adapters/
tests/unit/templates/
tests/unit/suite_runner/
tests/unit/standardise/
extra_ignore: ""
- shard: root
paths: ""
extra_ignore: ""
root_glob: "true"
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: Install UV
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Install dependencies
run: |
cd src/praisonai
uv pip install --system duckduckgo_search
uv pip install --system pytest pytest-asyncio pytest-timeout pytest-xdist
uv pip install --system "praisonaiagents[knowledge]"
- uses: ./.github/actions/install-monorepo-packages
with:
use-uv: "true"
wrapper-extras: ".[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
- name: Set environment variables
run: |
echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY || 'sk-test-key-for-github-actions-testing-only-not-real' }}" >> $GITHUB_ENV
echo "OPENAI_API_BASE=${{ secrets.OPENAI_API_BASE || 'https://api.openai.com/v1' }}" >> $GITHUB_ENV
echo "OPENAI_MODEL_NAME=gpt-4o-mini" >> $GITHUB_ENV
echo "PYTHONPATH=${{ github.workspace }}/src/praisonai-agents:$PYTHONPATH" >> $GITHUB_ENV
- name: Run unit shard
shell: bash
run: |
set -euo pipefail
WORKDIR="${{ matrix.workdir || 'src/praisonai' }}"
if [ "$WORKDIR" = "src/praisonai-sandbox" ]; then
bash "$GITHUB_WORKSPACE/scripts/check_c13_sandbox_imports.sh"
fi
if [ "$WORKDIR" = "src/praisonai-deploy" ]; then
bash "$GITHUB_WORKSPACE/scripts/check_c14_deploy_imports.sh"
fi
if [ "$WORKDIR" = "src/praisonai-mcp" ]; then
bash "$GITHUB_WORKSPACE/scripts/check_c12_mcp_imports.sh"
fi
cd "$WORKDIR"
if [ -n "${{ matrix.pythonpath || '' }}" ]; then
export PYTHONPATH="${{ matrix.pythonpath }}:${PYTHONPATH:-}"
fi
PYTEST_ARGS=(
-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"
-q --tb=line --disable-warnings
--timeout=60 -n 2 --dist loadfile
--ignore=tests/fixtures
--ignore=tests/e2e
--ignore=tests/live
--ignore=tests/unit/test_langflow_components.py
)
if [ "${{ matrix.root_glob || 'false' }}" = "true" ]; then
shopt -s nullglob
ROOT_TESTS=(tests/unit/test_*.py)
shopt -u nullglob
EXCLUDE=(tests/unit/test_langflow_components.py)
FILTERED=()
for t in "${ROOT_TESTS[@]}"; do
skip=false
for e in "${EXCLUDE[@]}"; do
if [ "$t" = "$e" ]; then skip=true; break; fi
done
if [ "$skip" = false ]; then FILTERED+=("$t"); fi
done
if [ ${#FILTERED[@]} -eq 0 ]; then
echo "No root unit tests matched"
exit 0
fi
python -m pytest "${FILTERED[@]}" "${PYTEST_ARGS[@]}"
else
python -m pytest ${{ matrix.paths }} "${PYTEST_ARGS[@]}" ${{ matrix.extra_ignore }}
fi
test-core-integration:
name: test-core-integration
needs: test-core-unit
runs-on: ubuntu-latest
timeout-minutes: 25
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: Install UV
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Install dependencies
run: |
uv pip install --system duckduckgo_search
uv pip install --system pytest pytest-asyncio pytest-timeout
uv pip install --system "praisonaiagents[knowledge]"
- uses: ./.github/actions/install-monorepo-packages
with:
use-uv: "true"
wrapper-extras: ".[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
- name: Set environment variables
run: |
echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY || 'sk-test-key-for-github-actions-testing-only-not-real' }}" >> $GITHUB_ENV
echo "OPENAI_API_BASE=${{ secrets.OPENAI_API_BASE || 'https://api.openai.com/v1' }}" >> $GITHUB_ENV
echo "OPENAI_MODEL_NAME=gpt-4o-mini" >> $GITHUB_ENV
echo "PYTHONPATH=${{ github.workspace }}/src/praisonai-agents:$PYTHONPATH" >> $GITHUB_ENV
- name: Run integration tests
shell: bash
run: |
set -euo pipefail
cd src/praisonai
python -m pytest tests/integration/ \
-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" \
-q --tb=line --disable-warnings \
--timeout=120 \
--ignore=tests/fixtures \
--ignore=tests/e2e \
--ignore=tests/live \
--ignore=tests/integration/test_tracker_complex_tasks.py \
--ignore=tests/integration/test_serve_integration.py
helm-charts:
name: helm-charts
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Setup Helm
uses: azure/setup-helm@v3
with:
version: v3.14.4
- name: Lint and template Helm charts
shell: bash
run: |
set -euo pipefail
bash "$GITHUB_WORKSPACE/scripts/check_helm_charts.sh"
test-core:
name: test-core
needs: [test-core-collect, test-core-unit, test-core-integration, helm-charts]
if: always()
runs-on: ubuntu-latest
timeout-minutes: 5
timeout-minutes: 20
steps:
- name: Report Core Tests result
shell: bash
- 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: Install UV
run: |
set -euo pipefail
collect='${{ needs.test-core-collect.result }}'
unit='${{ needs.test-core-unit.result }}'
integration='${{ needs.test-core-integration.result }}'
helm='${{ needs.helm-charts.result }}'
echo "collect=$collect unit=$unit integration=$integration helm=$helm"
if [ "$collect" != "success" ] || [ "$unit" != "success" ] || [ "$integration" != "success" ] || [ "$helm" != "success" ]; then
echo "Core Tests failed"
exit 1
fi
echo "Core Tests passed"
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Install dependencies
run: |
cd src/praisonai
uv pip install --system ."[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
uv pip install --system duckduckgo_search
uv pip install --system pytest pytest-asyncio pytest-cov pytest-timeout
uv pip install --system "praisonaiagents[knowledge]"
- name: Set environment variables
run: |
echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY || 'sk-test-key-for-github-actions-testing-only-not-real' }}" >> $GITHUB_ENV
echo "OPENAI_API_BASE=${{ secrets.OPENAI_API_BASE || 'https://api.openai.com/v1' }}" >> $GITHUB_ENV
echo "OPENAI_MODEL_NAME=${{ secrets.OPENAI_MODEL_NAME || 'gpt-5-nano' }}" >> $GITHUB_ENV
echo "PYTHONPATH=${{ github.workspace }}/src/praisonai-agents:$PYTHONPATH" >> $GITHUB_ENV
- name: Run Unit Tests
run: |
cd src/praisonai && python -m pytest tests/unit/ -v --tb=short --disable-warnings \
--cov=praisonai --cov-report=xml --cov-branch \
--timeout=60 || echo "Some unit tests failed"
continue-on-error: true
- name: Run Integration Tests
run: |
cd src/praisonai && python -m pytest tests/integration/ -v --tb=short --disable-warnings \
--timeout=60 \
--ignore=tests/integration/test_tracker_complex_tasks.py \
--ignore=tests/integration/test_serve_integration.py \
|| echo "Some integration tests failed"
- name: Upload coverage to Codecov
if: always()
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN || '' }}
files: src/praisonai/coverage.xml
flags: core-tests
fail_ci_if_error: false
+6 -8
View File
@@ -29,12 +29,11 @@ jobs:
- name: Install dependencies
run: |
cd src/praisonai
uv pip install --system ."[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
uv pip install --system duckduckgo_search
# Install knowledge dependencies from praisonai-agents
uv pip install --system "praisonaiagents[knowledge]"
- uses: ./.github/actions/install-monorepo-packages
with:
use-uv: "true"
wrapper-extras: ".[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
- name: Set environment variables
run: |
@@ -96,12 +95,11 @@ jobs:
- name: Install dependencies
run: |
cd src/praisonai
uv pip install --system ."[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
uv pip install --system pytest pytest-benchmark
# Install knowledge dependencies from praisonai-agents
uv pip install --system "praisonaiagents[knowledge]"
- uses: ./.github/actions/install-monorepo-packages
with:
use-uv: "true"
wrapper-extras: ".[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
- name: Set environment variables
run: |
+2 -4
View File
@@ -49,12 +49,10 @@ jobs:
- name: Install dependencies
run: |
cd src/praisonai
uv pip install --system ."[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
uv pip install --system duckduckgo_search
uv pip install --system pytest pytest-asyncio pytest-cov pytest-timeout
- uses: ./.github/actions/install-monorepo-packages
with:
use-uv: "true"
wrapper-extras: ".[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
- name: Set environment variables
run: |
+46 -463
View File
@@ -42,7 +42,7 @@ jobs:
# ============================================
smoke:
runs-on: ubuntu-latest
timeout-minutes: 12
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
with:
@@ -64,247 +64,28 @@ jobs:
- name: Install minimal dependencies
run: |
pip install --upgrade pip
# Install praisonai-agents with all deps (required for tests)
cd src/praisonai-agents
pip install -e ".[all]"
cd ../praisonai
pip install -e ".[chat,code]"
pip install pytest pytest-asyncio pytest-timeout prompt_toolkit
- uses: ./.github/actions/install-monorepo-packages
with:
agents-extras: ".[all]"
wrapper-extras: ".[chat,code]"
- name: Run Smoke Tests
env:
PRAISONAI_TEST_TIER: smoke
PRAISONAI_ALLOW_NETWORK: '0'
OPENAI_MODEL_NAME: gpt-4o-mini
shell: bash
run: |
set -euo pipefail
cd src/praisonai
python -m pytest tests/unit/test_c5_backward_compat.py \
tests/unit/test_c7_1_boundaries.py \
tests/unit/test_c9_backward_compat.py \
tests/unit/test_c9_1_boundaries.py \
tests/unit/test_c11_browser_backward_compat.py \
tests/unit/test_c12_mcp_backward_compat.py \
tests/unit/test_c13_sandbox_backward_compat.py \
tests/unit/test_c14_deploy_backward_compat.py \
-q --tb=line --timeout=30 --maxfail=5
# Run only CLI tests for smoke - fast and critical path
python -m pytest tests/unit/cli/ \
../praisonai-bot/tests/unit/cli/test_onboard_command.py \
../praisonai-bot/tests/unit/cli/test_onboard_non_interactive.py \
-m "not slow and not network" \
-q --tb=line \
--timeout=30 \
--ignore=tests/fixtures \
--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: |
set -euo pipefail
python -m venv /tmp/praisonai-code-standalone
/tmp/praisonai-code-standalone/bin/pip install -q -e src/praisonai-agents -e src/praisonai-code
/tmp/praisonai-code-standalone/bin/python -c "
import praisonai_code
from praisonai_code.cli.configuration.resolver import ConfigResolver
from praisonai_code._version import get_package_version
from praisonai_code.llm.credentials import is_configured
from praisonai_code.llm.catalogue import ModelCatalogue
import toml
assert hasattr(ConfigResolver, '__name__')
assert callable(is_configured)
assert ModelCatalogue().list_models(provider='openai')
import praisonai_code.cli.main
import praisonai_code.cli.app
from praisonai_code.tool_resolver import ToolResolver
assert ToolResolver is not None
print('standalone ok', get_package_version(), 'toml', toml.__version__)
"
/tmp/praisonai-code-standalone/bin/praisonai-code --version
/tmp/praisonai-code-standalone/bin/praisonai-code run --help
cd src/praisonai-code
for f in praisonai_code/cli/main.py praisonai_code/cli/app.py \
praisonai_code/cli/commands/run.py praisonai_code/cli/commands/chat.py \
praisonai_code/cli/commands/code.py; do
if grep -E '^from praisonai\.|^import praisonai' "$f"; then
echo "FAIL: module-level wrapper import in $f"
exit 1
fi
done
echo "C7 hot-path import gate ok"
bash "$GITHUB_WORKSPACE/scripts/check_c7_imports.sh"
# Standalone actions run (no wrapper); default run/chat require praisonai
set +e
RUN_OUT=$(timeout 90 /tmp/praisonai-code-standalone/bin/praisonai-code run --output actions "Say hello in one word" 2>&1)
RUN_CODE=$?
set -e
echo "$RUN_OUT" | tail -20
if echo "$RUN_OUT" | grep -qE 'ImportError|ModuleNotFoundError: No module named .praisonai'; then
echo "FAIL: standalone actions run hit wrapper import error"
exit 1
fi
echo "standalone actions run import gate ok (exit $RUN_CODE)"
set +e
DEFAULT_RUN=$(timeout 15 /tmp/praisonai-code-standalone/bin/praisonai-code run "hi" 2>&1)
set -e
echo "$DEFAULT_RUN" | tail -10
if ! echo "$DEFAULT_RUN" | grep -qi 'install praisonai'; then
echo "FAIL: default run should prompt for praisonai wrapper install"
exit 1
fi
echo "default run wrapper gate ok"
set +e
CHAT_RUN=$(timeout 10 /tmp/praisonai-code-standalone/bin/praisonai-code chat 2>&1)
set -e
echo "$CHAT_RUN" | tail -10
if ! echo "$CHAT_RUN" | grep -qi 'install praisonai'; then
echo "FAIL: chat should prompt for praisonai wrapper install"
exit 1
fi
echo "chat wrapper gate ok"
- name: Standalone praisonai-bot smoke (C9)
run: |
set -euo pipefail
python -m venv /tmp/praisonai-bot-standalone
/tmp/praisonai-bot-standalone/bin/pip install -q -e src/praisonai-agents -e src/praisonai-bot
/tmp/praisonai-bot-standalone/bin/python -c "
from praisonai_bot._version import __version__ as pkg_version
import praisonai_bot
from praisonai_bot.bots import Bot
from praisonai_bot.bots._registry import resolve_adapter
from praisonai_bot.scheduler.executor import ScheduledAgentExecutor
from praisonai_bot.gateway import WebSocketGateway
assert praisonai_bot.__version__ == pkg_version
telegram = resolve_adapter('telegram')
assert telegram.__name__ == 'TelegramBot'
assert telegram.__module__.startswith('praisonai_bot.bots.')
assert ScheduledAgentExecutor.__module__.startswith('praisonai_bot.')
print('bot standalone ok', praisonai_bot.__version__, telegram.__name__)
"
/tmp/praisonai-bot-standalone/bin/praisonai-bot --help
bash "$GITHUB_WORKSPACE/scripts/check_c9_bot_imports.sh"
bash "$GITHUB_WORKSPACE/scripts/check_bot_code_imports.sh"
/tmp/praisonai-bot-standalone/bin/python -c "
import praisonai_bot.cli.commands.bot as bot_cmd
import praisonai_bot.cli.commands.gateway as gw_cmd
print('bot command modules import ok', bot_cmd.app, gw_cmd.app)
"
- name: Standalone praisonai-train smoke (C10)
run: |
set -euo pipefail
python -m venv /tmp/praisonai-train-standalone
/tmp/praisonai-train-standalone/bin/pip install -q -e src/praisonai-agents -e src/praisonai-train
/tmp/praisonai-train-standalone/bin/python -c "
import sys
from praisonai_train._version import __version__ as pkg_version
import praisonai_train
assert praisonai_train.__version__ == pkg_version
trainer_cls = praisonai_train.AgentTrainer
scenario_cls = praisonai_train.TrainingScenario
assert trainer_cls.__module__.startswith('praisonai_train.')
for heavy in ('torch', 'unsloth', 'trl', 'datasets'):
assert heavy not in sys.modules, f'{heavy} imported eagerly'
print('train standalone ok', praisonai_train.__version__, trainer_cls.__name__)
"
/tmp/praisonai-train-standalone/bin/praisonai-train --help
/tmp/praisonai-train-standalone/bin/python -c "
import praisonai_train.cli.commands.train as train_cmd
print('train command module imports ok', train_cmd.app)
"
bash "$GITHUB_WORKSPACE/scripts/check_c10_train_imports.sh"
- name: Standalone praisonai-browser smoke (C11)
run: |
set -euo pipefail
python -m venv /tmp/praisonai-browser-standalone
/tmp/praisonai-browser-standalone/bin/pip install -q -e src/praisonai-agents -e src/praisonai-browser
/tmp/praisonai-browser-standalone/bin/python -c "
import sys
from praisonai_browser._version import __version__ as pkg_version
import praisonai_browser
assert praisonai_browser.__version__ == pkg_version
server_cls = praisonai_browser.BrowserServer
assert server_cls.__name__ == 'BrowserServer'
for heavy in ('fastapi', 'uvicorn', 'playwright'):
assert heavy not in sys.modules, f'{heavy} imported eagerly'
print('browser standalone ok', praisonai_browser.__version__, server_cls.__name__)
"
/tmp/praisonai-browser-standalone/bin/praisonai-browser --help
bash "$GITHUB_WORKSPACE/scripts/check_c11_browser_imports.sh"
python -m pytest src/praisonai-browser/tests/browser/test_protocol_compliance.py -q -m "not network"
- name: Standalone praisonai-mcp smoke (C12)
run: |
set -euo pipefail
python -m venv /tmp/praisonai-mcp-standalone
/tmp/praisonai-mcp-standalone/bin/pip install -q -e src/praisonai-agents -e src/praisonai-mcp
/tmp/praisonai-mcp-standalone/bin/python -c "
import sys
from praisonai_mcp._version import __version__ as pkg_version
import praisonai_mcp
assert praisonai_mcp.__version__ == pkg_version
for heavy in ('fastapi', 'uvicorn'):
assert heavy not in sys.modules, f'{heavy} imported eagerly'
print('mcp standalone ok', praisonai_mcp.__version__)
"
/tmp/praisonai-mcp-standalone/bin/praisonai-mcp --help
bash "$GITHUB_WORKSPACE/scripts/check_c12_mcp_imports.sh"
python -m pytest src/praisonai-mcp/tests/mcp_server -q -m "not network"
- name: Standalone praisonai-sandbox smoke (C13)
run: |
set -euo pipefail
python -m venv /tmp/praisonai-sandbox-standalone
/tmp/praisonai-sandbox-standalone/bin/pip install -q -e src/praisonai-agents -e src/praisonai-sandbox
/tmp/praisonai-sandbox-standalone/bin/python -c "
from praisonai_sandbox._version import __version__ as pkg_version
import praisonai_sandbox
assert praisonai_sandbox.__version__ == pkg_version
for heavy in ('docker', 'modal', 'e2b'):
assert heavy not in __import__('sys').modules
print('sandbox standalone ok', praisonai_sandbox.__version__)
"
/tmp/praisonai-sandbox-standalone/bin/praisonai-sandbox --help
bash "$GITHUB_WORKSPACE/scripts/check_c13_sandbox_imports.sh"
python -m pytest src/praisonai-sandbox/tests -q -m "not network"
- name: Standalone praisonai-deploy smoke (C14)
run: |
set -euo pipefail
python -m venv /tmp/praisonai-deploy-standalone
/tmp/praisonai-deploy-standalone/bin/pip install -q -e src/praisonai-agents -e src/praisonai-deploy
/tmp/praisonai-deploy-standalone/bin/python -c "
from praisonai_deploy._version import __version__ as pkg_version
import praisonai_deploy
assert praisonai_deploy.__version__ == pkg_version
assert 'main' not in __import__('sys').modules
print('deploy standalone ok', praisonai_deploy.__version__)
"
/tmp/praisonai-deploy-standalone/bin/praisonai-deploy --help
bash "$GITHUB_WORKSPACE/scripts/check_c14_deploy_imports.sh"
python -m pytest src/praisonai-deploy/tests -q -m "not network"
- name: Audit hybrid module imports (C8 full stack)
run: |
set -euo pipefail
pip install -q -e src/praisonai-agents -e src/praisonai-code -e src/praisonai-bot -e src/praisonai-train -e src/praisonai-browser -e src/praisonai-mcp -e src/praisonai-sandbox -e src/praisonai-deploy -e src/praisonai
python scripts/audit_hybrid_modules.py
--maxfail=5 \
|| echo "Some smoke tests failed"
- name: Smoke Test Summary
if: always()
@@ -317,7 +98,7 @@ jobs:
main:
needs: smoke
runs-on: ubuntu-latest
timeout-minutes: 30
timeout-minutes: 10
strategy:
matrix:
python-version: ['3.11']
@@ -343,37 +124,36 @@ jobs:
- name: Install dependencies
run: |
pip install --upgrade pip
# Install praisonai-agents with all deps (required for tests)
cd src/praisonai-agents
pip install -e ".[all]"
cd ../praisonai
pip install -e ".[crewai,autogen]"
pip install pytest pytest-asyncio pytest-timeout pytest-cov pytest-xdist
- uses: ./.github/actions/install-monorepo-packages
with:
agents-extras: ".[all]"
wrapper-extras: ".[crewai,autogen,ui,chat]"
- name: Run Main Tests
env:
PRAISONAI_TEST_TIER: main
PRAISONAI_ALLOW_NETWORK: '0'
OPENAI_MODEL_NAME: gpt-4o-mini
OPENAI_API_KEY: 'test-key'
ANTHROPIC_API_KEY: 'test-key'
GOOGLE_API_KEY: 'test-key'
shell: bash
run: |
set -euo pipefail
cd src/praisonai
python -m pytest tests/unit/ \
-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" \
python -m pytest tests/unit/ tests/integration/ \
-m "not provider_anthropic and not provider_google and not provider_ollama and not provider_grok_xai and not provider_groq and not provider_cohere" \
-q --tb=line \
--timeout=60 \
--ignore=tests/fixtures \
--ignore=tests/e2e \
--ignore=tests/live \
--ignore=tests/unit/jobs \
--ignore=tests/unit/test_langflow_components.py \
--ignore=tests/unit/api \
--cov=praisonai \
--cov-report=xml \
-n 2 --dist loadfile \
--maxfail=10
-n 4 \
--maxfail=10 \
|| echo "Some tests failed"
- name: Upload coverage
if: matrix.python-version == '3.11'
@@ -430,12 +210,11 @@ jobs:
if: steps.check-key.outputs.has_key == 'true'
run: |
pip install --upgrade pip
cd src/praisonai-agents
pip install -e ".[all]"
cd ../praisonai
pip install -e ".[openai]"
pip install pytest pytest-asyncio pytest-timeout
- uses: ./.github/actions/install-monorepo-packages
if: steps.check-key.outputs.has_key == 'true'
with:
agents-extras: ".[all]"
wrapper-extras: ".[openai]"
- name: Run OpenAI Live Tests
if: steps.check-key.outputs.has_key == 'true'
@@ -494,12 +273,11 @@ jobs:
if: steps.check-key.outputs.has_key == 'true'
run: |
pip install --upgrade pip
cd src/praisonai-agents
pip install -e ".[all]"
cd ../praisonai
pip install -e ".[anthropic]"
pip install pytest pytest-asyncio pytest-timeout
- uses: ./.github/actions/install-monorepo-packages
if: steps.check-key.outputs.has_key == 'true'
with:
agents-extras: ".[all]"
wrapper-extras: ".[anthropic]"
- name: Run Anthropic Tests
if: steps.check-key.outputs.has_key == 'true'
@@ -554,12 +332,11 @@ jobs:
if: steps.check-key.outputs.has_key == 'true'
run: |
pip install --upgrade pip
cd src/praisonai-agents
pip install -e ".[all]"
cd ../praisonai
pip install -e ".[google]"
pip install pytest pytest-asyncio pytest-timeout
- uses: ./.github/actions/install-monorepo-packages
if: steps.check-key.outputs.has_key == 'true'
with:
agents-extras: ".[all]"
wrapper-extras: ".[google]"
- name: Run Google/Gemini Tests
if: steps.check-key.outputs.has_key == 'true'
@@ -614,11 +391,11 @@ jobs:
if: steps.check-key.outputs.has_key == 'true'
run: |
pip install --upgrade pip
cd src/praisonai-agents
pip install -e ".[all]"
cd ../praisonai
pip install -e .
pip install pytest pytest-asyncio pytest-timeout
- uses: ./.github/actions/install-monorepo-packages
if: steps.check-key.outputs.has_key == 'true'
with:
agents-extras: ".[all]"
- name: Run Groq Tests
if: steps.check-key.outputs.has_key == 'true'
@@ -673,11 +450,11 @@ jobs:
if: steps.check-key.outputs.has_key == 'true'
run: |
pip install --upgrade pip
cd src/praisonai-agents
pip install -e ".[all]"
cd ../praisonai
pip install -e .
pip install pytest pytest-asyncio pytest-timeout
- uses: ./.github/actions/install-monorepo-packages
if: steps.check-key.outputs.has_key == 'true'
with:
agents-extras: ".[all]"
- name: Run xAI/Grok Tests
if: steps.check-key.outputs.has_key == 'true'
@@ -732,12 +509,11 @@ jobs:
if: steps.check-key.outputs.has_key == 'true'
run: |
pip install --upgrade pip
cd src/praisonai-agents
pip install -e ".[all]"
cd ../praisonai
pip install -e ".[cohere]"
pip install pytest pytest-asyncio pytest-timeout
- uses: ./.github/actions/install-monorepo-packages
if: steps.check-key.outputs.has_key == 'true'
with:
agents-extras: ".[all]"
wrapper-extras: ".[cohere]"
- name: Run Cohere Tests
if: steps.check-key.outputs.has_key == 'true'
@@ -759,204 +535,11 @@ 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.
OLLAMA_VERSION: v0.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)
# ============================================
legacy-unit:
if: github.event_name == 'schedule' || github.event.inputs.tier == 'extended' || github.event.inputs.tier == 'nightly'
runs-on: ubuntu-latest
timeout-minutes: 20
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: Install dependencies
run: |
pip install --upgrade pip
pip install pytest pytest-asyncio pytest-timeout
- uses: ./.github/actions/install-monorepo-packages
with:
agents-extras: ".[all]"
wrapper-extras: ".[chat,code,crewai,autogen]"
- name: Run post-C6 legacy unit tests
env:
PRAISONAI_TEST_TIER: extended
PRAISONAI_ALLOW_NETWORK: '0'
PRAISONAI_ALLOW_JOB_WORKFLOWS: 'true'
PRAISONAI_TOOL_SAFETY: 'off'
OPENAI_API_KEY: 'test-key'
run: |
set -euo pipefail
cd src/praisonai
LEGACY_FILES=(
tests/unit/test_warm_runtime.py
tests/unit/test_yaml_cli_backend.py
tests/unit/test_tool_resolver.py
tests/unit/test_serve_unified.py
tests/unit/test_security_fixes_core.py
tests/unit/test_query_rewriter_cli.py
tests/unit/test_kanban_sqlite_store.py
tests/unit/test_job_workflow_agent_steps.py
tests/unit/test_hybrid_workflow.py
../praisonai-bot/tests/unit/test_gateway_session.py
../praisonai-bot/tests/unit/test_gateway_gaps.py
tests/unit/test_discord_approval.py
tests/unit/test_decorator_simple.py
tests/unit/test_db_adapter_tracing_init.py
../praisonai-bot/tests/unit/test_bot_wiring.py
../praisonai-bot/tests/unit/bots/test_enable_shell.py
../praisonai-bot/tests/unit/gateway/test_preflight.py
../praisonai-bot/tests/unit/gateway/test_gateway_doctor.py
../praisonai-bot/tests/unit/test_bot_history.py
../praisonai-bot/tests/unit/test_bot_fixes.py
tests/unit/test_auto_lazy_loading.py
tests/unit/test_async_daemon_deployment.py
tests/unit/test_approval_interactive.py
tests/unit/test_approval_agent_integration.py
tests/unit/test_agents_yaml_validation.py
tests/unit/test_agent_os_wrapper.py
tests/unit/test_lazy_imports.py
tests/unit/test_profiler.py
tests/unit/test_profiler_advanced.py
tests/unit/test_serverless_postgres.py
)
python -m pytest "${LEGACY_FILES[@]}" \
-q --tb=line --timeout=120 --maxfail=10
# ============================================
# TEST SUMMARY
# ============================================
test-summary:
needs: [smoke, main, extended-ollama]
needs: [smoke, main]
if: always()
runs-on: ubuntu-latest
+3 -4
View File
@@ -61,12 +61,11 @@ jobs:
- name: Install dependencies
run: |
cd src/praisonai
uv pip install --system ."[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
uv pip install --system pytest pytest-asyncio pytest-cov
# Install knowledge dependencies from praisonai-agents
uv pip install --system "praisonaiagents[knowledge]"
- uses: ./.github/actions/install-monorepo-packages
with:
use-uv: "true"
wrapper-extras: ".[ui,gradio,api,agentops,google,openai,anthropic,cohere,chat,code,realtime,call,crewai,autogen]"
- name: Set environment variables
run: |
+12 -7
View File
@@ -7,20 +7,18 @@ on:
- 'src/**'
- 'pyproject.toml'
- '.github/workflows/test-windows.yml'
- '.github/actions/install-monorepo-packages/**'
pull_request:
branches: [main, develop]
paths:
- 'src/**'
- 'pyproject.toml'
- '.github/workflows/test-windows.yml'
- '.github/actions/install-monorepo-packages/**'
workflow_dispatch:
jobs:
test-windows:
runs-on: windows-latest
timeout-minutes: 10
timeout-minutes: 5
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_MODEL_NAME: gpt-4o-mini
@@ -35,10 +33,18 @@ jobs:
with:
python-version: "3.11"
- name: Install monorepo packages
run: python -m pip install --upgrade pip
- name: Install praisonaiagents
run: |
python -m pip install --upgrade pip
cd src/praisonai-agents
pip install -e .
shell: bash
- name: Install praisonai wrapper
run: |
cd src/praisonai
pip install -e .
shell: bash
- uses: ./.github/actions/install-monorepo-packages
# ==================== CORE IMPORTS ====================
- name: Test core imports
@@ -176,7 +182,6 @@ jobs:
echo "Testing both entry point methods..."
python -m praisonai --version
praisonai --version
praisonai-bot gateway --help
- name: Test doctor packaging command
run: |
+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 -18
View File
@@ -12,7 +12,6 @@ assets
.DS_Store
.pytest_cache
praisonAI.egg-info
src/*/build/
flagged
test.yaml
db
@@ -23,24 +22,11 @@ other
output
!src/praisonai/praisonai/cli/output/
!src/praisonai-agents/praisonaiagents/output/
!src/praisonai-code/praisonai_code/cli/output/
!src/praisonai-bot/praisonai_bot/cli/output/
!src/praisonai-ts/src/db/
!src/praisonai-ts/src/cli/output/
.files
threads.db
threads.db-journal
# Example run artifacts (generated at runtime, must not be committed)
examples/**/knowledge.db
examples/**/knowledge.db-journal
examples/python/managed-agents/managed_ids.json
examples/python/save_output/blog_post.md
examples/python/save_output/generated/
examples/python/token-metrics/token_metrics_export.json
examples/python/usecases/market_research_results.json
.chainlit
!praisonai/ui/config/.chainlit
@@ -115,7 +101,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/
*.egg-info
+1 -19
View File
@@ -7,22 +7,4 @@ 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.
- 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 |
- Follow protocol-driven design: core protocols in praisonaiagents/, heavy implementations in praisonai/
+38 -194
View File
@@ -1,27 +1,24 @@
# PraisonAI Architecture
> **Last updated:** 2026-07-31 (C14 — nine-package tiered model)
> **Last updated:** 2026-06-24
>
> Strategic architecture document for PraisonAI — a multi-agent AI framework.
> Covers **Python tiered package model (C7.1 + C9 + C10)**, system design, runtime
> architecture, data contracts, reliability, observability, and the road map.
> Covers system design, runtime architecture, data contracts, reliability,
> observability, and the 2-quarter road map.
---
## Table of Contents
1. [Executive Summary](#1-executive-summary)
2. [Python Tiered Package Model (C7.1 + C9 + C10 + C11 + C12 + C13 + C14)](#2-python-tiered-package-model-c71--c9--c10--c11--c12--c13--c14)
3. [System Overview](#3-system-overview)
4. [Layered Architecture](#4-layered-architecture)
5. [Core Data Contracts](#5-core-data-contracts)
6. [Reliability by Design](#6-reliability-by-design)
7. [Observability & Telemetry](#7-observability--telemetry)
8. [Replay & Checkpointing](#8-replay--checkpointing)
9. [Implementation Roadmap](#9-implementation-roadmap)
10. [Success Metrics](#10-success-metrics)
**Related boundary docs:** [`src/praisonai/tests/C7.1_BOUNDARIES.md`](src/praisonai/tests/C7.1_BOUNDARIES.md) · [`src/praisonai/tests/C9.1_BOUNDARIES.md`](src/praisonai/tests/C9.1_BOUNDARIES.md) · [`src/praisonai/tests/PRAISONAI_TRAIN_MANIFEST.md`](src/praisonai/tests/PRAISONAI_TRAIN_MANIFEST.md) · [`src/praisonai/tests/PRAISONAI_BROWSER_MANIFEST.md`](src/praisonai/tests/PRAISONAI_BROWSER_MANIFEST.md) · [`src/praisonai/tests/PRAISONAI_MCP_MANIFEST.md`](src/praisonai/tests/PRAISONAI_MCP_MANIFEST.md) · [`src/praisonai/tests/C7_VERIFICATION.md`](src/praisonai/tests/C7_VERIFICATION.md) · [`src/praisonai/tests/C9_VERIFICATION.md`](src/praisonai/tests/C9_VERIFICATION.md) · [`src/praisonai-agents/AGENTS.md`](src/praisonai-agents/AGENTS.md) §2.4
2. [System Overview](#2-system-overview)
3. [Layered Architecture](#3-layered-architecture)
4. [Core Data Contracts](#4-core-data-contracts)
5. [Reliability by Design](#5-reliability-by-design)
6. [Observability & Telemetry](#6-observability--telemetry)
7. [Replay & Checkpointing](#7-replay--checkpointing)
8. [Implementation Roadmap](#8-implementation-roadmap)
9. [Success Metrics](#9-success-metrics)
---
@@ -51,143 +48,7 @@ Orchestration + Observability** core to unlock adoption and trust:
---
## 2. Python Tiered Package Model (C7.1 + C9 + C10 + C11 + C12 + C13 + C14)
**Release:** v4.6.110+ · `praisonaiagents` · `praisonai-code` · `praisonai-bot` · `praisonai-train` · `praisonai-browser` · `praisonai-mcp` · `praisonai-sandbox` · `praisonai-deploy` · `praisonai`
The Python monorepo publishes nine packages in three tiers with strict dependency
direction. C7 delivered a standalone agentic hot path; C7.1 formalised
code/wrapper ownership; C9 extracted bots, gateway, and channel CLI into
`praisonai-bot`; C10 extracted LLM fine-tuning and agent training into
`praisonai-train`; C11 extracted browser automation into `praisonai-browser`;
C12 extracted the heavy MCP host into `praisonai-mcp`.
C13 extracted sandbox backends into `praisonai-sandbox`.
C14 extracted deployment (API, Docker, cloud) into `praisonai-deploy`.
```mermaid
flowchart TB
subgraph tier1 [Tier 1 — Core SDK]
Agents["praisonaiagents<br/>Agent, tools, memory, hooks, protocols"]
end
subgraph tier2 [Tier 2 — Terminal + Bot + Train + Browser + MCP + Sandbox + Deploy]
Code["praisonai-code<br/>run, chat, code, Typer, runtime, LLM"]
Bot["praisonai-bot<br/>bots, gateway, channel CLI, OS daemon"]
Train["praisonai-train<br/>LLM fine-tuning, agent training"]
Browser["praisonai-browser<br/>extension bridge, CDP, Playwright"]
MCP["praisonai-mcp<br/>MCP server host, auth, capability adapters"]
Sandbox["praisonai-sandbox<br/>Docker, E2B, Modal, Sandlock backends"]
Deploy["praisonai-deploy<br/>API, Docker, AWS/Azure/GCP deploy"]
end
subgraph tier3 [Tier 3 — Wrapper]
Wrapper["praisonai<br/>framework_adapters, serve, dashboard"]
end
Agents --> Code
Agents --> Bot
Agents --> Train
Agents --> Browser
Agents --> MCP
Agents --> Sandbox
Agents --> Deploy
Agents --> Wrapper
Code -.->|"lazy _bot_bridge"| Bot
Code -.->|"lazy _train_bridge"| Train
Code -.->|"lazy _browser_bridge"| Browser
Code -.->|"lazy _mcp_bridge"| MCP
Code -.->|"lazy _sandbox_bridge"| Sandbox
Code -.->|"lazy _deploy_bridge"| Deploy
Code -.->|"lazy _wrapper_bridge"| Wrapper
Bot -.->|"lazy _code_bridge / _wrapper_bridge"| Code
Bot -.->|"lazy _wrapper_bridge"| Wrapper
Train -.->|"lazy _code_bridge / _wrapper_bridge"| Code
```
**PyPI publish order:** `praisonaiagents` → `praisonai-code` + `praisonai-bot` + `praisonai-train` + `praisonai-browser` + `praisonai-mcp` + `praisonai-sandbox` + `praisonai-deploy` → `praisonai`
**Backward compatibility:** `praisonai.bots`, `praisonai.gateway`, `praisonai.train`,
`praisonai.browser`, `praisonai.mcp_server`, `praisonai.sandbox`, `praisonai.deploy`, and related CLI paths remain as
`alias_package` shims to `praisonai_bot.*` / `praisonai_train.*` /
`praisonai_browser.*` / `praisonai_mcp.*` / `praisonai_sandbox.*` / `praisonai_deploy.*`.
| Tier | Package | Owns | Must not depend on |
|------|---------|------|-------------------|
| 1 | `src/praisonai-agents/` | Agent, tools, memory, hooks, `frameworks/` protocols, sandbox protocols | `praisonai`, `praisonai-code`, `praisonai-bot`, `praisonai-train`, `praisonai-browser`, `praisonai-mcp`, `praisonai-sandbox`, `praisonai-deploy` |
| 2a | `src/praisonai-code/` | `run`/`chat`/`code`, Typer, runtime, LLM, tool resolution | **`praisonai` as a PyPI dependency** (optional lazy imports via `_wrapper_bridge` only) |
| 2b | `src/praisonai-bot/` | Bots, gateway, channel CLI, OS daemon, gateway scheduler tick | **`praisonai` as a PyPI dependency** (optional lazy `_wrapper_bridge` for jobs/UI) |
| 2c | `src/praisonai-train/` | LLM fine-tuning (Unsloth), agent training, `train` CLI, conda env setup | **`praisonai` as a PyPI dependency** (lazy `_code_bridge` for legacy dispatch) |
| 2d | `src/praisonai-browser/` | Extension bridge, CDP/hybrid automation, `browser` CLI | **`praisonai` as a PyPI dependency** (none required; depends on `praisonaiagents` only) |
| 2e | `src/praisonai-mcp/` | MCP server host, auth, transports, `mcp` CLI | **`praisonai` as a PyPI dependency** (lazy `_wrapper_bridge` for full capability registry) |
| 2f | `src/praisonai-sandbox/` | Sandbox backends (Docker, E2B, Modal, Sandlock, SSH), `SandboxRegistry` | **`praisonai` as a PyPI dependency** (lazy `_code_bridge` for `PluginRegistry`) |
| 2g | `src/praisonai-deploy/` | Deploy API/Docker/cloud, `deploy` CLI, scheduler integration | **`praisonai` as a PyPI dependency** (lazy `_plugin_registry` via `_code_bridge`) |
| 3 | `src/praisonai/` | `framework_adapters/`, serve, dashboard, async jobs API | — |
### Repo infra (not PyPI)
Deployment packaging that stays in the git checkout, not in any tier-2 wheel:
| Path | Runtime owner | Orchestration |
|------|---------------|---------------|
| `src/praisonai-bot/infra/helm/praisonai-gateway/` | `praisonai-bot` (gateway) | C14 `deploy helm` wrapper |
| `src/praisonai-deploy/infra/helm/praisonai-agents-api/` | Generated API from C14 | C14 cross-link |
| `src/praisonai-deploy/infra/compose/agents-stack/` | Docker Compose | `praisonai deploy compose up/down` |
| `src/praisonai-deploy/infra/starters/` | Starter scaffolds | `praisonai deploy create --template` |
| `docker/` | Mixed (wrapper/bot dev stacks) | Not C14 — see bot manifest for `docker/bots/` |
Same boundary as C14: Helm/K8s manifests are **repo infra**, not `pip install praisonai-deploy` content.
**Config kernel:** Phase 0 `praisonai/common/` was skipped; shared config lives in
`praisonai_code/cli/configuration/` and is reached by the bot tier via lazy
`_code_bridge` (see `src/praisonai/tests/CONFIG_KERNEL.md`).
### Dependency rule (validated)
| Question | Answer |
|----------|--------|
| Does `praisonai-code` declare `praisonai` in `pyproject.toml`? | **No** — only `praisonaiagents` + CLI/runtime deps |
| Does `praisonai` declare `praisonai-code`? | **Yes** — one-way chain, no PyPI cycle |
| Can `praisonai-code` import `praisonai.*` at runtime? | **Only lazily** via `praisonai_code._wrapper_bridge` for optional features; **not** on the agentic hot path |
| Does standalone `pip install praisonai-code` work? | **Yes** — CI smoke validates imports + `praisonai-code run` without the wrapper |
### CLI routing
```mermaid
flowchart LR
User[User] --> Entry{Entry point}
Entry -->|pip install praisonai-code| CodeCLI["praisonai-code run/chat/code"]
Entry -->|pip install praisonai| WrapperCLI["praisonai …"]
WrapperCLI --> Router["praisonai.__main__"]
Router --> CodePath["praisonai_code.cli.app"]
Router --> Legacy["praisonai.cli.main"]
CodePath --> HotPath["run / chat / code"]
CodePath --> Bridge{"wrapper_available?"}
Bridge -->|yes| WrapperCmds["bot, gateway, pairing, …"]
Bridge -->|no| Hidden["Wrapper commands hidden"]
```
### Import gates (CI enforced)
- **Hot path:** no module-level `from praisonai` in `main.py`, `app.py`, `run.py`, `chat.py`, `code.py`
- **Regression baseline:** 50 direct wrapper import lines max (`scripts/check_c7_imports.sh`; C8 achieved **0**)
- **Allowlist:** reviewed files only (`scripts/c7_wrapper_import_allowlist.txt`; empty post-C8)
- **Hybrid audit:** `scripts/audit_hybrid_modules.py` — repatriated cross-tier import paths
### C7 / C7.1 / C8 status
| Milestone | Status |
|-----------|--------|
| Standalone agentic hot path | **Complete** |
| `_wrapper_bridge` hardening | **Complete** |
| Import gate + allowlist | **Complete** |
| Boundary tests + CI smoke | **Complete** |
| C8 reverse import elimination (225 → 0 direct imports) | **Complete** — see [`C8_BACKLOG.md`](src/praisonai/tests/C8_BACKLOG.md) |
| C8.4 main.py physical decomposition | **Deferred** (separate epic) |
| PyPI package splits (`praisonai-bot`, etc.) | **Out of scope** |
---
## 3. System Overview
## 2. System Overview
```
User/SDK/CLI
@@ -214,42 +75,33 @@ User/SDK/CLI
### Existing Architecture (Python SDK)
The Python SDK is organised as three publishable tiers (see [§2](#2-python-three-tier-package-model-c71)):
The Python SDK is the primary runtime and is organised as follows:
- `src/praisonai-agents/` — Core SDK (`Agent`, tools, memory, hooks, protocols)
- `src/praisonai-code/` — Terminal CLI (`run`, `chat`, `code`, Typer, runtime, LLM)
- `src/praisonai/` — Wrapper (gateway, bots, `framework_adapters/`, integrations)
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
Wrapper-only surfaces remain in `src/praisonai/`:
- `gateway/`, `bots/` — Multi-bot orchestration (BotOS)
- `framework_adapters/` — CrewAI, AutoGen, PraisonAI adapters
- `cli/commands/` — Wrapper commands (`bot`, `gateway`, `pairing`, …)
- `src/praisonai/` — Core runtime and agent abstractions
- `agents/` — Agent types (chat, code, realtime, audio, vision, etc.)
- `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)
- `workflow/` — Workflow engine (YAML/SDK-based orchestration)
- `telemetry/` — Observability, monitoring, token tracking
- `reliability/` — Error classification, circuit breaker, failover
- `skill/` — Plugin/skill system
- `loop_detection/` — Doom loop detection and prevention
- `mcp/` — MCP protocol support
- `a2a/`, `a2ui/` — Agent-to-agent and agent-to-UI protocols
### Multi-SDK Layout
- **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/`.
- **Python SDK** — Primary runtime (`src/praisonai/`)
- **TypeScript SDK** — JS/TS runtime (`ts-sdk/`)
- **Rust SDK** — High-performance Rust runtime (`rust-sdk/`)
- **CLI** — Command-line interface (`cli/`)
- **UI** — Web UI (`ui/`)
---
## 4. Layered Architecture
## 3. Layered Architecture
```mermaid
flowchart TB
@@ -345,7 +197,7 @@ flowchart TB
---
## 5. Core Data Contracts
## 4. Core Data Contracts
### RunRequest
@@ -403,7 +255,7 @@ Checkpoint {
---
## 6. Reliability by Design
## 5. Reliability by Design
### Error Taxonomy
@@ -456,7 +308,7 @@ def test_agent_workflow():
---
## 7. Observability & Telemetry
## 6. Observability & Telemetry
### Current Telemetry Stack
@@ -483,7 +335,7 @@ PraisonAI already includes:
---
## 8. Replay & Checkpointing
## 7. Replay & Checkpointing
### Design
@@ -524,15 +376,7 @@ sequenceDiagram
---
## 9. Implementation Roadmap
### Completed (C7 / C7.1 — v4.6.110)
| Project | Description | Status |
|---------|-------------|--------|
| C7 hot path | Standalone `praisonai-code run/chat/code` without wrapper import | **Complete** |
| C7.1 boundaries | Three-tier ownership, `_wrapper_bridge`, import gates | **Complete** |
| CI parity | Smoke standalone block + pre-existing test fixes (#2560) | **Complete** |
## 8. Implementation Roadmap
### Quarter 1: Trust Foundation (Q3 2026)
@@ -570,7 +414,7 @@ gantt
---
## 10. Success Metrics
## 9. Success Metrics
### KPI Targets
-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`
+25 -294
View File
@@ -35,25 +35,16 @@ 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">
<img src=".github/images/dashboard.png" alt="PraisonAI Dashboard" width="800" />
</p>
<p align="center">
<img src=".github/images/agentflow.gif" alt="PraisonAI AgentFlow" width="800" />
</p>
```
██████╗ ██████╗ █████╗ ██╗███████╗ ██████╗ ███╗ ██╗ █████╗ ██╗
██╔══██╗██╔══██╗██╔══██╗██║██╔════╝██╔═══██╗████╗ ██║ ██╔══██╗██║
@@ -65,6 +56,12 @@ curl -fsSL https://praison.ai/install.sh | bash
pip install praisonai
```
<p align="center">
<img src=".github/images/latest_ai_news_and_crawl_each_url_to_find_info.gif" alt="PraisonAI command execution" width="800" />
</p>
\* `export TAVILY_API_KEY=xxxxx`
<div align="center">
<a href="https://docs.praison.ai">
<p align="center">
@@ -109,232 +106,6 @@ agent.start("Analyze the top 3 tech trends of 2026 and format as a markdown tabl
---
## 🧬 The Five-Layer Agent Stack
Most frameworks hand you one or two layers and leave the rest as homework. PraisonAI covers **all five** — plus the outer layer that decides *where* your agent actually runs.
Each layer wraps the one inside it. When an agent misbehaves, the layer tells you where to look.
```
┌─────────────────────────────────────────────────────────────────┐
│ ⬡ MANAGED AGENTS — Where does it actually run? │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 5 · GRAPH — Who runs when, and who checks whom? │ │
│ │ ┌─────────────────────────────────────────────────────────┐ │ │
│ │ │ 4 · LOOP — When do we stop? │ │ │
│ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ 3 · HARNESS — Can it act, and be checked? │ │ │ │
│ │ │ │ ┌─────────────────────────────────────────────────┐ │ │ │ │
│ │ │ │ │ 2 · CONTEXT — Is the right thing in the window? │ │ │ │ │
│ │ │ │ │ ┌─────────────────────────────────────────────┐ │ │ │ │ │
│ │ │ │ │ │ 1 · PROMPT — Did I say it clearly? │ │ │ │ │ │
│ │ │ │ │ └─────────────────────────────────────────────┘ │ │ │ │ │
│ │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │
│ │ │ └─────────────────────────────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
| Layer | The question it answers | PraisonAI |
|:--|:--|:--|
| **1 · Prompt** | Did I say it clearly? | `instructions=`, `role`/`goal`/`backstory`, `output=`, `templates=` |
| **2 · Context** | Is the right thing in the window? | `memory=`, `knowledge=`, `context=`, handoff `ContextPolicy` |
| **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 |
### Layer 1 · Prompt — *Did I say it clearly?*
Role, instructions, examples, output format.
```python
from praisonaiagents import Agent
agent = Agent(
role="Senior Data Analyst",
goal="Turn raw numbers into decisions",
output="verbose", # markdown-formatted output
)
agent.start("Summarise Q3 revenue trends")
```
### Layer 2 · Context — *Is the right thing in the window?*
Write, select, compress, isolate — the four context operations, one parameter each.
```python
from praisonaiagents import Agent
agent = Agent(
instructions="You are a support engineer.",
memory={"user_id": "u-42"}, # write — persists across runs (needs a user_id)
knowledge=["docs/"], # select — retrieves only what's relevant
context="summarize", # compress — auto-compacts before the limit
)
```
> **Isolate** is `handoffs=[specialist]` — a sub-agent inherits the last few messages and the intersection of your tools, not your whole transcript. [📖 Handoffs](https://docs.praison.ai/docs/concepts/handoffs)
### Layer 3 · Harness — *Can it act, and be checked?*
*Agent = Model + Harness.* Tool dispatch, plus the guides that steer before acting and the sensors that observe after.
```python
from praisonaiagents import Agent, MCP, tool
@tool
def deploy(env: str) -> str:
"""Deploy the current build to an environment."""
return f"Deployed to {env}"
agent = Agent(
name="ReleaseEngineer",
instructions="You are a release engineer.",
tools=[deploy, MCP("npx -y @modelcontextprotocol/server-filesystem /tmp")],
approval=True, # guide — human gate before risky tools run
)
agent.start("Deploy to staging, then list the files you can read")
```
### Layer 4 · Loop — *When do we stop?*
Hard iteration caps, budget ceilings, no-progress detection and completion checks — every brake is explicit.
```python
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"),
autonomy=True, # required to drive the loop with run_autonomous()
)
result = agent.run_autonomous("Refactor the auth module", max_iterations=5)
print(result.completion_reason)
# goal | no_tool_calls | max_iterations | timeout | doom_loop | needs_help | error
# (with on_budget_exceeded="stop", hitting the cap raises BudgetExceededError,
# surfaced here as completion_reason="error")
```
> **Doom-loop detection is on by default.** Repeated identical tool calls and A→B→A→B oscillation get caught — while a poller whose output keeps changing does not. [📖 Doom Loop Detection](https://docs.praison.ai/docs/features/doom-loop-detection)
### Layer 5 · Graph — *Who runs when, and who checks whom?*
Topology as a versionable artifact: prompt chaining, routing, parallelisation, orchestrator-worker.
```python
from praisonaiagents import AgentFlow
from praisonaiagents.workflows import route, parallel, repeat
flow = AgentFlow(steps=[
classifier,
route({"bug": [bug_agent], "feature": [feature_agent], "default": [triage]}),
parallel([reviewer, tester]), # fan out, join automatically
repeat(editor, until=lambda ctx: "approved" in ctx.previous_result.lower(),
max_iterations=3), # evaluator–optimizer
])
flow.run("Ticket #123: login fails on Safari")
```
> The same graph is expressible in YAML with no Python at all. [📖 AgentFlow](https://docs.praison.ai/docs/concepts/agentflow)
### ⬡ Outside the stack: Managed Agents — *Where does it actually run?*
The harness is commoditising; **where** the agent executes is the next multiplier. Rather than burning your laptop's CPU, hand an agent a short-lived cloud sandbox — repo, tools and tests run there.
```bash
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
writer = Agent(name="Writer", instructions="You write files.")
reader = Agent(name="Reader", instructions="You read files.")
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
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>"`
<sub>Stack framing adapted from [The Five-Layer Agent Stack](https://mer.vin/2026/07/five-layer-agent-stack-match-bug-to-right-layer/) and [Agent Harnesses vs Orbs](https://mer.vin/2026/08/agent-harnesses-vs-orbs-why-remote-sandboxes-beat-local-agent-loops/).</sub>
---
## 🌌 The PraisonAI Ecosystem
Start simple with the core SDK, or expand to full visual builders and dashboards when you're ready.
@@ -429,9 +200,9 @@ Powered by 100+ LLMs (OpenAI, Anthropic, Gemini & local models).
| 🧠 | **Planning Mode** — plan → execute → reason | `planning=True` |
| 🔍 | **Deep Research** — multi-step autonomous research | [Docs](https://docs.praison.ai/docs/agents/deep-research) |
| 🤖 | **External Agents** — orchestrate Claude Code, Gemini CLI, Codex | [Docs](https://docs.praison.ai/docs/code/external-agents) |
| 🔄 | **Agent Handoffs** — seamless conversation passing | `handoffs=[other_agent]` |
| 🔄 | **Agent Handoffs** — seamless conversation passing | `handoff=True` |
| 🛡️ | **Guardrails** — input/output validation | [Docs](https://docs.praison.ai/docs/concepts/guardrails) |
| | **Web Search + Fetch** — native browsing | `web=True` |
| | **Web Search + Fetch** — native browsing | `web_search=True` |
| 🪞 | **Self Reflection** — agent reviews its own output | [Docs](https://docs.praison.ai/docs/concepts/reflection) |
| 🔀 | **Workflow Patterns** — route, parallel, loop, repeat | [Docs](https://docs.praison.ai/docs/concepts/agentflow) |
| 🧠 | **Memory (zero deps)** — works out of the box | `memory=True` |
@@ -441,9 +212,9 @@ Powered by 100+ LLMs (OpenAI, Anthropic, Gemini & local models).
| | Feature | How |
|--|---------|-----|
| 💡 | **Prompt Caching** — reduce latency + cost | `caching=True` |
| 💡 | **Prompt Caching** — reduce latency + cost | `prompt_caching=True` |
| 💾 | **Sessions + Auto-Save** — persistent state across restarts | `auto_save="my-project"` |
| 💭 | **Thinking Budgets** — control reasoning depth | `agent.thinking_budget = 1024` |
| 💭 | **Thinking Budgets** — control reasoning depth | `thinking_budget=1024` |
| 📚 | **RAG + Quality-Based RAG** — auto quality scoring retrieval | [Docs](https://docs.praison.ai/docs/concepts/rag) |
| 📊 | **Model Router** — auto-routes to cheapest capable model | [Docs](https://docs.praison.ai/docs/features/model-router) |
| 🧊 | **Shadow Git Checkpoints** — auto-rollback on failure | [Docs](https://docs.praison.ai/docs/features/checkpoints) |
@@ -570,10 +341,8 @@ from praisonaiagents import Agent, db
agent = Agent(
name="Assistant",
memory={
"db": db(database_url="postgresql://localhost/mydb"),
"session_id": "my-session",
},
db=db(database_url="postgresql://localhost/mydb"),
session_id="my-session"
)
agent.chat("Hello!") # Auto-persists messages, runs, traces
```
@@ -799,7 +568,7 @@ praisonai agents.yaml
| Query Rewriter Agent | [Example](examples/python/agents/query-rewriter-agent.py) | [📖](https://docs.praison.ai/docs/agents/query-rewriter) |
| Native Web Search | [Example](examples/python/agents/websearch-agent.py) | [📖](https://docs.praison.ai/docs/agents/websearch) |
| Built-in Search Tools | [Example](examples/python/agents/websearch-agent.py) | [📖](https://docs.praison.ai/docs/tools/tavily) |
| Unified Web Search | [Example](examples/python/web_search_example.py) | [📖](https://docs.praison.ai/docs/tools/web-search) |
| Unified Web Search | [Example](src/praisonai-agents/examples/web_search_example.py) | [📖](https://docs.praison.ai/docs/tools/web-search) |
| Web Fetch (Anthropic) | [Example](examples/python/agents/web-fetch-agent.py) | [📖](https://docs.praison.ai/docs/features/model-capabilities) |
</details>
@@ -1019,16 +788,6 @@ PraisonAI is built for speed, with agent instantiation in around 14μs. This red
---
<p align="center">
<img src=".github/images/agentflow.gif" alt="PraisonAI AgentFlow" width="800" />
</p>
<p align="center">
<img src=".github/images/latest_ai_news_and_crawl_each_url_to_find_info.gif" alt="PraisonAI command execution" width="800" />
</p>
\* `export TAVILY_API_KEY=xxxxx`
## 🔍 Langfuse Tracing
```bash
@@ -1112,39 +871,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>
@@ -1158,10 +891,8 @@ from praisonaiagents import Agent, db
agent = Agent(
name="Assistant",
memory={
"db": db(database_url="postgresql://localhost/mydb"),
"session_id": "my-session",
},
db=db(database_url="postgresql://localhost/mydb"),
session_id="my-session"
)
```
@@ -1177,8 +908,8 @@ from praisonaiagents import Agent
agent = Agent(
name="Assistant",
# Enables file-based memory (no extra deps!)
memory={"user_id": "user123"},
memory=True, # Enables file-based memory (no extra deps!)
user_id="user123"
)
```
+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)
+395 -113
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,28 +16,32 @@ 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 ToolResult">ToolResult.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">is_multimodal</a>() -> bool</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">to_dict</a>() -> Dict[str, Any]</code>
# Agents
Types:
```python
from praisonaiagents import Agent, AutoAgents, AutoRagAgent, ContextAgent, DeepResearchAgent, ImageAgent, PlanningAgent, PraisonAIAgents, PromptExpanderAgent, QueryRewriterAgent, create_context_agent
from praisonaiagents import Agent, AutoAgents, AutoRagAgent, ContextAgent, DeepResearchAgent, ImageAgent, PlanningAgent, PromptExpanderAgent, QueryRewriterAgent, create_context_agent
```
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>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">auto_memory</a>(value: Optional[bool]) -> None</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">background</a>() -> Optional[bool]</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">background</a>(value: Optional[bool]) -> None</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">chat_history</a>()</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">chat_history</a>(value)</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">chat_with_context</a>(message: str, context: 'ContextPack', **kwargs) -> str</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">checkpoints</a>() -> Optional[bool]</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">checkpoints</a>(value: Optional[bool]) -> None</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">clone_for_channel</a>() -> 'Agent'</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">close</a>() -> None</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">console</a>() -> Optional[Any]</code>
@@ -56,7 +60,6 @@ Methods:
* <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_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>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">llm_instance</a>(value)</code>
@@ -68,20 +71,16 @@ 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>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">run_autonomous</a>(prompt: str, max_iterations: Optional[int] = None, timeout_seconds: Optional[float] = None, completion_promise: Optional[str] = None, clear_context: bool = False)</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">run_autonomous_async</a>(prompt: str, max_iterations: Optional[int] = None, timeout_seconds: Optional[float] = None, completion_promise: Optional[str] = None, clear_context: bool = False)</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">run_until</a>(prompt: str, criteria: str = '', threshold: float = 8.0, max_iterations: int = 5, mode: str = 'optimize', on_iteration: Optional[Callable[[Any], None]] = None, verbose: bool = False, goal: Optional[str] = None, goal_criteria: Optional[Any] = None, judge_model: Optional[str] = None) -> 'EvaluationLoopResult'</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">run_until</a>(prompt: str, criteria: str, threshold: float = 8.0, max_iterations: int = 5, mode: str = 'optimize', on_iteration: Optional[Callable[[Any], None]] = None, verbose: bool = False) -> 'EvaluationLoopResult'</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">run_until_async</a>(prompt: str, criteria: str, threshold: float = 8.0, max_iterations: int = 5, mode: str = 'optimize', on_iteration: Optional[Callable[[Any], None]] = None, verbose: bool = False) -> 'EvaluationLoopResult'</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">set_snapshot_root</a>(project_path: str) -> bool</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">skill_manager</a>() -> Optional[Any]</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">store_memory</a>(content: str, memory_type: str = 'short_term', action: str = 'add', **kwargs: Any) -> None</code>
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">stream_emitter</a>() -> Optional[Any]</code>
@@ -90,7 +89,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>
@@ -179,21 +177,20 @@ Methods:
* <code title="class BaseTool">BaseTool.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">get_schema</a>() -> Dict[str, Any]</code>
* <code title="class BaseTool">BaseTool.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">run</a>(**kwargs) -> Any</code>
* <code title="class BaseTool">BaseTool.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">safe_run</a>(**kwargs) -> ToolResult</code>
* <code title="class BaseTool">BaseTool.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">to_model_output</a>(result: Any) -> Optional[Any]</code>
* <code title="class BaseTool">BaseTool.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">validate</a>() -> bool</code>
* <code title="class BaseTool">BaseTool.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">validate_class</a>() -> bool</code>
* <code title="class BaseTool">BaseTool.<a href="./src/praisonai-agents/praisonaiagents/tools/base.py">validate_schema_roundtrip</a>() -> bool</code>
* <code title="class FunctionTool">FunctionTool.<a href="./src/praisonai-agents/praisonaiagents/tools/decorator.py">__call__</a>(*args, **kwargs) -> Any</code>
* <code title="class FunctionTool">FunctionTool.<a href="./src/praisonai-agents/praisonaiagents/tools/decorator.py">check_availability</a>() -> tuple[bool, str]</code>
* <code title="class FunctionTool">FunctionTool.<a href="./src/praisonai-agents/praisonaiagents/tools/decorator.py">get_schema</a>() -> Dict[str, Any]</code>
* <code title="class FunctionTool">FunctionTool.<a href="./src/praisonai-agents/praisonaiagents/tools/decorator.py">injected_params</a>() -> Dict[str, Any]</code>
* <code title="class FunctionTool">FunctionTool.<a href="./src/praisonai-agents/praisonaiagents/tools/decorator.py">run</a>(**kwargs) -> Any</code>
* <code title="class FunctionTool">FunctionTool.<a href="./src/praisonai-agents/praisonaiagents/tools/decorator.py">to_model_output</a>(result: Any) -> Optional[Any]</code>
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">clear</a>() -> None</code>
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">discover_plugins</a>() -> int</code>
* <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_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_tool_definitions</a>() -> 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>
* <code title="class ToolRegistry">ToolRegistry.<a href="./src/praisonai-agents/praisonaiagents/tools/registry.py">list_base_tools</a>() -> List[BaseTool]</code>
@@ -233,6 +230,13 @@ Methods:
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">repeat</a>(step: Any, until: Optional[Callable[[WorkflowContext], bool]] = None, max_iterations: int = 10) -> Repeat</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">route</a>(routes: Dict[str, List], default: Optional[List] = None) -> Route</code>
# DB
Types:
```python
from praisonaiagents import db
```
# Memory
Types:
@@ -242,12 +246,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>
@@ -255,13 +257,9 @@ Methods:
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">delete_memory</a>(memory_id: str, memory_type: Optional[str] = None) -> bool</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">delete_short_term</a>(memory_id: str) -> bool</code>
* <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>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">remember</a>(content: str, **kwargs) -> str</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">reset_all</a>()</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">reset_entity_only</a>()</code>
* <code title="class Memory">Memory.<a href="./src/praisonai-agents/praisonaiagents/memory/memory.py">reset_long_term</a>()</code>
@@ -269,8 +267,8 @@ 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, **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_long_term</a>(query: str, limit: int = 5, relevance_cutoff: float = 0.0, min_quality: float = 0.0, rerank: bool = False, **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, **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>
@@ -309,7 +307,7 @@ Methods:
* <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>()</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>
@@ -387,7 +385,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
@@ -471,7 +468,7 @@ Methods:
Types:
```python
from praisonaiagents import SkillLoader, SkillsConfig
from praisonaiagents import SkillLoader, SkillManager, SkillMetadata, SkillProperties, SkillsConfig
```
Methods:
@@ -483,6 +480,46 @@ 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_skill</a>(skill_path: str) -> 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">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) -> 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">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_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">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">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
@@ -524,14 +561,9 @@ from praisonaiagents import MCP
Methods:
* <code title="class MCP">MCP.<a href="./src/praisonai-agents/praisonaiagents/mcp/mcp.py">apply_tool_filters</a>(allowed_tools: Optional[List[str]] = None, disabled_tools: Optional[List[str]] = None) -> 'MCP'</code>
* <code title="class MCP">MCP.<a href="./src/praisonai-agents/praisonaiagents/mcp/mcp.py">get_prompts</a>() -> List[dict]</code>
* <code title="class MCP">MCP.<a href="./src/praisonai-agents/praisonaiagents/mcp/mcp.py">get_resources</a>() -> List[dict]</code>
* <code title="class MCP">MCP.<a href="./src/praisonai-agents/praisonaiagents/mcp/mcp.py">get_tools</a>() -> List[Callable]</code>
* <code title="class MCP">MCP.<a href="./src/praisonai-agents/praisonaiagents/mcp/mcp.py">list_active_server_names</a>() -> set</code>
* <code title="class MCP">MCP.<a href="./src/praisonai-agents/praisonaiagents/mcp/mcp.py">shutdown</a>()</code>
* <code title="class MCP">MCP.<a href="./src/praisonai-agents/praisonaiagents/mcp/mcp.py">to_openai_tool</a>()</code>
* <code title="class MCP">MCP.<a href="./src/praisonai-agents/praisonaiagents/mcp/mcp.py">with_tool_prefix</a>(prefix: str) -> 'MCP'</code>
# Telemetry
@@ -623,8 +655,6 @@ Methods:
* <code title="class AutonomyConfig">AutonomyConfig.<a href="./src/praisonai-agents/praisonaiagents/agent/autonomy.py">from_dict</a>(data: Dict[str, Any]) -> 'AutonomyConfig'</code>
* <code title="class CachingConfig">CachingConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class ExecutionConfig">ExecutionConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">from_dict</a>(data: Dict[str, Any]) -> 'ExecutionConfig'</code>
* <code title="class ExecutionConfig">ExecutionConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">resolved_max_steps</a>() -> int</code>
* <code title="class ExecutionConfig">ExecutionConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">resolved_max_tool_calls</a>() -> int</code>
* <code title="class ExecutionConfig">ExecutionConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class GuardrailConfig">GuardrailConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
* <code title="class HooksConfig">HooksConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
@@ -692,7 +722,7 @@ Methods:
Types:
```python
from praisonaiagents import AgentAppConfig, AgentAppProtocol, AgentFlow, AgentManager, AgentOSConfig, AgentOSProtocol, AgentTeam, AutoApproveBackend, EmbeddingResult, RetryBackoffConfig, RunOutcome, __version__, aembedding, aembeddings, embedding, embeddings, get_dimensions
from praisonaiagents import AgentAppConfig, AgentAppProtocol, AgentFlow, AgentManager, AgentOSConfig, AgentOSProtocol, AgentTeam, AutoApproveBackend, EmbeddingResult, RetryBackoffConfig, __version__, aembedding, aembeddings, embedding, embeddings, get_dimensions
```
Methods:
@@ -715,7 +745,6 @@ Methods:
* <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">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>
@@ -727,7 +756,6 @@ Methods:
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">arun_task</a>(task_id)</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">aspawn_sub_agent</a>(agent: Agent, task: Any, completion_callback: Optional[Callable[[SubAgentCompletionEvent], Any]] = None, metadata: Optional[Dict[str, Any]] = None) -> SpawnedSubAgent</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">astart</a>(content = None, return_dict = False, **kwargs)</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">astart_for_each</a>(inputs, **kwargs)</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">await_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">clean_json_output</a>(output: str) -> str</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">clear_state</a>() -> None</code>
@@ -752,28 +780,22 @@ Methods:
* <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>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">run_task</a>(task_id)</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">save_output_to_file</a>(task, task_output)</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">save_session_state</a>(session_id: str, include_memory: bool = True) -> bool</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">save_session_state</a>(session_id: str, include_memory: bool = True) -> None</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">set_state</a>(key: str, value: Any) -> None</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">spawn_sub_agent</a>(agent: Agent, task: Any, completion_callback: Optional[Callable[[SubAgentCompletionEvent], Any]] = None, metadata: Optional[Dict[str, Any]] = None) -> SpawnedSubAgent</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">start</a>(content = None, return_dict = False, output = None, **kwargs)</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">start_for_each</a>(inputs, **kwargs)</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">stream_emitter</a>()</code>
* <code title="class AgentTeam">AgentTeam.<a href="./src/praisonai-agents/praisonaiagents/agents/agents.py">todo_list</a>()</code>
* <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) -> 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>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/embedding/embed.py">aembedding</a>(input: Union[str, List[str]], model: str = 'text-embedding-3-small', dimensions: Optional[int] = None, encoding_format: str = 'float', timeout: float = 600.0, api_key: Optional[str] = None, api_base: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None, **kwargs) -> EmbeddingResult</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/embedding/embed.py">aembeddings</a>(input: Union[str, List[str]], model: str = 'text-embedding-3-small', dimensions: Optional[int] = None, encoding_format: str = 'float', timeout: float = 600.0, api_key: Optional[str] = None, api_base: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None, **kwargs) -> EmbeddingResult</code>
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/embedding/dimensions.py">get_dimensions</a>(model_name: str) -> int</code>
@@ -782,7 +804,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, CloudProvider, DB, Deploy, DeployConfig, DeployType, HostedAgent, HostedAgentConfig, LocalAgent, LocalAgentConfig, LocalManagedAgent, LocalManagedConfig, ManagedAgent, ManagedConfig, PraisonAI, __version__, arun, run, run_integrated_gateway
```
# CLI
@@ -790,14 +812,81 @@ from praisonai import Agent, AgentApp, AgentOS, AnthropicManagedAgent, HostedAge
Methods:
* <code title="cli">praisonai <a href="./src/praisonai/praisonai/cli/main.py">--help</a></code>
* <code title="cli">praisonai acp acp-main <a href="./src/praisonai/praisonai/cli/commands/acp.py">--help</a></code>
* <code title="cli">praisonai agent list <a href="./src/praisonai/praisonai/cli/commands/agent.py">--help</a></code>
* <code title="cli">praisonai agent show <a href="./src/praisonai/praisonai/cli/commands/agent.py">--help</a></code>
* <code title="cli">praisonai agents create <a href="./src/praisonai/praisonai/cli/commands/agents.py">--help</a></code>
* <code title="cli">praisonai agents info <a href="./src/praisonai/praisonai/cli/commands/agents.py">--help</a></code>
* <code title="cli">praisonai agents list <a href="./src/praisonai/praisonai/cli/commands/agents.py">--help</a></code>
* <code title="cli">praisonai app app <a href="./src/praisonai/praisonai/cli/commands/app.py">--help</a></code>
* <code title="cli">praisonai attach attach-main <a href="./src/praisonai/praisonai/cli/commands/attach.py">--help</a></code>
* <code title="cli">praisonai audit agent-centric <a href="./src/praisonai/praisonai/cli/commands/audit.py">--help</a></code>
* <code title="cli">praisonai auth list <a href="./src/praisonai/praisonai/cli/commands/auth.py">--help</a></code>
* <code title="cli">praisonai auth login <a href="./src/praisonai/praisonai/cli/commands/auth.py">--help</a></code>
* <code title="cli">praisonai auth logout <a href="./src/praisonai/praisonai/cli/commands/auth.py">--help</a></code>
* <code title="cli">praisonai auth status <a href="./src/praisonai/praisonai/cli/commands/auth.py">--help</a></code>
* <code title="cli">praisonai batch batch-run <a href="./src/praisonai/praisonai/cli/commands/batch.py">--help</a></code>
* <code title="cli">praisonai batch list <a href="./src/praisonai/praisonai/cli/commands/batch.py">--help</a></code>
* <code title="cli">praisonai batch report <a href="./src/praisonai/praisonai/cli/commands/batch.py">--help</a></code>
* <code title="cli">praisonai batch stats <a href="./src/praisonai/praisonai/cli/commands/batch.py">--help</a></code>
* <code title="cli">praisonai benchmark agent <a href="./src/praisonai/praisonai/cli/commands/benchmark.py">--help</a></code>
* <code title="cli">praisonai benchmark benchmark-callback <a href="./src/praisonai/praisonai/cli/commands/benchmark.py">--help</a></code>
* <code title="cli">praisonai benchmark cli <a href="./src/praisonai/praisonai/cli/commands/benchmark.py">--help</a></code>
* <code title="cli">praisonai benchmark compare <a href="./src/praisonai/praisonai/cli/commands/benchmark.py">--help</a></code>
* <code title="cli">praisonai benchmark litellm <a href="./src/praisonai/praisonai/cli/commands/benchmark.py">--help</a></code>
* <code title="cli">praisonai benchmark profile <a href="./src/praisonai/praisonai/cli/commands/benchmark.py">--help</a></code>
* <code title="cli">praisonai benchmark sdk <a href="./src/praisonai/praisonai/cli/commands/benchmark.py">--help</a></code>
* <code title="cli">praisonai benchmark workflow <a href="./src/praisonai/praisonai/cli/commands/benchmark.py">--help</a></code>
* <code title="cli">praisonai bot agentmail <a href="./src/praisonai/praisonai/cli/commands/bot.py">--help</a></code>
* <code title="cli">praisonai bot bot-callback <a href="./src/praisonai/praisonai/cli/commands/bot.py">--help</a></code>
* <code title="cli">praisonai bot discord <a href="./src/praisonai/praisonai/cli/commands/bot.py">--help</a></code>
* <code title="cli">praisonai bot email <a href="./src/praisonai/praisonai/cli/commands/bot.py">--help</a></code>
* <code title="cli">praisonai bot install-daemon <a href="./src/praisonai/praisonai/cli/commands/bot.py">--help</a></code>
* <code title="cli">praisonai bot linear <a href="./src/praisonai/praisonai/cli/commands/bot.py">--help</a></code>
* <code title="cli">praisonai bot list <a href="./src/praisonai/praisonai/cli/commands/bot.py">--help</a></code>
* <code title="cli">praisonai bot purge <a href="./src/praisonai/praisonai/cli/commands/bot.py">--help</a></code>
* <code title="cli">praisonai bot replay <a href="./src/praisonai/praisonai/cli/commands/bot.py">--help</a></code>
* <code title="cli">praisonai bot slack <a href="./src/praisonai/praisonai/cli/commands/bot.py">--help</a></code>
* <code title="cli">praisonai bot start <a href="./src/praisonai/praisonai/cli/commands/bot.py">--help</a></code>
* <code title="cli">praisonai bot telegram <a href="./src/praisonai/praisonai/cli/commands/bot.py">--help</a></code>
* <code title="cli">praisonai bot whatsapp <a href="./src/praisonai/praisonai/cli/commands/bot.py">--help</a></code>
* <code title="cli">praisonai browser browser-callback <a href="./src/praisonai/praisonai/cli/commands/browser.py">--help</a></code>
* <code title="cli">praisonai browser click <a href="./src/praisonai/praisonai/cli/commands/browser.py">--help</a></code>
* <code title="cli">praisonai browser navigate <a href="./src/praisonai/praisonai/cli/commands/browser.py">--help</a></code>
* <code title="cli">praisonai browser open <a href="./src/praisonai/praisonai/cli/commands/browser.py">--help</a></code>
* <code title="cli">praisonai browser profiles <a href="./src/praisonai/praisonai/cli/commands/browser.py">--help</a></code>
* <code title="cli">praisonai browser screenshot <a href="./src/praisonai/praisonai/cli/commands/browser.py">--help</a></code>
* <code title="cli">praisonai browser snapshot <a href="./src/praisonai/praisonai/cli/commands/browser.py">--help</a></code>
* <code title="cli">praisonai browser status <a href="./src/praisonai/praisonai/cli/commands/browser.py">--help</a></code>
* <code title="cli">praisonai browser type <a href="./src/praisonai/praisonai/cli/commands/browser.py">--help</a></code>
* <code title="cli">praisonai call call-main <a href="./src/praisonai/praisonai/cli/commands/call.py">--help</a></code>
* <code title="cli">praisonai chat <a href="./src/praisonai/praisonai/cli/main.py">--help</a></code>
* <code title="cli">praisonai chat chat-main <a href="./src/praisonai/praisonai/cli/commands/chat.py">--help</a></code>
* <code title="cli">praisonai checkpoint delete <a href="./src/praisonai/praisonai/cli/commands/checkpoint.py">--help</a></code>
* <code title="cli">praisonai checkpoint diff <a href="./src/praisonai/praisonai/cli/commands/checkpoint.py">--help</a></code>
* <code title="cli">praisonai checkpoint list <a href="./src/praisonai/praisonai/cli/commands/checkpoint.py">--help</a></code>
* <code title="cli">praisonai checkpoint restore <a href="./src/praisonai/praisonai/cli/commands/checkpoint.py">--help</a></code>
* <code title="cli">praisonai checkpoint save <a href="./src/praisonai/praisonai/cli/commands/checkpoint.py">--help</a></code>
* <code title="cli">praisonai claw claw <a href="./src/praisonai/praisonai/cli/commands/claw.py">--help</a></code>
* <code title="cli">praisonai code <a href="./src/praisonai/praisonai/cli/main.py">--help</a></code>
* <code title="cli">praisonai code code-main <a href="./src/praisonai/praisonai/cli/commands/code.py">--help</a></code>
* <code title="cli">praisonai command list <a href="./src/praisonai/praisonai/cli/commands/command.py">--help</a></code>
* <code title="cli">praisonai command show <a href="./src/praisonai/praisonai/cli/commands/command.py">--help</a></code>
* <code title="cli">praisonai commit commit-main <a href="./src/praisonai/praisonai/cli/commands/commit.py">--help</a></code>
* <code title="cli">praisonai completion bash <a href="./src/praisonai/praisonai/cli/commands/completion.py">--help</a></code>
* <code title="cli">praisonai completion completion-callback <a href="./src/praisonai/praisonai/cli/commands/completion.py">--help</a></code>
* <code title="cli">praisonai completion fish <a href="./src/praisonai/praisonai/cli/commands/completion.py">--help</a></code>
* <code title="cli">praisonai completion zsh <a href="./src/praisonai/praisonai/cli/commands/completion.py">--help</a></code>
* <code title="cli">praisonai config doctor <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
* <code title="cli">praisonai config env <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
* <code title="cli">praisonai config get <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
* <code title="cli">praisonai config list <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
* <code title="cli">praisonai config path <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
* <code title="cli">praisonai config reset <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
* <code title="cli">praisonai config set <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
* <code title="cli">praisonai config show <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
* <code title="cli">praisonai config sources <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
* <code title="cli">praisonai config validate <a href="./src/praisonai/praisonai/cli/commands/config.py">--help</a></code>
* <code title="cli">praisonai context add <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
* <code title="cli">praisonai context clear <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
* <code title="cli">praisonai context compact <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
@@ -807,7 +896,22 @@ Methods:
* <code title="cli">praisonai context show <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
* <code title="cli">praisonai context stats <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
* <code title="cli">praisonai context tail <a href="./src/praisonai/praisonai/cli/commands/context.py">--help</a></code>
* <code title="cli">praisonai daemon daemon-main <a href="./src/praisonai/praisonai/cli/commands/daemon.py">--help</a></code>
* <code title="cli">praisonai daemon start <a href="./src/praisonai/praisonai/cli/commands/daemon.py">--help</a></code>
* <code title="cli">praisonai daemon status <a href="./src/praisonai/praisonai/cli/commands/daemon.py">--help</a></code>
* <code title="cli">praisonai daemon stop <a href="./src/praisonai/praisonai/cli/commands/daemon.py">--help</a></code>
* <code title="cli">praisonai dashboard unified <a href="./src/praisonai/praisonai/cli/commands/dashboard.py">--help</a></code>
* <code title="cli">praisonai debug acp <a href="./src/praisonai/praisonai/cli/commands/debug.py">--help</a></code>
* <code title="cli">praisonai debug debug-callback <a href="./src/praisonai/praisonai/cli/commands/debug.py">--help</a></code>
* <code title="cli">praisonai debug interactive <a href="./src/praisonai/praisonai/cli/commands/debug.py">--help</a></code>
* <code title="cli">praisonai debug lsp <a href="./src/praisonai/praisonai/cli/commands/debug.py">--help</a></code>
* <code title="cli">praisonai debug trace <a href="./src/praisonai/praisonai/cli/commands/debug.py">--help</a></code>
* <code title="cli">praisonai deploy aws <a href="./src/praisonai/praisonai/cli/commands/deploy.py">--help</a></code>
* <code title="cli">praisonai deploy azure <a href="./src/praisonai/praisonai/cli/commands/deploy.py">--help</a></code>
* <code title="cli">praisonai deploy docker <a href="./src/praisonai/praisonai/cli/commands/deploy.py">--help</a></code>
* <code title="cli">praisonai deploy gcp <a href="./src/praisonai/praisonai/cli/commands/deploy.py">--help</a></code>
* <code title="cli">praisonai diag diag-callback <a href="./src/praisonai/praisonai/cli/commands/diag.py">--help</a></code>
* <code title="cli">praisonai diag export <a href="./src/praisonai/praisonai/cli/commands/diag.py">--help</a></code>
* <code title="cli">praisonai docs api-md <a href="./src/praisonai/praisonai/cli/commands/docs.py">--help</a></code>
* <code title="cli">praisonai docs generate <a href="./src/praisonai/praisonai/cli/commands/docs.py">--help</a></code>
* <code title="cli">praisonai docs list <a href="./src/praisonai/praisonai/cli/commands/docs.py">--help</a></code>
@@ -816,6 +920,29 @@ Methods:
* <code title="cli">praisonai docs run-all <a href="./src/praisonai/praisonai/cli/commands/docs.py">--help</a></code>
* <code title="cli">praisonai docs serve <a href="./src/praisonai/praisonai/cli/commands/docs.py">--help</a></code>
* <code title="cli">praisonai docs stats <a href="./src/praisonai/praisonai/cli/commands/docs.py">--help</a></code>
* <code title="cli">praisonai doctor cleanup <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
* <code title="cli">praisonai doctor docker <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
* <code title="cli">praisonai doctor env <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
* <code title="cli">praisonai doctor llm-providers <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
* <code title="cli">praisonai doctor mcp <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
* <code title="cli">praisonai doctor memory-store <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
* <code title="cli">praisonai doctor metadata-store <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
* <code title="cli">praisonai doctor network <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
* <code title="cli">praisonai doctor packaging <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
* <code title="cli">praisonai doctor performance <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
* <code title="cli">praisonai doctor runtime <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
* <code title="cli">praisonai doctor selftest <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
* <code title="cli">praisonai doctor skills <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
* <code title="cli">praisonai doctor troubleshoot <a href="./src/praisonai/praisonai/cli/commands/doctor.py">--help</a></code>
* <code title="cli">praisonai endpoints list <a href="./src/praisonai/praisonai/cli/commands/endpoints.py">--help</a></code>
* <code title="cli">praisonai endpoints test <a href="./src/praisonai/praisonai/cli/commands/endpoints.py">--help</a></code>
* <code title="cli">praisonai environment check <a href="./src/praisonai/praisonai/cli/commands/environment.py">--help</a></code>
* <code title="cli">praisonai environment doctor <a href="./src/praisonai/praisonai/cli/commands/environment.py">--help</a></code>
* <code title="cli">praisonai environment view <a href="./src/praisonai/praisonai/cli/commands/environment.py">--help</a></code>
* <code title="cli">praisonai eval accuracy <a href="./src/praisonai/praisonai/cli/commands/eval.py">--help</a></code>
* <code title="cli">praisonai eval judge <a href="./src/praisonai/praisonai/cli/commands/eval.py">--help</a></code>
* <code title="cli">praisonai eval list-judges <a href="./src/praisonai/praisonai/cli/commands/eval.py">--help</a></code>
* <code title="cli">praisonai eval performance <a href="./src/praisonai/praisonai/cli/commands/eval.py">--help</a></code>
* <code title="cli">praisonai examples find <a href="./src/praisonai/praisonai/cli/commands/examples.py">--help</a></code>
* <code title="cli">praisonai examples info <a href="./src/praisonai/praisonai/cli/commands/examples.py">--help</a></code>
* <code title="cli">praisonai examples list <a href="./src/praisonai/praisonai/cli/commands/examples.py">--help</a></code>
@@ -828,6 +955,40 @@ Methods:
* <code title="cli">praisonai flow import <a href="./src/praisonai/praisonai/cli/commands/flow.py">--help</a></code>
* <code title="cli">praisonai flow list <a href="./src/praisonai/praisonai/cli/commands/flow.py">--help</a></code>
* <code title="cli">praisonai flow version <a href="./src/praisonai/praisonai/cli/commands/flow.py">--help</a></code>
* <code title="cli">praisonai gateway channels <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
* <code title="cli">praisonai gateway gateway-callback <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
* <code title="cli">praisonai gateway install <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
* <code title="cli">praisonai gateway logs <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
* <code title="cli">praisonai gateway mint-link <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
* <code title="cli">praisonai gateway pause <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
* <code title="cli">praisonai gateway reconnect <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
* <code title="cli">praisonai gateway resume <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
* <code title="cli">praisonai gateway send <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
* <code title="cli">praisonai gateway start <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
* <code title="cli">praisonai gateway status <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
* <code title="cli">praisonai gateway stop <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
* <code title="cli">praisonai gateway uninstall <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
* <code title="cli">praisonai github triage <a href="./src/praisonai/praisonai/cli/commands/github.py">--help</a></code>
* <code title="cli">praisonai hooks add <a href="./src/praisonai/praisonai/cli/commands/hooks.py">--help</a></code>
* <code title="cli">praisonai hooks list <a href="./src/praisonai/praisonai/cli/commands/hooks.py">--help</a></code>
* <code title="cli">praisonai hooks remove <a href="./src/praisonai/praisonai/cli/commands/hooks.py">--help</a></code>
* <code title="cli">praisonai identity import <a href="./src/praisonai/praisonai/cli/commands/identity.py">--help</a></code>
* <code title="cli">praisonai identity link <a href="./src/praisonai/praisonai/cli/commands/identity.py">--help</a></code>
* <code title="cli">praisonai identity list <a href="./src/praisonai/praisonai/cli/commands/identity.py">--help</a></code>
* <code title="cli">praisonai identity unlink <a href="./src/praisonai/praisonai/cli/commands/identity.py">--help</a></code>
* <code title="cli">praisonai init init <a href="./src/praisonai/praisonai/cli/commands/init.py">--help</a></code>
* <code title="cli">praisonai kanban block <a href="./src/praisonai/praisonai/cli/commands/kanban.py">--help</a></code>
* <code title="cli">praisonai kanban boards <a href="./src/praisonai/praisonai/cli/commands/kanban.py">--help</a></code>
* <code title="cli">praisonai kanban comment <a href="./src/praisonai/praisonai/cli/commands/kanban.py">--help</a></code>
* <code title="cli">praisonai kanban complete <a href="./src/praisonai/praisonai/cli/commands/kanban.py">--help</a></code>
* <code title="cli">praisonai kanban create <a href="./src/praisonai/praisonai/cli/commands/kanban.py">--help</a></code>
* <code title="cli">praisonai kanban dispatch <a href="./src/praisonai/praisonai/cli/commands/kanban.py">--help</a></code>
* <code title="cli">praisonai kanban link <a href="./src/praisonai/praisonai/cli/commands/kanban.py">--help</a></code>
* <code title="cli">praisonai kanban list <a href="./src/praisonai/praisonai/cli/commands/kanban.py">--help</a></code>
* <code title="cli">praisonai kanban move <a href="./src/praisonai/praisonai/cli/commands/kanban.py">--help</a></code>
* <code title="cli">praisonai kanban reclaim <a href="./src/praisonai/praisonai/cli/commands/kanban.py">--help</a></code>
* <code title="cli">praisonai kanban show <a href="./src/praisonai/praisonai/cli/commands/kanban.py">--help</a></code>
* <code title="cli">praisonai kanban unblock <a href="./src/praisonai/praisonai/cli/commands/kanban.py">--help</a></code>
* <code title="cli">praisonai knowledge add <a href="./src/praisonai/praisonai/cli/commands/knowledge.py">--help</a></code>
* <code title="cli">praisonai knowledge index <a href="./src/praisonai/praisonai/cli/commands/knowledge.py">--help</a></code>
* <code title="cli">praisonai knowledge list <a href="./src/praisonai/praisonai/cli/commands/knowledge.py">--help</a></code>
@@ -845,19 +1006,44 @@ Methods:
* <code title="cli">praisonai langfuse test <a href="./src/praisonai/praisonai/cli/commands/langfuse.py">--help</a></code>
* <code title="cli">praisonai langfuse traces <a href="./src/praisonai/praisonai/cli/commands/langfuse.py">--help</a></code>
* <code title="cli">praisonai langfuse version <a href="./src/praisonai/praisonai/cli/commands/langfuse.py">--help</a></code>
* <code title="cli">praisonai loop help <a href="./src/praisonai/praisonai/cli/commands/loop.py">--help</a></code>
* <code title="cli">praisonai loop loop-main <a href="./src/praisonai/praisonai/cli/commands/loop.py">--help</a></code>
* <code title="cli">praisonai lsp logs <a href="./src/praisonai/praisonai/cli/commands/lsp.py">--help</a></code>
* <code title="cli">praisonai lsp lsp-callback <a href="./src/praisonai/praisonai/cli/commands/lsp.py">--help</a></code>
* <code title="cli">praisonai lsp start <a href="./src/praisonai/praisonai/cli/commands/lsp.py">--help</a></code>
* <code title="cli">praisonai lsp status <a href="./src/praisonai/praisonai/cli/commands/lsp.py">--help</a></code>
* <code title="cli">praisonai lsp stop <a href="./src/praisonai/praisonai/cli/commands/lsp.py">--help</a></code>
* <code title="cli">praisonai managed delete <a href="./src/praisonai/praisonai/cli/commands/managed.py">--help</a></code>
* <code title="cli">praisonai managed get <a href="./src/praisonai/praisonai/cli/commands/managed.py">--help</a></code>
* <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 mcp add <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
* <code title="cli">praisonai mcp auth <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
* <code title="cli">praisonai mcp describe <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
* <code title="cli">praisonai mcp list <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
* <code title="cli">praisonai mcp logout <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
* <code title="cli">praisonai mcp mcp-callback <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
* <code title="cli">praisonai mcp remove <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
* <code title="cli">praisonai mcp run <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
* <code title="cli">praisonai mcp status <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
* <code title="cli">praisonai mcp sync <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
* <code title="cli">praisonai mcp test <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
* <code title="cli">praisonai mcp tools <a href="./src/praisonai/praisonai/cli/commands/mcp.py">--help</a></code>
* <code title="cli">praisonai memory add <a href="./src/praisonai/praisonai/cli/commands/memory.py">--help</a></code>
* <code title="cli">praisonai memory clear <a href="./src/praisonai/praisonai/cli/commands/memory.py">--help</a></code>
* <code title="cli">praisonai memory search <a href="./src/praisonai/praisonai/cli/commands/memory.py">--help</a></code>
* <code title="cli">praisonai memory show <a href="./src/praisonai/praisonai/cli/commands/memory.py">--help</a></code>
* <code title="cli">praisonai memory status <a href="./src/praisonai/praisonai/cli/commands/memory.py">--help</a></code>
* <code title="cli">praisonai models describe <a href="./src/praisonai/praisonai/cli/commands/models.py">--help</a></code>
* <code title="cli">praisonai models list <a href="./src/praisonai/praisonai/cli/commands/models.py">--help</a></code>
* <code title="cli">praisonai models validate <a href="./src/praisonai/praisonai/cli/commands/models.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>
* <code title="cli">praisonai n8n list <a href="./src/praisonai/praisonai/cli/commands/n8n.py">--help</a></code>
@@ -865,6 +1051,38 @@ Methods:
* <code title="cli">praisonai n8n pull <a href="./src/praisonai/praisonai/cli/commands/n8n.py">--help</a></code>
* <code title="cli">praisonai n8n push <a href="./src/praisonai/praisonai/cli/commands/n8n.py">--help</a></code>
* <code title="cli">praisonai n8n test <a href="./src/praisonai/praisonai/cli/commands/n8n.py">--help</a></code>
* <code title="cli">praisonai obs obs-fallback <a href="./src/praisonai/praisonai/cli/commands/obs.py">--help</a></code>
* <code title="cli">praisonai onboard onboard-callback <a href="./src/praisonai/praisonai/cli/commands/onboard.py">--help</a></code>
* <code title="cli">praisonai package install <a href="./src/praisonai/praisonai/cli/commands/package.py">--help</a></code>
* <code title="cli">praisonai package list <a href="./src/praisonai/praisonai/cli/commands/package.py">--help</a></code>
* <code title="cli">praisonai package uninstall <a href="./src/praisonai/praisonai/cli/commands/package.py">--help</a></code>
* <code title="cli">praisonai pairing approve <a href="./src/praisonai/praisonai/cli/commands/pairing.py">--help</a></code>
* <code title="cli">praisonai pairing clear <a href="./src/praisonai/praisonai/cli/commands/pairing.py">--help</a></code>
* <code title="cli">praisonai pairing list <a href="./src/praisonai/praisonai/cli/commands/pairing.py">--help</a></code>
* <code title="cli">praisonai pairing revoke <a href="./src/praisonai/praisonai/cli/commands/pairing.py">--help</a></code>
* <code title="cli">praisonai paths show <a href="./src/praisonai/praisonai/cli/commands/paths.py">--help</a></code>
* <code title="cli">praisonai permissions allow <a href="./src/praisonai/praisonai/cli/commands/permissions.py">--help</a></code>
* <code title="cli">praisonai permissions ask <a href="./src/praisonai/praisonai/cli/commands/permissions.py">--help</a></code>
* <code title="cli">praisonai permissions deny <a href="./src/praisonai/praisonai/cli/commands/permissions.py">--help</a></code>
* <code title="cli">praisonai permissions export <a href="./src/praisonai/praisonai/cli/commands/permissions.py">--help</a></code>
* <code title="cli">praisonai permissions import-rules <a href="./src/praisonai/praisonai/cli/commands/permissions.py">--help</a></code>
* <code title="cli">praisonai permissions list <a href="./src/praisonai/praisonai/cli/commands/permissions.py">--help</a></code>
* <code title="cli">praisonai permissions remove <a href="./src/praisonai/praisonai/cli/commands/permissions.py">--help</a></code>
* <code title="cli">praisonai permissions reset <a href="./src/praisonai/praisonai/cli/commands/permissions.py">--help</a></code>
* <code title="cli">praisonai plugins create <a href="./src/praisonai/praisonai/cli/commands/plugins.py">--help</a></code>
* <code title="cli">praisonai plugins disable <a href="./src/praisonai/praisonai/cli/commands/plugins.py">--help</a></code>
* <code title="cli">praisonai plugins discover <a href="./src/praisonai/praisonai/cli/commands/plugins.py">--help</a></code>
* <code title="cli">praisonai plugins doctor <a href="./src/praisonai/praisonai/cli/commands/plugins.py">--help</a></code>
* <code title="cli">praisonai plugins enable <a href="./src/praisonai/praisonai/cli/commands/plugins.py">--help</a></code>
* <code title="cli">praisonai plugins info <a href="./src/praisonai/praisonai/cli/commands/plugins.py">--help</a></code>
* <code title="cli">praisonai plugins install <a href="./src/praisonai/praisonai/cli/commands/plugins.py">--help</a></code>
* <code title="cli">praisonai plugins list <a href="./src/praisonai/praisonai/cli/commands/plugins.py">--help</a></code>
* <code title="cli">praisonai plugins plugins-callback <a href="./src/praisonai/praisonai/cli/commands/plugins.py">--help</a></code>
* <code title="cli">praisonai plugins remove <a href="./src/praisonai/praisonai/cli/commands/plugins.py">--help</a></code>
* <code title="cli">praisonai port check <a href="./src/praisonai/praisonai/cli/commands/port.py">--help</a></code>
* <code title="cli">praisonai port kill <a href="./src/praisonai/praisonai/cli/commands/port.py">--help</a></code>
* <code title="cli">praisonai port list <a href="./src/praisonai/praisonai/cli/commands/port.py">--help</a></code>
* <code title="cli">praisonai port port-callback <a href="./src/praisonai/praisonai/cli/commands/port.py">--help</a></code>
* <code title="cli">praisonai profile imports <a href="./src/praisonai/praisonai/cli/commands/profile.py">--help</a></code>
* <code title="cli">praisonai profile optimize <a href="./src/praisonai/praisonai/cli/commands/profile.py">--help</a></code>
* <code title="cli">praisonai profile profile-callback <a href="./src/praisonai/praisonai/cli/commands/profile.py">--help</a></code>
@@ -872,6 +1090,8 @@ Methods:
* <code title="cli">praisonai profile snapshot <a href="./src/praisonai/praisonai/cli/commands/profile.py">--help</a></code>
* <code title="cli">praisonai profile startup <a href="./src/praisonai/praisonai/cli/commands/profile.py">--help</a></code>
* <code title="cli">praisonai profile suite <a href="./src/praisonai/praisonai/cli/commands/profile.py">--help</a></code>
* <code title="cli">praisonai publish publish-main <a href="./src/praisonai/praisonai/cli/commands/publish.py">--help</a></code>
* <code title="cli">praisonai publish pypi <a href="./src/praisonai/praisonai/cli/commands/publish.py">--help</a></code>
* <code title="cli">praisonai rag chat <a href="./src/praisonai/praisonai/cli/commands/rag.py">--help</a></code>
* <code title="cli">praisonai rag eval <a href="./src/praisonai/praisonai/cli/commands/rag.py">--help</a></code>
* <code title="cli">praisonai rag index <a href="./src/praisonai/praisonai/cli/commands/rag.py">--help</a></code>
@@ -886,8 +1106,9 @@ 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 registry list <a href="./src/praisonai/praisonai/cli/commands/registry.py">--help</a></code>
* <code title="cli">praisonai registry serve <a href="./src/praisonai/praisonai/cli/commands/registry.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>
* <code title="cli">praisonai replay dashboard <a href="./src/praisonai/praisonai/cli/commands/replay.py">--help</a></code>
@@ -895,7 +1116,20 @@ Methods:
* <code title="cli">praisonai replay flow <a href="./src/praisonai/praisonai/cli/commands/replay.py">--help</a></code>
* <code title="cli">praisonai replay list <a href="./src/praisonai/praisonai/cli/commands/replay.py">--help</a></code>
* <code title="cli">praisonai replay show <a href="./src/praisonai/praisonai/cli/commands/replay.py">--help</a></code>
* <code title="cli">praisonai research research-main <a href="./src/praisonai/praisonai/cli/commands/research.py">--help</a></code>
* <code title="cli">praisonai retrieval index <a href="./src/praisonai/praisonai/cli/commands/retrieval.py">--help</a></code>
* <code title="cli">praisonai retrieval query <a href="./src/praisonai/praisonai/cli/commands/retrieval.py">--help</a></code>
* <code title="cli">praisonai retrieval search <a href="./src/praisonai/praisonai/cli/commands/retrieval.py">--help</a></code>
* <code title="cli">praisonai rules add <a href="./src/praisonai/praisonai/cli/commands/rules.py">--help</a></code>
* <code title="cli">praisonai rules clear <a href="./src/praisonai/praisonai/cli/commands/rules.py">--help</a></code>
* <code title="cli">praisonai rules list <a href="./src/praisonai/praisonai/cli/commands/rules.py">--help</a></code>
* <code title="cli">praisonai run <a href="./src/praisonai/praisonai/cli/main.py">--help</a></code>
* <code title="cli">praisonai run run-main <a href="./src/praisonai/praisonai/cli/commands/run.py">--help</a></code>
* <code title="cli">praisonai sandbox explain <a href="./src/praisonai/praisonai/cli/commands/sandbox.py">--help</a></code>
* <code title="cli">praisonai sandbox list <a href="./src/praisonai/praisonai/cli/commands/sandbox.py">--help</a></code>
* <code title="cli">praisonai sandbox recreate <a href="./src/praisonai/praisonai/cli/commands/sandbox.py">--help</a></code>
* <code title="cli">praisonai sandbox sandbox-callback <a href="./src/praisonai/praisonai/cli/commands/sandbox.py">--help</a></code>
* <code title="cli">praisonai sandbox status <a href="./src/praisonai/praisonai/cli/commands/sandbox.py">--help</a></code>
* <code title="cli">praisonai schedule add <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
* <code title="cli">praisonai schedule blueprint <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
* <code title="cli">praisonai schedule blueprint-list <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
@@ -903,12 +1137,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>
@@ -917,32 +1146,108 @@ 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 serve a2a <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
* <code title="cli">praisonai serve a2u <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
* <code title="cli">praisonai serve acp <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
* <code title="cli">praisonai serve agents <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
* <code title="cli">praisonai serve docs <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
* <code title="cli">praisonai serve gateway <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
* <code title="cli">praisonai serve lsp <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
* <code title="cli">praisonai serve mcp <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
* <code title="cli">praisonai serve openai <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
* <code title="cli">praisonai serve rag <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
* <code title="cli">praisonai serve recipe <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
* <code title="cli">praisonai serve registry <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
* <code title="cli">praisonai serve scheduler <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
* <code title="cli">praisonai serve serve-callback <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
* <code title="cli">praisonai serve start <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
* <code title="cli">praisonai serve status <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
* <code title="cli">praisonai serve stop <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
* <code title="cli">praisonai serve ui <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
* <code title="cli">praisonai serve ui-gateway <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
* <code title="cli">praisonai serve unified <a href="./src/praisonai/praisonai/cli/commands/serve.py">--help</a></code>
* <code title="cli">praisonai session delete <a href="./src/praisonai/praisonai/cli/commands/session.py">--help</a></code>
* <code title="cli">praisonai session export <a href="./src/praisonai/praisonai/cli/commands/session.py">--help</a></code>
* <code title="cli">praisonai session import <a href="./src/praisonai/praisonai/cli/commands/session.py">--help</a></code>
* <code title="cli">praisonai session list <a href="./src/praisonai/praisonai/cli/commands/session.py">--help</a></code>
* <code title="cli">praisonai session resume <a href="./src/praisonai/praisonai/cli/commands/session.py">--help</a></code>
* <code title="cli">praisonai session show <a href="./src/praisonai/praisonai/cli/commands/session.py">--help</a></code>
* <code title="cli">praisonai setup config <a href="./src/praisonai/praisonai/cli/commands/setup.py">--help</a></code>
* <code title="cli">praisonai setup reset <a href="./src/praisonai/praisonai/cli/commands/setup.py">--help</a></code>
* <code title="cli">praisonai setup setup-callback <a href="./src/praisonai/praisonai/cli/commands/setup.py">--help</a></code>
* <code title="cli">praisonai setup wizard <a href="./src/praisonai/praisonai/cli/commands/setup.py">--help</a></code>
* <code title="cli">praisonai skills check <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
* <code title="cli">praisonai skills create <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
* <code title="cli">praisonai skills eligible <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
* <code title="cli">praisonai skills info <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
* <code title="cli">praisonai skills install <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
* <code title="cli">praisonai skills learn <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
* <code title="cli">praisonai skills list <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
* <code title="cli">praisonai skills search <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
* <code title="cli">praisonai skills validate <a href="./src/praisonai/praisonai/cli/commands/skills.py">--help</a></code>
* <code title="cli">praisonai standardise check <a href="./src/praisonai/praisonai/cli/commands/standardise.py">--help</a></code>
* <code title="cli">praisonai standardise fix <a href="./src/praisonai/praisonai/cli/commands/standardise.py">--help</a></code>
* <code title="cli">praisonai standardise init <a href="./src/praisonai/praisonai/cli/commands/standardise.py">--help</a></code>
* <code title="cli">praisonai standardise report <a href="./src/praisonai/praisonai/cli/commands/standardise.py">--help</a></code>
* <code title="cli">praisonai templates create <a href="./src/praisonai/praisonai/cli/commands/templates.py">--help</a></code>
* <code title="cli">praisonai templates list <a href="./src/praisonai/praisonai/cli/commands/templates.py">--help</a></code>
* <code title="cli">praisonai test info <a href="./src/praisonai/praisonai/cli/commands/test.py">--help</a></code>
* <code title="cli">praisonai test interactive <a href="./src/praisonai/praisonai/cli/commands/test.py">--help</a></code>
* <code title="cli">praisonai test run <a href="./src/praisonai/praisonai/cli/commands/test.py">--help</a></code>
* <code title="cli">praisonai todo add <a href="./src/praisonai/praisonai/cli/commands/todo.py">--help</a></code>
* <code title="cli">praisonai todo done <a href="./src/praisonai/praisonai/cli/commands/todo.py">--help</a></code>
* <code title="cli">praisonai todo list <a href="./src/praisonai/praisonai/cli/commands/todo.py">--help</a></code>
* <code title="cli">praisonai tools info <a href="./src/praisonai/praisonai/cli/commands/tools.py">--help</a></code>
* <code title="cli">praisonai tools list <a href="./src/praisonai/praisonai/cli/commands/tools.py">--help</a></code>
* <code title="cli">praisonai tools test <a href="./src/praisonai/praisonai/cli/commands/tools.py">--help</a></code>
* <code title="cli">praisonai tools validate <a href="./src/praisonai/praisonai/cli/commands/tools.py">--help</a></code>
* <code title="cli">praisonai traces disable <a href="./src/praisonai/praisonai/cli/commands/traces.py">--help</a></code>
* <code title="cli">praisonai traces enable <a href="./src/praisonai/praisonai/cli/commands/traces.py">--help</a></code>
* <code title="cli">praisonai traces list <a href="./src/praisonai/praisonai/cli/commands/traces.py">--help</a></code>
* <code title="cli">praisonai traces status <a href="./src/praisonai/praisonai/cli/commands/traces.py">--help</a></code>
* <code title="cli">praisonai tracker batch <a href="./src/praisonai/praisonai/cli/commands/tracker.py">--help</a></code>
* <code title="cli">praisonai tracker judge <a href="./src/praisonai/praisonai/cli/commands/tracker.py">--help</a></code>
* <code title="cli">praisonai tracker run <a href="./src/praisonai/praisonai/cli/commands/tracker.py">--help</a></code>
* <code title="cli">praisonai tracker tools <a href="./src/praisonai/praisonai/cli/commands/tracker.py">--help</a></code>
* <code title="cli">praisonai tracker tracker-main <a href="./src/praisonai/praisonai/cli/commands/tracker.py">--help</a></code>
* <code title="cli">praisonai train agents <a href="./src/praisonai/praisonai/cli/commands/train.py">--help</a></code>
* <code title="cli">praisonai train apply <a href="./src/praisonai/praisonai/cli/commands/train.py">--help</a></code>
* <code title="cli">praisonai train list <a href="./src/praisonai/praisonai/cli/commands/train.py">--help</a></code>
* <code title="cli">praisonai train llm <a href="./src/praisonai/praisonai/cli/commands/train.py">--help</a></code>
* <code title="cli">praisonai train show <a href="./src/praisonai/praisonai/cli/commands/train.py">--help</a></code>
* <code title="cli">praisonai train train-callback <a href="./src/praisonai/praisonai/cli/commands/train.py">--help</a></code>
* <code title="cli">praisonai ui agents <a href="./src/praisonai/praisonai/cli/commands/ui.py">--help</a></code>
* <code title="cli">praisonai ui bot <a href="./src/praisonai/praisonai/cli/commands/ui.py">--help</a></code>
* <code title="cli">praisonai ui realtime <a href="./src/praisonai/praisonai/cli/commands/ui.py">--help</a></code>
* <code title="cli">praisonai ui ui <a href="./src/praisonai/praisonai/cli/commands/ui.py">--help</a></code>
* <code title="cli">praisonai up logs <a href="./src/praisonai/praisonai/cli/commands/up.py">--help</a></code>
* <code title="cli">praisonai up status <a href="./src/praisonai/praisonai/cli/commands/up.py">--help</a></code>
* <code title="cli">praisonai up up-start <a href="./src/praisonai/praisonai/cli/commands/up.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>
* <code title="cli">praisonai version check <a href="./src/praisonai/praisonai/cli/commands/version.py">--help</a></code>
* <code title="cli">praisonai version show <a href="./src/praisonai/praisonai/cli/commands/version.py">--help</a></code>
* <code title="cli">praisonai version version-callback <a href="./src/praisonai/praisonai/cli/commands/version.py">--help</a></code>
* <code title="cli">praisonai workflow create <a href="./src/praisonai/praisonai/cli/commands/workflow.py">--help</a></code>
* <code title="cli">praisonai workflow list <a href="./src/praisonai/praisonai/cli/commands/workflow.py">--help</a></code>
* <code title="cli">praisonai workflow run <a href="./src/praisonai/praisonai/cli/commands/workflow.py">--help</a></code>
# TypeScript
Types/Exports:
```ts
export { Agent, AgentTeam, Agents, CodeAgent, EmbeddingAgent, OCRAgent, PraisonAIAgents, RealtimeAgent, Router, TASK_STATUS, VideoAgent, VisionAgent, createCodeAgent, createEmbeddingAgent, createOCRAgent, createRealtimeAgent, 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 { // Agent loop
createAgentLoop, // DevTools
enableDevTools, // MCP
@@ -959,7 +1264,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
@@ -981,27 +1285,23 @@ export { // Autonomy Mode
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";
@@ -1010,7 +1310,7 @@ export { // Bot types
FailoverManager, // Enums
SandboxStatus, // Gateway types
type GatewayConfig, // Other types
type ProviderStatus, 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
@@ -1022,11 +1322,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
@@ -1034,22 +1330,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, listHarnessProfiles, 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
@@ -1057,6 +1345,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";
@@ -1064,51 +1361,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
+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.6" \
"praisonai>=4.6.80" \
"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.6" \
"praisonai>=4.6.80" \
"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.6" \
"praisonai>=4.6.80" \
"praisonai[ui]" \
"praisonai[crewai]"
+6 -4
View File
@@ -16,11 +16,13 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
# Create praison config directory
RUN mkdir -p /root/.praison
# Install praisonai-bot with channel dependencies (slim bot image)
# Install praisonai with bot dependencies
RUN pip install --no-cache-dir \
"praisonai-bot[bot]" \
praisonai \
slack-bolt \
slack-sdk
slack-sdk \
"discord.py>=2.0.0" \
"python-telegram-bot>=20.0"
# Set environment variables for directory management
ENV PRAISON_CONFIG_DIR=/root/.praison
@@ -37,4 +39,4 @@ ENV DOCKER_CONTAINER=true
# For webhook mode, expose port 3000
# Default command - tokens from environment
CMD ["praisonai-bot", "bot", "slack"]
CMD ["praisonai", "bot", "slack"]
+3 -3
View File
@@ -17,7 +17,7 @@ services:
SLACK_BOT_TOKEN: ${SLACK_BOT_TOKEN}
SLACK_APP_TOKEN: ${SLACK_APP_TOKEN}
PRAISON_CONFIG_DIR: /root/.praison
command: [ "praisonai-bot", "bot", "slack" ]
command: [ "praisonai", "bot", "slack" ]
restart: unless-stopped
# Discord Bot
@@ -31,7 +31,7 @@ services:
OPENAI_API_KEY: ${OPENAI_API_KEY}
DISCORD_BOT_TOKEN: ${DISCORD_BOT_TOKEN}
PRAISON_CONFIG_DIR: /root/.praison
command: [ "praisonai-bot", "bot", "discord" ]
command: [ "praisonai", "bot", "discord" ]
restart: unless-stopped
# Telegram Bot
@@ -45,7 +45,7 @@ services:
OPENAI_API_KEY: ${OPENAI_API_KEY}
TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN}
PRAISON_CONFIG_DIR: /root/.praison
command: [ "praisonai-bot", "bot", "telegram" ]
command: [ "praisonai", "bot", "telegram" ]
restart: unless-stopped
volumes:
+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():
-1
View File
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
HTTP Approval Example
=====================
-1
View File
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
Slack Approval Example
======================
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
Completions Capability Example
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
Embeddings Capability Example
-1
View File
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
Images Capability Example
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
Moderations Capability Example
+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,4 +1,3 @@
# praisonai: skip=true
"""
Energy Industry Template
========================
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
Transportation Industry Template
================================
+1 -4
View File
@@ -1,4 +1,3 @@
# praisonai: skip=true
#!/usr/bin/env python3
"""
CI Integration Example - Using Doctor in CI/CD pipelines
@@ -20,9 +19,7 @@ def run_doctor_ci():
[sys.executable, "-m", "praisonai", "doctor", "ci"],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=120,
timeout=120
)
# Parse JSON output
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
Agent-centric Embedding Example
-1
View File
@@ -1,4 +1,3 @@
# praisonai: skip=true
#!/usr/bin/env python3
"""
PraisonAI Endpoints Example
@@ -36,12 +36,6 @@ if __name__ == "__main__":
print(f"Custom agent max_rpm: {agent.max_rpm}")
print(f"Custom agent max_execution_time: {agent.max_execution_time}")
print(f"Custom agent max_retry_limit: {agent.max_retry_limit}")
# max_rpm auto-creates a live RateLimiter (not just a stored number)
assert agent_rate_limited.max_rpm == 10
assert agent_rate_limited._rate_limiter is not None
assert agent_rate_limited._rate_limiter.requests_per_minute == 10
print(f"Rate-limited agent limiter: {agent_rate_limited._rate_limiter}")
# Test custom agent
print("\n--- Custom Execution ---")
-1
View File
@@ -1,4 +1,3 @@
# praisonai: skip=true
#!/usr/bin/env python3
"""
Fixed example demonstrating proper guardrail usage with PraisonAI Agents.
+6 -13
View File
@@ -149,28 +149,21 @@ def main():
print(f"\nOriginal document length: {len(content)} characters")
chunks = [{"text": content, "metadata": {"source": doc_path}}]
target_tokens = 2000
# Compress with different ratios
for ratio in [0.3, 0.5, 0.7]:
compressor = ContextCompressor(verbose=False)
result = compressor.compress(
chunks,
query="security verification code",
target_tokens=target_tokens,
compression_ratio=ratio,
compressor = ContextCompressor(
max_tokens=2000,
target_ratio=ratio,
)
result = compressor.compress([content], query="security verification code")
print(f"\nCompression ratio {ratio}:")
print(f" Original tokens: {result.original_tokens}")
print(f" Compressed tokens: {result.compressed_tokens}")
print(f" Actual ratio: {result.compression_ratio:.2f}")
print(f" Actual ratio: {result.compressed_tokens / max(result.original_tokens, 1):.2f}")
# Check if secret code is preserved
compressed_text = " ".join(
c.get("text", "") for c in result.chunks
) if result.chunks else ""
compressed_text = " ".join(result.chunks) if result.chunks else ""
if SECRET_CODE in compressed_text:
print(f" ✅ Secret code preserved in compressed output")
else:
@@ -65,6 +65,7 @@ def main():
name="Policy_Expert",
instructions="You are a policy expert.",
knowledge=[temp_dir],
agent_id="policy_agent_v1", # Knowledge scoped to this agent
)
response = shared_agent.chat("What are the expense report rules?")
@@ -80,6 +81,7 @@ def main():
instructions="You are a personal HR assistant.",
knowledge=[temp_dir],
memory={"user_id": "user_bob"},
agent_id="hr_bot_v2",
)
response = combined_agent.chat("What training is required?")
@@ -1,4 +1,3 @@
# praisonai: skip=true
#!/usr/bin/env python3
"""
ManagedAgent + ClickHouse Persistence — Real conversation with session resume.
@@ -1,4 +1,3 @@
# praisonai: skip=true
#!/usr/bin/env python3
"""
ManagedAgent + MongoDB Persistence — Real conversation with session resume.
@@ -1,4 +1,3 @@
# praisonai: skip=true
#!/usr/bin/env python3
"""
ManagedAgent + MySQL Persistence — Real conversation with session resume.
@@ -1,4 +1,3 @@
# praisonai: skip=true
#!/usr/bin/env python3
"""
ManagedAgent + PostgreSQL Persistence — Real conversation with session resume.
@@ -1,4 +1,3 @@
# praisonai: skip=true
#!/usr/bin/env python3
"""
ManagedAgent + Redis Persistence — Real conversation with session resume.
@@ -1,4 +1,3 @@
# praisonai: skip=true
#!/usr/bin/env python3
"""
ManagedAgent + SQLite Persistence — Real conversation with session resume.
-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,4 +1,3 @@
# praisonai: skip=true
#!/usr/bin/env python3
"""
Custom Tools MCP Server Example
@@ -1,4 +1,3 @@
# praisonai: skip=true
#!/usr/bin/env python3
"""
HTTP Stream MCP Server Example
@@ -1,4 +1,3 @@
# praisonai: skip=true
#!/usr/bin/env python3
"""
MCP Client Example
@@ -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,4 +1,3 @@
# praisonai: skip=true
"""Multi-Agent Shared Session Demo"""
from praisonaiagents import Agent
from praisonai.persistence import create_conversation_store
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
Langfuse Integration Example (Updated for TraceSinkProtocol)
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""MLflow Local Observability Demo (no API key needed)"""
import mlflow
import time
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
ClickHouse — Persistence Example
@@ -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")
-1
View File
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
Minimal Agent with Database Persistence
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
MongoDB StateStore — Full Persistence Example
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
MySQL ConversationStore — Full Persistence Example
@@ -1,4 +1,3 @@
# praisonai: skip=true
"""
PostgreSQL ConversationStore — Full Persistence Example
Loaded 100 of 3735 files, more files were not shown because too many files have changed in this diff. Show more