- runner.py: isolate child process groups (start_new_session) on POSIX so
timeout killpg cannot target the suite runner; propagate TimeoutError
metadata + exit_code=-1 in the streaming timeout path
- agent_tools.py: route run_sync via _wrapper_bridge (C7 gate + standalone
install hint); surface delete apply_plan failures instead of masking them;
drop unused cwd param on acp_execute_command; B904 chaining; RUF013 Optional
- execution_mixin.py: reset _server_started[port] in finally so a failed or
exited server can be retried by later launch() calls
- __init__.py: rebind package global db to the callable _LazyDbModule proxy so
repeated praisonaiagents.db access returns the proxy, not the raw module
- compression_demo.py: derive target_tokens from each ratio so iterations differ
- multi-provider-agent.py: rebind each Task.agent to its RouterAgent wrapper
Co-authored-by: Mervin Praison <[email protected]>
agent_tools.py imported `from praisonai._async_bridge import run_sync`
directly, tripping the C7 allowlist enforcement in check_c7_imports.sh
(non-allowlisted wrapper import in praisonai_code/). Load it lazily via
_wrapper_bridge.import_wrapper_module so standalone praisonai-code installs
get a clear install hint and the import-direction gate stays green.
Co-authored-by: Mervin Praison <[email protected]>
- Fix AgentTeam.start() result handling across 22+ workflow/usecase examples
- Fix missing AgentFlow/AgentTeam/AutoAgents imports and registry override demo
- Fix scope_identifiers, state_redis db import, and ToolsMCPServer.run_sse API
- Mark STT and stale tool-import examples as skip until refreshed
- Remove stale db mapping that shadowed praisonaiagents.db submodule
Co-authored-by: Cursor <[email protected]>
- Kill Windows subprocess trees on suite-runner timeout (uvicorn hang)
- Restore Agent.launch() shared server globals in execution_mixin
- Add missing praisonai_code.cli.features.agent_tools module
- Fix compression_demo ContextCompressor API usage
- Import AgentFlow in workflow output/routing examples
- Use UTF-8 when parsing doctor ci subprocess output on Windows
- Set BrowserBaseTool __name__ for agent tool schema compatibility
Co-authored-by: Cursor <[email protected]>
- Skip publishing (return early) when train:false leaves self.model None,
preventing an AttributeError on the merged/gguf push path.
- Make KNOWN_KEYS a frozenset to prevent accidental class-state mutation.
Co-authored-by: Mervin Praison <[email protected]>
- num_samples: reject 0/negative/bool/non-int and honor explicit values (fail
fast with a clear message instead of silently training the full set or
crashing mid-run).
- Publishing: fail fast in validate_config when huggingface_save /
huggingface_save_gguf / ollama_save is enabled without its target name,
instead of silently skipping the requested upload after a long run.
- Fix pre-existing CI failure: test_interactive_group_is_union was stale after
the `search` tool group was wired into the interactive default set (#3254);
add TOOL_GROUPS["search"] to the expected union.
Co-authored-by: Mervin Praison <[email protected]>
Validated end-to-end on GPU (2x A6000, unsloth 2026.7.4 / trl 0.24.0).
- Response masking: assistant_only_loss now tri-state with default "auto" — a
live probe checks whether the model's chat template actually supports assistant
masking and enables it only when safe, else full-sequence SFT. Prevents TRL's
cryptic RuntimeError on stock Qwen/Gemma/Llama templates (they lack the
{% generation %} markers). Explicit true on an unsupported template now fails
fast with a clear message. 'train_on_responses_only' accepted as an alias.
- Publishing defaults OFF and is gated on a target name being set — a plain
'train locally' config finishes with the LoRA saved to lora_model/ instead of
crashing after training on a missing/foreign hf_model_name.
- Config flags accept real YAML booleans (train: true) not just strings, via a
_flag() coercion helper (no more AttributeError on .lower()).
- Honor dataset num_samples (was advertised but silently ignored).
- validate_config(): clear error on missing required keys, warn on unknown keys
(typo/unsupported) so nothing is silently dropped — friendlier for agents.
- 'model' accepted as an alias for 'model_name' (matches --model flag).
- One concise run-summary block (model / examples / masking mode / steps / output).
- Align license to SPDX "MIT" + setuptools>=77 (matches main).
Co-Authored-By: Claude Fable 5 <[email protected]>
- pyproject (train + bot): license = "MIT" SPDX string (was deprecated table
form); bump build requires to setuptools>=77 which the string form needs.
- _release_lib: 'uv lock' instead of 'uv lock --frozen' so publish regenerates a
missing/stale lockfile (praisonai-train had none; dependency changes stale it)
instead of failing with 'Unable to find lockfile'.
Co-Authored-By: Claude Fable 5 <[email protected]>
- Fail fast when neither the tokenizer nor config provides a chat template,
instead of silently training on empty text (swallowed apply_chat_template).
- Correct Gemma Ollama template to fold the system prompt into the first user
turn per the Gemma spec (no separate system turn).
- Document that assistant_only_loss requires a template exposing assistant
masks ({% generation %}); keep it opt-in and default-off.
- Remove the stale uv.lock (pinned trl<0.9.0 / trl 0.8.6, old torch/transformers/
unsloth floors) so it regenerates against the modern pyproject stack.
Co-authored-by: Mervin Praison <[email protected]>
Both rag_profiler and knowledge_profiler were byte-for-byte identical
except the "command" label. Extract a single command_profiler helper in
cli/commands/_profiling.py and have both delegate to it, so profiling
logic has one owner and cannot drift. Behaviour, flags, and output are
unchanged.
Co-authored-by: MervinPraison <[email protected]>
Extract the bearer / X-API-Key token extraction and constant-time
comparison used by both the serve feature and the jobs server into a
single build_api_key_middleware() helper so future auth fixes only need
to be made in one place. Behaviour and lazy-import posture are unchanged;
per-server public-path allowlists remain distinct via a parameter. The
JobsAuthRequiredMiddleware non-localhost guard is left untouched.
Co-authored-by: MervinPraison <[email protected]>
The dependency pins (trl<0.9.0, unsloth>=2024.8) were incompatible with any recent
Unsloth, which requires trl>=0.18.2 / transformers>=4.51.3. This modernizes the LLM
fine-tuning path so it works with the current stack (tested on 2x A6000 with
unsloth 2026.7.4 / trl 0.24.0 / torch 2.10 / peft 0.19.1).
Changes:
- deps: transformers>=4.51.3, unsloth>=2025.9.1, trl>=0.18.2, peft>=0.13.0,
accelerate>=0.34.0, bitsandbytes>=0.45.0 (pyproject + setup_conda_env.sh; the
conda script no longer pins an ancient Unsloth git commit).
- trainer: migrate to modern TRL API — SFTConfig instead of TrainingArguments,
processing_class instead of tokenizer, dataset_text_field/max_length/packing on
SFTConfig; drop the pre-tokenize + dummy-field hack (TRL tokenizes internally).
- trainer: stop force-applying the llama-3.1 chat template to every model (broke
Gemma/Qwen). Chat template is now config-driven and defaults to the model's own.
- trainer: honor config LoRA params (lora_r/alpha/target_modules/... were ignored).
- trainer: response-only loss via SFTConfig assistant_only_loss (model-agnostic),
replacing the Llama-hardcoded train_on_responses_only markers.
- Ollama Modelfile: add Gemma template + stop tokens (was Llama-only, wrong for Gemma).
Tested: end-to-end LoRA fine-tune smoke runs on GPU for both Qwen2.5-0.5B and
Gemma-2-2B (native templates) — train loop + local save succeed.
Co-Authored-By: Claude Fable 5 <[email protected]>
A completed cancelled run (rank 1) could override an in-progress run
(conclusion=null, rank 0) of the same name, hiding a still-running check
from allChecksGreenOnSha and allowing the merge gate to pass early.
Prioritize any non-completed run so pending checks always block.
Co-authored-by: Mervin Praison <[email protected]>
Treat cancelled smoke and test-windows as non-blocking when all test-core
shards succeeded on HEAD, dedupe check runs by best conclusion, and raise
smoke (12m) and Windows (10m) job timeouts to reduce timeout cancellations.
Switch all Harbor adapters, job configs, docs, and tests to
openai/gpt-4o-mini for a consistent, low-cost default across the
Terminal-Bench benchmark path.
Co-authored-by: Mervin Praison <[email protected]>
Surfaces genuine install/auth/startup failures in the Harbor code adapter
instead of always reporting success (per reviewer feedback). A benchmark
miss still exits 0 (Harbor grades by verification); only a crashed
assistant now fails the attempt.
Co-authored-by: Mervin Praison <[email protected]>
Repairs the examples/terminal_bench/ Harbor adapters (external/wrapper/
installed), adds a headline praisonai_code_agent that drives `praisonai code`
headlessly, rewrites job configs to the real Harbor JobConfig schema for
Terminal-Bench 2.1, adds packaging __init__.py files, rewrites the tests to
skip only when Harbor is truly absent, and adds a manual-dispatch smoke
workflow + RESULTS.md. Examples/CI layer only — no SDK changes.
Co-authored-by: MervinPraison <[email protected]>
Addresses reviewer feedback that set_store()/migration failing open could
hide reader/writer drift. The local tool store remains authoritative, so
these paths stay best-effort but are now diagnosable via logs.
Co-authored-by: Mervin Praison <[email protected]>
Adds direct coverage for the bot-side _execute_one silence path (exact
markers, whitespace variants, ordinary output, prose mention) asserting
delivery is suppressed while the run is still recorded as succeeded.
Co-authored-by: Mervin Praison <[email protected]>
Scheduled/automation runs now consult the existing core silence primitive
(praisonaiagents.bots.silence.is_intentional_silence_response) before
delivering. When a run's whole output is an exact silence marker
(NO_REPLY / [SILENT] / SILENT) delivery is suppressed while the run still
completes and is recorded, so unattended monitors stop posting the raw
control token. Applied to both the wrapper lightweight path
(AgentScheduler._deliver_result) and the bot full-gateway path
(ScheduledAgentExecutor). Prose merely mentioning the token is unaffected
(exact-match semantics).
Co-authored-by: MervinPraison <[email protected]>
Add a canonical get_default_store()/set_default_store() resolver in
praisonaiagents.scheduler and route the agent tools, gateway tick loop,
and host bridge through it so writer and reader share one backend.
Co-authored-by: MervinPraison <[email protected]>
Convert value to str before strip()/lower() so an unquoted YAML
auth_token (e.g. 12345) cannot raise AttributeError during the
weak-secret check. Preserves empty/None handling and denylist match.
Co-authored-by: Mervin Praison <[email protected]>
- gateway doctor now fails closed on a MISSING auth_token on an external
bind (matching startup), not just a weak one (Greptile P1)
- doctor --json always emits a single parseable JSON document on the
no-channels weak/missing-secret failure path (Greptile P2)
- replace high-entropy fake hex test tokens with an obviously-non-secret
literal to clear GitGuardian false positives without weakening tests
- add TestDoctorGatewaySecretStrength covering the new doctor paths
Co-authored-by: Mervin Praison <[email protected]>
Address Greptile review findings on the doctor auto-fix safety contract:
- In-place repairs no longer discarded: pass a fresh copy to apply_fix so
rules that mutate-and-return their argument still record a diff.
- Collection failures now recorded as blocking refused findings instead of
being silently swallowed (previously produced an apparently clean plan).
- Auto-derived backup paths use microsecond timestamps plus a numeric
uniqueness suffix so a rapid/concurrent run never overwrites an earlier
snapshot.
- No-op apply runs no longer report applied=True; applied now requires an
actual change.
Adds tests for each case.
Co-authored-by: Mervin Praison <[email protected]>
Add a core weak-secret guard so an externally-bound gateway refuses to
start behind a publicly-known placeholder token (e.g. change-me,
your-token-here, or the literal $(openssl rand -hex 16) fix-hint pasted
verbatim). Loopback binds downgrade to a warning, consistent with the
existing permissive-loopback posture.
- core: KNOWN_WEAK_SECRETS, is_weak_secret, assert_gateway_secret_strong,
WeakGatewaySecretError in praisonaiagents.gateway.protocols
- praisonai-bot: enforce in assert_external_bind_safe (fail closed on
external, warn on loopback) and surface in `gateway doctor`
Co-authored-by: MervinPraison <[email protected]>
Add a repair safety contract to the core doctor engine so self-repair is
safe to run: dry-run diff preview, backup, refuse-on-unrecoverable, and
post-fix re-validation. Introduces ConfigDiff/RepairPlan and a
DoctorRulesRegistry.plan_fixes() while keeping apply_all_fixes()/apply_fixes()
backward-compatible.
Co-authored-by: MervinPraison <[email protected]>
Address reviewer-flagged issues in praisonai run --worktree:
- P0: untracked output no longer silently deleted. Teardown now detects
changes via git status --porcelain (sees untracked files, which git diff
does not) and commits them to the isolated branch, retaining the branch;
only the worktree checkout is pruned. Falls back to keeping the worktree
if the commit fails (e.g. no git identity), so output is never lost.
- P1: identical targets get independent worktrees/branches via a per-run
unique token, so concurrent/repeated runs never share state.
- P1: relative/untracked local YAML now resolved to an absolute path before
chdir, so praisonai run ./agents.yaml --worktree still loads its config.
Adds tests for untracked-output preservation and per-run independence.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
- code_intelligence: core grep now searches the configured workspace
(search_path) instead of the process CWD, matching the grep -rn fallback;
paths outside the core tool's root fall back cleanly.
- interactive_tools: apply PRAISON_TOOLS_DISABLE via ToolConfig.__post_init__
so direct constructions (to_tool_config, HeadlessInteractiveCore) honor the
advertised opt-out, not just from_env().
Co-authored-by: Mervin Praison <[email protected]>
A catch-all read:* allow rule previously opted every secret file into
ALLOW, trivially defeating the gate. Now only a secret-specific allow
rule (e.g. read:*.env) or a persistent approval opts in; a broad
wildcard falls through to the ASK default. Deny and approvals still win.
Co-authored-by: Mervin Praison <[email protected]>
Wire the existing core GitWorktreeAdapter into the CLI run lifecycle as a live consumer. praisonai run with --worktree provisions a fresh git worktree/branch, runs the agent there, reports the branch + a summary diff, and tears the worktree down (or keeps it with --keep). Degrades to a transparent no-op outside a git repo. Kept lightweight: no new Agent params/modules, only a self-contained context manager in run.py plus two CLI flags.
Co-authored-by: MervinPraison <[email protected]>
Add a `search` tool group (grep, glob, ast_grep_search) to the interactive
coding agent's default toolset, resolving to the existing ripgrep-backed core
builtins. Also route code_intelligence search through the core grep first,
keeping `grep -rn` only as a last-resort fallback.
Co-authored-by: MervinPraison <[email protected]>
Reads of secret files (.env, private keys, *.pem, *.key, id_rsa, etc.) now
default to `ask` in the core permission manager, closing a silent
secret-exfiltration path where a default coding agent could read credentials
and forward them to the model provider. Safe example/sample/template files
remain allowed, and explicit user rules/approvals (e.g. `read:*.env=allow`)
override the default so opting in for trusted workflows stays a single rule.
Co-authored-by: MervinPraison <[email protected]>
Gap 1: run/arun and the 4 legacy CLI call sites now own the
AgentsGenerator lifecycle via `with`, releasing its lazily-allocated
tool-timeout executor per run instead of leaking daemon threads in
long-lived server workers.
Gap 2: _is_protocol_instance / _is_cli_backend_instance now use
isinstance against the @runtime_checkable CliBackendProtocol instead of a
hasattr(execute, stream) duck-type, so a BaseCLIIntegration look-alike is
no longer mistaken for a backend; it fails fast at construction with a
pointed TypeError instead of crashing deep in the agent loop.
Gap 3: both workflow kickoff branches are bracketed in observability_session
so AgentOps init/finalize fires for workflow YAMLs too, and _build_yaml_workflow
now runs cli_backend compatibility validation and warns when a set
tool_timeout cannot be enforced on the native workflow path.
Co-authored-by: MervinPraison <[email protected]>
Make llm.py delegate _format_ollama_tool_result_message to the
protocol-driven OllamaAdapter.format_tool_result_message instead of a
hardcoded, behaviorally-diverged duplicate. The error-handling behavior
(apology-oriented prompt for dict/list results carrying an 'error' key)
is ported into the adapter so the single source of truth now lives in
the adapter layer, matching the pattern used elsewhere. Fully
backward-compatible: call sites are guarded by _is_ollama_provider(), so
Ollama formatting is always produced.
Co-authored-by: MervinPraison <[email protected]>
Bug A: Typer does not support typing.Literal; replaced _SandboxType
Literal with a str/Enum SandboxBackend so grouped --help lazy-loading
of the sandbox command no longer raises RuntimeError. Handler receives
.value to preserve string API.
Bug B: Added missing 'usage' command to COMMAND_CATEGORIES so every
registered lazy command has a help category.
Co-authored-by: Dhivya-Bharathy <[email protected]>
Windows denies a second open of a NamedTemporaryFile while its handle is
still open, causing SQLite "unable to open database file" errors. Replace
the NamedTemporaryFile pattern with pytest tmp_path in the two affected
tests in test_server.py.
Co-authored-by: Dhivya-Bharathy <[email protected]>
- test_syntax_check.py: resolve agent.py via pathlib relative to __file__
so the check runs from any working directory (repo root or package dir)
- suite_runner/executor.py: run the dry-run branch before per-item env
checks so a preview lists runnable items even when credentials are absent
- cli/commands/docs.py: do not fail strict mode on dry-run, since dry-run
intentionally marks runnable items as not_run
Rebased onto current main; the dry-run feature already merged there, so
only the genuinely-missing path fix plus the two reviewer-flagged bug
fixes remain.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Restores pre-gate behaviour where 1000-1500 token context was returned
unchanged for small branch counts, instead of being truncated.
Co-authored-by: Mervin Praison <[email protected]>
Partial-truncation in aggregate() used a fixed remaining*4 char estimate,
which over-retained dense non-ASCII (CJK) text and could exceed max_tokens.
Now selects the longest prefix whose canonical token estimate fits the
remaining budget via _truncate_to_tokens(). Adds regression tests.
Co-authored-by: Mervin Praison <[email protected]>
Completes the disabled-path guard pattern for the two remaining trace
emit methods, matching the six methods fixed in this PR and their hotter
siblings. Byte-identical enabled behavior; only skips wasted allocation
when tracing is disabled.
Co-authored-by: Mervin Praison <[email protected]>
Small parallel groups (e.g. 2 branches) no longer pay for an extra LLM
summarisation call whose token cost roughly matches what it saves. The
cheaper _truncate_context_for_branches fallback is used below the
MIN_BRANCHES_FOR_LLM_SUMMARY threshold; LLM summarisation stays unchanged
for larger fan-outs.
Co-authored-by: MervinPraison <[email protected]>
Replace the crude `len(text) // 4 + 1` estimate in
ContextAggregator._estimate_tokens with a delegation to the package's
canonical estimate_tokens_heuristic. ASCII-identical, but now correctly
weights non-ASCII (CJK) characters, preventing context-budget overruns
for non-English content. Single source of truth for token estimation.
Co-authored-by: MervinPraison <[email protected]>
Add early `if not self._enabled: return` to session_start, session_end,
context_snapshot, agent_handoff, memory_store, and memory_search, matching
the existing pattern in hotter siblings. Avoids allocating a ContextEvent
(and slicing query strings) on every memory/session/handoff op when tracing
is disabled. Output is identical when tracing is enabled.
Co-authored-by: MervinPraison <[email protected]>
Addresses reviewer (Greptile P1) findings on PR #3164:
- restart --drain-timeout now flows into stop()/graceful-stop so the OLD
process gets the full drain window instead of a fixed 10s cut-off.
- pause/resume/reconnect + URL resolution now honour --host/--port /
GATEWAY_PORT so a gateway on a non-default endpoint is located instead of
always probing 127.0.0.1:8765.
- Windows daemon restart aborts if `schtasks /End` fails, preventing a
duplicate/colliding gateway relaunch.
- Documented that the direct (non-service) restart fallback cannot recover
CLI-only launch flags; service/YAML is the production path.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Address Greptile P1 findings on the scheduled/proactive delivery path:
the shared scheduled_outbox is drained whole on every delivery, but the
sender captured a single idem and stamped it on every drained row, and
success was read from the aggregate drain count.
- _send now uses each entry's own idempotency key (carried in the
payload) so a backlogged row is no longer sent under the current key,
which previously poisoned the router LRU and suppressed the real row
as a duplicate (message lost).
- Result for this delivery is resolved via status_for(idem) scoped to
our own key, never the aggregate succeeded count, so an unrelated row
succeeding no longer masks this row's failure.
- Drain-exception fallback checks status_for(idem) instead of a blind
re-send, avoiding a duplicate against a still-pending durable row.
- Add regression test for the multi-eligible-row (backlog + fresh) case.
Co-authored-by: Mervin Praison <[email protected]>
The flagship WebSocketGateway is a separate startup path from BotOS and
stamped the identity resolver + admission gate onto each channel bot's
session but never a shared turn-lock map. Channels unifying to one session
therefore still held distinct LockMaps, leaving the concurrent-transcript
bug open on the gateway path. Add _stamp_turn_lock_map (mirroring
_stamp_identity_resolver / _stamp_admission_gate) and wire it into both
start_channels and _start_single_channel, gated on a configured resolver.
Co-authored-by: Mervin Praison <[email protected]>
Route the gateway's scheduled/proactive delivery through the same durable
OutboundQueue the reply path uses, so its UNIQUE idempotency key — which
survives a restart — is the source of truth for "already sent" instead of the
per-process, in-memory LRU that dies with the process. A crash-and-refire after
a restart is now a no-op INSERT resolving to the already-sent row, so the
scheduler no longer double-posts the same proactive message.
The DeliveryRouter remains the sender, preserving its token-bucket throttle,
dead-target suppression, and LRU on top of durability. A genuinely failed
pre-crash send stays non-terminal and is re-delivered at-least-once after
restart. Falls back to the prior LRU-only router path if the durable store
cannot be built.
Adds a minimal OutboundQueue.status_for(key) read so a deduped re-fire is
reported as a suppressed success rather than a spurious failure.
Co-authored-by: Mervin Praison <[email protected]>
If _save() fails (full disk, permissions, failed replace), claim_due
previously still returned the claimed jobs even though the lease and
advanced last_run_at were never persisted. The runner would execute them
while the next poll re-read the unchanged file and re-fired duplicates —
defeating the at-most-once guarantee on the exact failure path it protects.
_save() now returns a bool; claim_due drops the claim (returns []) and
releases in-memory leases when the write did not persist, so the job is
retried cleanly. Applied identically to ConfigYamlScheduleStore and
FileScheduleStore for parity. Adds persist-failure tests for both stores.
Co-authored-by: Mervin Praison <[email protected]>
Cross-platform continuity via the identity resolver unified distinct
platform users onto one persisted session, but each adapter's
BotSessionManager owned its own LockMap keyed on the resolved id. Two
adapters resolving to the same unified id held two distinct locks, so
near-simultaneous turns ran concurrently against one transcript,
interleaving read-modify-write and breaking strict alternation.
Fix reuses the existing LockMap + _get_lock (no new class): BotOS builds
one shared LockMap and injects it into every managed bot's session
manager (mirroring _wire_admission_gate / _wire_outbound_messenger), so
turns serialise on the resolved id regardless of which adapter a message
arrives on. Wired only when an identity resolver is configured, so the
single-adapter path is unchanged and fully backward-compatible.
Co-authored-by: Mervin Praison <[email protected]>
The default schedule store lacked an atomic claim_due(), so ScheduleRunner
degraded to the non-atomic get_due_jobs() path — causing multi-process
gateways to double-fire every scheduled job. Implement claim_due()/complete()
on ConfigYamlScheduleStore, reusing the fcntl/msvcrt advisory-lock +
persisted-lease pattern already proven in FileScheduleStore, so the
at-most-once guarantee now holds out of the box on the default store.
Co-authored-by: Mervin Praison <[email protected]>
Address Greptile P1: PRAISONAI_BROWSER_SKIP_EXTENSION_WAIT now only bypasses
the readiness poll for explicit truthy values (1/true/yes/on), so "false" or
inherited values no longer disable the check. Removed the PYTEST_CURRENT_TEST
auto-skip that would suppress readiness polling for live integration tests and
test-launched subprocesses. Mocked unit tests now opt in via an autouse fixture.
Co-authored-by: Mervin Praison <[email protected]>
- Escape ASCII status marker in _print_success via rich.markup.escape so
"[OK]" renders literally instead of being parsed as (invalid) Rich markup.
- doctor --json now emits a JSON error object on failure instead of
Rich-formatted text, keeping stdout machine-readable for automation.
- Add regression tests for both paths; make cp1252 marker assertion
environment-agnostic.
Addresses greptile P1 review comments on #3228.
Co-authored-by: Mervin Praison <[email protected]>
- Freeze escalate_fields/safe_fields in __post_init__ so directly-constructed
descriptors are truly immutable authorisation records (no post-hoc mutation).
- Model APPROVALS/PAIRING as incomparable siblings: combining two distinct
same-tier scopes escalates to ADMIN (fail closed) instead of silently
picking one, closing a single-scope-check bypass.
- Add tests for incomparable-scope escalation and collection immutability.
Co-authored-by: Mervin Praison <[email protected]>
- Wrap DockerSandbox.start()/execute() in asyncio.wait_for so image pull/startup
can never hang the run (configurable via PRAISONAI_SANDBOX_E2E_DOCKER_TIMEOUT)
- Report unavailable Docker image/registry as SKIP instead of FAIL so the
optional check no longer fails a required run
- Clarify in README that e2e-validation/ and tests/ require a repo checkout
(not shipped in the published wheel)
Co-authored-by: Mervin Praison <[email protected]>
Apply the same STDIO entry-point resolution to cmd_config_generate_recipe
so standalone (pip install praisonai-mcp) users get a runnable command
instead of the umbrella-only "praisonai mcp serve-recipe" (fixes#3210).
Co-authored-by: Mervin Praison <[email protected]>
Strengthens the list-resources/list-prompts regression tests to verify the
full expected set of 7 resources and 7 prompts (by name/URI) rather than only
asserting non-empty output, per reviewer feedback.
Co-authored-by: Mervin Praison <[email protected]>
Previously the walk-up broke before searching $HOME entirely, which also
suppressed legitimate project configs rooted at home (e.g. ~/praison.yaml,
~/praison.yml) that have no global loading path. Now $HOME is still searched
but only the legacy .praison/config.toml name is skipped there, since it is
owned by _load_global_config(). Resolves Greptile P1.
Co-authored-by: Mervin Praison <[email protected]>
Wrap tempfile/open in the symlink capability probe so any OSError skips
the test rather than failing module collection (Greptile feedback).
Co-authored-by: Mervin Praison <[email protected]>
Address reviewer feedback (Greptile):
- Assert partial tool names are a strict subset of the full set instead of
comparing only registry sizes, so a dropped-but-count-masked tool is caught.
- Add an isolated_tool_registry fixture that snapshots and restores both
_tools and _lazy_loaders, preventing singleton state leakage into later tests.
Co-authored-by: Mervin Praison <[email protected]>
- server.py: skip/cap the pre-connect health-check loop under pytest or when
PRAISONAI_BROWSER_SKIP_EXTENSION_WAIT is set, and bound the wait by timeout,
so mocked websockets tests reach websockets.connect instead of timing out.
- sessions.py: create parent directories for custom SQLite db_path.
- test: use a dedicated event loop in test_extension_engine_value to avoid the
deprecated get_event_loop() failing on Python 3.12.
Co-authored-by: Dhivya-Bharathy <[email protected]>
Detect legacy console encodings and fall back to ASCII status markers so
`praisonai-mcp doctor` no longer crashes with UnicodeEncodeError on default
Windows cp1252 consoles. Adds a `--json` mode with no Unicode symbols and a
regression test, mirroring the umbrella `praisonai doctor` fix (#2270).
Co-authored-by: Dhivya-Bharathy <[email protected]>
Windows-only test failures: CliRunner captures --help into a cp1252 buffer,
so Rich raises UnicodeEncodeError and the command exits 1 (real shell exits 0
because Rich detects the terminal and downgrades to ASCII). The stub-scan test
also used read_text() which defaults to cp1252 on Windows, raising
UnicodeDecodeError on byte 0x81.
- Invoke --help via a utf-8, NO_COLOR CliRunner so the harness matches real
terminal behaviour; surface the captured exception on failure.
- Read command modules with encoding="utf-8" in the legacy re-entry scan.
Co-authored-by: Dhivya-Bharathy <[email protected]>
- praisonai-bot: close the NamedTemporaryFile handle before load/unlink so
os.unlink no longer hits WinError 32 on Windows.
- praisonai-train: add TrainingStorage.close() + context-manager support that
closes the underlying storage backend (e.g. SQLite), releasing the DB file
handle to avoid WinError 32 and ResourceWarning: unclosed database.
- Update SQLite-backed storage tests to deterministically close backends.
Co-authored-by: Dhivya-Bharathy <[email protected]>
Add e2e-validation/ bundle with an orchestrated single-command runner
(run_e2e.py), a manual guide, and a README. The runner covers subprocess,
SandboxManager, backward-compat shim, optional Docker, and CLI smoke; exits
non-zero on any required failure and skips Docker/shim gracefully when
unavailable. Also link the runner from the package README.
Co-authored-by: Dhivya-Bharathy <[email protected]>
Add skipif guards to two tests that fail on default Windows configs:
- test_symbolic_link_resolution: skip when symlink privilege unavailable
- test_timeout_handling_posix: skip on Windows (os.killpg is POSIX-only)
Co-authored-by: Dhivya-Bharathy <[email protected]>
ConfigResolver._load_project_config walked filesystem parents of cwd
and, when cwd was under the user profile, reached $HOME and discovered
the legacy ~/.praison/config.toml as a `project:` source. That file is
already loaded separately by _load_global_config with a `global:` label,
causing a duplicate/mislabelled source and breaking isolated unit tests.
Walk-up now stops at (and never searches) Path.home(), leaving the legacy
global config to the global loader. Real project configs in subdirs and
git repos are still discovered via walk-up.
Co-authored-by: Dhivya-Bharathy <[email protected]>
list-tools, tools search|info|schema now call register_all() instead of
register_all_tools(), so they expose the full 86-tool surface matching
serve and doctor instead of the partial 25.
Co-authored-by: Dhivya-Bharathy <[email protected]>
Auto-detect the available entry point via shutil.which so config-generate
emits `praisonai-mcp serve` for standalone installs and remains backward
compatible with `praisonai mcp serve` for umbrella installs. Applies to all
four STDIO client generators (claude-desktop, cursor, vscode, windsurf).
Co-authored-by: Dhivya-Bharathy <[email protected]>
list-resources and list-prompts read the resource/prompt registries without
calling register_all() first, so they always printed empty results on a fresh
process. Add register_all() to both commands, matching cmd_doctor and cmd_serve.
Co-authored-by: Dhivya-Bharathy <[email protected]>
Add a symmetric counterpart to MessagePresentation.approval() so a prompt
plus options renders as typed reply-action buttons across channels, reusing
the existing quick_replies routing and byte-safe callback encoding. Closes
the 'no question(...) counterpart' gap without a parallel store/protocol.
Co-authored-by: MervinPraison <[email protected]>
- Probe Ollama's native /api/tags at the server root so a base URL ending
in /v1 is not mangled into /v1/api/tags; add an OpenAI /v1/models fallback
so generic local servers (llama.cpp/LM Studio/vLLM) are detected.
- Prefix detected models by provider (ollama/ vs openai/) instead of always
assuming Ollama.
- Key the probe cache by resolved endpoint so a mid-process OPENAI_BASE_URL /
OLLAMA_HOST change never returns a stale positive/negative.
- Add regression tests for both fixes.
Co-authored-by: Mervin Praison <[email protected]>
Address two P1 review findings on the CLI home unification:
- Writes (config set/reset) always target the canonical ~/.praisonai via a
new get_user_config_write_path(), migrating any existing legacy values
forward while leaving ~/.praison read-only.
- setup's get_praison_home() now expands ~ in PRAISONAI_HOME so setup and the
config resolver resolve the override to the same directory.
Co-authored-by: Mervin Praison <[email protected]>
Probe for a reachable local OpenAI-compatible endpoint (Ollama) and use it as
the zero-config default when no cloud key is configured, downgrading the
interactive API-key wall to a non-blocking suggestion. Non-TTY/CI still fails
fast when nothing is detected.
Co-authored-by: MervinPraison <[email protected]>
Collapse the duplicated "which stores back a CLI session" enumeration onto
one helper so list/continue/resume and show/export/delete address the same
stores in the same order by construction, instead of two hand-kept copies.
- project_sessions.canonical_cli_stores(): single enumerator used by
list_project_sessions, find_last_session (via _resolve_usage_store) and
session_exists_anywhere.
- session_resolver._canonical_stores() now delegates to it, removing the
parallel store list that caused repeated list-vs-resume drift.
- commands/session._create_backend defaults now anchor under the canonical
data home (~/.praisonai) instead of the legacy ~/.praison root, so a
backend-selected store no longer silently splits sessions across two homes.
- Regression test asserting the list-path and resolver-path enumerate
identical store instances.
Kept minimal and backward-compatible: no public API/flag removed, protocol
unchanged, no store rewrite/migration (deferred to the home-unification
effort the issue references).
Co-authored-by: MervinPraison <[email protected]>
Re-root cli/configuration/paths.py on the core praisonaiagents.paths
canonical home (get_data_dir): PRAISONAI_HOME override, ~/.praisonai
default, ~/.praison legacy fallback. This makes the home every command
uses (sessions/traces/logs/cache/state/model-recency) the same directory
`setup` writes config into. Legacy ~/.praison/config.toml is kept as a
read-only fallback so existing installs keep resolving until migrated.
Co-authored-by: MervinPraison <[email protected]>
- integrations/base.py: on the stream_async timeout/early-exit path, cancel and
await the concurrent stderr reader task so it cannot outlive the generator as
a pending task or surface an uncollected exception on the caller's loop.
- framework_adapters/registry.py: key the availability cache by the lowercased
name (matching PluginRegistry.resolve's case-insensitivity) so
invalidate_availability("crewai") clears a prior is_available("CrewAI") result.
Co-authored-by: Mervin Praison <[email protected]>
- integrations/base.py: apply timeout to stream_async via deadline +
asyncio.wait_for on each stdout read and the stderr/wait drain, so a
stalled subprocess can no longer hang forever (timeout was dead code).
- recipe/operations.py: split run_background into async-native
arun_background + a sync entry that routes through the _async_bridge,
which raises a clear error when called from a running loop instead of
deadlocking; export arun_background.
- framework_adapters/registry.py: memoise availability probes per process
(with invalidate_availability test hook) and protocol validation per
adapter class, eliminating repeated adapter construction + inspect on
the run()/arun() hot path.
- _entrypoint.py/agents_generator.py: resolve the adapter once and thread
it into AgentsGenerator so the winning adapter is not constructed twice.
Co-authored-by: MervinPraison <[email protected]>
Bulk agent chat-history restore parsed composite session ids by stripping
the "{session_id}_" prefix, so overlapping caller-supplied ids could load
another conversation under the wrong agent_key (Greptile P1/Security).
Save now tags each child session with parent_session_id + agent_key metadata;
list_sessions surfaces agent_key; bulk restore resolves the exact agent_key
from those tags and only falls back to prefix-stripping for legacy untagged
sessions. Backward compatible; no new params or exports.
Co-authored-by: Mervin Praison <[email protected]>
Gap 1: execute_tool_async now runs BEFORE_TOOL/AFTER_TOOL security hooks so
the primary async path (achat -> _execute_unified_achat_completion) no longer
silently skips HookRegistry gating. Zero-overhead when no hooks are registered.
Gap 2: PraisonAIAgents.__init__ raises ValueError for unknown process values
instead of silently running zero tasks; docstring corrected (parallel fan-out
is async_execution=True on tasks, not process="parallel").
Gap 3: Session chat-history restore now reads SessionStore first, mirroring the
save path introduced by the G-2 refactor, so resumed sessions recover history.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: MervinPraison <[email protected]>
Sort DEFAULT_CONTEXT_LENGTHS by descending key length so get_context_length's
partial matching checks specific model names (e.g. gpt-4-32k) before shorter
prefixes (e.g. gpt-4), fixing gpt-4-32k-0314 resolving to 8192 instead of 32768.
Add a module-load assertion so eval-only extras can never silently shadow
canonical MODEL_LIMITS entries.
Co-authored-by: Mervin Praison <[email protected]>
Cache the resolved ToolSearchConfig (including the ImportError fallback)
in a module-level sentinel so _resolve_tool_search_config() returns a
single stable class object. Without this, the fallback path defined a
fresh dataclass per call, breaking isinstance() passthrough across
resolve_tool_search() invocations. This also converges the __getattr__
and resolve_tool_search resolution paths on the same cached class.
Co-authored-by: Mervin Praison <[email protected]>
Build eval/tokens DEFAULT_CONTEXT_LENGTHS from the canonical
context/budgeter.MODEL_LIMITS plus eval-specific extras, so gpt-5 and
gpt-4.1 resolve to their real 1M window in the eval path. All prior eval
keys (dated snapshots, o1/mistral/deepseek/groq) are preserved.
Co-authored-by: MervinPraison <[email protected]>
Replace the eager module-level ToolSearchConfig alias in
config/feature_configs.py with a PEP 562 __getattr__ so that
`from praisonaiagents import Agent` no longer initialises the entire
tools subsystem just to re-export a backward-compat alias.
- ToolSearchConfig is resolved (and tools.tool_search imported) only on
first attribute access, then cached.
- ToolSearchParam uses a string forward reference.
- resolve_tool_search lazy-imports the config when tool search is used.
- ImportError fallback semantics and all import paths preserved.
Co-authored-by: MervinPraison <[email protected]>
- tool_panel.py ApprovalResponse message now carries optional reason
(backward-compatible default None), matching the interactive events
ApprovalResponse contract for the live y/n approval path.
- Clarify ApprovalDialog.on_button_pressed docstring: the live Textual
TUI approves/denies via the tool_panel y/n handler and does not yet
collect a reason, so the param is not misleadingly presented as active.
Addresses Greptile P2 on PR #3180.
Co-authored-by: Mervin Praison <[email protected]>
Add an optional reason field to the interactive ApprovalResponse and prompt
for a one-line correction on Reject in the Rich and Textual frontends,
mirroring the console approval backend. Empty input preserves plain denial.
Co-authored-by: MervinPraison <[email protected]>
- rich_frontend/textual_frontend: escape preview via rich.markup.escape so
diffs/content containing bracket chars no longer raise MarkupError and crash
the approval dialog (Greptile P1)
- events.render_change_preview: synthesise a unified diff from old_string/
new_string for edit calls lacking a precomputed diff (Greptile P2)
Co-authored-by: Mervin Praison <[email protected]>
Render a unified diff (edit/apply_patch) or truncated new content (write)
before prompting for approval in the interactive Rich and Textual frontends,
so users approve the concrete change rather than just a tool label. Adds a
shared render_change_preview helper and ApprovalRequest.change_preview() in
the interactive events module, mirroring approval_backend._render_preview.
Co-authored-by: MervinPraison <[email protected]>
Empty/whitespace-only profile strings were passing through _as_opt_str
as non-None values, making them indistinguishable from a named scope
and violating the documented 'None means unscoped' fail-closed contract.
Mirror the existing trust normalisation in __post_init__ and add tests.
Co-authored-by: Mervin Praison <[email protected]>
Adds an optional `profile` field to the core RouteBinding routing contract
and surfaces the matched profile via RouteMatch/resolve_route. This gives
each route a named isolation scope the wrapper can key its memory namespace
/ secret scope off, so one gateway can multiplex tenants. Resolution is
most-specific-wins and fails closed: an unmatched route carries profile=None
and never inherits another tenant's profile.
Core-only, backward-compatible (all fields optional), no new deps/exports.
Co-authored-by: MervinPraison <[email protected]>
Addresses reviewer findings on PR #3187:
- Graceful at-least-once fallback when metadata.message:write scope is absent
(was a permanent message-loss regression on apps without the scope).
- Reconcile threaded sends via conversations.replies (history excludes replies).
- Forward idempotency_key on the initial send path, not just crash-recovered drain.
- Gate key forwarding on an explicit idempotency_key param (drop **kwargs fallback).
Co-authored-by: Mervin Praison <[email protected]>
Slack, Telegram, Discord, and Webhook approval backends persisted the
pending approval before polling but skipped _resolve_pending on their
early-return guards (missing chat/channel id, failed send) and outer
except handlers. This left stale status='pending' rows that rehydrate()
surfaced as false-positive outstanding approvals after a restart.
Each missed exit path now records the fallback denial via _resolve_pending
before returning, matching the HTTP reference backend. Adds regression
tests for the early-exit and exception paths.
Co-authored-by: Mervin Praison <[email protected]>
The Discord native upload branch caught all exceptions and returned
False, so a transient blip bypassed deliver_with_retry entirely and the
file was silently dropped — the exact gap this PR set out to close, but
only Telegram/Slack got it. Let transport errors propagate (matching the
other transports) so the retry wrapper backs off and retries; only the
missing optional discord dependency is caught locally, and unresolved
channels still fall through to the no-primitive False return.
Co-authored-by: Mervin Praison <[email protected]>
The outbox's crash-reconciliation seam shipped unused: no channel adapter
implemented was_delivered() or declared reconciles_unknown_send, so every
built-in channel fell back to at-least-once (a duplicate on restart).
Slack can read back recent channel history and match a client-side idempotency
key stamped into the message metadata, so it now opts into effectively-once:
- SlackBot declares reconciles_unknown_send=True and implements was_delivered()
via conversations.history, matching the key in message metadata.
- send_message stamps the idempotency key into Slack metadata when provided.
- DurableDelivery carries the idempotency key in the outbox payload and
forwards it to adapters whose send_message accepts it (backward-compatible).
- _build_reconciler now passes the target channel to was_delivered when its
signature accepts it (legacy single-arg form still supported).
Channels whose platform cannot confirm delivery stay at-least-once by design.
Co-authored-by: MervinPraison <[email protected]>
Chat approval backends (Slack, Telegram, Discord, Webhook, HTTP) held the
pending approval only in an in-memory polling coroutine, so a gateway/bot
restart while an approval was outstanding stranded the blocked agent run.
Add an opt-in DurableApprovalMixin to the shared _approval_base module that
wraps the existing ApprovalStore: it persists the request before polling,
records the final decision, and exposes rehydrate() to recover outstanding
approvals on startup. Each chat backend now accepts an optional store= and a
shared DEFAULT_APPROVAL_TIMEOUT. When no store is given behaviour is unchanged
(fully backward-compatible); no new dependency is introduced.
Co-authored-by: MervinPraison <[email protected]>
Media uploads via DeliveryRouter.send_media were attempted exactly once —
a transient transport blip (HTTP 5xx, rate limit, connection reset) silently
dropped the file while the accompanying text was delivered durably. Text gets
resilience because adapters wrap send_message in deliver_with_retry (bounded
exponential backoff honouring server Retry-After); the raw media upload path
reached none of it.
Wrap the media upload in the same deliver_with_retry helper, reusing the
adapter's already-configured backoff policy when present. Non-retryable
outcomes are preserved: an adapter with no upload primitive still returns
False without retrying, and a permanent error still surfaces as a failed send.
No new params, modules, exports, or schema — the minimal wiring that closes
the reliability gap.
Co-authored-by: MervinPraison <[email protected]>
- events.py: log at DEBUG on unexpected derive_pattern failure instead of
silently swallowing (ImportError still falls back cleanly)
- rich_frontend.py / textual_frontend.py: drop extraneous f-string prefixes (Ruff F541)
- textual_frontend.py: Session Only button now shows the narrow pattern it
persists; derive both patterns once in __init__ so displayed and stored
patterns are guaranteed identical
Co-authored-by: Mervin Praison <[email protected]>
Interactive Rich/Textual frontends persisted a blanket action_type:* pattern
on "Always allow", silently whitelisting every use of a tool from a single
benign approval. Add a shared derive_permission_pattern helper (mirroring the
console backend's _create_pattern) that scopes shell commands to a reusable
command-prefix and other tools to the concrete path, never emitting the blanket
grant unless the user picks the explicit "allow all uses of this tool" option.
Co-authored-by: MervinPraison <[email protected]>
- _resolve_effective_tool_timeout and _validate_cli_backend_compatibility now
fall back to {} when roles/agents are explicit YAML nulls, preventing a
TypeError on dict unpacking (CodeRabbit).
- Remove redundant inner `import os` in _strict_validation_enabled; os is
already imported at module top (Greptile).
Co-authored-by: Mervin Praison <[email protected]>
- _normalize_yaml_config: detect duplicate agent/task names and unknown
task agent refs; raise under PRAISONAI_VALIDATE_STRICT or warn loudly and
preserve both entries by suffixing, instead of silently dropping.
- _resolve_effective_tool_timeout: apply the tightest (min) declared
tool_timeout to the shared tool dict and warn on any overridden agent,
so a fast agent is never silently forced to wait a slower agent's budget.
- BasicSkillMutator._is_safe_path: allow nested skill files (references/,
scripts/, assets/) while still rejecting absolute paths and parent
traversal; real containment stays enforced by the caller's resolve().
Co-authored-by: MervinPraison <[email protected]>
- execution_mixin: nest sync_display_callbacks restore fully under the
status_tool_callback identity check so a finishing agent can't clobber a
concurrent agent's live callback (3+ verbose-TTY agents race).
- agents: shallow-copy self.variables into task.variables so tasks can't
mutate shared AgentTeam state.
- agents: add async-aware _arun_task_start_hook/_arun_task_complete_hook that
await coroutine lifecycle callbacks and offload sync ones to the executor,
so arun_task never blocks the loop or silently drops an async hook.
Co-authored-by: Mervin Praison <[email protected]>
Gap 1: arun_task() now fires on_task_start/on_task_complete and propagates
self.variables, matching run_task(). Extracted _run_task_start_hook and
_run_task_complete_hook shared helpers so both sync and async paths stay in
sync (mirrors the _apply_task_guardrail helper pattern).
Gap 3: verbose+TTY status animation now swaps and restores the shared
sync_display_callbacks['tool_call'] entry under main.py's existing
_callbacks_lock, with an identity check on restore so concurrent verbose
agents don't clobber each other's entry.
Gap 2 (Memory adapter-protocol bypass) intentionally deferred: routing 34
vendor-specific call sites through the protocol is a large architectural
refactor with high regression risk, out of scope for a minimal fix.
Co-authored-by: Mervin Praison <[email protected]>
Deep-copy of formatted tool schemas now only runs when a
BEFORE_TOOL_DEFINITIONS hook is actually registered, matching the
has_hooks fast-path idiom used by every other hook event.
Co-authored-by: MervinPraison <[email protected]>
Addresses Greptile review: on_agent_start/aon_agent_start constructed a
ConversationSession eagerly on every call, reading agent.name/agent.agent_id
and building an object discarded on resume. Move construction into the
build_session factory (create-branch only) while capturing the created
instance for identical cache identity/timestamps.
Co-authored-by: Mervin Praison <[email protected]>
Deduplicate the create-or-resume session logic that PraisonAIDB and
PersistenceOrchestrator each reimplemented across their sync and async
surfaces. A new internal helper persistence/conversation/_ops.py owns the
store-level flow; both classes call it while keeping their distinct
return-type contracts (DbMessage vs ConversationMessage), caching, and
lock/cooldown machinery. Behaviour-preserving.
Co-authored-by: MervinPraison <[email protected]>
Address final-review findings on #3166:
- event_allowed: namespaced sub-type (issues.opened) now requires the
payload to carry a matching action; a missing action no longer admits a
bare base event (fail-closed).
- HookConfig: a secret with no explicit signature_header defaults to
X-Hub-Signature-256 instead of rejecting every request.
- gateway server: deliver_only with an empty rendered message is a no-op
and never hits the delivery backend (avoids 500/retry loops).
- Add unit tests for the fail-closed subtype filter and default header.
Co-authored-by: Mervin Praison <[email protected]>
Extend the generic inbound trigger (POST /hooks/<path>) so a hook can
verify a provider HMAC signature over the raw body, filter by event
type, and deliver a rendered message with no LLM turn — all additive
and backward compatible.
- core (gateway/hooks.py): add pure verify_webhook_signature() helper and
additive HookConfig fields (secret/signature_header/signature_algo/
signature_prefix/events/event_header/deliver_only) plus verify_signature/
resolve_event/event_allowed; round-trip via to_dict/from_dict (secret
redacted); export the helper.
- wrapper (gateway/server.py): read the raw body once, reject invalid/
missing signatures with 401 before any agent runs, 200 no-op on
filtered-out events, and short-circuit deliver_only straight to delivery.
- wrapper (bots/_config_schema.py): mirror the new fields on HookSchema.
Co-authored-by: MervinPraison <[email protected]>
The pre-existing test_windows_scheduled_task_command_is_well_formed asserted
`--config` appeared inline in the /TR value. This PR intentionally routes the
scheduled task at the generated .cmd wrapper (single, well-formed quoting) to
avoid the nested-quote hazard for paths with spaces, so /TR now points at the
wrapper path and the wrapper owns the --config invocation. Assert the new,
intended contract instead of the outdated implementation detail.
Co-authored-by: Mervin Praison <[email protected]>
Close the observability gap flagged in review: a channel skipped at
startup because its credential was unavailable was never added to
_channel_bots, so health() omitted it entirely — indistinguishable
from a channel that was never configured. Track skipped channels in
_degraded_channels (cleared on recovery in both start_channels and the
hot-reload single-channel path) and report them as `degraded` in
health(). No new config keys, params, or API surface.
Co-authored-by: Mervin Praison <[email protected]>
The installed daemons run python -m praisonai_bot gateway start, whose Typer command discarded GatewayHandler.start()'s int return. Typer ignores a returned int, so the process always exited 0 - the generated units' Restart=on-failure / RestartPreventExitStatus=78 / KeepAlive.SuccessfulExit directives never saw the real fatal-config (78) / transient (75) code, making the whole feature inert. Surface it via typer.Exit.
Also replace the Windows scheduled-task's fragile inline cmd /c string (nested quotes that misparse paths under C:\Program Files) with a single quoted path to the generated .cmd wrapper, which already owns the exit-78 translation.
Co-authored-by: Mervin Praison <[email protected]>
A gateway fronting several channels no longer fails closed for the whole
process when a single channel credential is temporarily unavailable
(rotation, expiry, unset env var, secret-store blip). The channel token
validator now returns an empty (degraded) token instead of raising, so the
runtime's existing "no token -> skip channel" path isolates just that
channel while every healthy channel keeps serving. gateway status/doctor
already report per-channel availability, so the degraded state stays
queryable. Fail-closed remains for structurally invalid config and the
gateway's own ingress/auth secret.
Co-authored-by: MervinPraison <[email protected]>
Installed supervisor units restarted the gateway unconditionally, so the
runtime's fatal-config exit (78, EX_CONFIG) was discarded and a broken
config crash-looped forever. Map the existing core exit-code contract to
per-OS restart directives:
- systemd: Restart=on-failure + RestartPreventExitStatus=78 +
StartLimitBurst=5 (bounded restarts).
- launchd: KeepAlive dict (SuccessfulExit=false, Crashed=true) +
ThrottleInterval so a clean/fatal exit is not relaunched every few
seconds.
- windows: wrap the scheduled task / startup script so exit 78 maps to a
clean stop instead of a relaunch.
Co-authored-by: MervinPraison <[email protected]>
Addresses reviewer P1 findings on `praisonai usage`:
- Read current-project and global stores separately so `--by project`
no longer collapses both sources into a single `current` bucket.
- Raise per-store scan limit (1000 -> 100_000) so large stores are not
silently truncated (also affects `--days 0`).
- Surface store-open/read failures via warnings (table) and an `errors`
field (JSON) instead of reporting them as empty usage.
Co-authored-by: Mervin Praison <[email protected]>
- Honor PRAISONAI_HOME in legacy-session migration lookup via
get_sessions_dir() instead of a hardcoded ~/.praisonai/sessions path.
- Gate legacy name-only fallback to global scope only, so a workspace-scoped
run never silently adopts another project's history (isolation, #3154).
- Resolve workspace identity per-directory (cache keyed by cwd) so a
long-lived process that changes directory gets the correct identity while
still avoiding repeated git calls per directory.
- Add tests for directory-change identity and global-scoped legacy fallback.
Co-authored-by: Mervin Praison <[email protected]>
Strict-mode toolset resolution flattened ToolResolutionError (a ValueError
subclass) into a plain ValueError, dropping .unknown/.suggestions. Re-raise
the typed error before the generic handler and export the resolver API from
praisonaiagents.tools so strict callers can catch it.
Co-authored-by: Mervin Praison <[email protected]>
Aggregates already-persisted per-session total_tokens/cost from the local
session store(s) and reports by day/model/project with --days/--project/--json
filters. Wrapper-only reporting surface; no core change, no new deps.
Co-authored-by: MervinPraison <[email protected]>
Same-named agents in different projects no longer share conversation
history by default. The auto-derived session id now folds in a stable
workspace/project component (git root-commit, else resolved cwd, else
global) instead of being a function of the agent name alone.
- Add praisonaiagents/session/workspace.py with workspace_id() resolver
- Derive auto id as history_<sha256("{workspace}:{name}")[:8]>
- Keep name-only sha256 + md5 fallbacks so prior sessions still resolve
- Add PRAISONAI_GLOBAL_SESSIONS=true opt-out for name-only continuity
Co-authored-by: MervinPraison <[email protected]>
Address the two Greptile findings on PR #3152:
P1 (resolved name not a scoped-registry key): assert_framework_available now
falls back to the process-default registry when an injected/scoped registry
does not recognise a name. A router/alias that resolves to a concrete built-in
(e.g. autogen -> autogen_v2) is no longer falsely rejected, while the DI seam
for genuinely scoped adapters is preserved.
P2 (setup ran outside observability): adapter.setup() moved out of
_prepare_for_run into a small _run_adapter_setup seam invoked INSIDE the
observability_session on both the sync and async kickoff paths, so setup events
and any setup/import failure are recorded and finalized instead of slipping
outside observability.
Tests: added DI fallback coverage and a regression guard that setup runs inside
the session on both paths.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Gap 3 from the wrapper-architecture review:
3a. assert_framework_available / get_install_hint now accept an optional
registry and AgentsGenerator threads its injected _adapter_registry
through them, so a scoped/per-tenant adapter is no longer rejected just
because it is absent from the process-default registry.
3b. Observability init+finalize is now owned by AgentsGenerator via the
existing observability_session context manager (bracketing both the sync
and async kickoff paths). The by-convention per-adapter finalize calls in
the CrewAI and PraisonAI adapters are removed. This closes the AutoGen
leak, where the adapter never called finalize_observability and left an
ObservabilityRun / emitter swap alive on every invocation.
Tests updated to assert the new invariant (generator owns the lifecycle;
adapters do not finalize) and to cover the DI seam end to end.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: MervinPraison <[email protected]>
Address two greptile P1 findings on PR #3150:
- command_parser: broaden unsafe-expansion detector to match bare $VAR
(e.g. `rm -rf $HOME`) in addition to $IFS/${...}; $(...) and backticks
remain excluded so command substitution stays on the deny-checked
decomposition path.
- memory.reset_long_term: re-raise on Chroma delete_collection failure so
a failed reset is not silently reported as success while long-term
memories remain available.
Co-authored-by: Mervin Praison <[email protected]>
- permissions: escalate shell commands containing unresolvable parameter
expansion ($IFS / ${...}) to ASK instead of letting a broad allow rule
short-circuit a specific deny. Command substitution ($()/backticks) is
left to the existing per-op decomposition.
- llm: _setup_event_tracking now no-ops on empty events and merges into the
process-global litellm.callbacks (tracking per-instance ownership) instead
of overwriting it, so unrelated agents' callbacks are preserved.
- memory: reset_long_term() deletes only this instance's own Chroma
collection rather than resetting the entire shared client; collection name
is now an opt-in config override (default unchanged).
Co-authored-by: Mervin Praison <[email protected]>
Address reviewer-found gaps so the opt-in gateway lifecycle policies from
#3021 actually take effect at runtime:
- Wake tracking: notify_inbound()/wake() now fire from the single client
frame entry point (_handle_client_message) so a live handshake can never
be quiesced mid-flight.
- CLI scale-to-zero arming: treat the open listening socket as an inherent
self-wake path so --scale-to-zero (no wake_url) still arms and quiesces;
an external wake_url is only required when an on_quiesce host-suspend hook
is configured.
- Idle clock: reset _last_inbound_ts when the idle loop arms so serving,
not construction, starts the idle interval.
- Crash-loop breaker: consult RestartLoopGuard around the supervised channel
run so a channel crash-looping within the window stops auto-resuming.
- Reload reconciliation: _reconcile_lifecycle rebuilds policies and cancels/
relaunches idle + drain-marker loops on hot-reload; unchanged blocks leave
running tasks untouched.
All opt-in and off by default; always-on gateways are unaffected.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Wire the existing pure core lifecycle policies into the primary gateway
runtime (WebSocketGateway), reusing them rather than duplicating BotOS's
machinery:
- ScaleToZeroPolicy: opt-in idle-quiesce loop that quiesces the gateway
(dormant + optional host-suspend hook) when idle for N minutes with no
in-flight turn / queued inbound, and wakes on inbound.
- DrainMarkerPolicy + current_epoch: a port-less external drain marker
watcher checked against the process instantiation epoch, so a stale
marker left on a durable volume by a machine restart is ignored.
- RestartLoopGuard: a new pure, dependency-free rolling-window predicate
in core (beside classify_exit_reason) so the crash-loop breaker is
protocol-driven and reusable by both runtimes.
Exposed via a 3-way surface (YAML `lifecycle:` block, CLI flags
--scale-to-zero/--idle-minutes/--drain-marker, and Python), all opt-in so
always-on gateways are unchanged and fully backward-compatible. Lifecycle
state is surfaced in health() for observability.
Co-authored-by: Mervin Praison <[email protected]>
Greptile P1: the identity resolver was only built at startup, so a changed
top-level identity: block (which triggers a full channel restart) never
rebuilt or cleared _identity_resolver — enabling stayed a no-op and
disabling/re-pointing the store kept the stale resolver.
Route startup and hot-reload through _reconcile_identity_resolver, run
before channels (re)start. Explicit constructor/CLI resolvers always win;
an unchanged block preserves the live resolver (link cache/store handle);
enable installs, disable/remove clears back to per-platform keys.
Co-authored-by: Mervin Praison <[email protected]>
Resolve the Greptile review finding: when 'auto' resolved to 'off' on a
non-editable channel the reason was logged at info, and the explicit
warning branch below was skipped (mode already OFF), so operators
filtering info logs couldn't see why streaming was disabled. Log the
auto->off degradation at warning; auto->draft stays at info. Adds a test.
Co-authored-by: Mervin Praison <[email protected]>
The flagship WebSocketGateway never injected an identity_resolver into the
channel bots it created, so each platform fell back to a per-platform session
key (bot_{platform}_{user_id}) and cross-platform conversation continuity
silently broke. Mirror BotOS by stamping a shared resolver onto every channel
bot's session manager.
- WebSocketGateway.__init__ gains an optional identity_resolver (Python override)
- _stamp_identity_resolver() mirrors _stamp_admission_gate; called from
start_channels and _start_single_channel (hot-reload)
- start_with_config builds a StoreBackedIdentityResolver from a declarative
top-level identity: block (constructor value wins); degrades gracefully
- CLI gains --identity-store to enable continuity without a YAML block
Co-authored-by: Mervin Praison <[email protected]>
Adds StreamingMode.AUTO which resolves to DRAFT where the channel can
edit and OFF otherwise, so streaming turns on where supported instead of
every platform silently sitting on OFF. Degradation is now logged at
warning level, and mode resolution preserves user-set config fields via
dataclasses.replace() instead of resetting to defaults.
Co-authored-by: Mervin Praison <[email protected]>
Address reviewer P1s in the CLI session resolver:
- Honor DefaultSessionStore.delete_session() return value so an I/O
failure is no longer reported as a successful deletion.
- Delete against the single store resolve_session() selected instead of
every canonical store, so a project-scoped delete can't remove an
unrelated same-id session living only in the global store.
- Always sweep the legacy SessionManager store on delete so a duplicate
legacy record can't resurface a session the CLI reported as deleted.
- On a read failure of the owning canonical store, report not-found for
that record instead of silently falling through to a different same-id
session or the legacy store.
Adds regression tests for the failed-delete, legacy-duplicate sweep, and
read-failure cases.
Co-authored-by: Mervin Praison <[email protected]>
DeliveryRouter.send_media discarded the resolved thread_id, so a threaded
target (Slack thread_ts, Telegram forum topic, Discord thread) delivered
text into the thread but attachments to the parent chat. Thread the
thread_id through deliver_media_to_adapter, forwarded via each transport's
native keyword (Telegram message_thread_id, Slack thread_ts, Discord thread
channel) only when the primitive accepts it, so adapters lacking thread
support are unaffected. Mirrors the text path's _accepts_thread_id guard.
Co-authored-by: Mervin Praison <[email protected]>
Both AgentScheduler and AsyncAgentScheduler constructed SchedulerDelivery
without the persisted origin, so deliver="origin" silently no-op'd in
production despite the resolution logic existing. Add
SchedulerDelivery.origin_from_config to normalise the origin carried in
the job config (DeliveryTarget or serialised dict) and pass it through at
both call sites, so origin delivery actually activates on the lightweight
path.
Co-authored-by: Mervin Praison <[email protected]>
The lightweight scheduler delivery path silently no-op'd for the symbolic
'origin' token because it required a live gateway session. ScheduleJob.origin
already persists a concrete DeliveryTarget at creation, so SchedulerDelivery
now accepts it and rewrites 'origin' to a concrete channel:channel_id[:thread_id]
route — delivering back to the point of origin without the full gateway. 'all'
and the absent-origin case still degrade with a clear warning.
Co-authored-by: MervinPraison <[email protected]>
DeliveryRouter.resolve() now returns (platform, channel_id, thread_id) and
deliver() threads thread_id into the adapter's send_message so proactive/
scheduled/agent-initiated messages targeting a Slack thread, Telegram forum
topic, or Discord thread land in the intended thread instead of the parent
channel. Adapters lacking a thread_id parameter are introspected and left
untouched (no behaviour change). The lightweight scheduler route and the
gateway scheduled-delivery path now build platform:channel:thread routes and
drop the _ThreadBindingBot workaround, routing through the shared resilient
router directly.
Co-authored-by: MervinPraison <[email protected]>
The gateway server constructed DeadTargetRegistry() with no store, so it
persisted dead targets to the node-local dead_targets.json sidecar even
though a canonical SQLite dead_targets table already exists in the shared
DeliveryControlStore. This retires the duplicate JSON store for the gateway
path, making the SQLite table the single, cross-worker source of truth.
Falls back to the JSON default only if the SQLite store cannot be built.
Co-authored-by: MervinPraison <[email protected]>
Path-scoped rules (activation: glob) never fired at runtime because the
single rule-injection point built the system prompt with no file path.
Now _build_system_prompt scans the conversation for touched file paths
and appends matching glob rules per-turn, deduped against always/manual
rules and appended after the cache so it is never baked in. Gated on
RulesManager.has_glob_rules() so glob-free workspaces pay nothing. Also
fixes matches_file so a recursive pattern like **/*.py matches a bare
filename (foo.py).
Co-authored-by: MervinPraison <[email protected]>
Give the native OpenAI-client path the same default retry contract as the
LiteLLM path. A default Agent now applies RetryBackoffConfig() (max_retries=3)
so transient errors (429/overload/5xx) are retried consistently regardless of
which internal LLM path executes the turn. retry=False disables; retry=True/
dict/RetryBackoffConfig remain fully overridable. Reuses the existing native
retry loop (is_retryable classification, jittered backoff, ON_RETRY hook).
Co-authored-by: MervinPraison <[email protected]>
Route `session show`/`delete`/`export` through a single `resolve_session`
resolver that searches the same project-scoped + global stores used by
`session list`/`resume`/`run --continue`, so any id a user can list or
resume is fully manageable. Keeps a best-effort fallback to the legacy
SessionManager store during a deprecation window.
Co-authored-by: MervinPraison <[email protected]>
Introduced new HTML documents outlining the praisonai-mcp server host features, installation instructions, and usage guidelines. This enhances the understanding of the MCP server's capabilities and integration with other packages.
- Accept config= kwarg so SandboxManager can construct DaytonaSandbox
(mirrors sibling backends); derive image/timeout/cpu/memory from config
while keeping legacy direct kwargs backward-compatible
- Combine cd/export/command into a single process.exec call so env vars
and working_dir actually apply to the executed command
- execute_file now reads local file content and dispatches by language
(mirrors E2B), falling back to remote path execution when not local
Co-authored-by: Mervin Praison <[email protected]>
Wire Typer run/shell/backends to SandboxHandler, implement daytona-sdk
DaytonaSandbox, expand live Docker/manager tests and backward-compat gates,
and refresh praisonai-sandbox and praisonai-agents lockfiles.
Co-authored-by: Cursor <[email protected]>
- MCP.shutdown(): fall back to close() for HTTPStreamMCPClient/
WebSocketMCPClient, which expose close()/aclose() but not shutdown().
Previously these transports leaked on shutdown; also fixes Agent.aclose()
fallback path.
- Agent.close()/aclose(): remove this agent's id(self)-scoped tool circuit
breakers via new _cleanup_circuit_breakers(), preventing a reused object
id from inheriting a stale OPEN breaker and keeping the registry bounded.
Co-authored-by: Mervin Praison <[email protected]>
- close()/aclose(): shut down MCP clients passed via tools=[MCP(...)] by
walking self.tools (mirrors remove_mcp_server) instead of the always-empty
self._mcp_clients check, so subprocesses/threads are cleaned up deterministically.
- circuit breaker: scope the registry key to the Agent instance
(tool_{id(self)}_{name}) so one agent's failing tool no longer trips the
breaker for another agent's same-named tool.
- llm resolution: prefer llm_instance (the real LLM object) over the always-truthy
llm model-name string in the token-tracking, token-metrics and guardrail paths,
restoring set_current_agent/last_token_metrics wiring and configured-instance reuse.
Co-authored-by: MervinPraison <[email protected]>
- manager.get_available_types(): invoke callable is_available before bool();
ImportError path no longer advertises subprocess as available
- _plugin_registry fallback: translate non-ImportError loader failures to
ValueError; raise AttributeError from None in lazy __getattr__
- Realign E2B tests to the real to_thread-based implementation (25 active,
was 20 skipped); rewrite Daytona tests to the fail-loud stub contract
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
- e2b: use importlib.import_module in is_available (aligns with tests, fixes CI)
- e2b: run_command list uses shlex.join to avoid shell reinterpretation
- _plugin_registry: self-contained fallback so praisonai-sandbox works without praisonai-code
- subprocess: guard sys.executable None/empty for PATH and cmd
- sandlock: allow sandbox root in _safe_sandbox_path so list_files("/") works
Co-authored-by: Mervin Praison <[email protected]>
Move sandbox backend implementations out of the wrapper into praisonai-sandbox,
preserve praisonai.sandbox shims, wire agents bridge/registry dispatch, and add
CI gates, publish slot, and backward-compat tests for the eight-package model.
Co-authored-by: Cursor <[email protected]>
- Never deliver start_automation to an idle CLI connection (filter is_cli
in the send + retry loops, not just the caller).
- Allow the side panel (extension) to start its own session without being
self-excluded as NO_EXTENSION.
- Claim the extension's session_id before awaiting send_text so concurrent
start_session calls cannot both pass the busy guard; release on failure.
- Stamp ended_at when a session is finalized as "cancelled".
- Add tests for side-panel start, idle-CLI exclusion, cancel_session
routing, and cancelled ended_at.
Co-authored-by: Mervin Praison <[email protected]>
Reject concurrent start_session when the extension is busy or absent,
avoid reporting "running" when start_automation was never delivered, and
release/finalize session locks on disconnect so /health returns to
sessions:0. Adds extension_busy/active_session_id to /health.
Co-authored-by: Mervin Praison <[email protected]>
Extract shared _extract_schema helper and let validate_tool_schema_consistency
optionally return built schemas so get_sorted_tool_schemas reuses them instead
of rebuilding every tool schema a second time. No behaviour change.
Co-authored-by: MervinPraison <[email protected]>
Delegate the delta-accumulation loop in _process_stream_delta() to the
existing _process_tool_calls_from_stream() method, removing the byte-for-byte
duplicate accumulator. The outer guard in _process_stream_delta() is unchanged,
so whether/when accumulation runs is unaffected.
Co-authored-by: MervinPraison <[email protected]>
Addresses reviewer feedback on the ToolResolver memoisation:
- Cap the resolver cache as a small LRU (OrderedDict) so a long-running
process that builds many workflows no longer retains every registry,
its tool callables/modules and resolver caches for the process life.
- Skip caching entirely when registry is None: a fresh registry is built
per call so its id() is never stable, meaning caching it only leaked
memory and never produced a hit.
Co-authored-by: Mervin Praison <[email protected]>
create_tool_registry_with_overrides re-implemented praisonai-tools package
discovery that ToolResolver._resolve_from_praisonai_tools already owns and that
resolve_tools() already consults via the canonical resolver. Remove the
duplicate so external-tool discovery has a single implementation.
The template/cwd tools.py autoload is intentionally kept in the wrapper because
it carries the distinct PRAISONAI_ALLOW_TEMPLATE_TOOLS gate (with a
skip-on-error contract and explicit-template-dir-outside-CWD support) that the
resolver's PRAISONAI_ALLOW_LOCAL_TOOLS + CWD-boundary gate does not provide, so
delegating it would change the security gate.
Co-authored-by: MervinPraison <[email protected]>
Callers invoke resolve_tools() once per agent/step over the same registry
object, and each call rebuilt a fresh ToolRegistry + ToolResolver, discarding
per-instance caches and re-executing local tools.py once per agent. Extract
resolver construction into _get_resolver(), memoised on
(id(registry), template_dir, autoload_enabled), so one resolver is reused
across a workflow build. Resolution order, results, and the security gate are
unchanged.
Co-authored-by: MervinPraison <[email protected]>
Add pre-flight bridge health check, step watchdog, and explicit
start_automation delivery signalling so the extension-mode CLI run
no longer hangs silently on keepalive PING/PONG until timeout.
- server.py: /health now reports extension_connections; start_session
response includes start_automation_sent so the CLI knows whether the
task actually reached an extension.
- browser.py: _require_bridge() pre-flight check (exit 2 fast); handle
server error messages; 30s first-step watchdog; verbose automation
delivery status; infra/timeout/task exit codes (2/3/1).
Co-authored-by: Mervin Praison <[email protected]>
Prioritize [question] title detection before the bug substring check and
guard autoClaude against the question label, so bracket-prefixed titles
like "[QUESTION] Is this a bug?" no longer auto-trigger Claude analysis.
Co-authored-by: Leebana <[email protected]>
Address reviewer findings on PR #3115:
- P1: /health counted all WebSocket clients, so a stray CLI connection could
produce a false-positive extension check. Track chrome-extension origin
connections server-side and expose extension_connections in /health; doctor
and launch polling prefer it (fallback to connections for older servers).
- P2: launch --no-server failure guidance pointed to a dead /health endpoint.
Guidance is now conditional: with --no-server it tells the user to start the
bridge first, otherwise it shows the curl verification step.
Co-authored-by: Mervin Praison <[email protected]>
- Treat malformed UTF-8 as JSON-RPC -32700 parse error instead of
terminating the transport (CodeRabbit)
- Bound the stdin queue and apply backpressure via timeout-based put so a
fast/malicious client cannot grow memory without limit (CodeRabbit)
- stop() now enqueues the EOF sentinel to wake a blocked line_queue.get so
shutdown exits cleanly even when stdin stays open (Greptile, CodeRabbit)
- Add regression test for invalid UTF-8 input
Co-authored-by: Mervin Praison <[email protected]>
- Remove early-return from BrowserServer.start() so programmatic callers
retain the documented blocking contract (CodeRabbit Major)
- Move proactive _port_in_use pre-check into the CLI, reusing the rich
'already running' message and exiting 0
- Make _port_in_use family-agnostic via getaddrinfo so IPv6 (::1) hosts
are probed correctly (Greptile P1)
- Use errno.EADDRINUSE instead of magic numbers; drop redundant f-prefixes
Co-authored-by: Mervin Praison <[email protected]>
The friendly error reported ws://127.0.0.1 but the run command actually
connects to ws://localhost and the health checks elsewhere use localhost.
Align the message (and its test) to localhost for accuracy.
Co-authored-by: Mervin Praison <[email protected]>
Zero-arg / all-optional tools serialized inputSchema.required as JSON null,
which strict MCP clients (Cursor's Zod validation) reject, discarding the
entire tools/list batch so only mcp_auth appeared. Omit the required key when
empty and defensively normalize any legacy required:null at the export
boundary.
Co-authored-by: Mervin Praison <[email protected]>
Replaces loop.connect_read_pipe(sys.stdin) which crashes on Windows
ProactorEventLoop + Python 3.13 with WinError 6. Reads stdin in a daemon
thread and hands lines to the asyncio loop via run_in_executor; writes
responses to sys.stdout.buffer with flush. Portable across platforms.
Co-authored-by: Mervin Praison <[email protected]>
- launch: on auto-load failure, print exact 'Load unpacked' dist path and
bridge verification steps (Chrome 137+/Windows blocks automated load)
- doctor extension: check bridge /health connection (ground truth) and treat
CDP :9222 as optional, so it no longer reports a false negative when the
extension works in the user's daily Work Chrome profile
Co-authored-by: Mervin Praison <[email protected]>
- Remove emoji from server.py start() banner and CLI run banners to avoid charmap crashes on cp1252 Windows consoles
- Reconfigure stdout/stderr to UTF-8 in __main__ before console import
- Add pre-bind port probe so a duplicate start reports "already listening" instead of raising a bind error
- Map WinError 10048 / EADDRINUSE to a friendly "already running" message with exit code 0 in the start command
Co-authored-by: Mervin Praison <[email protected]>
Translate cross-platform connection-refused errors (WinError 1225,
ECONNREFUSED 111/61) into an actionable message naming the bridge URL,
the `praisonai browser start` command, and the health-check URL.
Co-authored-by: Mervin Praison <[email protected]>
External bug reports like [praisonai-mcp] and [praisonai-browser] were
skipped because only [bug] matched the title gate. Match any leading
[...] prefix when the issue has bug or enhancement labels.
Address session-isolation gaps in agent-invoke:
- _supports_session_isolation now uses isinstance(Agent) so application
Agent subclasses are cloned per session instead of sharing state.
- resolve_session_agent raises on clone failure instead of falling back
to the shared template, preventing cross-session history leaks.
- Handlers surface isolation failures as clean errors.
- Add tests for subclass isolation and clone-failure behaviour.
Co-authored-by: Mervin Praison <[email protected]>
Short-circuit resolve_all_from_yaml's project-tool discovery with the cheap
local_tools_enabled() opt-in check before walking directories and shelling out
to `git rev-parse`. Loading is gated anyway, so the git subprocess added no
value when PRAISONAI_ALLOW_LOCAL_TOOLS is off (the default). Also narrow the
import guard to ImportError.
Co-authored-by: Mervin Praison <[email protected]>
- Optimizer now swaps goal/backstory (which drive the cached system prompt),
not just instructions, and clears the system-prompt cache, so each candidate
is scored with its own effective prompt instead of the original.
- Isolate eval chats via agent.ephemeral() to stop history pollution.
- Parse multiline candidates as complete blocks (delimiter + blank-line
fallback) instead of per-line fragments.
- Floor non-finite metric scores (NaN/inf) to 0.0 in both optimizer and loop.
- Move EvaluationLoopResult.best after metadata to avoid positional shift.
- Add tests for goal/backstory swap, apply, multiline parsing, non-finite scores.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Wire the existing gated .praisonai/tools/ discovery into resolve_all_from_yaml
so @tool-decorated functions dropped into the convention are available to
YAML-defined agents by their tool name. Additive and backward compatible with
the cwd tools.py / tools/ paths.
Co-authored-by: MervinPraison <[email protected]>
The agent-serving endpoint dispatched every request against a single
shared Agent instance and ignored session_id, leaking mutable
chat_history across concurrent callers and making the advertised
"conversation continuity" a no-op.
Introduce resolve_session_agent() in the wrapper: the registry now holds
a template, and each request gets an isolated clone (via the existing
clone_for_channel safe-copy path). A provided session_id binds the clone
to the shared, file-locked DefaultSessionStore for continuity + cross-
session isolation; its absence yields an ephemeral conversation. Non-
Agent objects (e.g. mocks) fall back to the shared instance for backward
compatibility. Composes existing core primitives only — no new core
surface.
Co-authored-by: MervinPraison <[email protected]>
- EvaluationLoop now keeps and returns the highest-scoring iteration
(best/best_score/best_output) instead of the last one, and supports an
optional numeric metric to drive scoring instead of the LLM Judge.
- New PromptOptimizer (eval/prompt_optimizer.py) generates instruction
variants, scores each over an eval set, keeps the best, and optionally
writes it back to agent.instructions.
- Agent.optimize_instructions / aoptimize_instructions convenience methods.
- Lazy exports for PromptOptimizer/OptimizeResult.
Co-authored-by: MervinPraison <[email protected]>
- server.py: resolved secret string now wins over a raw {source,id} reference
dict in the runtime channel merge, so adapters (e.g. WhatsApp verify_token)
never receive an unresolved reference.
- gateway doctor --json: emit a single combined JSON document ({probes,secrets})
instead of two top-level documents so json.load(s) can parse the output.
- gateway doctor: exec-sourced credentials are reported as "configured" without
running their command, so a one-shot/rate-limited/rotating secret command is
not executed twice (availability report + probe).
Co-authored-by: Mervin Praison <[email protected]>
Add a lightweight, protocol-first secret-reference contract so credential
fields (token, app_token, verify_token) can be sourced from an env var, a
mounted secret file, or a command/secret-manager, with resolved values
registered for log redaction.
- core: praisonaiagents/secrets.py — SecretRef, SecretResolver protocol,
stdlib DefaultSecretResolver (env/file/exec), resolver registry, and a
redaction registry (register_secret_for_redaction / redact_secrets).
- config schema: credential fields accept the additive {source, id} form;
plaintext and ${ENV} stay fully backward compatible; resolved values are
registered for redaction.
- gateway doctor: reports each credential as available |
configured-but-unavailable | missing without printing the value.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: MervinPraison <[email protected]>
Addresses reviewer feedback on PR #3100:
- Add local_tools_enabled() mirroring the loader's exact acceptance
(lower() == "true") so false/0 no longer suppress the hint while the
loader still denies (no silent no-op). Used by both the auto-discovery
hint and the --tools file path.
- Make --allow-local-tools a strictly per-invocation grant: set the env
gate for the run and restore the prior value in a finally, so a later
in-process run never inherits authorization to exec local Python.
- count_project_tool_files() now returns (project_count, user_count) so
the hint names the correct source (.praisonai/tools/ vs
~/.praisonai/tools/) instead of mislabelling user-global files.
- Tests: local_tools_enabled semantics (incl. false/0), tuple counts,
and false-value hint coverage.
Co-authored-by: Mervin Praison <[email protected]>
Playwright and CDP automation engines call agent.process_observation()
synchronously from inside async methods that run via asyncio.run().
The praisonaiagents runner rejects execute_sync() when an event loop
is already active, causing RuntimeError on every LLM observation
step and blocking all headless browser automation.
Changed to await agent.aprocess_observation() in both engines,
matching the async pattern already used by server.py.
Fixes#3093
- memory_mixin: feature-detect whether a session store's add_message accepts
tool_calls/tool_call_id; fall back to a plain-text turn for legacy protocol
stores so tool turns are never silently dropped by a swallowed TypeError.
- hierarchy: HierarchicalSessionStore.add_message now accepts and preserves the
optional tool fields so hierarchical stores also resume faithfully.
- store: count-based history slicing (get_chat_history / get_working_history)
no longer begins on an orphaned role="tool" result, avoiding invalid
transcripts on resume when the boundary lands mid tool-exchange.
- tests: cover legacy-store fallback, hierarchical store tool fields, and
the trim boundary skipping orphaned tool results.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
When .praisonai/tools/*.py files are present but PRAISONAI_ALLOW_LOCAL_TOOLS
is unset, run now prints a concise hint on the default path explaining how to
enable them, instead of silently skipping. Adds a discoverable
--allow-local-tools flag and aligns the init scaffold copy so the enable step
is stated up front.
Co-authored-by: MervinPraison <[email protected]>
The zero-config JSON session store only round-tripped role/content text,
so resuming a tool-using agent via --continue / session resume silently
dropped its tool calls and results. Add optional, additive tool_calls /
tool_call_id fields to SessionMessage (tolerant of absence for old files),
preserve them through get_chat_history / get_working_history / set_chat_history,
and teach _persist_message to forward assistant tool-call and tool-result
turns so resume reconstructs the same message list the model saw before.
Co-authored-by: MervinPraison <[email protected]>
Checkpoints already form an ordered shadow-git sequence, so a "turn" is an
index into it. Add a lightweight CheckpointService.rewind(steps) helper over
the existing list_checkpoints + restore, and surface it as
`praisonai checkpoint rewind [steps]` (default 1 = undo the last turn's file
changes). No changes to agent.py, the tool loop, or the session store.
Co-authored-by: MervinPraison <[email protected]>
apply_diff, search_replace and append_to_file previously gated the
workspace confinement check behind `if workspace:`, silently skipping it
when no workspace was passed. This let an LLM escape cwd via an absolute
path (e.g. apply_diff("/etc/hosts", ...)). Align all three with the
existing write_file/read_file behaviour: default the workspace to cwd and
always enforce is_path_within_directory.
Co-authored-by: MervinPraison <[email protected]>
- execute_tool_async now routes through the tool MiddlewareManager (parity
with sync execute_tool): before_tool/after_tool/wrap_tool_call gate async
tool calls too, so security/audit hooks are no longer bypassed. Fast path
(no hooks) unchanged. Core retry loop extracted to
_execute_tool_async_with_retry.
- Workflow._execution_lock uses module-level double-checked locking so two
threads first entering run()/astart() concurrently share one lock object
instead of each minting and acquiring its own, closing the run-guard race.
Co-authored-by: Mervin Praison <[email protected]>
Gap 1: Route Agent(hooks=[...]) tool middleware (before_tool/after_tool/
wrap_tool_call) through execute_tool via a lazily-built MiddlewareManager.
Zero overhead when no hooks are registered.
Gap 2: Apply the same retry / guardrail-retry / output_file policies used by
the top-level run() loop to nested-pattern steps (Parallel/Loop/Route/If/
Repeat) and hierarchical mode via a shared _apply_step_policies helper.
Gap 3: Guard run()/astart() against concurrent re-entrancy on a shared
Workflow instance with a lazily-created lock, raising a clear error instead
of silently corrupting per-run state.
Co-authored-by: Mervin Praison <[email protected]>
- execution_mixin: async tool path now runs the same policy/guardrail gate as
sync (_check_tool_policy_and_guardrails), closing the async bypass.
- sqlite/in_memory adapters + memory.py: delete_memory takes an optional tier
so delete_short_term/delete_long_term only touch their own tier — prevents a
short-term delete from erasing an unrelated long-term row with the same id.
- tool_execution: policy check_tool and guardrail validate_tool_call exceptions
now fail closed (deny) instead of silently executing the tool.
- workflows: a step whose guardrail never validates after exhausting retries is
now treated as failed, honoring on_error='stop' and the final workflow status;
on_step_complete is fired on a stopped failure.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Addresses reviewer feedback on #3080:
- Falsy tool results (""/[]/{}/0/False) were dropped by the upstream
detector's truthiness check, disabling no-progress detection; now wrapped
so they remain fingerprintable.
- Identical-result streak now requires the same tool, so unrelated tools
returning a common value (e.g. "ok"/True) are not lumped into one run.
- Progress markers now record an action-index boundary so mark_progress()
actually resets the streak.
- Count-based idempotent/mutating BLOCK/HALT is now gated on an actual
stuck (identical-result) streak, preventing false halts on legitimate
high-frequency-but-progressing tool use (fixes wiring-test regression).
- Post-execution loop-guard BLOCK/HALT decisions are now surfaced to the
model instead of being silently discarded.
Co-authored-by: Mervin Praison <[email protected]>
- Remove dynamic -attempt suffix; use `git worktree add -B` so path()/reset()/
remove() deterministically resolve the same target (fixes resource leak /
state-mismatch flagged by CodeRabbit & Greptile)
- Fail fast with a clear exception when isolation is requested but provisioning
fails, instead of silently clobbering the shared root
- Check exit status in reset()/remove() and raise on failure
- Prune stale worktree bookkeeping before create so re-create after remove works
- Collision-resistant slug (append short name hash) so distinct names never
share a worktree
- Sort __all__ alphabetically
Co-authored-by: Mervin Praison <[email protected]>
Address reviewer-flagged P0/P1 issues in per-task worktree isolation:
- Anchor all git ops to a fixed repo dir (env override / resolved cwd)
instead of the mutable process cwd.
- Validate caller-supplied task ids before using them as path/ref
components (reject traversal, invalid ref chars); refuse worktree
creation on unsafe ids instead of failing open to the shared cwd.
- Commit uncommitted worker edits before integrating so a clean-but-empty
merge no longer discards work on teardown.
- Honor the integration `git commit` return code and abort on failure so a
rejected commit blocks the task rather than reporting success.
- Route non-conflict merge failures and integration exceptions to 'blocked'
so a task is never marked 'done' with an unmerged branch.
- Add `workspace_kind` to the public update allowlist so opting an existing
task into 'worktree' actually persists.
Adds tests for uncommitted-edit preservation, failed integration commit,
integration exception blocking, and unsafe-id rejection.
Co-authored-by: Mervin Praison <[email protected]>
Address reviewer findings on PR #3081:
- _extract_summary_text no longer falls back to instance _previous_summary,
so a reused compactor cannot persist an outdated summary on a later
non-summarizing pass (drops intervening turns on resume).
- async compaction persists the checkpoint via asyncio.to_thread so the
locked JSON read/modify/write disk I/O no longer stalls the event loop.
- add regression test for the stale-summary leak.
Co-authored-by: Mervin Praison <[email protected]>
Addresses 3 P1 review findings on #3049 reload observability:
- health() overlays live _reload_watcher_active onto the recorded
ReloadStatus snapshot so a watcher that exits after the last reload is
reported as disabled instead of stale active.
- initial config load defers applied_config_revision until channels/agents
are actually up, and records a failed reload (and re-raises) if setup
throws, so a partial start no longer reports the config as live.
- bump praisonaiagents minimum to >=1.6.152 so standalone praisonai-bot
installs resolve an agents release containing ReloadStatus /
compute_config_revision (avoids ImportError at import time).
Adds a regression test for the stale-watcher case.
Co-authored-by: Mervin Praison <[email protected]>
HealthMonitorSchema used invented keys (check_interval, failure_threshold,
etc.) that the runtime never reads. server.py passes gateway.health straight
to HealthMonitorConfig.from_dict, which only consumes interval/startup_grace/
stale_after/stuck_after/max_restarts_per_hour/enabled. As written the PR would
reject a valid `interval: 60` (extra="forbid") and silently accept ignored
keys. Fixes the schema to mirror the real runtime keys and adds a regression
guard test.
Co-authored-by: Mervin Praison <[email protected]>
Update the seen set while merging extra_tools so duplicates within the
resolved --tools/--toolset/discovered set are removed (not just against
frontmatter), matching _auto_discover_project_tools. Adds a regression test.
Co-authored-by: Mervin Praison <[email protected]>
- tool_execution: gate resolved tool calls through the attached PolicyEngine
(check_tool) and tool-call guardrails (validate_tool_call) before dispatch;
denials are non-retryable and bypass the circuit breaker
- memory: delegate reset_/delete_short_term/long_term to the active adapter
(sqlite/in_memory) to avoid the legacy short_mem/long_mem schema mismatch,
and return the memory id from store_short_term/store_long_term
- memory adapters: add delete_memory/reset_short_term/reset_long_term to the
sqlite and in-memory adapters
- workflows: AgentFlow.run() honors step.on_error="stop" (aborts + marks
failed) and the final status reflects any unresolved step failure instead
of always reporting "completed"
Co-authored-by: Mervin Praison <[email protected]>
Opt-in worktree isolation (workspace_kind="worktree") gives each parallel
worker a dedicated git worktree/branch (kanban/<task_id>) as its cwd, and
integrates the branch back on completion with conflict-on-merge detection —
routing conflicts to 'blocked' instead of silently overwriting. Default
"default" keeps today's shared-cwd behaviour.
Co-authored-by: Mervin Praison <[email protected]>
Core (praisonaiagents/gateway): add a frozen ReloadStatus dataclass and a
pure compute_config_revision() helper to protocols.py + exports, giving both
the SDK and bot a canonical reload-status shape and deterministic config hash.
Wrapper (praisonai-bot): record the last reload outcome (ok/failed/no_changes)
and the applied-config revision in _reload_config_locked/start_with_config,
track watcher liveness, and surface reload/applied_config_revision/
on_disk_config_revision/config_drift in health(). CLI `gateway status` now
prints reload result, config drift, and watcher state.
Co-authored-by: Mervin Praison <[email protected]>
The durable persist+resume machinery (Issue #2741) already existed, but
CompactionResult.summary was always "", so the persister received nothing
and every summary was lost on process exit. Populate it from the produced
summary (LLM/naive/fallback) and persist in the async proactive path too.
Co-authored-by: Mervin Praison <[email protected]>
The LoopGuard "no progress" heuristic counted every tool call as no-progress
because its progress counter was never updated (mark_progress is not called in
the tool loop), so any turn with 8+ tool calls halted regardless of results.
This broke legitimate async / long-running MCP tool workflows that poll a job
status (IN_PROGRESS -> COMPLETE) or pace with a wait tool.
Now no-progress is measured by the trailing run of identical tool-result
fingerprints: distinct/changing results reset the streak (progress), while a
genuinely stuck loop of identical results still halts. The real tool result is
passed through record() so the fingerprint reflects actual output.
Co-authored-by: Mervin Praison <[email protected]>
Replace the opaque `gateway: Optional[Dict[str, Any]]` in GatewayConfigSchema
with load-time validation via a typed GatewayServerSchema (extra="forbid",
ranges mirroring core's GatewayConfig) plus HookSchema/HealthMonitorSchema, so
a misspelled or mistyped server knob (drain_timout, "quick") now fails closed
with a friendly, field-named error instead of being silently dropped at
runtime. Blocks stay stored as dicts for backward-compatible downstream access.
Co-authored-by: Mervin Praison <[email protected]>
_run_custom_agent ignored --tools/--toolset and never auto-discovered
.praisonai/tools/*.py, so the "auto-loaded on every run" promise only held
for the default and actions paths. Wire explicit tools/toolsets plus
project-local auto-discovery into the --agent path, unioned with the
frontmatter tools: list (dedup by callable identity), still gated by
PRAISONAI_ALLOW_LOCAL_TOOLS.
Co-authored-by: Mervin Praison <[email protected]>
Adds a protocol-first messaging module giving agents a first-class
sender->named-recipient channel that the bus (type-filtered, no target),
kanban comments (task-scoped, pull-based board) and handoff (sync
parent->child) all lack.
- messaging/protocols.py: AgentMessage + AgentMailboxProtocol
- messaging/inprocess.py: light in-process default impl (InProcessMailbox)
- messaging/__init__.py: lazy exports (zero overhead unless used)
Scoped minimal per AGENTS.md: core protocol + in-process default only.
No Agent param, no agent.py changes, no heavy deps. The wrapper can add
a Redis-backed impl of the same protocol for cross-host fleets.
Co-authored-by: Mervin Praison <[email protected]>
Adds the core primitive for per-step workspace checkpoints by encoding a
step index into the shadow-git commit message (zero extra storage):
- CheckpointService.save(..., step=N) tags a per-step checkpoint
- CheckpointService.restore(step=N) + get_checkpoint_by_step() rewind to a step
- Checkpoint.step field surfaced in list/to_dict
- CLI: `praisonai checkpoint restore --step N`
Backward-compatible and opt-in; no new Agent params or config modules.
Co-authored-by: Mervin Praison <[email protected]>
Add WorkspaceIsolationProtocol with NoIsolationAdapter (default,
zero-overhead) and GitWorktreeAdapter (real git worktree per run via
lazy stdlib subprocess) so concurrent agents can edit the same repo
without clobbering each other. Degrades gracefully when cwd is not a
git repo. Lazily exposed from praisonaiagents.workspace.
Co-authored-by: Mervin Praison <[email protected]>
- Remove unreachable StepResult lazy branch in workflows/__init__.py
(StepResult is bound at module top and in __all__, so __getattr__ never
fires for it; the branch also returned a different results.py class,
masking a latent class-identity mismatch)
- Remove orphaned structured_execution.py and its lazy branch
(StructuredWorkflowExecutor/execute_step_structured had zero consumers)
Live branches (StepError, WorkflowResult, StepStatus, ErrorStrategy),
results.py, and the memory/workflows shim are untouched.
Co-authored-by: MervinPraison <[email protected]>
Standalone `pip install praisonai-mcp` (without praisonai-code) previously
crashed config subcommands (list/add/remove/test/sync/run/auth/logout/…) and
bare `praisonai-mcp` (callback→list) with an uncaught ImportError and empty
output. Add a lazy `_require_code()` guard that exits cleanly (code 1) with an
actionable install hint. Host commands (serve/list-tools/doctor) are unaffected
and still route directly to the server without needing praisonai-code.
Co-authored-by: Mervin Praison <[email protected]>
- Fix ModuleNotFoundError: praisonai_mcp.cli.output — route
get_output_controller through the lazy praisonai_code bridge in
_configuration.py (resolves CI test_mcp_list, test_cli_shim_app_identity,
and test_get_command_resolves_mcp which failed because the command import
raised before resolution).
- Scope logging/setLevel to the actual running package logger tree derived
from __name__ (praisonai_mcp.mcp_server) instead of the legacy
praisonai.mcp_server shim namespace.
- Guarantee subprocess cleanup on TimeoutExpired (test) and KeyboardInterrupt
(stdio serve); always shut down MCP instances in sync/SSE via try/finally.
- Always clear temporary OAuth state/code verifier via finally.
- Remove redundant/broken assignment and unused var in check_c12_mcp_imports.sh.
Co-authored-by: Mervin Praison <[email protected]>
Route get_available_tools() through the canonical ToolResolver (single
source of truth) and demote the frozen AVAILABLE_TOOLS list to a
last-resort fallback, so all three auto-generators advertise the
actually-installed tools instead of the drifting legacy CrewAI list.
Move the resolver helpers to BaseAutoGenerator and use the shared
context-local _get_default_resolver() so the auto phase and
AgentsGenerator reuse one warm cache per run instead of repeating full
tool discovery. AgentsGenerator now accepts an optional tool_resolver
for DI.
Co-authored-by: MervinPraison <[email protected]>
Remove dead-code Chainlit-era UI modules with zero importers anywhere in
the codebase:
- ui/callbacks.py (CallbackManager)
- ui/components/aicoder.py (AICoder)
Neither module is imported, re-exported, or in any __all__. The live
`praisonai ui` loader resolves bundled presets from ui_*/default_app.py,
which are unaffected.
Co-authored-by: MervinPraison <[email protected]>
Default publish_all.py to git-diff based selection: only bump and ship
packages with changes since the last v* tag, plus the wrapper when any
tier-2 or agents package publishes so pins stay current. Use --all for
the previous seven-package coordinated release.
Move the heavy MCP server host out of the wrapper into praisonai-mcp with
backward-compat shims, _MCP_RESIDENT_COMMANDS routing, CI/publish wiring,
and regression gates so pip install praisonai keeps working.
Restrict labeled-event bypass to praisonai-triage-agent[bot] only (P1).
Require actionable title prefix for external bug/enhancement auto-trigger (P2).
- browser CLI: point relocated engine/utility imports at praisonai_browser.*
package root (were resolving to nonexistent cli.commands.* submodules)
- cdp_agent/playwright_agent: implement advertised clear_input action
- agent.process_observation: return the round-tripped dict so executors
always receive a normalized value field (populated from text)
- _protocol_bridge: pass extra actions through as strings instead of
re-raising on BrowserActionType()
- tests: fix REPO parents[] index in C11 compliance/backward-compat suites
Co-authored-by: Mervin Praison <[email protected]>
Move extension bridge, CDP, and Playwright automation out of the wrapper with backward-compat shims, protocol bridge to agents-tier browser types, and full CI/release wiring.
Addresses Greptile P1 review feedback on #3052:
- Only set early_stopped when iterations remain (iteration_num < self.iterations),
so reaching 9.5 on the last iteration is reported as a full run.
- Gate "Requested Iterations" summary row on the early_stopped flag instead of a
cross-scope count comparison (unreliable with multiple scenarios).
- Add regression test for 9.5-on-final-iteration.
Co-authored-by: Mervin Praison <[email protected]>
- Clarify --iterations is a maximum in CLI help, README and docstrings
- Add --no-early-stop / no_early_stop to force all iterations
- Log one INFO line on early stop (visible even in --quiet)
- Show Requested/Completed and Stopped Early in summary when truncated
- Track early_stopped in report metadata
- Add unit tests for early-stop behavior
Co-authored-by: Mervin Praison <[email protected]>
Greptile P1: --storage-backend file --storage-path <dir> was silently
ignored by list/show/apply (read default ~/.praison/train instead), so
sessions written to a custom file directory were invisible.
- _resolve_backend now returns a FileBackend for file+custom path
- list scans the custom dir via list_training_sessions(storage_dir=...)
- adds TestCustomFileBackedSessions regression coverage
Co-authored-by: Mervin Praison <[email protected]>
SQLite-backed training sessions were invisible to list/show/apply because
those commands scanned ~/.praison/train/*.json and gated on JSON file
existence, ignoring the backend used during training.
- Add backend-aware TrainingStorage.exists() (delegates to _store.exists())
- Add list_sessions_from_backend() to enumerate sessions from a backend
- Add --storage-backend/--storage-path flags to list/show/apply
- Thread backend through get_training_profile for apply
Co-authored-by: Mervin Praison <[email protected]>
Address Greptile P1s on ScheduleLoop:
- start() now checks is_running before mutating _store/_runner/_on_due so
calling it on a live loop is a true no-op (no callback/store swap mid-tick).
- fire_due() runs its claim+fire under a per-instance lock so overlapping
external calls / the loop thread cannot double-fire a due job on stores
using the non-atomic get_due_jobs fallback.
Co-authored-by: Mervin Praison <[email protected]>
Make the scheduler *firing* mechanism as pluggable as the *store* already is.
Adds SchedulerProviderProtocol (the "when to fire" contract) beside
ScheduleStoreProtocol in core, with the existing ScheduleLoop as the default
implementation (aliased InProcessScheduleProvider). ScheduleLoop now exposes
fire_due() (one tick's claim+fire, callable without an always-on thread) and a
start(on_due=..., store=...) signature satisfying the protocol, enabling
event-driven/serverless/external firing while staying fully backward-compatible.
Protocol-only, no heavy deps, no new Agent params; heavy providers
(APScheduler/webhook) remain out of scope for the wrapper.
Co-authored-by: Mervin Praison <[email protected]>
The async chat path calls execute_tool_async, which bypassed
_execute_tool_with_context and never populated _turn_tools_used. This
left the async self-improve review seeing an empty tool list. Record the
tool name in execute_tool_async using the same guarded pattern as the
sync path, and add async tracking tests.
Co-authored-by: Mervin Praison <[email protected]>
Narrow the UnicodeEncodeError=success handler so it only wraps the
post-run summary display, not trainer.run() itself. A verbose mid-run
encoding error (before save_report) is now a real failure, while a
display-only error after persistence downgrades to a warning. Guards
the verbose post-save print_summary() in the orchestrator too.
Addresses Greptile P1 on PR #3055.
Co-authored-by: Mervin Praison <[email protected]>
Address reviewer feedback on PR #3053:
- Greptile P1: uv.lock omitted litellm from praisonai-train's dependencies
and requires-dist even though pyproject.toml declared it, so frozen
installs could reject the stale lock or omit litellm. Added litellm to
both sections.
- Greptile P2: the `train llm` bridge caught every ImportError and printed
the praisonai-train[llm] hint, masking nested failures from installed
transitive deps. Now only shows the install hint when praisonai_code is
truly absent (code_available()); otherwise surfaces the real cause.
Co-authored-by: Mervin Praison <[email protected]>
Addresses Greptile P1: a create staged (propose=True) in project A then
approved from a different cwd re-resolved _skills_base_dir(), writing the
skill into the approver's directory (or user home) instead of project A.
Now the resolved destination is captured at staging time and honoured by
create_skill via an internal base_dir override.
Co-authored-by: Mervin Praison <[email protected]>
Track per-turn tool names in a _turn_tools_used buffer and default the
after-agent hook's tools_used from it when chat paths omit the argument.
Without this the DefaultSkillReviewPolicy always saw an empty list and the
self-improve review never fired.
- agent.py: initialize _turn_tools_used buffer
- chat_mixin.py: reset buffer at start of each _chat_impl/_achat_impl turn;
fix verbose display NameError (undefined `tools` -> self.tools)
- tool_execution.py: record tool name in _execute_tool_with_context (skipped
during guarded review turns); default hook tools_used from buffer in sync
and async after-agent hooks and consume it
Co-authored-by: Mervin Praison <[email protected]>
Training could complete and persist a session, then crash while printing
the Rich summary because the Status row contained emoji that cp1252 stdout
cannot encode. The broad except handler mislabeled this display error as
"Training failed" with exit 1.
- Add console_supports_unicode() probe in models.py
- Use ASCII labels for Status/markers when stdout is not UTF-8
- Fall back to ASCII summary on UnicodeEncodeError in print_summary
- Split CLI handler so encoding failures report success (exit 0)
- Guard show/orchestrator emoji markers behind the same probe
- Add cp1252 encoding unit tests
Co-authored-by: Mervin Praison <[email protected]>
- SkillManager._skills_base_dir() now prefers project ./.praisonai/skills/
when it exists, so skill writes land where discovery reads and users look;
falls back to user home (~/.praisonai/skills/) otherwise.
- Keep the pending-mutation store in user home regardless of cwd so proposals
remain discoverable for approval.
- Log an INFO line with the SKILL.md path when a skill is created directly,
complementing the existing "staged for approval" pending log.
Co-authored-by: Mervin Praison <[email protected]>
Issue A: praisonai-train requires litellm at runtime for agent training
(LLM-as-judge grading), but it was not declared in pyproject.toml. Add
litellm>=1.0 to core dependencies so a clean `pip install praisonai-train`
resolves all runtime imports for live grading.
Issue B: `praisonai-train llm` printed a wrong install hint when ML/code
deps were missing. Catch the bridge ImportError and surface the correct
`pip install "praisonai-train[llm]"` remediation.
Co-authored-by: Mervin Praison <[email protected]>
External bug/enhancement reports never received the claude label, and
bot-applied claude labels were blocked by the actor filter—so claude-response
never ran after issue-triage.
- server.py: consume the cancellation marker once it is honoured (both the
active-task and pre-registration paths) so a later request reusing the same
JSON-RPC id is not spuriously cancelled by a stale entry.
- server.py: make in-flight registration cleanup identity-safe so a concurrent
client reusing an id cannot have its task evicted by another's finally block.
- server.py: scope logging/setLevel to the `praisonai.mcp_server` package logger
(parent of the loggers this module actually emits through) so the change takes
effect, while still never touching the process root.
- agents_generator.py: key the timeout-wrapper marker by owning executor_factory
identity and retain the original callable, so a shared framework-tool object is
idempotent within a generator but correctly re-wrapped (not stacked) when a
different generator takes ownership — avoiding a retained, possibly-closed
executor and wrong timeout.
Co-authored-by: Mervin Praison <[email protected]>
Gap 1 — MCP notifications/cancelled was a dead path: _active_requests was
never populated so task.cancel() was unreachable, and set.pop() evicted an
arbitrary id on overflow. Register id-bearing requests as tasks and use an
OrderedDict for FIFO overflow eviction.
Gap 2 — _wrap_with_timeout mutated shared framework tool objects with no
idempotency marker, stacking wrappers on every generate_crew_and_kickoff()
and capturing foreign generators' executors. Stamp a marker so a callable /
tool method is never wrapped twice.
Gap 3 — logging/setLevel mutated the process-wide root logger and bypassed
scope enforcement. Scope it to the server's namespaced logger, validate the
level, and add an OPERATION_SCOPES entry requiring the admin scope.
Co-authored-by: Mervin Praison <[email protected]>
- Pin checkout to main (ref: main) so workflow_dispatch cannot publish
a non-main branch to production
- persist-credentials: false (no git creds retained in workspace)
- Replace TOFU ssh-keyscan with pinned known_hosts from
MIGRATION_TARGET_KNOWN_HOSTS secret + StrictHostKeyChecking=yes
- Add trap to remove SSH private key on exit
- Add BatchMode + ConnectTimeout to fail fast on unreachable host
- Upload to temp files then atomic mv into place to avoid serving
truncated or mixed-commit installers
Co-authored-by: Mervin Praison <[email protected]>
Ignore pipeline status comments that mention @claude as text, and restrict
bot-triggered runs to intentional FINAL, merge-conflict, and CI-fix phrases.
Addresses reviewer feedback on PR #3034:
- Gemini: claudeFinalReady got stuck awaiting Copilot when a required
reviewer timed out. Now short-circuits Copilot when prior.requiredTimeout.
- Gemini: maybeTriggerClaudeFinal now spreads all options into claudeFinalReady
so requiredWaitMs/skipCopilot are honored. It already injects prCreatedAt from
the fetched PR, so the fallback workflow caller activates the 30m timeout
without passing prCreatedAt (Greptile concern).
- Update self-tests to assert auto-skip without explicit skipCopilot.
Co-authored-by: Mervin Praison <[email protected]>
HTTPStreamMCPClient rewrote bare-host URLs by appending '/mcp', causing a
POST to a non-existent endpoint (HTTP 404) that the official SDK surfaces as
McpError "Session terminated" during initialize(). Use the server URL exactly
as provided, matching the official streamablehttp_client behaviour.
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
Prevent leaking the httpx/anyio connection or a half-open remote MCP
session when ClientSession entry or session.initialize() fails mid-handshake,
and best-effort schedule async teardown from _force_cleanup().
Co-authored-by: Mervin Praison <[email protected]>
The HTTP-Stream client passed a hand-rolled async generator and a plain
function to the official ClientSession, which expects anyio memory-object
streams. This broke session.initialize() with
"'function' object has no attribute 'send'" before any request reached the
wire. Wire HTTPStreamMCPClient to the official SDK's streamablehttp_client
(mirroring the working SSE client), relax the obsolete aiohttp requirement
(the SDK is httpx-based), and update cleanup accordingly.
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
The Windows install.ps1 installed only praisonaiagents, but the onboarding step runs 'python -m praisonai setup', which failed with 'No module named praisonai'. Bring install.ps1 to parity with install.sh by installing praisonai[all] by default.
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
Move key lowercasing and length-descending sort to module scope so
exact mixed-case matches resolve on the fast path and per-call sorting
is eliminated. Behavior unchanged.
Co-authored-by: Mervin Praison <[email protected]>
voyage-3 (1024) is a substring of voyage-3-lite (512); sort keys by length
descending so more specific keys win. Adds test coverage.
Co-authored-by: Mervin Praison <[email protected]>
resolve_batch() had zero callers anywhere in the package, tests, or
public API, and re-implemented resolve()'s parameter-precedence ladder,
risking silent drift. Removed the ~110-line dead helper; resolve()
remains the single source of truth.
Co-authored-by: MervinPraison <[email protected]>
Extract _reset_loop_warnings() and _consume_self_correction() to remove
~40 lines of verbatim copy-paste across run_autonomous/run_autonomous_async,
eliminating sync/async drift risk and trimming agent.py per AGENTS.md.
Co-authored-by: Mervin Praison <[email protected]>
Factor the ~40 byte-identical lines shared by _trigger_after_agent_hook
(sync) and _atrigger_after_agent_hook (async) into two loop-agnostic
helpers: _build_after_agent_input (AfterAgentInput construction) and
_after_agent_side_effects (auto-memory, auto-learning, periodic nudge).
The async boundary is untouched: hook dispatch (execute_sync vs
await execute) and skill-review (run_review vs await arun_review) remain
in their respective public methods. No behaviour or async-safety change.
Co-authored-by: MervinPraison <[email protected]>
Adds explicit "lightweight & powerful, no scope creep for the sake of it"
guidance plus "critical review at each stage" to AGENTS.md (root +
praisonai-agents) and the Claude review/merge-gate/merge-conflict prompt
workflows, so issue triage, fix planning, and PR review all reject
duplicate/dead-knob scope creep and prefer existing Agent capabilities.
Co-authored-by: Leebana <[email protected]>
Cap only the non-system tail so an existing compaction summary (stored
as a system message) is never dropped when trimming oversized history
before compaction. Removes the now-redundant post-compactor safety valve.
Co-authored-by: Mervin Praison <[email protected]>
Apply the max_history hard cap before token counting and summarisation so
large short-message batches do not hang offline hosts when tiktoken attempts
a first-use network download during compaction.
- Route critical loop-block through shared blocked_result path so
trace spans, stream events, AFTER_TOOL hooks, doom-loop and
loop-guard teardown still run (CodeRabbit: dangling tool_call_start).
- Reset _pending_self_correction / _loop_warned_this_turn at the start
of run_autonomous(_async) so stale nudges never leak across calls.
- Tighten _response_indicates_failure: drop broad "i am/was unable to"
markers that flagged partial-success responses (Greptile P2).
Co-authored-by: Mervin Praison <[email protected]>
Address remaining reviewer findings on the AutoGenerator registry path:
- Reject built-in provider prefixes before consulting the registry, reusing
the same _BUILTIN_MODEL_PREFIXES set as PraisonAIModel so both hot paths
agree on which providers are 'registered' (CodeRabbit).
- Preserve provider-specific config fields (aws_region, timeout, model_kwargs,
...) when constructing a registered provider, only dropping empty credential
entries so the provider's own credential chain is not shadowed (Greptile P1).
- Add 2 regression tests covering the built-in-prefix guard and config
preservation.
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
- auto.py: forward config_list[0] api_key/base_url into create_llm_provider
so registered providers authenticate for generate_structured (Greptile P1).
- models.py: omit None api_key/empty base_url from provider config so a
provider's own credential chain is not shadowed (Greptile P2).
- models.py: surface a registered provider's construction failure instead of
silently falling into the uncredentialed built-in OpenAI ladder (Greptile P1).
- tests: add coverage for construction-failure surfacing and no-None-key config.
Co-authored-by: Mervin Praison <[email protected]>
PraisonAIModel and AutoGenerator now consult praisonai.llm.registry for
user-registered providers before their built-in resolution ladders, so
register_llm_provider("bedrock", ...) takes effect through both hot paths.
Built-in prefixes and unregistered providers keep their existing behaviour;
registry lookups fail closed and never raise.
Co-authored-by: MervinPraison <[email protected]>
Addresses reviewer findings on PR #2990:
- P2: falsy/unknown self_improve strings ("false"/"off"/typos) no longer
silently enable inline review; unknown values disable + warn.
- P1: background review now uses a unique per-invocation job id so a later
review never replaces an earlier queued one in the job-manager map.
- P1: guarded review runs under a per-agent lock so a background review
cannot interleave chat_history/re-entrancy state with a concurrent turn.
Co-authored-by: Mervin Praison <[email protected]>
Add a "background" execution mode for self_improve so the guarded skill-review
LLM turn runs on the core BackgroundJobManager after the reply is returned,
instead of blocking the user-visible turn. Default remains inline/blocking for
backward compatibility. Preserves re-entrancy, chat-history isolation and
best-effort failure swallowing.
Co-authored-by: MervinPraison <[email protected]>
The canonical schema treats agents/token as optional; routing the runtime
loader solely through it dropped three gateway-runtime guarantees the
integration tests assert. Re-assert 'agents'/'channels' required sections and
per-channel 'token' (except email/agentmail/whatsapp-web) on the
pre-substitution config so a wholly-missing token fails closed while a ${VAR}
that resolves to empty is left for start_channels() to skip. Platform/typo
fail-closed remains delegated to the canonical validator.
Co-authored-by: Mervin Praison <[email protected]>
Addresses two review findings on the canonical-validator routing:
- P1: merge validator-populated channel fields (resolved platform,
plugin descriptor env fallbacks/required fields, canonical defaults)
back into existing `channels:` dicts, not just when absent, so
adapters start with fully-resolved settings.
- P2: resolve `${VAR}` env substitution before validation so an unset
token for a currently-unused channel stays an empty string that
start_channels() skips, instead of tripping the schema token
validator and aborting the whole gateway. Platform-typo fail-closed
still applies.
Co-authored-by: Mervin Praison <[email protected]>
The gateway runtime loader (WebSocketGateway.load_gateway_config) hand-rolled
a second config validator with a hardcoded platform allowlist that only
warned on unknown platforms and started anyway — a fail-open hole where a
typo'd channel platform passed validation, the gateway reported healthy, but
the channel silently never ran.
Route the runtime loader through the existing canonical validator
(migrate_legacy_config + validate_gateway_config), which:
- fails closed (ValueError -> exit 78) on unknown/typo'd platforms
- is registry-aware (accepts built-in, entry-point, and register_platform()
channels), fixing the spurious "not a known platform" warning for linear
- migrates legacy single-bot / BotOS config shapes on the runtime path too
The raw dict is preserved (all top-level keys the runtime reads survive) and
env-var substitution is still owned by the loader.
Co-authored-by: MervinPraison <[email protected]>
ConsoleBackend now uses rich Prompt.ask (once/session/always/no) instead
of Confirm.ask. The approval-integration tests patched Confirm.ask to
auto-approve the require_approval decorator path; the mock no longer
intercepted, so the prompt read stdin under pytest and denied the tool.
Patch Prompt.ask -> "o" (once/approve) to match the intentional change.
Co-authored-by: Mervin Praison <[email protected]>
- auth list/status: always surface active env-var keys even when a stored
key exists, labelled as overriding the stored one, so the live credential
is never hidden (Greptile P1 #1, #2)
- setup: derive default model from the catalogue for catalogue providers so
non-interactive setup (e.g. --provider groq) no longer fails (P1 #3)
- setup: use the same authoritative env-var name as auth for divergent
providers (perplexity -> PERPLEXITYAI_API_KEY) (P1 #4)
- tests: cover perplexity env-var parity and catalogue default model
Co-authored-by: Mervin Praison <[email protected]>
Address reviewer (Greptile) security findings on scoped approvals:
- utils.py: add src key so move_file/copy_file grants pin to the concrete
source path instead of a tool-wide tool:move_file rule; remove apply_patch
from the path-prefix map (it takes multi-file patch text, not a single
path) so it stays tool:apply_patch rather than a misleading edit:<...>.
- registry.py: keep "this session" grants in-memory only (new
_session_scoped_targets store) so they never persist to approvals.json and
outlive the run; only "always" grants bridge to PermissionManager.
- registry.py: skip durable persistence for "always" when agent_name is None
(a nameless grant would match any agent); degrade to in-memory session
scope for the run instead.
- Session grants are cleared by clear_approved and wired into both sync/async
approval fast-paths.
- Tests updated/added for move/copy src mapping, apply_patch tool-scoping,
session non-persistence, nameless-always skip, and clearing.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
The published JSON schema and the praisonai-code CLI resolver place model
settings under a top-level `agent` section (e.g. `agent.small_model`), while
the praisonaiagents loader uses `defaults.*`. Since both read the same
`.praisonai/config.yaml`, a user following schema autocomplete set
`agent.small_model` that title generation silently ignored (greptile finding).
get_small_model() now also reads `agent.small_model` / `agent.model` from the
raw config, closing the schema-path gap. Fully additive and backward
compatible; `defaults.*` still takes precedence.
Co-authored-by: Mervin Praison <[email protected]>
Add a declarative auxiliary/"small" model default so internal LLM calls
(session-title generation, and future summarisation/compaction paths) can
target a cheap/fast/local model independent of the primary model.
- config/loader.py: add `small_model` to DefaultsConfig, validation keys,
dict round-tripping, and a `get_small_model()` resolver
(small_model -> primary_model -> config model -> fallback).
- session/title.py: resolve the auxiliary model via `get_small_model`,
falling back to the primary model then the historical `gpt-4o-mini`
default so existing behaviour is preserved when nothing is configured.
- praisonai config.schema.json: document `agent.small_model`.
- Tests for resolution order and backward compatibility.
Co-authored-by: MervinPraison <[email protected]>
Adds a scope field to ApprovalDecision, a once/session/always/no choice to
ConsoleBackend, and bridges session/always decisions into PermissionManager
so grants persist and future matching calls short-circuit without re-asking.
Default remains "once" — non-interactive/auto-approve paths are unchanged.
Co-authored-by: MervinPraison <[email protected]>
Addresses greptile P1 review findings on tool_execution.py:
- BYPASS now skips the PermissionManager deny gate (via _is_bypass_mode)
so it truly bypasses permission checks as documented.
- DONT_ASK and PLAN now consult a shared _tool_would_prompt() that mirrors
the needs_approval criteria (dangerous, external trust_level,
registry-required, explicit ask rule), so external/registry-required
tools are auto-denied under DONT_ASK and denied under PLAN instead of
falling through to a prompt/hang.
Co-authored-by: Mervin Praison <[email protected]>
Three safety/reliability controls that previously silently no-op'd:
1. MCP SSE (mcp_server.py, mcp_security.py): run_sse now derives the bind
address from SecurityConfig.get_bind_address() when host is unset, so
bind_localhost_only (secure-by-default) actually controls the interface,
and fails fast when require_auth=True but no token is available instead
of serving every request unauthenticated. Adds resolve_sse_security() as
the shared source of truth.
2. Permissions (manager.py, tool_execution.py, agent.py, protocols.py):
_resolve_approval_decision now consults the PermissionManager so explicit
``ask`` rules drive the approval prompt (not just hard ``deny``), and wires
PermissionMode (BYPASS/PLAN/DONT_ASK) into the call-time gate via a new
ApprovalConfig.permission_mode. Adds PermissionManager.resolve_tool_action()
which only reports explicit rule matches so unlisted tools keep prior
behaviour.
3. Compaction (compactor.py): iterative summary baseline is now tracked off
the returned compacted list length rather than len(messages) of the input,
so later passes summarize new turns instead of silently dropping them; and
compact()/compact_async() now set was_skipped_due_to_low_savings=True when
the anti-thrashing streak halts compaction.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Root cause of the CI failure and Greptile P1: praisonai_train.cli.commands.train
imported ..output.console but that module was never moved into the extracted
package, so every agents/list/show/apply command raised ModuleNotFoundError
before doing any work.
- Add self-contained praisonai_train.cli.output.console: delegates to
praisonai-code's OutputController when co-installed (via lazy _code_bridge),
falls back to a minimal rich/typer controller so `pip install praisonai-train`
works standalone. Fixes test_cli_show_displays_iterations.
- pyproject: `[llm]` extra now pulls praisonai-code (the llm command bridges into
the code CLI runner); register `setup-conda-env` entry point advertised by README.
- apply: resolve real agent name for the training profile and reuse the loaded
agent for --run instead of loading the file twice.
- Expand ~ in file/sqlite storage paths; open scenarios/vision configs as utf-8.
- check_c10_train_imports.sh: use POSIX `grep -Ev` for portability.
62 train unit tests pass; standalone agents --dry-run / list / show verified with
the fallback controller (no wrapper/code installed).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Mervin Praison <[email protected]>
Extract LLM fine-tuning (Unsloth) and agent training from the praisonai
wrapper into a new installable package src/praisonai-train (PyPI:
praisonai-train, import: praisonai_train), following the C9 praisonai-bot
playbook.
- Move praisonai/train/{llm,agents,_ollama}, train_vision.py,
upload_vision.py, cli/commands/train.py, and the conda setup scripts
into praisonai_train (git mv, history preserved)
- Wrapper keeps backward-compat shims: alias_package for praisonai.train,
sys.modules shims for the command/vision modules, setup-conda-env
delegate; all old import paths resolve to the same module objects
- praisonai-code routing: new _TRAIN_RESIDENT_COMMANDS + _train_bridge;
train removed from _WRAPPER_RESIDENT_COMMANDS; hidden on standalone
installs without praisonai-train
- Legacy dispatch (praison_ai.py) prefers praisonai_train, falls back to
wrapper shims
- Packaging: wrapper depends on praisonai-train>=0.0.1; extra
train = ["praisonai-train[llm]"] now actually installs the ML stack
- CI: 5-package install order, standalone train smoke, train test shard,
check_c10_train_imports.sh gate
- Release: agents -> code -> bot -> train -> wrapper across
_release_lib.py, bump_and_release.py (--train/--train-pin/--wait-train),
publish_all.py, pypi-release.yml
- Tests: test_c10_train_backward_compat.py (20 tests), unit tests moved
into src/praisonai-train/tests (62 tests, run standalone)
- Docs: PRAISONAI_TRAIN_MANIFEST.md, ARCHITECTURE.md five-package model,
AGENTS.md refresh, beginner-friendly package README
Co-Authored-By: Claude Fable 5 <[email protected]>
Route compaction token counting through a one-time, timeout-bounded probe of
the accurate tiktoken tokeniser. tiktoken downloads its BPE vocab on first use,
which blocks indefinitely on network-isolated CI hosts and caused
test_max_history_hard_cap_in_compaction_mode to hit the 60s pytest-timeout.
The probe runs once in a short-lived daemon thread; if the tokeniser is not
locally available it permanently falls back to the fast character heuristic,
keeping the hot path offline-safe while preserving accuracy where tiktoken is
cached.
Co-authored-by: Mervin Praison <[email protected]>
Addresses reviewer feedback on PR #2995:
- Strip in_loop_compaction/clear_threshold_pct/compact_threshold_pct/
keep_recent_tool_results from LiteLLM completion params so local
context-management settings never leak to the provider.
- Wire keep_recent_tool_results from extra_settings into the loop compactor.
- Rebuild the cached loop compactor when the active model changes
(auth/model failover) so budgets and tokenizer model stay aligned.
Co-authored-by: Mervin Praison <[email protected]>
Run two-tier context management continuously inside the tool-calling loops
(sync + async) in llm/llm.py instead of only once before a turn. Between
iterations the loop now clears re-fetchable tool results at a low threshold
and summarises dialogue at a higher one, gated on an accurate token count
derived from the model's real context window via context/budgeter.
- compaction/compactor.py: accurate tokeniser (reuses context/tokens.py) with
heuristic fallback; count tool_calls payloads; new clear_tool_results().
- compaction/config.py: add model + in-loop threshold config fields.
- llm/llm.py: _resolve_context_window + _manage_context_in_loop helpers,
wired into both tool loops; no-op below threshold and when disabled.
Co-authored-by: MervinPraison <[email protected]>
- goal/loop.py: self-contained continuation nudge restating goal + criteria so
history-clearing configs don't lose the task; resume persisted paused runs for
the same goal instead of restarting at turns_used=0; persist terminal state on exit.
- goal/judge.py: replace greedy {.*} regex with brace-depth scanning that respects
string literals, so a valid verdict followed by a note/second object still parses.
- agent/agent.py: fold string `criteria` into GoalCriteria on run_until(goal=) so it
is not silently dropped; document the AutonomyResult return-shape when goal is set.
- tests: add regression coverage for parsing and continuation context.
Co-authored-by: Mervin Praison <[email protected]>
Addresses reviewer P1 findings on the spend kill-switch:
- Baseline cumulative spend at loop start so a reused Agent's prior
chat/run usage no longer counts against a new run's budget cap
(sync + async loops).
- Validate max_budget_usd/max_tokens in AutonomyConfig.__post_init__
(reject non-numeric and negative values) so invalid caps fail fast
instead of raising TypeError mid-loop or instantly exhausting.
- Tests: add invalid-cap validation + Agent-reuse baseline coverage;
update pause-recoverable test for per-run semantics.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Resolves rebase conflict between the negotiated-hello handshake (#2988) and
main's terminal-vs-transient connect-error classification (#2984):
- Extract shared _raise_for_connect_error() used by both the modern
hello_error path and the legacy join fallback, so a non-recoverable
code (revoked token / unpaired device / agent-not-found) stops the
reconnect loop instead of retrying forever, while transient codes
honour the server retry_after floor.
- _coerce_limit() now rejects negative numeric policy limits (returns
None) so a bogus max_payload: -1 does not block every outbound frame.
Co-authored-by: Mervin Praison <[email protected]>
Adds an independent budget kill-switch to run_autonomous / run_autonomous_async:
max_budget_usd and max_tokens caps checked each iteration against the existing
per-run cost/token tracking (Agent._total_cost/_total_tokens_*). On exceed the
run stops cleanly and returns a typed budget_exhausted outcome carrying spend
so far and the partial result; budget_action="pause" marks it recoverable.
Promotes AutonomyResult.completion_reason to a TerminationReason StrEnum
(co-located with AgentRunOutcome), keeping the historical string values stable
and adding budget_exhausted, plus a mapping to the shared RunStatus vocabulary.
Caps default unset = unlimited, so behaviour is unchanged until a cap is set.
Co-authored-by: MervinPraison <[email protected]>
- Enable loop_detection.py by default and invoke it in the tool-execution
path: record_tool_call + detect_tool_loop before execution (critical -> block),
record_tool_outcome after (result-aware, so polling with changing output is
not flagged). Two-tier response: warning queues a one-shot self-correction
nudge, critical hard-stops.
- Stop feeding DoomLoopTracker a hard-coded success=True in run_autonomous /
run_autonomous_async; derive a real per-iteration success signal via
_response_indicates_failure so the consecutive-failure detector can fire.
- Inject the queued self-correction nudge at the top of each autonomous turn.
- Document DoomLoopConfig.similarity_threshold as superseded by the result-aware
detector (kept for backward compatibility).
- Add wiring tests covering polling-not-flagged, identical-output-flagged,
generic-repeat, two-tier self-correction, detector-invoked-in-tool-loop,
backward-compat, and the real failure signal.
Co-authored-by: MervinPraison <[email protected]>
Adds a first-class, independent goal-completion judge that gates the
tool-using autonomous loop against the agent's stated goal, instead of the
keyword/promise heuristics that could declare success prematurely.
- goal/models.py: GoalCriteria (outcome/verification/constraints) + GoalState
(persistent goal/budget/verdict state with JSON round-trip).
- goal/judge.py: judge_goal() — independent, criteria-anchored, JSON-verdict
judge reusing eval.Judge's litellm loader; fails open to "continue".
- goal/loop.py: GoalLoopMixin — the per-iteration judge gate, continuation
nudge, budget-exhaust recoverable pause, consecutive-parse-failure auto-pause,
session persistence, and run_goal()/run_goal_async() public API.
- agent.py: gate run_autonomous (+ async) on the judge before the existing
heuristics (no-op/unchanged when no goal set); extend run_until(goal=,
goal_criteria=) to delegate to the tool-using goal loop.
Default-off: when no goal is set the autonomous loop is byte-for-byte
unchanged. Heavy logic lives outside agent.py to keep it lean.
Co-authored-by: MervinPraison <[email protected]>
Addresses two P1 findings from Greptile review on the run-state journal:
- open_run: on ON CONFLICT reopen, COALESCE the supplied checkpoint_id so a
run that crashed before set_checkpoint can resume from a caller-supplied
checkpoint; a None reopen preserves the existing binding.
- assert_replay_order: compare full (seq, kind) sequence (not bare kinds) so a
replay with the same shape at wrong seq positions fails loud, matching the
(seq, kind)-keyed replay_index lookup. Backward compatible with kind-only
input.
Adds 3 tests covering both fixes.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
- honour max_payload==0 as a real bound instead of skipping the guard
- coerce string/float policy limits via _coerce_limit to avoid TypeError on send
- apply same coercion to heartbeat_ms accessor
Co-authored-by: Mervin Praison <[email protected]>
The bundled Python gateway client now sends the capability-aware `hello`
frame, reads the server-advertised `features`/`policy` from `hello_ok`,
and self-configures: exposes the negotiated method/event manifest,
drives heartbeat from `heartbeat_ms`, and guards outbound frames against
`max_payload` locally (raising `PayloadTooLarge`). The legacy `join`
path is retained purely as a compatibility fallback for older gateways
that reply with a bare `joined` or reject the hello frame.
Co-authored-by: MervinPraison <[email protected]>
Adds praisonaiagents/runtime/journal.py, a tiny append-only SQLite journal
keyed by run_id that records the execution cursor of a run — model decisions,
tool calls/results, iteration index, approval decisions. On resume the loop is
re-driven from the top and journalled steps return recorded results via a
memoised replay index (no re-execution of side-effecting tools, no re-billing
of LLM calls); real work restarts at the first un-journalled step.
- Reuses the stdlib sqlite3 persistence pattern from runs/sqlite_ledger.py and
session/store.py (WAL, busy_timeout, lock-guarded shared connection).
- Binds the files store via checkpoint_id and exposes interrupted_runs() so a
gateway can resume runs left 'running' after a crash — the prerequisite for
durable pause-for-approval HITL.
- Determinism guardrail: assert_replay_order() fails loud on divergence.
- Default-off / zero-overhead: lazy-imported from runtime; inert until a run
opts in (no writes unless open_run/append are called).
Co-authored-by: MervinPraison <[email protected]>
_drain_steering_messages previously returned only raw content, so an
in-flight INTERRUPT/URGENT steer was injected as plain [steering]
guidance, silently downgrading a mid-run interrupt (Greptile P1).
Extract priority-aware formatting into a shared _format_steering_note
helper used by both _check_steering_messages (pre-turn) and
_drain_steering_messages (mid-run), so INTERRUPT/URGENT priority is
preserved on every injection path. Add a regression test.
Co-authored-by: Mervin Praison <[email protected]>
Address reviewer findings on prompt-cache marking:
- Deep-copy the boundary message before marking so caller-owned chat
history is not rewritten to Anthropic form (prevents OpenAI/Gemini
reuse from carrying invalid cache_control metadata).
- Count pre-existing cache_control markers before adding a new one so
the provider 4-breakpoint budget is never exceeded.
- Fix _mark_message_cache_control return contract: return False for an
already-marked list block (no new breakpoint applied).
Co-authored-by: Mervin Praison <[email protected]>
Wire cooperative cancellation and mid-run steering into the tool-calling
loops themselves rather than only at turn boundaries.
- Add a second cancel check immediately before dispatching tool calls in
both LiteLLM (llm.py) and OpenAI-native (openai_client.py) loops, sync
and async, so a /stop mid-iteration skips running tools.
- Drain pending steering messages at the top of every loop iteration and
inject them as user messages before the next model call.
- Add SteeringMixin._drain_steering_messages() returning all pending notes.
- Thread a bound steering_drain callable from chat_mixin into get_response
and get_response_async alongside the existing cancel_token.
- Add unit tests covering interrupt-between-iterations, interrupt-before-
tool-dispatch, mid-run steering injection, and no-op when unconfigured.
Co-authored-by: MervinPraison <[email protected]>
Extend Anthropic prompt caching beyond the single system block: add a
cache breakpoint on the stable conversation-history prefix while leaving
the most recent turns (volatile tail) uncached. Refactor the inline
system-block marker into reusable helpers and enforce the provider
4-breakpoint budget. OpenAI/Gemini rely on automatic prefix caching, so
no explicit markers are emitted.
Co-authored-by: MervinPraison <[email protected]>
- Add AGENT_NOT_FOUND to the non-recoverable connect codes: the gateway
emits it with a DO_NOT_RETRY recovery step, so the client must pause the
reconnect loop instead of backing off forever (greptile P1).
- Clear self._retry_after in connect() so a backoff floor left over from a
prior connection loop cannot pin an unrelated reconnect attempt (greptile P2).
- Update classifier docstring and tests accordingly; add coverage for the
terminal AGENT_NOT_FOUND path and stale-floor clearing.
Co-authored-by: Mervin Praison <[email protected]>
- resources.py: prompt messages with image/audio/blob/embedded content now
reuse the resource content normaliser (size/mime summaries) instead of
emitting raw object reprs; _normalize_content_item now handles `data`
(image/audio) and nested embedded resources.
- mcp.py: _generate_resource_tools now skips creating a synthetic helper when
a server tool already owns that name, keeping the callable and its OpenAI
schema consistent (no orphaned schema the LLM cannot invoke).
- tests: add coverage for binary/embedded prompt normalisation and server
tool name collision precedence.
Co-authored-by: Mervin Praison <[email protected]>
User-global tools (~/.praisonai/tools/*.py, source="user") live outside
the project CWD, so the safe loader's CWD boundary silently dropped them.
Opt those explicitly user-owned paths out of the boundary (mirroring an
explicit --tools path); project-local tools keep the default CWD check.
Co-authored-by: Mervin Praison <[email protected]>
The reconnecting gateway client classified connect failures by exception
type alone, so AUTH_UNAUTHORIZED / pairing / config rejections fell into a
generic except-branch and reconnected forever. It also discarded the
server's structured next_step and retry_after guidance.
- core: add is_recoverable(code) classifier beside ConnectErrorCode so the
client and any alternative client share one terminal/transient taxonomy.
- client: parse structured hello_error frames (code/next_step/retry_after);
raise GatewayConnectError and pause the reconnect loop (via a new
on_reconnect_paused callback) on non-recoverable failures; honour
retry_after as a backoff floor without shortcutting the exponential
sequence for transient failures.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: MervinPraison <[email protected]>
Addresses Greptile P2 review on PR #2982. Default Knowledge() no longer
emits a misleading "retrieval quality may be reduced" warning when falling
back from the implicit default provider (e.g. chroma/mem0 not installed).
The loud logger.warning now fires only when the user explicitly configured
a vector-store provider that degrades; implicit-default fallbacks are logged
at verbose/debug level as before.
Co-authored-by: Mervin Praison <[email protected]>
Addresses reviewer feedback on PR #2981:
- Pin the no-workspace fallback base at construction (cwd captured then, or
explicit root) so a later os.chdir cannot drift the containment boundary of
a long-lived instance or the module-level default helpers.
- Remove the naive '..' substring rejection so legitimate in-workspace names
like 'v1..2.md' are accepted; containment is enforced solely via
realpath + commonpath.
- Add regression tests for cwd-stable boundary and valid '..' filenames.
Co-authored-by: Mervin Praison <[email protected]>
store_long_term_async delegated through self.store_long_term, which for
Memory instances resolves to the overriding weighted-metrics signature
(completeness/relevance/...). Positional quality_score/user_id bound to the
wrong params, silently corrupting quality metadata and dropping user_id.
Call MemoryCoreMixin.store_long_term(self, ...) explicitly to always use the
mixin's own (content, metadata, quality_score, user_id) signature.
Co-authored-by: Mervin Praison <[email protected]>
Extend the .praisonai/ filesystem-discovery convention to a tools/ directory
so `praisonai run` picks up project-local Python tools with the same
zero-config ergonomics as agents/ and commands/.
- CustomDefinitionsDiscovery now scans .praisonai/tools/*.py (user-global +
project walk-up), namespacing callables as <module>.<func> and preferring
@tool-decorated functions. Loading stays gated by PRAISONAI_ALLOW_LOCAL_TOOLS
via the shared safe loader.
- run.py (actions mode) and the default direct_prompt path auto-merge the
discovered tools; explicit --tools take precedence and --no-tools opts out.
- `praisonai init` scaffolds .praisonai/tools/example.py with a commented
@tool stub so the convention is self-documenting.
Backward compatible: explicit --tools and PRAISONAI_ALLOW_LOCAL_TOOLS behave
as before; projects without .praisonai/tools/ are unaffected.
Co-authored-by: MervinPraison <[email protected]>
MCP client previously exposed only server tools, ignoring resources and
prompts. During connect, the stdio runner now negotiates resource/prompt
capabilities (list_resources / list_resource_templates / list_prompts) and
registers synthetic agent-callable tools (list_mcp_resources,
list_mcp_resource_templates, read_mcp_resource, list_mcp_prompts,
get_mcp_prompt) only for servers that advertise them. Contents are normalised
(text/blob/embedded, prompt messages) with size/mime guards. Fully additive,
capability-gated, filterable, and namespaced via the existing tool-prefix
scheme; tools-only servers are unaffected.
Co-authored-by: MervinPraison <[email protected]>
Restores behavior lost when media.py's regex-based parser was removed in
favor of the shared helper. Fixes two regressions flagged in review:
- Mixed-case labels (Score:/score:) now parse instead of defaulting to 5.0
- Multi-line REASONING text is preserved instead of truncated to first line
Uses re.IGNORECASE + re.DOTALL, matching the original media parser. Fully
backward compatible: existing uppercase single-line responses parse identically,
and score clamping to [1, 10] is retained. Benefits all callers (accuracy,
criteria, comparison, media).
Co-authored-by: Mervin Praison <[email protected]>
The async _atrigger_after_agent_hook still built AfterAgentInput (incl.
os.getcwd()) and called execute unconditionally on every achat() turn.
Mirror the sync AFTER_AGENT / BEFORE_AGENT guards so the work is skipped
when no AFTER_AGENT hook is registered.
Co-authored-by: Mervin Praison <[email protected]>
Mem0Adapter and MongoDBKnowledgeAdapter constructors did not accept the
`verbose` keyword passed by Knowledge.memory via the registry, raising a
TypeError that was swallowed and silently replaced with SQLiteKnowledgeAdapter.
- Add `verbose`/`**kwargs` to both adapter constructors
- Raise the fallback log from debug to warning so silent quality
regressions are visible
- Add regression tests asserting adapters accept `verbose` and the
configured provider is constructed instead of degraded
Co-authored-by: MervinPraison <[email protected]>
The no-workspace fallback in EditTools._validate_path only rejected the
literal '..' substring, so an absolute path like /etc/passwd resolved
straight through, letting read_file/search_files/edit_file reach any file
the host process could access. Mirror file_tools.py's containment: confine
resolution to a base directory (defaulting to cwd, overridable via a new
`root` argument) and reject paths that escape it. Adds regression tests.
Co-authored-by: MervinPraison <[email protected]>
Collapse the three secondary rag/ token-estimation helpers to reuse the
single exported implementation in rag/budget.py:
- summarizer.py and compressor.py now import budget.estimate_tokens
(identical output; both had the +1 heuristic with empty-text guard).
- context.py delegates its private _estimate_tokens to budget while
preserving its historical empty-text behaviour (returns 1, whereas
budget returns 0) to keep output identical.
Public rag.estimate_tokens export unchanged. compaction/ left untouched.
Co-authored-by: MervinPraison <[email protected]>
MemoryCoreMixin's async storage methods (store_short_term_async,
store_long_term_async) internally called self.compute_quality_score(content,
metadata) expecting the mixin's 2-arg content scorer. On a Memory instance this
resolved via the MRO to Memory's unrelated 4-required-arg weighted scorer,
raising TypeError on every call.
Call MemoryCoreMixin.compute_quality_score(self, ...) explicitly at all five
mixin call sites so the mixin always uses its own scorer regardless of subclass
shadowing. Preserves the documented public Memory.compute_quality_score 4-arg API.
Co-authored-by: MervinPraison <[email protected]>
Delete the duplicate _parse_score_response in eval/media.py and call the
canonical parse_score_reasoning from grader.py, matching accuracy/criteria/
comparison evaluators. This adds the missing [1,10] score clamp for
out-of-range model outputs, ensuring consistent grades across evaluators.
Co-authored-by: MervinPraison <[email protected]>
Mirror the tool-level hook pattern: only build BeforeAgentInput/
AfterAgentInput (including os.getcwd() and the tools list comprehension)
when a BEFORE_AGENT/AFTER_AGENT hook is actually registered. The
auto-memory / auto-learning / nudge side-effects in the after-agent path
stay outside the guard and continue to run unconditionally.
Co-authored-by: MervinPraison <[email protected]>
get_dimensions() lowercases the input model name but compared it against the
raw MODEL_DIMENSIONS keys. The only mixed-case key, "all-MiniLM-L6-v2" (384),
therefore matched neither the exact-match nor the substring check and fell
through to the 1536 default. That model is the most common local
sentence-transformers embedder (and the default in knowledge/chunking.py), so
memory.py sized the vector store to 1536 while the model emits 384-dim vectors.
Lowercase the key in the substring comparison. Add a regression test.
Consolidate the wrapper's hand-rolled OpenAI/AsyncOpenAI client lifecycle and
beta.chat.completions.parse fallback onto the core-owned
praisonaiagents.llm.openai_client.OpenAIClient. The LiteLLM-first orchestration,
sync/async bridging, lifecycle (close/aclose, context managers), and 3-way
CLI+YAML+Python surface are preserved; only the duplicated OpenAI leg is removed.
Co-authored-by: MervinPraison <[email protected]>
test_cache_integration.py asserted the old empty-string contract, which
contradicted test_cache_optimization.py. Update it to validate the current
contract: boundary equals CACHE_BOUNDARY when enabled, empty when disabled.
Co-authored-by: Leebana <[email protected]>
Addresses Greptile review feedback on PR #2961:
- P2: skip registering the built-in /automations and /blueprint Telegram
handlers when a custom @bot.on_command handler of the same name already
exists, so these generic names no longer unconditionally shadow user
commands (an improvement over the blanket-shadow behaviour of all builtins).
- P1: document that the underlying SuggestionStore is a single global
single-tenant store with no per-user field (mirrors the CLI); per-user
scoping is a core data-model change, out of scope for this wrapper wiring.
Access remains gated by CommandAccessPolicy, re-checked on every tap.
Co-authored-by: Mervin Praison <[email protected]>
Surfaces the already-built consent-first SuggestionEngine + BlueprintCatalogue
through the conversational gateway. Adds a shared _automations helper module
(list/accept/dismiss/blueprint-materialize) and wires it into the Telegram
adapter with inline Accept/Dismiss buttons via the existing callback-query path.
Nothing is auto-created; MAX_PENDING_CAP and dedup are inherited from the store.
Co-authored-by: MervinPraison <[email protected]>
The executor accepted timeout_ms/cancel_token but the tool-calling loop in
llm.py never forwarded them, leaving the feature as dead code. Read an
optional tool_timeout_ms from extra_settings (no new required param, no Agent
bloat) and forward it at both execute_batch call sites. Defaults to None for
zero regression. Adds a regression test guarding the wiring.
Co-authored-by: Mervin Praison <[email protected]>
Close a migration gap in SqliteSessionStore: a store upgraded from a
prior release has session_meta populated but the new session_route table
empty, so backfill skipped those sessions and get_by_gateway_session
returned None instead of resolving them (Greptile P1). _indexed_ids now
requires a session to be present in both session_meta AND session_route
before treating it as fully indexed, forcing route rows to be backfilled
on first read. Adds a regression test.
Co-authored-by: Mervin Praison <[email protected]>
Run each tool via a cancellable boundary that enforces timeout_ms and
returns a typed, discriminated result (timeout/cancelled/error) instead
of hanging the turn or flattening failures to an opaque string.
Co-authored-by: MervinPraison <[email protected]>
Override get_by_gateway_session and list_sessions_by_gateway_agent in
SqliteSessionStore with an indexed session_route table so inbound routing
is an O(1)/O(log n) lookup instead of an O(N) os.listdir + JSON-parse of
every stored session. The route table is populated at write time and
backfilled from existing JSON transcripts; falls back to the parent scan
if the index is unavailable.
Co-authored-by: MervinPraison <[email protected]>
Address two P1 security findings on the managed-config layer:
- SSRF: managed URLs are now validated before fetch. Only http(s) schemes
resolving to a public host are allowed (http restricted to loopback), so a
configured URL can no longer coerce the CLI into hitting localhost, internal
services, cloud metadata (169.254.169.254), or file:// paths. Rejected URLs
fall through to the existing fail-soft cache path.
- Policy enforcement: enforced managed-policy keys (permissions,
model_allowlist) now REPLACE their local counterparts wholesale via
_enforce_policy instead of deep-merging, so local nested sub-keys
(permissions.default/rules) can no longer survive a narrowing org policy.
Adds regression tests for both.
Co-authored-by: Mervin Praison <[email protected]>
Addresses reviewer feedback on PR #2954:
- install.sh: fail loudly if uv bootstrap download is empty/failed and verify
uv is on PATH afterwards (no more masked "uv: command not found").
- install.sh (bash): also wire the applicable login profile
(.bash_profile/.bash_login/.profile) so login shells find praisonai.
- install.sh (fish): guard fish_add_path with a POSIX-safe fallback for
older fish versions.
- self_manage/app: schedule a detached, non-blocking background refresh of the
update-check cache on start-up so the "update available" hint is actually
populated (never performs network I/O in the caller, never blocks/raises).
Co-authored-by: Mervin Praison <[email protected]>
The subagent resolver passed a definition's declarative `permissions` block
straight into `Agent(**config)`, which has no `permissions` kwarg, so any
delegated named agent carrying a permission/mode restriction raised TypeError
at construction. Mirror the primary-run path (_run_custom_agent): pop
`permissions` and translate it into an `approval` config so the delegated
agent runs under its intended restrictions.
Co-authored-by: Mervin Praison <[email protected]>
Adds an optional, opt-in managed configuration layer to ConfigResolver so an
organisation can distribute defaults and enforce policy across many machines
from one source of truth (remote URL and/or managed/enterprise directory).
- Managed non-policy defaults sit BELOW local config (teams suggest, not clobber)
- Managed policy keys (permissions, model_allowlist) enforced ABOVE local config
- Remote fetch: short timeout + on-disk cache under ~/.praison/state/, fail-soft
to last-good copy or skips the layer offline (never blocks a run)
- resolve_with_provenance labels managed:/managed-policy: origins and marks
enforced keys non-overridable
- Fully backward-compatible: no managed source => resolution unchanged
Co-authored-by: MervinPraison <[email protected]>
Add a backward-compatible agent_resolver seam to create_subagent_tool so a
running agent can delegate sub-tasks to user-defined .praisonai/agents/*.md
named agents by name, each running under its own model/tools/permissions.
- Core: create_subagent_tool accepts agent_resolver + resolvable_agents; the
resolver runs a named agent when agent_name matches, else falls back to the
existing generic spawn. Description lists the delegatable named agents.
- Wrapper: custom_definitions gains list_delegatable_agents /
build_subagent_resolver (mode: subagent marker or --subagents allow-list) and
_agent_config_from_definition; mode: subagent is treated as a marker, not a
permission mode.
- run.py: adds --subagents and wires the resolver into _run_custom_agent.
Co-authored-by: MervinPraison <[email protected]>
- install.sh (repo root): POSIX installer that provisions an isolated managed
env via uv tool / pipx (auto-detects, bootstraps uv when absent), wires PATH
for bash/zsh/fish idempotently, supports PRAISONAI_VERSION pin and
non-interactive/CI mode. pip install praisonai remains untouched.
- praisonai upgrade: updates the managed install in place using the detected
tool manager; --check reports whether a newer release exists without mutating.
- praisonai uninstall: cleanly removes the managed env/shim (--yes for CI).
- self_manage helper: install detection (uv/pipx/pip) + non-blocking, cached,
time-boxed "update available" hint, opt-out via PRAISONAI_NO_UPDATE_CHECK.
- Registered both commands in app.py:_LAZY_COMMANDS and the Get started help
panel. No changes to praisonaiagents (CLI self-management only).
Co-authored-by: MervinPraison <[email protected]>
1. scoped_telemetry_disable now shadows the per-instance telemetry object
(race-free by construction) and falls back to a locked + reference-counted
class-level patch, eliminating the concurrent CrewAI class-corruption race.
2. _entrypoint.arun resolves the framework and builds config_list off the
event loop via asyncio.to_thread, and both run/arun share one resolver.
3. MCP dispatcher enforces per-method OPERATION_SCOPES: handle_message accepts
granted_scopes, the HTTP transport plumbs APIKeyAuth (with a --keys-file CLI
flag), and insufficient_scope surfaces as 403 with a WWW-Authenticate
challenge. Backward compatible: no auth configured => allow all.
Co-authored-by: Mervin Praison <[email protected]>
Use is_loopback() instead of a 3-string exact match for the client host
so local peers reported as 127.0.0.2, 127.255.255.255 or the expanded
IPv6 0:0:0:0:0:0:0:1 no longer get a spurious 401 on loopback-bound
gateways (Greptile #2945). Adds test coverage for semantic loopback
clients and non-loopback rejection.
Co-authored-by: Mervin Praison <[email protected]>
The now-truthy CACHE_BOUNDARY marker was being appended directly to the
system prompt in chat_mixin when a prompt-caching-capable model is used.
No provider consumes it as structured cache metadata, so this only
polluted the outgoing system prompt with a literal "--- CACHE_BOUNDARY ---"
string. Stop appending the raw sentinel while keeping the constant defined
for future structured-cache use.
Co-authored-by: Mervin Praison <[email protected]>
The deferred `wrap_if_external` import in the non-multimodal tool-result
branch was unguarded, so a partial/broken install where
`praisonaiagents.tools.trust` is unavailable would raise during plain-text
tool execution and abort the agent. Wrap it in try/except to match the
sibling `_fence_external_text` site, falling through with the raw result.
Addresses Greptile P2 review feedback.
Co-authored-by: Mervin Praison <[email protected]>
Deduplicate the context-truncation summariser and effective-worker-count
clamp/log that were copied verbatim between _execute_parallel and
_execute_loop. Both are now single private helpers
(_truncate_context_for_branches, _effective_workers) with byte-for-byte
identical runtime behaviour.
Co-authored-by: MervinPraison <[email protected]>
Move `wrap_if_external` import from module top level in
agent/tool_execution.py to its two lazy call sites, matching the
existing lazy-import pattern already used for `is_external_tool` and
`artifact_tools` in the same file. This prevents `from praisonaiagents
import Agent` from eagerly loading `tools.trust`. No behaviour or API
changes.
Co-authored-by: MervinPraison <[email protected]>
The file-loading path is now consolidated onto
ToolResolver.load_functions_from_module, so ToolResolver is legitimately
instantiated for file paths. Update the smoke test to assert file paths
use load_functions_from_module and never the name-based resolve().
Co-authored-by: Mervin Praison <[email protected]>
The consolidated load_functions_from_module returns empty both when local
tool loading is disabled/missing AND when a valid module has no public
functions. The if-funcs else-branch previously asserted loading disabled
and told users to set PRAISONAI_ALLOW_LOCAL_TOOLS, misdiagnosing a valid
but empty module. Reword the warning across all 4 CLI sites to cover both
causes without falsely blaming the env var (fixes greptile review regression).
Co-authored-by: Mervin Praison <[email protected]>
Route the four hand-rolled load_user_module + inspect.getmembers copies in
the legacy CLI handlers through the canonical
ToolResolver.load_functions_from_module, eliminating silent behaviour drift.
Add optional functions_only / skip_private / module_name arguments to the
canonical method so the CLI's existing "functions-only, exclude underscore"
semantics are preserved through a single owner instead of re-implemented.
Co-authored-by: MervinPraison <[email protected]>
Resolve tool names in the local-managed backend via the canonical
ToolResolver chain (local -> core -> praisonai-tools -> plugins) instead
of an ad-hoc praisonaiagents.tools getattr lookup. Falls back to the
historical getattr behaviour when praisonai-code/ToolResolver is
unavailable, so this is additive and non-breaking.
Co-authored-by: MervinPraison <[email protected]>
- fold thread_id into delivery idempotency key so two threads in the same
chat do not dedup-collapse (router itself does not thread-route)
- wire delivery into async execute_once() one-time path
- forward --deliver into the daemon child command (start + restart) and
persist it in scheduler state; also fix restart dropping recipe_name
- thread deliver= through build_from_recipe / from_recipe and honour a
recipe schedule-config deliver default
- give symbolic origin/all targets a clear, actionable single warning in
the lightweight delivery path
Co-authored-by: Mervin Praison <[email protected]>
Address reviewer (greptile) P1 findings on PR #2933:
- Backfill legacy JSON exactly once via a one-time flag instead of a
COUNT(*)==0 guard, so a new add_message before first search no longer
permanently skips pre-existing transcripts (_ensure_backfilled +
_indexed_ids skip already-indexed sessions).
- Override _modify_session_locked so transcript-replacing paths
(set_chat_history, append_compaction_checkpoint) that bypass
_save_session keep the FTS index in sync — no more stale/missing hits.
- Drop parent_session_id from the lineage dedup key; it is an immediate
parent, not a continuation chain, so sibling sessions forked from one
parent no longer suppress each other.
Adds 3 regression tests. All session_store / session_protocol /
sqlite_store tests pass (pre-existing _session and hierarchy cache
failures are unrelated and fail on baseline).
Co-authored-by: Mervin Praison <[email protected]>
Addresses two Greptile P1 findings on the error classifier:
- HTTP 400 is overloaded by some providers (e.g. OpenAI returns
context_length_exceeded as a 400); refine broad 400s via message
signals so context overflow triggers compression and auth failures
are not retried as invalid requests.
- BudgetExceededError is a billing state, not a temporary rate limit;
map it to PERMANENT so it surfaces immediately instead of burning
retry attempts.
Co-authored-by: Mervin Praison <[email protected]>
The interval AgentScheduler accepted a deliver= token but never sent
anywhere — it only printed the result. Wire the deliver token through the
existing DeliveryTarget / DeliveryRouter machinery (rate limiting,
idempotency dedup, dead-target self-heal) so a scheduled/interval agent
can message a chat target without the full gateway, from Python, YAML and
CLI.
- core: add DeliveryTarget.parse() token parser (no new protocol)
- wrapper: SchedulerDelivery helper builds a lightweight single-channel
router; graceful no-op when praisonai-bot isn't installed
- wire deliver= into AgentScheduler / AsyncAgentScheduler success paths,
from_blueprint, YAML (schedule.deliver / every alias) and CLI (--deliver)
Co-authored-by: MervinPraison <[email protected]>
Addresses parallel-progress attribution (greptile P2): ToolProgress now
carries tool_call_id/function_name, stamped by the executor so a streaming
consumer can attribute concurrent updates to the correct tool call. Also
documents the sync-path cross-loop coroutine behaviour so async tools that
must stay on the caller's loop use the async executor path.
Co-authored-by: Mervin Praison <[email protected]>
Address reviewer feedback on the durable run ledger:
- recover_orphans() now transitions active runs to LOST via a single
atomic conditional UPDATE (WHERE status IN active) so a concurrently
completed run is never overwritten with LOST.
- SQLite connection opened with busy_timeout + WAL so overlapping gateway
processes wait instead of failing with "database is locked".
- Metadata serialisation falls back to json default=str so a run with
datetime/set/bytes/custom metadata is still durably recorded.
- Unknown persisted status maps to a new non-terminal UNKNOWN sentinel
(neither active nor terminal) instead of terminal LOST, so a status a
newer writer introduced is not wrongly finalised or dropped.
Added tests for each; all 16 run-ledger tests pass.
Co-authored-by: Mervin Praison <[email protected]>
Addresses the three P1/P2 issues raised by Greptile:
1. direct_prompt.py: _run_direct_prompt_via_adapter no longer rebuilds
llm_config with only {"model": ...}, which dropped base_url/api_key/
api_type from self.config_list. It now copies the full first config
entry and only overrides model when --llm is given, so the adapter's
_resolve_llm (which reads base_url/api_key) and the AutoGen config_list
keep custom endpoints and credentials.
2. observability/hooks.py: the process-global trace emitter swap/restore is
now serialised under a process-wide lock and tracks each run's own
installed emitter. Restore only reclaims the global slot when this run is
still the live top; otherwise it re-parents the newer run's prev_emitter
onto this run's prev, so overlapping cross-thread runs and out-of-order
(non-LIFO) finalizes never route traces through the wrong or a closed sink.
Cross-session recall (session_search) previously re-read and lexically scanned
every session JSON file on disk for each query — an O(all sessions) directory
scan that degrades linearly for a long-lived gateway bot.
- Add SqliteSessionStore (stdlib sqlite3 + FTS5, lazy-imported) that keeps JSON
transcripts as the durable record but maintains a full-text index so recall
is a bounded MATCH lookup. Falls back to the substring scan if sqlite3/FTS5
is unavailable.
- Anchored discovery: SessionHit now carries opening/closing bookends so a hit
reconstructs goal -> match -> resolution in one call.
- Recall ranking demotes high-volume automated (scheduled) sessions so they
can't crowd interactive history out of the top results.
- Lineage-aware dedup collapses reset/compacted continuations to a single hit.
- Export SqliteSessionStore from praisonaiagents.session; update docstrings.
Co-authored-by: MervinPraison <[email protected]>
- classify_error now prefers exception type / HTTP status code (incl.
provider error codes) over message substring matching, so a 429 whose
message lacks the word "rate" is still classified correctly
- extract_retry_after reads structured Retry-After headers/attributes
before falling back to message parsing
- LLM retry loop honours RetryBackoffConfig.max_retries via a new
_max_retry_depth() helper instead of the hardcoded _retry_depth < 2
(defaults to 2 when no retry config is set, preserving prior behaviour)
Co-authored-by: MervinPraison <[email protected]>
Add a structured tool-execution result protocol in core so slow tools no
longer block the whole turn with no feedback:
- ToolProgress: incremental progress update a tool can stream via on_progress
- DeferredToolResult + defer(): a tool may return "started; resolve later"
handle instead of blocking the turn
- ToolResult gains progress[], deferred, is_deferred, and structured_error
(error type/message) so the model sees more than a flattened string
- run_single_tool_call(): shared executor path that injects on_progress when
the tool supports it, awaits async def tools natively, records deferred
handles, and captures structured errors
- Sequential/Parallel executors accept optional on_progress; fully backward
compatible with existing 3-arg execute_tool_fn callables
Exported ToolProgress/DeferredToolResult/defer from praisonaiagents.tools.
Co-authored-by: MervinPraison <[email protected]>
Add a first-class run ledger so long-running/async agent runs survive a
gateway restart. Core gains a thin RunLedgerProtocol + RunRecord/RunStatus
contract plus a zero-dependency SQLite default store that reconciles orphaned
runs on boot via recover_orphans().
Co-authored-by: MervinPraison <[email protected]>
1. Route the direct-prompt CrewAI/AutoGen path through FrameworkAdapterRegistry
so default selection and the adapter contract (scoped_telemetry_disable,
_resolve_llm, expected_output, cli_config/callbacks, entry-point plugins)
are honoured instead of a hardcoded chain that bypassed every adapter.
2. Preserve framework BaseTool schema/name/description when applying
tool_timeout: detect framework tool objects and wrap only their execution
method in place; only bare callables get function-level wrapping.
3. Scope observability sinks and the trace emitter per-run via a thread-local
stack + ObservabilityRun handle so concurrent runs no longer close each
other's sinks or leave the global emitter in a Fanout state. Backward
compatible: init returns a handle and finalize accepts an optional handle.
Co-authored-by: MervinPraison <[email protected]>
Addresses reviewer (Greptile) findings on PR #2922:
- #3 (critical): AFTER_TOOL hook additional_context was discarded in
tool_execution.py, making subtree injection a no-op. Now aggregate and
append it to the tool result (mirrors the loop-guard injection pattern)
so the model actually sees subtree rules.
- #2: interactive Agent now gets a dedicated per-session HookRegistry via
Agent(hooks=...) instead of registering the session closure on the shared
global default registry, preventing stale cross-session leakage.
- #1: file_tool_matcher now covers the real ACP edit tools
(acp_create_file/acp_edit_file/acp_delete_file) and list_files, and path
extraction handles filename/target_file keys.
Tests: context_files (25) + interactive core (37) + core hooks (65) pass.
Co-authored-by: Mervin Praison <[email protected]>
- Agent toolset expansion now calls resolve_toolsets_for_model so the
advertised edit primitive order matches the harness prompt (Greptile P1).
- Add ToolsetRegistry.resolve_toolsets_for_model + module-level helper; extract
shared _apply_preferred_edit_order (DRY with resolve_toolset_for_model).
- chat_mixin/_resolve_harness_base_prompt and agent.py pass the string model id
only, so object-based LLMs no longer silently disable the harness.
- Add test coverage for the runtime multi-toolset path.
Co-authored-by: Mervin Praison <[email protected]>
Address reviewer feedback on #2920:
- cached-id: use exclusive create; losers of a concurrent first-run re-read
the persisted winner instead of returning an unpersisted id (Greptile P1)
- normalize_git_remote: accept host-less remotes (file:///... , bare paths)
by yielding a path-only identity instead of falling through (Greptile P2)
- root-commit: enumerate root commits across all refs and pick the smallest
SHA so orphan-branch switches keep a stable identity (Greptile P2)
- add regression tests for all three cases
Co-authored-by: Mervin Praison <[email protected]>
The nearest-wins walk-up resolver (load_context_files) and the on-demand
PathContextAttacher already existed but the attacher was never connected to
a running agent, so subpackage-local AGENTS.md rules never surfaced when the
agent worked in that subtree.
Add build_subtree_context_hook()/file_tool_matcher() in the wrapper's
context_files module: an AFTER_TOOL FunctionHook backed by PathContextAttacher
that extracts the touched file path from tool args, resolves the nearest
instruction file for that directory, and returns it as additional_context
(the neutral core hook mechanism) once per session. Wire it into the
interactive core, seeded with the up-front context and respecting the char
budget and --no-context opt-out. No core changes, no new deps.
Co-authored-by: MervinPraison <[email protected]>
Add praisonaiagents/model_harness with a data-driven, overridable
HarnessProfile registry that resolves, from the model id, an optional
base-prompt fragment and a preferred file-edit format. _build_system_prompt
prepends the fragment; the coding toolset can advertise the preferred edit
primitive first via resolve_toolset_for_model. Unknown models fall back to a
behaviour-preserving default (no fragment, unchanged ordering). Resolution is
lazy with zero import-time cost.
Co-authored-by: MervinPraison <[email protected]>
Derive project identity from the repository (git remote -> root commit ->
cached id) instead of the absolute git-root path, so session history follows
the repo across directory moves/renames, second checkouts, and git worktrees.
Falls back to the legacy resolved-path hash for non-git dirs, and transparently
migrates pre-existing path-scoped sessions so no history is lost.
Co-authored-by: MervinPraison <[email protected]>
Core parse_schedule requires a unit suffix for */N intervals, so
delegating bare */30 (unit-less) would raise ValueError and regress
previously-valid scheduler/deployment configs. Restore the wrapper's
unit-less */N → N seconds fast-path before delegating shared grammar.
Fixes Greptile P1 review finding on #2916.
Co-authored-by: Mervin Praison <[email protected]>
Deduplicate the schedule-expression grammar by making core
praisonaiagents.scheduler.parser.parse_schedule the single owner of the
shared grammar (keywords, */N{m,h,s}, raw seconds). The wrapper
ScheduleParser now delegates to it and derives an integer interval for
the polling loop, while keeping the wrapper-specific cron -> interval
collapse local. Preserves the int return contract, all three call sites,
and the "Unsupported schedule format" error message; transparently gains
weekly support.
Co-authored-by: MervinPraison <[email protected]>
The SandboxToComputeAdapter class in sandbox/_compat.py had zero call
sites across the entire src/ tree and was not exported anywhere. Its
docstring falsely advertised deduplication between the sandbox/* and
integrations/compute/* hierarchies that never occurred.
Removed the dead class and trimmed the module docstring to accurately
describe the actively-used safe_sandbox_path() path-traversal guard,
which remains unchanged and is still imported by docker.py/subprocess.py.
Co-authored-by: MervinPraison <[email protected]>
- Cap output reserve at window//4 so small-context models (e.g. gpt-4)
retain a usable input budget instead of compacting almost every turn.
- Skip the reactive context-length compaction when auto_compact is
disabled so a user's history is never rewritten without consent.
- Rebuild the agent from the compacted history before the retry so the
safety net no longer re-sends the stale oversized backstory.
- Add unit test for the small-model reserve cap.
Co-authored-by: Mervin Praison <[email protected]>
The interactive worker loop set auto_compact into session state but never
read it, so long sessions drifted toward context-window overflow with only
the manual /compact command as mitigation.
- Add _maybe_auto_compact() which reads auto_compact + threshold and reuses
the existing _handle_compact_command summariser when the running token
estimate (via core estimate_messages_tokens) nears the model's usable
context budget (window minus output reserve).
- Add a reactive safety net: catch context-length errors from agent.chat(),
compact, and retry the turn once instead of surfacing the raw error.
- Add best-effort model context-window lookup (litellm registry with a
coarse fallback map).
Co-authored-by: MervinPraison <[email protected]>
The plugin bridge now emits blocking HookResults for before_llm/
before_tool_definitions, but their runtime callers only adopted payload
rewrites and never checked is_blocked, so guardrail plugins failed open
(the denied LLM request / tool surface still reached the model). This
wires block enforcement into both paths and hardens the async BEFORE_TOOL
reason extraction so a HookResult-originated block can no longer fall
through to tool execution.
- chat_mixin: BEFORE_LLM now aborts the request when blocked
- chat_mixin: BEFORE_TOOL_DEFINITIONS fails closed (drops tool surface)
- chat_mixin: async BEFORE_TOOL reason lookup no longer re-opens a block
- tests: caller-enforcement regression tests for LLM/tool-definition block
Co-authored-by: Mervin Praison <[email protected]>
The _adapt_plugin_hooks bridge hardcoded HookResult.allow() for every
before_* event, so POLICY/GUARDRAIL plugins could only redact and never
deny a tool call, message, LLM request, or agent run. Now each before_*
adapter forwards an explicit deny/block decision to the runtime's existing
is_blocked enforcement.
- Add PluginDecision (allow/deny/block) and GuardrailBlocked exception to
the core Plugin contract; plugins may also return a HookResult directly.
- before_tool/before_llm/before_agent/before_message/before_tool_definitions
adapters coerce decision returns and catch GuardrailBlocked -> HookResult.
- Fully backward-compatible: dict/str/tuple/None keep rewrite/no-op semantics.
- Export PluginDecision/GuardrailBlocked from praisonaiagents.plugins.
Co-authored-by: Mervin Praison <[email protected]>
Closes the gap flagged by Greptile: resolve_reliability('production')
resolved outbound_ordering="strict" but the durable adapter never passed
it to OutboundQueue, so the production preset silently stayed best_effort
and same-conversation messages could still overtake after retry/backoff.
- setup_durable_delivery(..., ordering=...) forwards the discipline to
OutboundQueue so strict per-lane FIFO is actually enforced.
- BotOS exposes the resolved _outbound_ordering for adapters to consume.
- Regression test asserts the adapter forwards ordering into the queue.
Confined to the praisonai-bot wrapper; no core SDK / Agent changes.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Add an opt-in strict per-lane ordering mode to OutboundQueue so an earlier
undelivered message to a chat blocks later same-lane messages until it is
sent or permanently dead, preventing user-visible reordering under retries,
rate limits and multi-worker drains.
- New lane_key column (defaults to target) with migration/backfill for
existing databases.
- ordering="strict"|"best_effort" knob on OutboundQueue; strict gates each
lane to its head entry in _get_pending_entries while draining lanes in
parallel. Default best_effort preserves current behaviour.
- enqueue accepts an optional lane_key for custom conversation grouping.
- Reliability preset exposes outbound_ordering; production selects strict,
default/off stay best_effort, explicit value always wins.
Co-authored-by: MervinPraison <[email protected]>
The older-core fallback in _load_edit_tools only preserved YAML-approved
tools that were in TOOL_GROUPS["interactive"], silently dropping any other
approved tool (e.g. workflow tools) when merging in edit_file/apply_patch.
Read the current approved set directly from the registry contextvar so the
merge is non-clobbering regardless of installed praisonaiagents version.
Co-authored-by: Mervin Praison <[email protected]>
- Fix stale test_interactive_group_is_union to include the new `edit` group
in the interactive union (root cause of CI failure — feature intentionally
folded `edit` into `interactive`).
- Fail closed in _load_edit_tools: if Workspace containment cannot be built,
do not expose edit_file/apply_patch (the engine's no-workspace fallback
accepts absolute paths, which under auto-approval could escape the
workspace). (Greptile P1 security)
- Add ApprovalRegistry.add_yaml_approved_tools() to merge into the
context-local YAML-approved set instead of replacing it; use it so
auto-approving edit tools no longer clobbers previously approved tools.
(Greptile P1)
- Add regression tests for both behaviours.
Co-authored-by: Mervin Praison <[email protected]>
Both the worker and sync interactive paths re-ran the prompt via
chat() when iter_stream() raised. If chunks were already emitted to the
live display, the user saw partial streamed tokens followed by a full
second answer. Now the non-streamed fallback only fires when nothing
streamed yet; otherwise the partial streamed text is kept and finalised.
Fixes greptile P1 findings on PR #2906.
Co-authored-by: Mervin Praison <[email protected]>
Register the existing praisonaiagents edit_tools engine (edit_file,
apply_patch) into the default interactive TOOL_GROUPS so the CLI coding
agent performs targeted string-replace edits with a fuzzy fallback and
atomic multi-file patches instead of whole-file rewrites. Whole-file
creation (write_file/acp_create_file) remains available and
--tools/PRAISON_TOOLS_DISABLE overrides are honoured. Auto approval mode
context-approves the high-risk edit tools to match ACP behaviour.
Co-authored-by: MervinPraison <[email protected]>
Addresses Greptile P2: the previous collapse routed sync generate() through
agenerate() -> _astructured_completion (async provider clients), changing the
historical behavior where a sync call used the blocking sync client/network
stack. Each generator now shares one _agenerate_impl(is_async=...) body: the
async agenerate() drives is_async=True and the sync generate() drives
is_async=False via _run_coro_sync, so both entry points share merge/save/path
logic while each keeps its own provider path. No API or behavior regression.
Co-authored-by: Mervin Praison <[email protected]>
Addresses Gap 2b from the issue: the three auto-generator classes
(AutoGenerator, WorkflowAutoGenerator, JobWorkflowAutoGenerator) each
duplicated their generate()/agenerate() bodies, differing only in
_structured_completion vs await _astructured_completion.
Each sync generate() now delegates to its async agenerate() twin through
a single _run_coro_sync helper on BaseAutoGenerator, following the
established PraisonAIAdapter pattern (sync goes through the async bridge).
The helper preserves the existing running-event-loop fallback (FastAPI /
Jupyter / async test) that _structured_completion already implemented, so
behavior is backward-compatible.
Co-authored-by: MervinPraison <[email protected]>
- verify(): a Judge failure is now inconclusive (criteria stay 'pending',
achieved=False) instead of being recorded as a real 'unmet' failure
- verify(): return an independent snapshot of criteria (dataclasses.replace)
so mutating the result never corrupts the goal's own state
- Goal.progress: clamp non-positive weights and fall back to equal counting
so invalid/zero/negative weights can't push progress outside 0.0-1.0
- add regression tests for judge-unavailable, snapshot isolation, and
zero/negative-weight progress
Co-authored-by: Mervin Praison <[email protected]>
The package-level lazy loader silently converted real ImportError/
AttributeError into a cached None, masking genuine import bugs and stale
mapping entries as confusing NoneType errors far from the root cause.
This additive change preserves the graceful None fallback but logs the
swallowed exception: DEBUG for a genuinely-absent optional module and
WARNING for a missing attribute (which usually signals a stale mapping).
Co-authored-by: MervinPraison <[email protected]>
The OnRetryInput hook dataclass declared the `attempt` field twice with
conflicting defaults (1 then 0), so the intended 1-based default was
silently shadowed by 0. The to_dict() also wrote the "attempt" key twice.
Removed the shadowing declaration and the duplicate dict key, keeping a
single `attempt` field with the intended 1-based default. All production
constructors pass attempt= explicitly, so runtime behaviour is unchanged.
Co-authored-by: MervinPraison <[email protected]>
Adds a protocol-driven, lazily-loaded `goal/` module to praisonaiagents that
turns a vague objective into a structured, measurable Goal (statement +
weighted success criteria + constraints) and verifies output against it,
reusing the existing eval Judge for scoring (DRY). Complements CHL engineering.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: MervinPraison <[email protected]>
The wrapper copy at src/praisonai/praisonai/_lockmap.py had zero importers
and had drifted from the canonical praisonai_bot._lockmap copy (extra break
in _evict_stale_from_bucket LRU loop). All live consumers import from
praisonai_bot._lockmap, which is untouched.
Co-authored-by: MervinPraison <[email protected]>
An explicit `--var input=` (empty) is now respected instead of falling
back to the YAML default_input. Only a genuinely absent input key falls
back to default_input, addressing Greptile P1 review feedback.
Co-authored-by: Mervin Praison <[email protected]>
The CLI handler always called workflow.start("") which dropped the parsed
default_input, leaving every {{input}} step empty. Now the CLI uses the
YAML input (via workflow.default_input), lets --var input override it, and
AgentFlow.run() falls back to default_input when no input is passed so
programmatic start() calls also honor YAML defaults.
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
Addresses reviewer feedback on PR #2887:
- Gate suspicious system-dir check on looks_absolute (not allow_absolute) so
sensitive roots like /etc/passwd stay blocked even with allow_absolute=True,
and relative paths containing substrings (myapp/home/config.txt,
project/users/context.txt) are no longer falsely rejected (CodeRabbit, Greptile).
- Require a separator after the drive colon so drive-relative "C:context.txt"
is not misclassified as absolute (Greptile).
- Treat leading "~" as a home reference so ~/context.txt remains blocked.
- Add regression tests for all cases.
Co-authored-by: Mervin Praison <[email protected]>
Greptile P1: capitalized candidates were ranked entirely before lowercase
action candidates, so a repeated greeting (e.g. 'Hello') could win over a
more-frequently-discussed lowercase topic. Now all repeated candidates are
scored by frequency, with capitalization used only as a tie-breaker. Also
bound the action/focus object capture to a short noun phrase so real topics
survive the length filter instead of being swallowed by a greedy match.
Co-authored-by: Mervin Praison <[email protected]>
The standalone run_all_tests() runner subscripted [0] on
test_basic_instantiation(), which uses pytest-style assert and returns
None on success, causing a TypeError that aborted 5 of 7 QA groups.
Add a _run_check() wrapper that normalizes pytest-style functions to a
bool (None/truthy -> pass, AssertionError/exception -> fail) and honors
@pytest.mark.skip. Removes the erroneous [0] subscript so all 7 groups
run without crashing while keeping pytest collection working.
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
validate_monitor_path relied on Path.is_absolute() and POSIX substring
checks, which are OS-specific. On Windows, POSIX-style paths like
/etc/passwd were not treated as absolute and normalized to \etc\passwd,
so security checks silently passed.
- Detect POSIX leading slashes and Windows drive letters explicitly
- Match suspicious patterns against a normalized posix haystack
- Add Windows-sensitive dirs (/windows/, /system32/, /users/)
- Fix test_set_path to use as_posix() for cross-platform assertion
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
_build_tool_schemas conflated tools=None with an explicit empty override,
dropping agent.tools from PreparedTurnContext. Now None and omitted kwarg
both fall back to agent.tools, while tools=[] still explicitly disables tools.
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
Run the capitalized-phrase regex on original-case text instead of a
lowercased copy so acronyms/proper nouns like "REST API" are preserved.
Action/focus patterns still run on lowercase text. Capitalized candidates
are preferred over lowercase action-object fragments.
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
Greptile flagged that Path(str(traversable)) breaks under non-filesystem
importers (zipimport wheels): the packaged CSV would appear missing even
though it ships in the wheel. Use importlib.resources.as_file() so the
resource is materialized to a real filesystem path that the CSV runner can
open, kept alive for the process lifetime via a module-level ExitStack.
Co-authored-by: Mervin Praison <[email protected]>
- LoopEvaluator now detects DoomLoopEvent (loop_type) and DoomLoopResult
(is_loop) guard shapes, not just generic type/event markers (P1/security).
- In review mode, convergence defers to the loop result's own success /
final score so health never disagrees with the loop result (P2).
- Add tests for real guard shapes and review-mode transient scores.
Co-authored-by: Mervin Praison <[email protected]>
Addresses reviewer feedback on PR #2876:
- Read tool calls from `tool_trace` (real harness key) in addition to
`tool_calls`/`tools`.
- Match structured/dict artifacts (path/name/file keys) and dict-shaped
artifact maps, not just flat file-name strings.
- Aggregate HarnessResult on the shared /10 scale in EvalSuite via a
`score_scale` marker so mixed CI gates are not understated.
- Fail (not crash) on malformed non-numeric `judge_score`.
- Surface the `min_tool_calls` gate in `to_eval_result().criteria_scores`.
Adds regression tests for each fix.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Address Greptile P2 findings: clarify that ContextEvaluator/
HarnessEvaluator/LoopEvaluator are planned follow-ups and map each
pillar's current coverage to real modules (context/tokens.py,
eval/tokens.py, compaction/compactor.py ContextCompactor,
runtime/turn_context.py, runtime/example_harness.py, eval/loop.py
EvaluationLoop, agent/autonomy.py, agent/context_agent.py).
Co-authored-by: Mervin Praison <[email protected]>
Add the four built-in interactive test suite fixtures (smoke, tools,
refactor, multi_agent) that `praisonai test interactive --suite` expects
but which were never committed because `src/praisonai/.gitignore` ignored
`tests/fixtures`.
- Add fixtures as tracked package data under praisonai_code/data so they
resolve for PyPI wheel installs via importlib.resources.
- Un-ignore tests/fixtures/interactive/*.csv so editable/dev checkouts also
resolve, while keeping other fixture dirs ignored.
- Make _get_builtin_suite_path robust: try packaged data, dev-tree src
layout, and cwd instead of a fragile parent.parent chain.
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
Migrate context-flow (handoff) scoring from the praisonai wrapper's
replay judge into the core SDK eval framework so SDK-only installs and
CI can score Context Engineering behaviour.
- Add praisonaiagents/eval/context_eval.py with ContextEvaluator plus
ContextHandoffResult, BudgetComplianceResult, ContextEvalResult
- LLM-free, dependency-free deterministic scoring (handoff token overlap
+ token-budget compliance); composes into EvalSuite via overall_score
- Export ContextEvaluator + result types from eval/__init__.py (lazy)
- Wrapper _evaluate_context_flow now delegates to the SDK evaluator,
removing duplicate scoring logic (graceful fallback on ImportError)
- Add tests/unit/eval/test_context_eval.py (no OPENAI_API_KEY needed)
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
Adds a lightweight, dependency-free HarnessEvaluator to praisonaiagents.eval
that scores Interactive Test Harness trace artifacts (tool call count,
tool-schema consistency hash, artifact completeness, optional rubric judge
score) and a harness_row_to_eval_case CSV->EvalCase adapter, so harness runs
can be aggregated inside EvalSuite and exported as an EvalReport.
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
Adapters whose start() spawns a background task/server and returns
immediately (Email IMAP poll, Linear/WhatsApp/AgentMail webhook servers)
ended supervision the instant they came up: the ChannelSupervisor treated
the returned start() as "stopped cleanly" and exited, so the inbound loop
ran unsupervised and Bot.run() returned right after starting.
The supervised _start wrapper now blocks until the adapter reports it is no
longer running (awaiting its background task when exposed, else watching
is_running), presenting a uniform "runs until stopped" contract for both
blocking (Slack/Discord) and returning adapters. Adds a regression test.
Co-authored-by: Mervin Praison <[email protected]>
Wire the existing ChannelSupervisor + health-restart layer into Bot.start()
so every channel (not just Telegram) auto-reconnects with capped backoff,
matching BotOS/gateway robustness. Add a protocol-only start/stop +
supervised_inbound seam to the core BasePlatformAdapter contract; Telegram
and the relay adapter opt out to keep their self-managed reconnect loops.
Co-authored-by: MervinPraison <[email protected]>
Addresses reviewer (Greptile) P1 findings on PR #2868:
- Agents loader now discovers .praisonai/config.yaml / praisonai.yaml so the
documented unified YAML `plugins:` surface actually reaches
maybe_enable_from_config() (PyYAML lazily imported, optional).
- PluginManager.set_plugin_options(replace=True) replaces the option set
authoritatively so removed plugin blocks no longer deliver stale options
via on_config on a reused manager.
- config.schema.json drops the unsupported string `enabled` type; loader now
handles a bare-string plugin name gracefully in is_plugins_enabled/
get_enabled_plugins instead of silently falling through.
Adds 8 tests; 144 plugin unit tests pass.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Add a first-class `plugins:` section to the unified project config
(`.praisonai/config.yaml`) resolved by the existing resolver, and deliver
each plugin its own options dict via the `on_config` hook.
- wrapper: `plugins` block in resolver.py (ResolvedConfig + merge + provenance
+ typo-suggestion validation) and config.schema.json.
- core: loader.py collects per-plugin option maps and exposes
get_plugin_options(); manager.py stores options and delivers them via
apply_plugin_options(); enable() accepts options_by_name and
maybe_enable_from_config feeds YAML-sourced options.
- backward compatible: legacy config.toml / PRAISONAI_PLUGINS still work.
Co-authored-by: MervinPraison <[email protected]>
Data-driven help categories map every top-level command to a stable panel
(Get started, Run & chat, Sessions & state, Config & setup, Tools & knowledge,
Serve & integrate, Advanced). Categories are applied lazily as Typer's
rich_help_panel in LazyCommandGroup.get_command, keeping the registry the single
source of truth without importing every command up front. Fully additive: no
command is renamed, removed, or hidden.
Co-authored-by: MervinPraison <[email protected]>
Exclude PermissionError from transient-init classification so fatal
misconfiguration (bad credentials/file mode) surfaces on every call
instead of being memoized for the cooldown window. Genuinely transient
filesystem/network OSErrors and driver DBAPI errors remain memoized.
Co-authored-by: Mervin Praison <[email protected]>
Two regressions/gaps surfaced by CI and reviewers on PR #2862:
1. agents_generator: the narrowed except in _validate_cli_backend_compatibility
dropped ValueError/TypeError/AttributeError. The registry create()/
is_available() raise (Value|Type|Import)Error for unknown/unavailable
frameworks, and minimal/mock generators (object.__new__) lack
_adapter_registry (AttributeError). These now fall back to native-only
behaviour with a warning instead of aborting setup, fixing the
test_cli_backend_* CI failures and greptile P1 adapter-lookup-escapes.
2. db/adapter: init-failure memoization only caught ConnectionError/
TimeoutError/OSError, so psycopg2.OperationalError (not an OSError subclass)
escaped the cooldown and hammered a down Postgres on every callback. Added
_is_transient_init_error() matching driver DBAPI errors by class name across
the MRO (no optional-dep import), so genuine outages are memoized for the
cooldown while fatal misconfiguration still surfaces immediately.
Co-authored-by: Mervin Praison <[email protected]>
Gap 1 (DRY / lazy):
- Extract the four CLI-backend loaders into a shared integrations/_cli_loaders.py
so IntegrationRegistry and ExternalAgentRegistry no longer hand-duplicate them.
- Make INTEGRATIONS_REGISTRY lazy via get_integrations_registry(); importing
praisonai.integrations no longer constructs a registry at import time.
Gap 3 (safe by default):
- agents_generator: narrow adapter-resolution except to (ImportError, KeyError,
LookupError) and warn instead of swallowing every failure.
- agents_generator: retry-policy resolution narrows to ImportError and logs a
warning rather than silently dropping requested --tool-retry-* settings.
- db/adapter: only memoize transient (ConnectionError/TimeoutError/OSError)
init failures for the cooldown window; fatal misconfiguration now surfaces.
- db/adapter: build Postgres URL with quote_plus so special chars in the
user/password no longer produce a malformed/round-trip-unsafe URL.
- security/injection: the injection hook no longer derives trust from a plain
payload attribute; hook inputs are always scanned as external.
Co-authored-by: MervinPraison <[email protected]>
Phone-number→agent routing is a channel/gateway concern, not a core Agent
identity attribute (the Agent never reads its own number). Revert the
misrouted `phone_number` param on core Agent and add a lightweight
`AgentRegistry` in praisonai-bot that maps normalised phone numbers to
agents for inbound WhatsApp/SMS dispatch, mirroring OpenClaw's multi-agent
routing model.
- Remove `phone_number` from praisonaiagents Agent (__init__, docstring, to_dict)
- Add praisonai_bot.bots.AgentRegistry + normalize_number (E.164-ish keys)
- Export both via the lazy bots __init__
- Tests: tests/unit/bots/test_agent_registry.py (16 passing, offline-safe)
Co-authored-by: Mervin Praison <[email protected]>
Replace non-standard PRAISONAI_ALLOW_NETWORK guard (treated any value,
including "false"/"0", as truthy) with the repo-standard OPENAI_API_KEY
check used by other live LLM tests.
Co-authored-by: Mervin Praison <[email protected]>
Add an optional lightweight `phone_number` identity attribute to the core
Agent so an agent can be assigned a contact number (used by channel/bot
layers such as WhatsApp/SMS for routing). Preserved across channel clones.
Co-authored-by: MervinPraison <[email protected]>
Deletes the unreferenced private lazy-loader that hardcoded a duplicate
copy of the 17-class praisonai_tools list. No callers exist and its
lazy_get cache key was unique to the function. Tool resolution remains
via ToolResolver; no user-facing behaviour changes.
Co-authored-by: MervinPraison <[email protected]>
Addresses Greptile review on PR #2856:
- telegram/discord/slack now call BotConfig.is_explicitly_allowed() instead
of the equivalent inline `bool(allowed_users) and is_user_allowed()` check,
eliminating dead code and making the config docstrings truthful. Runtime
behavior is unchanged (secure-by-default preserved).
- Add hot-reload (_start_single_channel) test coverage for unknown_user_policy
and owner_user_id wiring so the startup and hot-reload paths can't drift.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
- Wire unknown_user_policy + owner_user_id from gateway.yaml into BotConfig
in both start_channels() and the hot-reload path.
- Replace misleading "accepts messages from everyone" startup warning with a
deny-aware message: empty allowed_users + default deny now warns that unknown
DMs are SILENTLY DROPPED; allow/pair get accurate text.
- Add BotConfig.is_explicitly_allowed() and clarify is_user_allowed() /
allowed_users docstrings (empty list defers to unknown_user_policy).
- Add gateway tests for YAML wiring and warning text.
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
- _wrapper_bridge: propagate non-zero SystemExit codes from the wrapper
instead of swallowing them (clean exit 0/None still returns normally)
- registry: remove leftover unused `import sys` in registry_list
- tests: expand parametrize to all 14 stub modules; tighten legacy grep
to catch both `praison.main()` and `PraisonAI().main()` forms
Co-authored-by: Mervin Praison <[email protected]>
Fourteen Typer stub command groups (agents, workflow, registry, memory,
skills, hooks, rules, eval, package, templates, todo, research, commit,
call) re-entered legacy PraisonAI().main() by mutating sys.argv, which on a
standalone `pip install praisonai-code` crashed with a Rich ImportError
traceback from the wrapper-only argparse_builder before any handler ran.
Add run_wrapper_command() to _wrapper_bridge.py that fails fast with a
single-line install hint (exit 1, no traceback) when the wrapper is absent,
and performs the legacy re-entry only when praisonai is installed. Migrate
all 34 stub entry points to use it. Native subcommands (e.g. memory learn,
skills check) are untouched, so they keep working standalone.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
The prior commit routed default/plain/verbose/silent standalone `run` text
modes through the in-process Agent, which broke the C7 hot-path smoke contract
(`smoke` job asserts default `run "hi"` gates with an install hint and keeps the
heavy `praisonaiagents` import off the default hot path). Revert the routing
change; preserve the genuinely valuable fix — the corrected error hint that
cites `praisonai-code run --output actions` instead of `praisonai`.
- run.py: text modes gate again on standalone; `--output actions` unchanged;
keep `_IN_PROCESS_OUTPUT_MODES` constant + clarified docstrings.
- README: standalone matrix corrected to reflect gating of text modes.
- tests: assert text modes gate, structured modes run in-process, and the
hint cites praisonai-code (error path is now live, no force-monkeypatch).
Co-authored-by: Mervin Praison <[email protected]>
- Drop over-broad "[ssl:" marker in _is_ssl_error so TLS handshake
failures (WRONG_VERSION_NUMBER, NO_SHARED_CIPHER, HANDSHAKE_FAILURE)
no longer soft-fail preflight — only true cert-verify failures do.
- Make PRAISONAI_SSL_CA_BUNDLE authoritative: override a pre-existing
SSL_CERT_FILE/REQUESTS_CA_BUNDLE instead of silently no-op via setdefault.
- Warn instead of silently ignoring a configured-but-missing CA bundle path.
- Add regression tests for all three behaviors.
Co-authored-by: Mervin Praison <[email protected]>
Address reviewer feedback on #2838:
- Guard check_console_script_execution with skip_if_no_wrapper() so it SKIPs
instead of FAILing on standalone praisonai-code installs (the unguarded
check still forced doctor exit 1 via calculate_exit_code).
- Remove unreachable dead branch in check_praisonai_package_structure.
- Mock subprocess.run in wrapper-present module-execution test; add
console_script_execution to the SKIP regression matrix.
Co-authored-by: Mervin Praison <[email protected]>
- Move _secure_secret_permissions() into the read try/except else clause so
a chmod OSError propagates (fail closed) instead of being swallowed by the
file-read handler and silently regenerating/rotating the HMAC secret.
- Invoke icacls via its absolute System32 path to avoid PATH hijack while
hardening the secret, and quote the username so principals with spaces are
parsed as a single principal.
- Add regression test asserting a chmod failure raises and does not rotate
the persisted secret.
Co-authored-by: Mervin Praison <[email protected]>
Assert get_command/list_commands directly so hyphen/underscore
resolution is verified deterministically and cannot pass vacuously
when the optional mint_link sub-app fails to import.
Co-authored-by: Mervin Praison <[email protected]>
- Correct sys.path block in test to inject praisonai-bot (was praisonai/agents)
- Isolate tests to tmp_path so ~/.praisonai is not mutated
- Suppress Ruff BLE001 on intentional advisory broad-except
Co-authored-by: Mervin Praison <[email protected]>
Address reviewer feedback (greptile, coderabbit):
- assert exit_code == 0 instead of != 2 for the list alias
- verify "list" appears as a standalone command, not just within "list-judges"
Co-authored-by: Mervin Praison <[email protected]>
- memory_show now appends --limit to argv so `memory show/list --limit N`
is respected instead of silently using the backend default (issue #2840)
- strengthen test_memory_list_is_a_known_command to assert exit_code == 0
- add end-to-end test verifying --limit reaches the argv passed to PraisonAI
Co-authored-by: Mervin Praison <[email protected]>
Standalone `praisonai-code run` with plain/verbose/silent/default output
modes was blocked by `_require_wrapper_for_default_run`, which delegated to
the wrapper-only `handle_direct_prompt`. Now, when the `praisonai` wrapper is
not installed, these human-readable text modes route through the in-process
Agent path (like `actions`). The standalone error hint also references
`praisonai-code` instead of `praisonai`.
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
Telegram/channel probes on SSL-inspecting (MITM/corporate proxy) networks
failed cert verification and hard-aborted gateway start with a misleading
"fix credentials" message, even though the runtime adapter connects fine.
- Classify SSLCertVerificationError/CERTIFICATE_VERIFY_FAILED separately from
auth/token failures (_is_ssl_error)
- Soft-fail preflight when only SSL errors occur: warn and continue start;
still hard-abort on genuine token failures
- Honor SSL_CERT_FILE / REQUESTS_CA_BUNDLE / PRAISONAI_SSL_CA_BUNDLE in the
probe SSL stack (_apply_probe_ca_bundle)
- Actionable per-channel doctor message pointing at SSL/proxy, not credentials
- Regression tests for classification, render, CA bundle, soft/hard fail
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
Addresses reviewer feedback on PR #2836:
- strip_leading_timestamps now anchors on the YYYY-MM-DD HH:MM date-time
instead of hardcoded English weekday names, so replay de-duplication
works under non-English server locales and for custom templates that
omit %a (Greptile P1 / CodeRabbit).
- _with_timestamp now() fallback uses timezone-aware UTC so %Z renders
"UTC" instead of an empty string / trailing space (Greptile P2).
- _with_timestamp error fallback returns the de-duplicated base, never a
stale prefix (CodeRabbit).
- chat_with_run_control now threads received_at through to chat() for the
original turn; pending follow-ups correctly fall back to now() (CodeRabbit).
- Documented timestamp_template date-time constraint in the config schema.
- Added tests for non-English weekday / no-%a dedup, UTC fallback, and
custom-template replay.
Co-authored-by: Mervin Praison <[email protected]>
praisonai-code doctor exited 1 on standalone installs (no praisonai wrapper)
because four wrapper-presence checks were scored as hard failures instead of
skips. Apply the existing skip_if_no_wrapper() helper to
performance_praisonai_import, acp_module and praisonai_package_structure, and
make python_module_execution target the product's own entry point
(python -m praisonai_code) when the wrapper is absent.
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
🤖 Generated with [Claude Code](https://claude.ai/code)
os.kill(pid, 0) can raise SystemError/ValueError on Windows, which
propagated through `gateway status` as an ERROR while /health was OK.
Catch these in _is_process_running (returns False) and make the PID
lock block in GatewayHandler.status advisory-only so the authoritative
/health probe is always reached.
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
Root --help renders the underscore key mint_link as mint-link, but
_BotCommandGroup.get_command only matched exact keys, so the advertised
hyphen form failed with exit code 2. Normalize hyphen/underscore in
get_command and list_commands so displayed names are invocable.
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
Address reviewer feedback on PR #2835:
- Cap _list_channels_async pagination at _MAX_LIST_CHANNELS_PAGES (50 pages
x 200 = 10k channels) so a misbehaving Slack API cannot paginate forever.
- Pass future.result(timeout=_LIST_CHANNELS_TIMEOUT=30s) so an unresponsive
Slack API degrades to an empty list instead of blocking the sync refresh
loop indefinitely.
- Add test covering the pagination cap.
Co-authored-by: Mervin Praison <[email protected]>
praisonai-code memory list now resolves via Typer for parity with tools/session/config list.
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
Add opt-in per-message arrival-time stamping to the bot session turn
builder so an always-on gateway agent has a clock: it can compute
relative-time requests ("in 2 hours"), reason about conversation gaps
and interpret "this morning"/"by end of day".
- `session.timestamps` / `session.timestamp_template` config (off by
default for prompt-cache stability)
- `_with_timestamp` renders each turn's real arrival time; applied to
both DM (per_user) and group (per_chat) scopes, after sender
attribution so the time sits outermost
- `strip_leading_timestamps` de-dupes on replay so prefixes never
accumulate as `[ts] [ts] … text`
- `chat()` gains an optional `received_at` (real platform receive time),
falling back to `datetime.now()`
Co-authored-by: MervinPraison <[email protected]>
Adds the missing enumeration half of the durable ChannelDirectory: Discord
walks cached guild text channels synchronously; Slack paginates
conversations.list via a sync-safe async wrapper. A shared ChannelRef
descriptor feeds the already-wired refresh_from_adapters, so reachable
channels become addressable without prior inbound traffic. Platforms that
cannot enumerate remain observed-only via the existing not-callable guard.
Co-authored-by: MervinPraison <[email protected]>
- Add `type="hello"` discriminant to HelloParams so uniform frame.type
dispatch across the ClientFrame union no longer AttributeErrors on hello
- Chain re-raised FrameDecodeError with `from None` (ruff B904)
- Reuse top-of-file MIN_CLIENT_PROTOCOL_VERSION / GATEWAY_PROTOCOL_VERSION
constants instead of hardcoded 1 and late-defined forward references
- Add tests asserting the type discriminant on every frame type
Co-authored-by: Mervin Praison <[email protected]>
Add a single, dependency-light frame codec in the core gateway protocol
module that turns raw inbound WebSocket frames into validated, typed,
discriminated objects (or rejects them deterministically with the existing
structured HelloError envelope).
- New typed frames: MessageParams, LeaveParams, JoinParams (completing the
advertised message/leave methods and the legacy join handshake), plus
validated hello decoding into the existing HelloParams.
- decode_client_frame(data) dispatcher discriminated on `type`, and a
FrameDecodeError carrying a structured HelloError for the transport to
reject with.
- No pydantic/jsonschema: a small hand-rolled validator preserves core's
no-heavy-import rule. Additive and backward-compatible; the wrapper server
can migrate to decode-once at its own pace.
- Export new symbols from the gateway package and add 26 unit tests.
Co-authored-by: MervinPraison <[email protected]>
Address reviewer (Greptile) P2 findings:
- agent.py: guard isinstance(PermissionManager) before constructing a
throwaway manager, avoiding storage-dir creation + rules.json/approvals.json
disk reads when a pre-built manager is passed.
- test_permissions.py: add async call-time deny test exercising
_check_tool_approval_async (mirrors the existing sync coverage).
Skipped the is_denied "namespaced-allow overrides wildcard-deny" concern:
the PermissionManager uses deny-wins semantics globally, so short-circuiting
on any DENY is the intended, consistent behaviour.
Co-authored-by: Mervin Praison <[email protected]>
Adds Accept: application/json headers to the device-code and token-exchange
POST requests so providers that default to form-encoded responses (notably
GitHub) parse correctly instead of raising JSONDecodeError. Also prints an
actionable OAuth hint when auto-mode falls back to API-key login for a known
OAuth provider lacking a client id, and passes overrides directly.
Co-authored-by: Mervin Praison <[email protected]>
Denied tools are now hidden from the advertised tool schema and blocked
uniformly at call time (native + MCP), reusing the existing filtering seam.
- Add PermissionManager.is_denied(tool_name, agent) exposure helper
- Consult the manager in _tool_name_allowed / _prune_denied_tools (deny = hide)
- Gate every tool-call path (native + MCP) via _check_permission_manager_deny
- Auto-wire a PermissionManager from the declarative permissions: config
- Extend registry.get_tool_definitions() with an optional permission_resolver
Co-authored-by: MervinPraison <[email protected]>
Close the remaining wrapper-config gaps in the layered ConfigResolver:
- env_utils: add {file:./path} include + {env:VAR:-default} directives and a
centralised interpolate() applied uniformly to config values.
- resolver: interpolate values at load time in _read_config_file; add
resolve_with_provenance() returning per-key (value, layer, source).
- config command: add `praisonai config provenance` to show the reconciled
resolved view and which file/layer supplied each key.
Co-authored-by: MervinPraison <[email protected]>
Populate the previously-empty OAUTH_PROVIDERS registry with curated
device-code (RFC 8628) providers (github, google, gemini, azure) so
`auth login <provider> --method oauth` works without hand-supplying
every endpoint. Add --client-id and endpoint overrides to `auth login`,
treat registry providers as OAuth-capable, and surface a precise
"pass --client-id" message instead of silently falling back.
Co-authored-by: MervinPraison <[email protected]>
- factories.py: hoist `logging` to module-level import + shared logger
instead of re-importing inside each exception handler
- retrieval.py: correct the "no chunks stored" hint (embedding failures
now raise RuntimeError, so this branch means no supported files/content)
- test_embedding_failure.py: use pytest.raises instead of manual try/except
Co-authored-by: Mervin Praison <[email protected]>
Greptile P1: registry_serve defaulted to 8080 while cmd_serve and the
canonical 'serve registry' both default to 7777, silently landing users
on a different port when following the deprecation migration path.
Co-authored-by: Mervin Praison <[email protected]>
- knowledge.py: raise RuntimeError when store() returns AddResult(success=False)
instead of appending empty id and counting failed chunk as success
- factories.py (ChromaKnowledgeAdapter): log + include underlying error in
AddResult.message; log embedding failures in search(); respect
OPENAI_EMBEDDING_MODEL env var instead of hardcoded text-embedding-3-small
- retrieval.py index CLI: filter empty ids, print actionable error and exit 1
when no chunks stored
- add regression tests for embedding-failure handling
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
praisonai registry list/serve crashed with ModuleNotFoundError because
praisonai_code.cli.features.registry did not exist and the Typer stubs
re-entered legacy PraisonAI().main(). Mirror the #2787 endpoints fix:
- Add cmd_list + handle_registry_command to wrapper RegistryHandler
- Add praisonai-code bridge module re-exporting the wrapper handler
- Replace Typer stubs to dispatch directly (no sys.argv mutation)
- Add regression tests in test_registry_bridge.py
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
- db/adapter.py: add async-safe `_dispatch_async` helper that awaits coroutine
methods and off-loads sync methods to a thread. Fixes silent data loss where
AsyncPostgres/AsyncSQLite `async def add_message` was run via asyncio.to_thread,
producing an un-awaited coroutine so INSERTs never ran.
- db/adapter.py: fix wrong `..persistence.conversation.models` import (module
does not exist) to `.base`.
- db/adapter.py: `aon_agent_start` now creates-or-fetches the session and returns
prior messages for async session resume (mirrors sync on_agent_start).
- db/adapter.py: replace non-existent `set_agent_state`/`async_set_agent_state`
calls with the real StateStore `set`/`aset` protocol methods.
- db/adapter.py: add missing `aon_run_start`/`aon_run_end` mirroring sync twins.
- integrations/managed_local.py: shlex.quote pip package names and enforce a
strict allowlist before shelling out inside the compute sandbox, preventing
shell injection from attacker-controlled YAML package lists.
Co-authored-by: MervinPraison <[email protected]>
Previously hoisting the call outside the try/except blocks removed the
defensive fallback that was implicitly present in the original code.
Add explicit try/except so a failure in clean_json_output gracefully
falls back to the raw agent_output instead of propagating upward.
artifact_ref was initialized inside the `if len(result_str) > limit:` block, so
short-result paths left it unbound, causing `if artifact_ref:` guards to raise a
silently-swallowed NameError that skipped context-aware truncation.
Co-authored-by: Mervin Praison <[email protected]>
- agents.py: Hoist clean_json_output() call before output_json/output_pydantic
branches so it runs at most once instead of twice when both flags are set.
- tool_execution.py: Replace redundant "'artifact_ref' in locals()" guards with
direct "if artifact_ref:" checks. artifact_ref is already initialised to None
at line 476, so the locals() lookup always returns True and adds no value.
Also adds explicit "blocked_result = None" initialisation before the try block,
removing reliance on locals().get() which is not guaranteed by the language spec.
- agents.py / chat_mixin.py: Add cross-reference comments to the two identical
clean_json_output() implementations so maintainers know to keep them in sync.
- agents.py save_output_to_file: Guard os.makedirs() with "if dir_name:" to
avoid FileNotFoundError when output_file has no directory component (e.g. 'out.txt').
Add a module-level __getattr__ to autoagents.py that lazily resolves
get_openai_client and LLM as module attributes, preserving the lazy
import goal (llm.llm / llm.openai_client are not loaded on the
praisonaiagents.agents import path) while keeping them patchable via
patch('...autoagents.get_openai_client') / patch('...autoagents.LLM').
_generate_config now references get_openai_client / LLM through the
module object so test patches take effect, and keeps
supports_structured_outputs imported from ..llm at call time to match
its existing patch target (praisonaiagents.llm.supports_structured_outputs).
Co-authored-by: Mervin Praison <[email protected]>
The dead-code removal in PR #2815 deleted the private telemetry
`_initialized` flag, but debug_auto_instrument.py:11 still read it,
raising AttributeError on run. Point the debug output at the live
`_atexit_registered` flag instead.
Co-authored-by: Mervin Praison <[email protected]>
Move get_openai_client and LLM into the method that uses them (next to the
existing lazy import of supports_structured_outputs) and drop the unused
OpenAIClient import. This restores the llm package's lazy-loading design so
that importing the agents package no longer eagerly loads llm/llm.py and
llm/openai_client.py.
Co-authored-by: MervinPraison <[email protected]>
Delete the unreferenced private _initialize_telemetry() function and its
_initialized module global. The function had a no-op body and zero call
sites; top-level __init__.py owns telemetry initialisation. The live
atexit path (_ensure_atexit) is unchanged.
Co-authored-by: MervinPraison <[email protected]>
The praisonai-code lazy loader resolves the 'app' command via
getattr(module, 'app') on praisonai.cli.commands.app, but the module
only defined 'app_command'. The resulting AttributeError was silently
swallowed and dispatch always fell through to the inline duplicate.
Add 'app = app_command' alias so the wrapper owns the command when
installed (C8.2 intent). The inline _get_app_command is retained as the
no-wrapper fallback per the issue's Keep-unchanged guidance.
Co-authored-by: MervinPraison <[email protected]>
Addresses CodeRabbit + Greptile review: result.stdout and result.output
return the same captured stdout in Click 8/Typer, so concatenating them
doubled the output. Use result.output alone.
Co-authored-by: Mervin Praison <[email protected]>
The shipped praisonai knowledge search command (wrapper-resident handler in
praisonai/cli/commands/knowledge.py) treated Knowledge.search() output as a
dict/sliceable sequence, crashing with 'SearchResult object is not
subscriptable'. PR #2779 fixed the parallel feature module but not this
canonical command path.
Apply the same to_legacy_format/dict/list normalization used elsewhere and
fix the empty-result guard (empty SearchResult is truthy). Add wrapper-path
regression tests covering SearchResult, empty SearchResult, and legacy dict.
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
Port hasSuccessfulReleaseToday from PraisonAIUI — the monorepo gate was
releasing on every green Core Tests push to main (4 releases today).
Also skip release-gate on Release/Bump bot commits to avoid cascades.
Unify Agent tool resolution via a canonical resolver chain, auto-enable
plugins from PRAISONAI_PLUGINS/config on Agent init, and resolve optional
sandbox backends through praisonai.sandbox entry points.
Bridge closures previously only mutated data.credentials/data.config when
the attribute was already a dict, silently dropping a plugin's returned dict
during first-time auth/config (attribute None). Add a _write_back helper that
mutates in place when a dict exists, else assigns directly.
Co-authored-by: Mervin Praison <[email protected]>
- Use truthy check (with callable support) for plugin is_available
- Store SandboxRegistry.default() once to avoid double init
- Generic plugin-install hint instead of hardcoded capsule
- Registry tests inject fake module so they run without praisonai; add ImportError-branch coverage
Co-authored-by: Mervin Praison <[email protected]>
Extend Plugin base class with session_start, session_end, and on_error
lifecycle methods, and wire on_config, on_auth, session_start, session_end,
and on_error through _adapt_plugin_hooks so they fire via the runtime hook
registry.
Co-authored-by: MervinPraison <[email protected]>
SandboxManager falls back to SandboxRegistry for types like capsule
registered by praisonai-plugins. Adds SandboxConfig.capsule() factory only;
implementation stays in external plugin packages.
Phase 2 restarted at 5 after Phase 1 gained a 6th (repo-routing) item,
colliding with Phase 1 items 5-6. Renumber Phase 2 to 7-9 and Phase 3
to 10-11 so the prompt has a single monotonic list.
Co-authored-by: Mervin Praison <[email protected]>
Address reviewer feedback on PR #2802:
- onboard wizard now prompts for ChannelField entries without an `env`
fallback (e.g. IRC `server`) and writes them into bot.yaml, closing the
silent-gap that caused a runtime error at gateway start (Greptile P1).
- invoke the optional descriptor `setup(io)` hook when present, merging
returned values; document `setup` as optional so a minimal descriptor is
valid and the Protocol is not used as an isinstance guard (Greptile/CodeRabbit P2).
- isolate global-registry mutations with an autouse teardown fixture.
- replace secret-looking test literal to satisfy secret scanners.
Add regression test for config-only field onboarding.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Agent-callable integrations belong in PraisonAI-Tools; lifecycle plugins
in PraisonAI-Plugins. Update issue creation, FINAL review, merge gate,
and CI-fix prompts so PRs like Composio (#2771) are caught early.
Add an optional ChannelDescriptor contract (with ChannelField) to core
protocols so a channel plugin declares its config fields, system-prompt
hint, and setup hook once, and the config schema, onboarding wizard, and
agent prompt pick them up with zero core edits.
- core: ChannelField + ChannelDescriptor in praisonaiagents.bots.protocols
- registry: register_platform(..., descriptor=...) storage + getters incl.
get_platform_descriptor / get_channel_system_prompt_hint and adapter
channel_descriptor fallback
- config schema: extra="allow" so plugin keys are preserved (no longer
silently dropped) + apply_channel_descriptor resolves env fallbacks and
enforces required plugin fields
- onboard: enumerate list_platforms() and render plugin descriptors
Co-authored-by: MervinPraison <[email protected]>
Address reviewer feedback on PR #2800:
- register_approval_backend keeps _channel_send_func and _target coupled so a
backend already owning a sender for another transport is never split (only a
bare backend adopts this channel's sender + default target).
- _handle_approve_callback fails closed on an unknown decision value instead of
letting the backend silently treat it as a deny.
- Drop the redundant approver arg (backend defaults approver to actor).
Adds tests for existing-sender preservation and unknown-decision rejection.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Connect the previously-orphaned durable PresentationApprovalBackend to the
live Telegram interactive-callback path:
- Add TelegramBot.register_approval_backend() to attach the durable backend
with a channel sender (render_presentation) + target so Allow/Deny buttons
render onto the chat.
- Route cmd:/approve <approval_id> <decision> button taps (which decode to the
"command" namespace) to backend.handle_callback(), so decisions resolve
against the SQLite store: single-use, actor-authorised, id-correlated.
- Rehydrate pending approvals on start so a late tap survives a restart.
- Auto-wire a durable backend configured on the agent (--approval presentation)
so the path is reachable without extra user code.
Co-authored-by: MervinPraison <[email protected]>
Add the transport-agnostic building blocks the gateway needs to detect and
reap half-open connections, closing the loop on the advertised-but-inert
heartbeat_ms policy:
- protocols.py: PING/PONG EventType members, a LIVENESS_TIMEOUT
GatewayCloseCode, and a pure, import-free LivenessDecision + LivenessPolicy
(with LivenessPolicyProtocol), mirroring the existing SendPolicy /
DrainTimeoutPolicy / ConcurrencyLimitPolicy pattern. The policy owns only
the KEEP vs REAP decision over (last_activity, now); interval_ms=0 disables
reaping so behaviour is unchanged.
- config.py: opt-in LivenessConfig wired into GatewayConfig (Python + YAML +
status dict) with a to_policy() bridge to the core policy.
- gateway/__init__.py: export the new symbols.
- tests: 19 unit tests for the policy, config bridge and YAML parsing.
Disabled by default, fully backward-compatible.
Co-authored-by: MervinPraison <[email protected]>
- command list --all now includes built-in commands (pass DEFAULT_COMMANDS
to create_default_registry) so listing matches interactive invocation
- registry build failure now stores a sentinel to avoid retrying import on
every slash command (repl.py + async_tui.py)
- custom/skill/mcp commands no longer silently shadow a built-in name in the
registry, since built-ins dispatch before the registry is consulted; the
shadowed definition is logged instead
- add regression test for built-in shadow precedence
Co-authored-by: Mervin Praison <[email protected]>
Fixes an off-by-one flagged in review: undoing a turn checkpointed
before any conversation messages existed called revert_to_message(sid, 0),
which under core HierarchicalSessionStore semantics keeps messages[:1] and
leaves a stale first message. Now clears via an explicit clear primitive
when available, else no-ops safely instead of leaving a ghost message.
Aligns the test fake with real core semantics and adds two edge-case tests.
Co-authored-by: Mervin Praison <[email protected]>
- get_spill_dir: on mkdir failure, fall back to a fresh mkdtemp() instead of
retrying the identical failing path (addresses silent fallback loop)
- bounded_with_pointer: reserve room for the pointer so head+pointer+tail
never exceeds max_output_size
- spill: register atexit cleanup to prune process-created artifacts, avoiding
unbounded temp accumulation in long-running agents
- tests: cover spill-write-failure graceful degradation and preview budget
Co-authored-by: Mervin Praison <[email protected]>
Introduce a CommandRegistry in praisonai-code that aggregates pluggable
command sources (built-ins, .praisonai/commands/*.md custom commands, skills,
MCP prompts, and praisonai.commands entry-point plugins) into one namespace.
Interactive surfaces now drive /name resolution and /help from the registry:
- async_tui._handle_command resolves custom commands via the shared
interpolate_command_template path (byte-for-byte parity with
`praisonai run --command`) and lists them under /help.
- repl._handle_command does the same, merging custom commands into /help.
- `praisonai command list --all` lists the same unified namespace so listing
and invocation cannot drift.
Backward compatible: built-ins are registered as a built-in source and still
dispatch via their existing branches; failures degrade to built-ins only.
Co-authored-by: MervinPraison <[email protected]>
Wire the core HierarchicalSessionStore.revert_to_message primitive into the
CLI session-checkpoint layer so /undo restores files AND rewinds chat_history
to the same turn boundary, keeping workspace and conversation coherent.
Co-authored-by: MervinPraison <[email protected]>
When shell output exceeds max_output_size, persist the full buffer to a
session-scoped artifact and return a bounded head/tail preview plus a
pointer, so the omitted middle (often where the real error lives) can be
inspected via read_file/grep. Zero overhead when within budget; legacy
middle-truncation remains available via spill=False.
Co-authored-by: MervinPraison <[email protected]>
- orchestrator.close(): wrap conversation/knowledge/state close() in _sync()
so async-backend coroutines are awaited instead of discarded (no more
leaked connections / queued writes on shutdown or context-manager exit).
- _factory_wants_tag(): only count positional-capable params so **kwargs-only
sink factories aren't mis-called as factory(tag) and silently dropped.
- finalize_observability(): guard flush() and close() independently so a
raising flush() no longer skips close() and leaks the sink.
Co-authored-by: Mervin Praison <[email protected]>
Gap 1: PersistenceOrchestrator sync hooks now drive async-store coroutines to
completion via a _sync() helper (shared _async_bridge.run_sync) instead of
silently discarding them, restoring writes for mode="async" backends.
Gap 2: enable_injection_defense() default threshold lowered CRITICAL -> HIGH to
match the class default and scoring model; scan_text() now NFKC-normalizes,
strips zero-width chars and folds common Cyrillic/Greek homoglyphs before
matching so full-width/zero-width/homoglyph one-liner attacks are blocked.
Gap 3: init_observability() now honours the documented
"praisonai.observability_sinks" entry-point group, wiring discovered sinks
through a _FanoutSink so multiple TraceSinkProtocol sinks coexist;
finalize_observability() flushes/closes them.
Co-authored-by: Mervin Praison <[email protected]>
- endpoints_test now dispatches to the handler's `invoke` command instead
of the nonexistent `test` command, which returned "Unknown command: test"
(EXIT_GENERAL_ERROR). Fixes Greptile P1.
- Replace vacuous `exit_code is not None` with isinstance(int) check and add
regression coverage for `endpoints test`. Fixes Greptile P2 / CodeRabbit nit.
Co-authored-by: Mervin Praison <[email protected]>
- Print command hint to stderr (not stdout) so redirection/scripts can
distinguish diagnostic output from normal output
- Annotate classify_unknown_command as Optional[str]/Sequence[str] to
reflect that None is accepted and complete the signature
- Move `import difflib` to module scope instead of mid-function
Co-authored-by: Mervin Praison <[email protected]>
Route get_wrapper_version() through the sanctioned _wrapper_bridge instead
of a direct `from praisonai.version import __version__`, which violated the
C7/C8 reverse-import boundary and failed check_c7_imports.sh allowlist gate.
The bridge imports the live praisonai.version source module, so editable
installs still match `praisonai --version`, preserving the #2782 fix. Also
removes the now-redundant fallback flagged by Greptile.
Co-authored-by: Mervin Praison <[email protected]>
praisonai endpoints list crashed because the Typer handler re-entered the
full legacy PraisonAI().main() with mutated sys.argv, and the feature module
praisonai_code.cli.features.endpoints referenced by the legacy dispatcher and
doctor check did not exist.
- Add praisonai_code/cli/features/endpoints.py bridge re-exporting
EndpointsHandler / handle_endpoints_command from the praisonai wrapper
(mirrors the existing workflow/tools bridge pattern).
- Replace the broken Typer shim in commands/endpoints.py to dispatch directly
to handle_endpoints_command instead of PraisonAI().main().
- Add regression tests for the bridge and list dispatch.
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
`praisonai show` (and other reserved/mistyped single-word verbs) were silently
routed to handle_direct_prompt, incurring a ~10s paid LLM call instead of a
"command not found" error.
Add classify_unknown_command() in argparse_builder to detect reserved verbs
(e.g. `show`) and obvious typos of known commands via difflib, and fail fast
with a hint + exit code 2 in parse_args before treating the token as a
one-shot prompt. Multi-word natural-language prompts remain unaffected.
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
praisonai doctor showed misleading "Fix bot.yaml syntax" remediations for
5 bot/gateway checks when the real cause was the optional praisonai-bot
package not being installed. Add a skip_if_no_bot_package guard so these
schema-dependent checks are skipped with an actionable install hint
(pip install 'praisonai[bot]') instead of failing.
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
Address greptile p2 review comments on #2778:
- Only swallow the Knowledge(verbose=) TypeError when the message
confirms it's about verbose; re-raise otherwise so unrelated
errors surface instead of being masked.
- Warn the user when inspect.signature fails so a silently dropped
verbose flag is visible.
Co-authored-by: Mervin Praison <[email protected]>
Knowledge.search() returns a typed SearchResult dataclass, but the CLI
search paths only checked for dict/list and fell through, passing the
dataclass to .get()/subscript access and raising "'SearchResult' object
is not subscriptable".
Normalize results via SearchResult.to_legacy_format() in both the
`knowledge search` and top-level `search` commands, plus regression tests.
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
The `praisonai query` command passed retrieval_config= to Agent.__init__,
which the Agent-first API does not accept (RAG is consolidated under
knowledge=/_retrieval_config), crashing every invocation with TypeError.
- Drop the invalid retrieval_config= kwarg
- Build Agent kwargs via inspect.signature so only supported params are
passed, mapping verbose -> output= on newer Agent APIs
- Attach agent._retrieval_config so .query()/.rag use the intended config
- Add regression tests
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
The legacy argparse router hijacked the top-level `search` command and only
accepted --max-results (web search). Documented KB-retrieval flags
(--collection, --top-k, --hybrid) failed with 'unrecognized arguments'.
handle_search now detects retrieval flags and delegates to the Typer
retrieval.search_command, while preserving the web-search default for plain
queries. Backward-compatible; typer is only imported on the retrieval path.
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
- computer_screenshot(path=...) now requires approval before writing to disk
(P1 security: closes arbitrary-file-overwrite bypass of the safety gate)
- computer_type reports actual ASCII chars typed vs total (pyautogui drops non-ASCII)
- computer_key rejects empty/invalid key strings instead of silent no-op
- computer_scroll rejects invalid directions instead of defaulting to down
- add tests covering the above
Co-authored-by: Mervin Praison <[email protected]>
Add computer_tools.py providing agent-callable screen control
(screenshot, click, type, key, scroll, move) following the
schedule_tools pattern. Control actions require an explicit
human-approval callback (safe by default) and the pyautogui
backend is lazy-imported as an optional dependency, so there
is no Agent class bloat and no import-time cost.
Co-authored-by: Mervin Praison <[email protected]>
Top-level --help renders each subcommand's own Typer help= (Click's default
format_commands loads the module), not the _LAZY_COMMANDS tuple description.
dashboard/langfuse wrapper modules still had emoji help, so cp1252 consoles
could still crash. Strip those emoji and add a cp1252-encodability regression
test (excluding Rich box-drawing chars, which Rich downgrades automatically).
Co-authored-by: Mervin Praison <[email protected]>
Emoji prefixes in _LAZY_COMMANDS and the ui/port command help strings
caused UnicodeEncodeError when praisonai-code --help was rendered on a
Windows cp1252 console. Replace them with ASCII-safe text and add a
regression test asserting the help output contains no emoji.
Co-authored-by: Mervin Praison <[email protected]>
truncate_context computed a byte cut point as `max_tokens * 4` and sliced
`&context[..char_limit]` directly. When the context contains multi-byte
UTF-8 (CJK, accents, emoji), that byte index can land inside a character,
panicking with "byte index N is not a char boundary". Walk the index back
to the nearest char boundary before slicing. Fixes both the free function
and the RAG::truncate_context method; ASCII output is unchanged.
Post structured @claude comments when internal PRs fail Core Tests, with
extracted pytest failures, critical review verdict requirements, and SDK
guardrails against weakening tests. Hooks into pipeline label sync and
workflow_run for fast response.
test_queue_or_execute_when_not_processing spawned real daemon threads
via _execute_in_background that made live LLM calls and logged to stderr
after the test's pytest capture stream was closed, intermittently failing
a later unrelated test (test_config_list) with "I/O operation on closed
file". Mock _execute_in_background so the test only verifies its intended
behavior without leaking threads or making network calls.
Co-authored-by: Mervin Praison <[email protected]>
- Assign temp_path before json.dump so temp file is cleaned up if json.dump
raises IOError mid-write (disk full)
- Catch TypeError/ValueError from json.dump so non-serializable data returns
False + cleans up temp file instead of leaking an unhandled exception
Co-authored-by: Mervin Praison <[email protected]>
Factor the debug-logging predicate out of _log_llm_config into a cheap
_should_log_config() and guard the get_response / get_response_async call
sites with it, so the ~40 attribute reads, two argument dicts and
str(kwargs) serialisation are only performed when DEBUG (or verbose>=10)
logging is actually enabled. Behaviour is byte-for-byte identical when
logging is on; masking/truncation formatting is unchanged.
Co-authored-by: MervinPraison <[email protected]>
Agent's class-body _truncate_chat_history shadowed the MemoryMixin copy
via MRO, creating a silent drift hazard between two divergent lock idioms.
Delete Agent's duplicate so the inherited MemoryMixin implementation is the
single source of truth. Behavior is unchanged ([:length] semantics preserved,
verified thread-safe via the re-entrant AsyncSafeState/DualLock RLock), the
mixin stays standalone-usable, and agent.py shrinks.
Co-authored-by: MervinPraison <[email protected]>
Extract a single _atomic_write_json helper and use it from
_modify_session_locked, _save_session, and add_message. This removes
~25 duplicated lines and harmonises the makedirs discrepancy where
_save_session previously lacked directory creation.
Co-authored-by: MervinPraison <[email protected]>
Restores the --dry-run option to docs_generate whose parameter was
accidentally removed while two references to dry_run remained in the
body (guaranteed NameError). Also removes spurious blank lines in
docs_run call, cli_run_all signature, and the executor dry_run block.
Co-authored-by: Mervin Praison <[email protected]>
Agent BEFORE_AGENT/BEFORE_TOOL hook post-processing read res.output.modified_data,
but HookRunner returns HookExecutionResult whose .output is a HookResult which has
modified_input (not modified_data). This was dormant because Agent._hook_runner
previously used an empty registry; the plugin bridge switched it to the populated
default registry, surfacing an AttributeError once any hook is registered.
Co-authored-by: Mervin Praison <[email protected]>
Address reviewer findings on PR #2744:
- set_chat_history / clear_session now clear last_compaction so a replaced
transcript is not misread as a compacted tail (fixes silent message drop
via api.save_state()).
- _persist_compaction_checkpoint writes to _history_session_id (matching the
resume read path) to avoid key divergence when both session_id and
MemoryConfig(session_id) are supplied.
- append_compaction_checkpoint treats blank summaries as a no-op, preventing
an empty system message from being replayed on resume.
- Added regression tests for all three paths.
Co-authored-by: Mervin Praison <[email protected]>
Close the loop between in-run context compaction and session persistence so
`--continue`/`session resume` reconstructs the compacted context (summary +
retained tail) instead of replaying the raw transcript.
Core changes (praisonaiagents/session/store.py):
- Add optional CompactionCheckpoint dataclass + SessionData.last_compaction,
serialized atomically alongside messages (backward compatible).
- Add DefaultSessionStore.append_compaction_checkpoint() and get_working_history();
SessionData.get_working_history() falls back to raw history when no checkpoint.
- SessionData.trim_messages() keeps the checkpoint anchor aligned when the
transcript head is trimmed to max_messages.
Integration:
- agent/chat_mixin.py: persist the summary after a successful in-run compaction
(guarded: only when a session store + session_id are bound and a summary exists),
and prefer get_working_history() when injecting resumed history.
- agent/memory_mixin.py: restore compacted working history on resume, falling back
to raw chat history for pre-existing sessions.
Tests: add TestCompactionCheckpoint (roundtrip, persist, working-history
reconstruction, backward-compat, cross-instance, trim-shift, empty-summary) and
update history-injection mocks for the new resume path.
Co-authored-by: MervinPraison <[email protected]>
- openai_client.py: work on a local copy of messages in both sync/async
tool loops so the graceful wrap-up control message never leaks into the
caller's conversation history (Greptile P1).
- agent.py: last_stop_reason reads the raw name-mangled __openai_client
attribute (never the lazy property), guaranteeing no OpenAI client is
created for LiteLLM-only agents (Greptile P1).
- feature_configs.py: decouple resolved_max_tool_calls() from max_steps so
the per-turn parallel tool-call guardrail stays independent of the outer
loop budget (Greptile P2).
- tests: cover messages non-mutation, agent.last_stop_reason == max_steps
via the Agent API, and no lazy client creation on read.
Co-authored-by: Mervin Praison <[email protected]>
Address reviewer feedback on the run-status controller:
- _use_reactions() now also honours a truthy/falsy `reactions` field
(attribute or dict-style ChannelCapabilities) so capability gating is
no longer a no-op with non-supports_reactions caps.
- StallWatchdog logs a warning when hard_s < soft_s instead of silently
clamping, keeping the existing clamp behaviour backward-compatible.
- Test helper _run() uses a fresh event loop instead of the deprecated
asyncio.get_event_loop() pattern (warns on 3.10+, raises on future).
- Add caps-gating tests for dict-style and attribute-style reactions.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Add a pure, transport-agnostic RunStatusController and StallWatchdog in core
bots/ that advance an in-chat reaction (or status-label edit) through the run
lifecycle (queued -> thinking -> tool -> done/error) with debounced intermediate
transitions and immediate terminal ones, plus soft/hard stall signals when a run
goes quiet. Capability-gated (reactions vs label), off by default, opt-in.
Co-authored-by: MervinPraison <[email protected]>
Raise MAX_CANDIDATES to 3, route scan/merge/pipeline-sync through the GitHub App token to avoid PAT rate limits, and stop scanning once enough candidates are found.
Addresses Greptile P1/P2 review feedback on PR #2757:
- P1: RulesConfig.files was advertised but silently ignored. Add a public
RulesManager.add_rule_file() (heavy impl stays in the manager, not Agent)
supporting concrete paths and globs, and consume config.files in
_init_rules_manager before the discovery gate so workspaces whose only
rules are explicit files are not dropped.
- P2: get_rules_context() used a falsy check on char_budget, treating
char_budget=0 as "no budget"; use `is not None`.
- Add RulesConfig to feature_configs __all__ for export consistency.
Co-authored-by: Mervin Praison <[email protected]>
Wire the dormant RulesManager into the Agent run path so AGENTS.md,
CLAUDE.md and .praisonai/rules/*.md are discovered and injected into the
system prompt automatically. Adds a consolidated rules= param
(None/True=auto-apply, False=opt out, RulesConfig=custom) with
discovery-gated activation for zero-config, zero-cost runs.
Co-authored-by: MervinPraison <[email protected]>
Addresses reviewer feedback (Greptile P1/P2, CodeRabbit Major) on PR #2755:
- _mentions_self now matches on whole identifier components instead of bare
substring, so unrelated services (api-gateway, kong-gateway) are no longer
denied while the real praisonai-gateway unit still matches.
- Default process_names drops the over-generic "gateway" token; "praisonai"
(specific) is the default, with the CLI-form rule catching the gateway
sub-command.
- Rule 1 (CLI form) now honours process_names and enforces the documented
<cli> gateway <verb> adjacency instead of loose membership.
- Removed phantom _KILL_CMDS entries kill9/kill-9 (not real executables).
- Added regression tests for the false-positive cases, custom-CLI rule-1
coverage, and adjacency.
Co-authored-by: Mervin Praison <[email protected]>
Add a pure, import-free LifecycleCommandGuardPolicy + protocol in core
gateway/protocols.py that default-denies agent-issued commands whose effect
is to stop/restart/kill this gateway process (self-DoS via shell tool or
scheduled job). Command-anchored matching avoids prose false positives; on
by default, opt-outable.
Co-authored-by: MervinPraison <[email protected]>
- presentation.py: max_button_label 20->24 so overflow list-row titles use
the full WhatsApp budget (renderer still caps reply-button titles at 20).
- _presentation_renderer.py: keep full 4096-char body for text/list paths;
apply the tighter 1024 cap only to reply-button bodies. Preserve non-URL
tappable buttons when a select block is present by inlining their labels
into the list body. Add trailing newline.
- whatsapp.py: split render from send so durable-delivery failures propagate
(parity with send_message) instead of being swallowed by a blanket except.
- tests: update limit assertion; add select+buttons preservation test.
Co-authored-by: Mervin Praison <[email protected]>
Introduces a dependency-free, inheritable BasePlatformAdapter (ABC) plus a
SendResult dataclass in the core SDK (praisonaiagents.bots.base). New channels
now implement a minimal 4-method contract (connect/disconnect/send/
get_chat_info) and inherit capability-driven default delivery: long-message
chunking, typing heartbeat, retry/backoff honouring retry_after, and graceful
edit/delete "not supported" fallbacks — all keyed off PlatformCapabilities.
Co-authored-by: MervinPraison <[email protected]>
Address greptile P2 feedback on test_interrupt.py: the _make_llm skip
guard now catches all exceptions on LLM construction (not just
ImportError) so partial litellm installs skip cleanly instead of
failing, and the file ends with a trailing newline.
Co-authored-by: Mervin Praison <[email protected]>
- disable() now unwires previously-registered hooks from the runtime
registry (adds unwire_from_hook_registry + tracks wired hook IDs), so
disabling a plugin actually stops its lifecycle methods from firing
- re-enable after disable correctly re-wires (sentinel reset on unwire)
- guard tool_input/tool_definitions mutation with isinstance instead of
hasattr to avoid AttributeError/TypeError when those fields are None
- hoist repeated `import logging` in plugins.enable() to a single import
- merge duplicate typing imports in manager.py
- add tests: disable-after-wire, re-enable, None tool_input no-crash
Co-authored-by: Mervin Praison <[email protected]>
- auto.py: resolve installed tool registry once per get_user_content()
(avoids duplicate ToolResolver.list_available() call) and use an
accurate prompt header ("AVAILABLE TOOLS (installed)") when the flat
resolver list replaces the category grouping
- agents_generator.py: update runtime-features ValueError message to
reference SUPPORTS_RUNTIME_FEATURES flag instead of stale praisonai-only text
- workflow_framework.py: derive the "supported frameworks" guidance from
adapters advertising SUPPORTS_WORKFLOW and drop praisonai-only wording
- tests: align regression assertions with the clearer error messages
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Gap 1: AutoGenerator.discover_tools_for_topic / get_user_content now consult
the canonical ToolResolver.list_available() so only installed tools are
surfaced; keyword hints prioritise but are intersected with the real registry.
Falls back to legacy keyword/static behaviour when resolver is unavailable.
Gap 2: Add SUPPORTS_WORKFLOW / SUPPORTS_RUNTIME_FEATURES capability flags to
the core BaseFrameworkAdapter (default False) and set them True on
PraisonAIAdapter. Replace the three hardcoded framework == "praisonai" checks
(agents_generator runtime validation, workflow_framework validation, jobs
executor dispatch) with adapter-flag reads so third-party adapters can opt in.
Gap 3: AutoGenerator now retains the resolved adapter (self._adapter) as the
canonical dispatch object and derives self.framework from adapter.name,
mirroring AgentsGenerator instead of discarding the adapter after validation.
All changes are backward-compatible: new flags default False, error messages
and native praisonai behaviour are preserved via safe fallbacks.
Co-authored-by: Mervin Praison <[email protected]>
- run.py: strip .py suffix before name resolution so 'foo.py' resolves as 'foo'
- run.py: emit unknown-tool hint when .py fallback fails (was silent)
- run.py: surface empty-module case when PRAISONAI_ALLOW_LOCAL_TOOLS is set
- tools.py: escape user query with rich.markup.escape to prevent MarkupError crash
Co-authored-by: Mervin Praison <[email protected]>
- Clarify --tools help on run/chat/code: accepts comma-separated tool
names (resolved via ToolResolver) or a tools.py path
- Wire --tools/--toolset into `run` actions mode (previously dropped),
reaching parity with the default/YAML/Python surfaces
- Add `praisonai tools search <query>` to enumerate/filter the catalogue
- Print an inline hint when a local tools.py is skipped because
PRAISONAI_ALLOW_LOCAL_TOOLS is unset, and near-miss guidance for
unknown tool names
Co-authored-by: MervinPraison <[email protected]>
Greptile P1/P2: _save_credentials used CredentialStore(explicit_path), which
sets legacy_credentials_path=None, bypassing the legacy ~/.praison migration
and diverging from the default CredentialStore() that auth/inject read from.
- setup now uses the default CredentialStore() when PRAISONAI_HOME is not
overridden, so it writes the same ~/.praisonai/credentials.json that
auth list / inject_credentials_into_env read, and legacy entries migrate
on write (no data loss). Explicit path only when PRAISONAI_HOME differs.
- add test_setup_key_visible_to_default_store (default-store convergence)
- add test_setup_migrates_legacy_credentials (legacy auth key survives setup)
Co-authored-by: Mervin Praison <[email protected]>
Credentials were split across two locations: `setup` wrote API keys to
~/.praisonai/.env while `auth` and inject_credentials_into_env() read from
~/.praison/credentials.json via CredentialStore. Keys set by `setup` were
therefore invisible to the credential-store code paths.
- CredentialStore now writes to a single canonical location
(~/.praisonai/credentials.json) and reads ~/.praison/credentials.json only
as a read-only backward-compat fallback; any write transparently migrates
legacy entries onto the canonical file.
- `setup` now mirrors the API key into the CredentialStore (anchored under
PRAISONAI_HOME) so keys set via setup are picked up by auth/injection.
Backward-compatible: legacy files continue to be read during the deprecation
window; no core SDK / Agent changes.
Co-authored-by: MervinPraison <[email protected]>
Address reviewer feedback on PR #2686:
- Clamp max_events_per_window to >= 1 in __post_init__ so a zero config
value cannot permanently block every bot pair
- Add a per-instance threading.Lock to make observe()/reset() safe under
concurrent inbound bot turns
- Reclaim empty deques for idle pairs to prevent unbounded growth
- Add trailing newline; add tests for zero-budget clamping
Co-authored-by: Mervin Praison <[email protected]>
Add BotLoopGuard/BotLoopPolicy — a pure, dependency-free sliding-window
pair-budget decision primitive in core (praisonaiagents/bots/silence.py),
mirroring evaluate_channel_health. It tracks each participant pair (our bot
<-> the other sender bot) in both directions and suppresses further replies
for a cooldown once the per-pair budget is exceeded, breaking runaway 1-in/
1-out loops between two bots. Human-authored and single-bot traffic is
unaffected (guard is only consulted for bot-authored inbound turns).
Co-authored-by: MervinPraison <[email protected]>
The function-name allow-list branch returned getattr(module, name), which is
byte-for-byte identical to the fallback immediately below it. Removing it is
behaviour-preserving and eliminates the ~90-entry maintenance tax. The *_tools
module-object branch and the fallback are retained unchanged.
Co-authored-by: MervinPraison <[email protected]>
LegacyMemoryAdapter and create_legacy_memory_adapter were unreferenced
Strangler-Fig scaffolding never wired into the adapter registry. The file
was not imported in adapters/__init__.py and no "legacy" name was registered
anywhere. Removing it is a pure dead-code cleanup with no behaviour change.
Co-authored-by: MervinPraison <[email protected]>
The actions output branch re-imported build_cli_memory_config and
apply_cli_session_continuity from ..utils.project, shadowing the correct
..state.project_sessions versions. The utils.project variant lacks the
auto_save keyword, causing a TypeError when session continuity is wired.
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
run's --output only accepts silent/actions/verbose/json/stream (run.py:480);
plain is invalid. chat's --output does accept plain (chat.py:114) so that row
is kept. Added valid-mode notes for both commands.
Co-authored-by: Mervin Praison <[email protected]>
chat/code/run resolve standalone via LazyCommandGroup and are not in
_WRAPPER_RESIDENT_COMMANDS. Correct README.md and C7_VERIFICATION.md
matrices to reflect actual standalone support, add wrapper-resident
list and known Windows limitations.
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
Address reviewer feedback (CodeRabbit/Greptile): the stat-based pipe
check only classified the handle type, so an open pipe with no EOF could
still block sys.stdin.read() on Windows (CI/CD, Docker, IDE terminals).
Perform the Windows read on a daemon thread with a bounded timeout so it
never hangs, and add tests covering the redirected-read and no-block paths.
Co-authored-by: Mervin Praison <[email protected]>
Replace the platform guard that silently dropped piped stdin on Windows
with a stat-based pipe/file detection plus bounded read. Extracts the
data-availability check into _stdin_has_data() so Unix uses select() and
Windows uses a non-blocking pipe check, keeping the TTY/EOF safety and
_MAX_STDIN_BYTES cap intact.
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
Guard the lazy `Agent` construction in `praisonai/agent.py.__getattr__`
with a lock and write the built class back into the module's globals so
concurrent first-time accesses all observe the same class object,
eliminating the double-checked-init race flagged in review (isinstance
consistency across threads). Subsequent accesses now resolve directly
without re-entering __getattr__.
Co-authored-by: Mervin Praison <[email protected]>
Gap 3: praisonai/agent.py eagerly imported praisonaiagents at module top,
costing ~420ms on praisonai.Agent first access even though __init__.py
defers correctly. Move the heavy import into a lazily-built, cached class
factory exposed via module-level __getattr__, keeping reaching the module
cheap and preserving backward compatibility (from praisonai.agent import Agent
and from praisonai import Agent both still work). TYPE_CHECKING import keeps
IDE/static-analysis support.
Co-authored-by: MervinPraison <[email protected]>
Address reviewer feedback on PR #2712: the command-scoped path of
_create_pattern still returned the blanket ``tool_name:*`` for a
missing/blank/non-string bash command, and for missing/non-string/
oversized non-shell arguments (including the persisted-deny path).
Now scope="command" never emits ``tool_name:*``; malformed/missing
arguments fall back to a literal single-use target so a persisted
allow/deny can only match the exact invocation the user acted on.
``tool_name:*`` remains reachable solely via the explicit [T] choice.
Co-authored-by: Mervin Praison <[email protected]>
Rewrite InteractiveCLIApprovalBackend._create_pattern to derive the
narrowest reasonable persisted pattern via the shared core
praisonaiagents.permissions.derive_pattern helper, so approving a single
shell command (e.g. `rm -rf build/`) persists `bash:rm *` rather than the
blanket `bash:*` that silently auto-approved every future shell command.
- Blanket `tool:*` now requires an explicit "[T] Always allow ALL uses"
choice; it is never the silent default of "always".
- Unknown/compound commands fall back to a literal (single-use) pattern,
preserving fail-closed behaviour.
- Approval prompt shows a change preview (diff/content) for edit/write/
apply_patch so the user approves the actual change.
- Denial can carry an optional reason surfaced to the agent via decision
metadata for steering.
Co-authored-by: MervinPraison <[email protected]>
- loader: defer prefix decision until actual loaded instances counted so a
lone surviving server (others yielding None) keeps bare names (backward compat)
- loader: catch identical-name collisions, not just differently-sanitized ones
- loader: delegate sanitization to MCP._sanitize_prefix (single source of truth)
- loader: use new public MCP.apply_tool_filters instead of private internals
- mcp: add public apply_tool_filters() contract
- mcp: with_tool_prefix() raises on empty prefix instead of silently skipping
- mcp: _apply_prefix_to_openai_tools returns copies (idempotent, no in-place mutation)
- tests: add identical-name collision + single-loaded-instance backward-compat tests;
fake MCP delegates to real sanitizer and mirrors public filter API
Co-authored-by: Mervin Praison <[email protected]>
Implement the previously no-op prefix_tools contract in load_mcp_tools:
- Add MCP.with_tool_prefix() to namespace tool names (schema + callable)
while dispatch still routes to original server-side names.
- Wire prefixing in the loader when >1 server is loaded; keep bare names
for single-server loads (backward compatible).
- Honour include/exclude tool filters per server.
- Detect duplicate server-name collisions and raise a clear error.
- Update docstring; add unit tests.
Co-authored-by: MervinPraison <[email protected]>
- _summarise_overflow now forwards the prior summary text and accumulates a
running compacted_count so repeated rollups don't collapse to "2 messages"
(Greptile P1: context lost after 2nd compaction).
- Use len(to_archive) for compacted_count/log so re-compaction doesn't overstate
(Greptile P2).
- Early return in _enforce_window when only the prior summary overflows with no
new raw turns, avoiding a redundant rollup and misleading "compacted 0" log
(CodeRabbit Major).
- Warn once when archived_messages exceeds ARCHIVE_WARN_THRESHOLD (Greptile P2).
- Strengthen compaction-logging test to exact count + add running-count test.
Co-authored-by: Mervin Praison <[email protected]>
DefaultSessionStore silently dropped the oldest turns beyond 100 messages
on every write, permanently losing early context on session resume.
Introduce a retention policy:
- "compact" (new default): summarise overflow into one synthetic summary
message (reusing the deterministic ContextCompressor summariser) and
archive the raw turns in a durable append-only archive — nothing is
silently dropped.
- "truncate": legacy hard-slice behaviour.
- "keep_all": never trim the active window.
Explicit non-default max_messages keeps the legacy truncate behaviour for
backward compatibility. Rollups are logged once. Retention/active_window
are configurable via constructor args and PRAISONAI_SESSION_RETENTION /
PRAISONAI_SESSION_ACTIVE_WINDOW env vars for CLI/YAML surfaces.
Co-authored-by: MervinPraison <[email protected]>
Addresses reviewer (Greptile) feedback on PR #2724:
- DLQ routing on the receive() redelivery-path quarantine (was silently
dropped; only replay() routed). Now returned to receive()/_key_for_existing
and routed outside the journal lock, mirroring replay().
- Attempt cap now also applies to stale 'pending' entries (a handler that
keeps raising resets to 'pending' via _release_claim), not just crash-loop
'claimed' entries — closes the infinite-redelivery gap.
- Quarantined entries are now aged out by TTL (keyed on ts) and shed by
overflow eviction (completed -> quarantined -> pending cascade) so inert
poison rows can never crowd out healthy pending traffic.
Adds tests for DLQ routing via redelivery, pending-cap quarantine, and
quarantined TTL/overflow eviction.
InboundJournal.replay() reset every stale-claimed entry to 'pending'
unconditionally, letting a crash-inducing inbound message loop the
gateway forever. Mirror the outbound queue: cap attempts at max_attempts,
route exhausted entries to the existing InboundDLQ, and mark them
'quarantined' so they are never replayed. Also guard the receive()
redelivery path so a quarantined poison message is not reoffered.
Co-authored-by: MervinPraison <[email protected]>
- Discord publish_command_menu: replace per-iteration closure with a
factory (_make_callback) so captured name/prefix are not exposed in the
callback signature. discord.py 2.x inspects the full callback signature
and raises TypeError on unannotated params after `interaction`, which was
silently swallowed and left zero commands registered.
- Drop unused `import discord` (only app_commands is used).
- Move `import asyncio` to module top-level in _commands.py.
Co-authored-by: Mervin Praison <[email protected]>
Make the shared CommandRegistry the single source of truth for both the
native command menu and dispatch, so typing "/" surfaces available commands
with descriptions on Telegram and Discord.
- CommandRegistry.menu_entries(): policy-filtered (name, description) pairs
- CommandRegistry.dispatch(): invoke a bound handler (sync or async), or None
- ChatCommandMixin.build_command_menu_entries()/publish_command_menu():
shared no-op base capability, merging registry built-ins with adapter
custom commands and per-command access policy
- Telegram: publish_command_menu via set_my_commands(BotCommand(...)),
called at startup
- Discord: publish_command_menu registers application commands on the
CommandTree and syncs, called on_ready
Additive and backward-compatible; menu publish is best-effort and never
blocks startup.
Co-authored-by: MervinPraison <[email protected]>
Addresses reviewer findings on PR #2726:
- P1 SSRF: Slack _transcribe_audio now vets url_private_download via
_media.is_safe_url before downloading, blocking forged events pointing
at internal/metadata endpoints.
- P2: echo_transcripts is now honored in Slack and WhatsApp (was Telegram-only).
- P2: Slack placeholder/detection now shares _is_audio_file so a voice note
with a non-audio mimetype but audio filetype is not dropped.
- Adds regression tests for the SSRF guard and audio detection.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Inbound voice notes were silently dropped: STT defaulted to off, had zero
callers, no config surface, and was inert/absent for Slack/WhatsApp.
- Add shared bots/_stt.py helper: resolve_stt_config() (metadata passthrough,
on-by-default, bare-bool shorthand) and transcribe_media_path() (reuses
tools.audio.stt_tool wrapping core AudioAgent.transcribe).
- Add stt config block to _config_schema.py (SttConfigSchema + ChannelConfigSchema.stt).
- Gateway server carries stt into config.metadata and calls bot.enable_stt() on
both the start_channels and hot-reload paths.
- Telegram: never drop a voice note — fall back to a visible placeholder; use
the shared helper with language/model/echo options.
- Slack: add _transcribe_audio (downloads voice file via bot token through the
SSRF-safe media cache) and wire it into the message handler.
- WhatsApp: transcribe inbound audio, forward transcript + cached audio, with a
graceful placeholder fallback.
- Add regression test asserting a voice note is not dropped when STT is off.
Co-authored-by: MervinPraison <[email protected]>
Covers normal return, the mem0 MongoDB 'vectors' TypeError fallback to [],
re-raising of other TypeErrors, and Memory._safe_mem0_search delegation.
Co-authored-by: Mervin Praison <[email protected]>
Extract the mem0 #3185 defensive guard into a single shared helper
`safe_mem0_search` in memory/adapters/factories.py and delegate to it
from both Memory._safe_mem0_search and Mem0MemoryAdapter.search_long_term.
This removes drift and restores the diagnostic warning on the adapter path
while keeping identical catch/return-empty semantics.
Co-authored-by: MervinPraison <[email protected]>
Thread a cancel token into get_response/get_response_async and check it at
the top of each tool iteration so /stop promptly halts a mid-flight run on
every litellm provider (Anthropic, Gemini, Groq, local, ...), matching the
existing OpenAI-path behaviour. chat_mixin passes the active interrupt
controller through to the loop.
Co-authored-by: MervinPraison <[email protected]>
Add a WhatsApp Cloud API presentation renderer (interactive reply buttons for
<=3 buttons, list messages for overflow/selects) plus a platform-keyed
renderer-selection registry (render_for) with a plain-text fallback so new
channels plug in uniformly and interactive content never silently drops.
- core: PresentationLimits.whatsapp() capability descriptor
- wrapper: WhatsAppPresentationRenderer, PresentationRenderer protocol,
_RENDERERS registry + render_for/get_renderer/fallback_text
- wrapper: WhatsAppBot now implements SupportsPresentation
(render_presentation / presentation_limits / truncate_presentation)
Co-authored-by: MervinPraison <[email protected]>
Installed/enabled plugins were discovered via the praisonai.plugins entry-point
group but never invoked, because the Agent's HookRunner defaulted to a fresh
empty registry. This wires the two systems together:
- Add PluginManager.wire_into_hook_registry() plus a Plugin -> HookDefinition
adapter that maps overridden lifecycle methods (before/after agent, llm, tool,
message, permission) onto the runtime hook engine, honouring in-place payload
mutation (e.g. PII redaction on AFTER_LLM).
- Default Agent._hook_runner to the global default registry so bridged plugins
fire during real agent/bot execution.
- Wire enabled plugins into the registry from plugins.enable().
- Remove the duplicate discover_entry_points (keep the praisonai.plugins group).
Co-authored-by: MervinPraison <[email protected]>
Consolidate the two divergent tool-call caps (OpenAI-native max_iterations=10
and LiteLLM max_iter=20 / max_tool_calls_per_turn=10) behind a single
ExecutionConfig.max_steps knob honoured identically by both loops.
- Add ExecutionConfig.max_steps + resolved_max_steps()/resolved_max_tool_calls()
helpers (backward compatible: unset falls back to max_iter/per-turn cap).
- Wire both tool-execution loops to the same budget via chat_mixin resolvers
and agent.py (LLM.max_iter now derives from resolved_max_steps()).
- Inject a graceful wrap-up instruction on the final permitted step so the
model produces a coherent final answer instead of being hard-cut.
- Surface a structured stop reason (completed | max_steps) via
agent.last_stop_reason so CLI/CI callers can branch on truncation instead
of parsing a magic string.
- Add tests locking both backends to identical budget behaviour, plus a
real agentic test gated on OPENAI_API_KEY.
Co-authored-by: MervinPraison <[email protected]>
Wire the dormant RulesManager into the Agent run path so AGENTS.md,
CLAUDE.md and .praisonai/rules/*.md are discovered and injected into the
system prompt automatically. Adds a consolidated rules= param
(None/True=auto-apply, False=opt out, RulesConfig=custom) with
discovery-gated activation for zero-config, zero-cost runs.
Co-authored-by: MervinPraison <[email protected]>
Clear provider env vars so auto-detect does not consume mocked prompts,
use a key that passes validation, and skip the offline smoke test via
--no-verify so smoke/test-core stay green after the setup wizard changes.
Co-authored-by: Cursor <[email protected]>
- agents_generator: validate both the YAML-declared framework and the instance
framework in _build_yaml_workflow so a non-native workflow YAML (e.g.
framework: crewai) can no longer bypass validation when the generator holds
the default 'praisonai' (CodeRabbit).
- mcp_server/registry: run lazy loaders while holding a re-entrant lock across
all three registries so a concurrent caller blocks until loading completes
instead of observing an empty/partial registry mid-load (Greptile).
Co-authored-by: Mervin Praison <[email protected]>
Agents is a deprecated alias (emits DeprecationWarning) for AgentTeam,
the primary class since v1.0+. Switch telemetry auto-instrumentation to
the canonical AgentTeam name in the TYPE_CHECKING import, runtime import,
annotations and docstrings.
Co-authored-by: Mervin Praison <[email protected]>
Gap 1: WorkflowAutoGenerator._save_workflow now writes self.framework
instead of hardcoded 'praisonai', so framework="crewai" is persisted.
Gap 2: agenerate_crew_and_kickoff routes blocking config load and
run-preparation (heavy imports + adapter.setup) through asyncio.to_thread
via new _aload_config/_aprepare_for_run helpers. Collapsed the duplicated
_run_yaml_workflow/_arun_yaml_workflow into a shared _build_yaml_workflow
builder + _finalise_workflow_result helper.
Gap 3: MCPToolRegistry/MCPResourceRegistry/MCPPromptRegistry guard their
lazy-loader state with a threading.Lock and use an atomic swap in
_ensure_loaded, fixing the mutation-during-iteration / double-run race.
Co-authored-by: MervinPraison <[email protected]>
Treat an existing Claude FINAL reply as sufficient after rebase-only
updates, ignore superseded detect-and-trigger cancellations in CI
green checks, and scope merge-conflict workflow concurrency per PR.
Co-authored-by: Cursor <[email protected]>
- Seed dedup per-chunk so a file injected inside a concatenated up-front
load (ROOT\n\nFOO) is recognised and not re-attached (CodeRabbit/Greptile P1)
- Initialise _seen_texts in __init__; drop lazy getattr pattern (Greptile P2)
- Keep truncation marker within max_chars so the budget is enforced (CodeRabbit)
- Defer committing _seen state until content is actually emitted so a
budget-suppressed discovery is not permanently locked out (Greptile P2)
- Prefix unused `root` bindings with `_` in tests (Ruff RUF059)
- Add test for concatenated already_loaded dedup
Co-authored-by: Mervin Praison <[email protected]>
Extend the existing wrapper loader (praisonai-bot context_files) with
path-driven, session-cached discovery instead of a parallel core module.
PathContextAttacher walks up from a touched file's directory to the project
root collecting AGENTS.md/CLAUDE.md candidates, dedupes against up-front
rules and earlier touches, bounds output by a char budget, and caches per
directory. Reuses the existing _discover_search_dirs walk-up helper.
Co-authored-by: Mervin Praison <[email protected]>
- _detect_provider_from_env now returns the env-var actually present (e.g.
GOOGLE_API_KEY) instead of the provider's canonical GEMINI_API_KEY, fixing
a KeyError in the interactive path when only GOOGLE_API_KEY is set.
- Detected model now uses the clean refreshed _provider_defaults() value
rather than the older prefixed string from llm/env.py, keeping the model
shown to the user consistent with the smoke test.
- _prompt_and_validate_key is now bounded (max 3 attempts) so it cannot loop
forever when rich's Confirm is unavailable and validation keeps rejecting.
- Removed stale PROVIDERS/PROVIDER_NAMES dicts from commands/setup.py; the
single source of truth is SetupHandler._provider_defaults().
- Added regression tests for the GOOGLE_API_KEY path and refreshed model.
Co-authored-by: Mervin Praison <[email protected]>
Make the first-run onboarding wizard world-class:
- Auto-detect a provider whose *_API_KEY is already in the environment and
offer it as a single Y/n confirmation (reuses llm/env inference helper).
- Validate an entered key with the shared auth-login format check and
re-prompt on failure instead of silently persisting a bad key.
- Run a post-setup smoke test (Agent.start) so users leave with a verified
working agent; skippable via --no-verify for offline/CI.
- Refresh dated default models to current strong choices.
Co-authored-by: MervinPraison <[email protected]>
- cleanup_completed now evicts LOST jobs (prevents monotonic memory leak
where reconciled orphans accumulated for the process lifetime across restarts)
- reconcile_on_start transitions RUNNING/PENDING -> LOST before inserting into
the in-memory map, closing the race where a concurrent get_status observed
the transient RUNNING status; stamps completed_at so LOST is age-evictable
- orphaned PENDING jobs (process died before first run) are now reconciled to
LOST instead of being silently dropped; protocol docstring updated
- tests: PENDING-orphan reconciliation and LOST eviction
Co-authored-by: Mervin Praison <[email protected]>
Add a BackgroundJobStore protocol, LOST terminal state, delivered flag,
and reconcile_on_start() to BackgroundJobManager so long-running jobs and
their deferred deliver-back survive a process restart. Persistence is
opt-in: when no store is supplied the runner is pure in-memory exactly as
before. The protocol lives in core beside the runner; a concrete SQLite
impl belongs in the wrapper/bot layer.
Co-authored-by: MervinPraison <[email protected]>
- Dispatch StreamEventType.ERROR to the compositor in DraftStreamer.on_event so
a failing tool renders the ✗ glyph (was dead code at the integration layer)
- Drop the unused `state` param from _tool_text
- Prevent a late RUNNING event from downgrading a completed (DONE) line
- Gate the progress-mode throttle bypass on a _progress_dirty flag so redundant
edits are not scheduled for every text token after a tool has run
- Add regression test for the DONE->RUNNING guard
Co-authored-by: Mervin Praison <[email protected]>
Add a pure, dependency-free progress compositor in core that turns typed
StreamEvents into a stable, bounded, multi-line status view. A tool's start
and finish share one correlation id so "running" becomes "done"/"error" on the
same line instead of a single overwritten tool label.
- core: praisonaiagents/streaming/progress.py (ProgressLine, merge_progress_line,
render_progress) exported lazily from streaming/__init__.py
- bot: opt-in progress_style="feed" on StreamingConfig/StreamingConfigSchema;
DraftStreamer folds tool start/finish/output/error events and renders the
bounded rolling window. Default "line" behaviour is unchanged.
Co-authored-by: MervinPraison <[email protected]>
Addresses Greptile P1 (botos.py): complete_run clears the lease on disk,
but the in-memory job still held _lease_until=now+300, so the subsequent
mark_run -> store.update(job) re-wrote the stale lease back, blocking
crash-recovery for sub-lease-interval jobs. Now clear _lease_until /
_lease_owner in-memory right after complete_run.
Also hoist os/socket/uuid imports to module level in scheduler/loop.py.
Co-authored-by: Mervin Praison <[email protected]>
BotOS._run_schedule_loop and the ScheduleLoop daemon now reserve each due
job via runner.claim_due_jobs before executing, releasing the lease with
complete_run, so a due job fires at most once across processes/hosts —
matching the WebSocket gateway's ScheduledAgentExecutor. Falls back to the
non-atomic get_due_jobs path when the store has no atomic claim, so
single-process behaviour is unchanged.
Co-authored-by: Mervin Praison <[email protected]>
Move `from ..llm import LLM` from module top level into
`_create_llm_instance`, matching the deferred-import pattern already
used in agent/agent.py, workflows/workflows.py and task/task.py. This
avoids eagerly loading the large llm.py module on every multi-agent
import for sequential/plain workflows that never construct a manager LLM.
Co-authored-by: MervinPraison <[email protected]>
Address Greptile review on build_tool_definition:
- P1: use docstring.split('\n\n')[0] (first paragraph) instead of first line
to avoid silently truncating descriptions on the llm.py path. This is a
superset of the single-line extraction previously used by agent.py and
openai_client.py, so those paths are unchanged for single-line docstrings.
- P2: add missing trailing newline at end of file.
Co-authored-by: Mervin Praison <[email protected]>
Extract a single build_tool_definition helper in the core tools/schema.py that
performs signature introspection, docstring Args parsing, the type map and the
recursive array 'items' normalisation. All three copies (agent.py, llm.py,
openai_client.py) now delegate to it, removing the duplication and the
divergence where the agent-layer copy emitted arrays without 'items'.
Also make llm/schema_utils.fix_array_schemas re-export the canonical core
implementation so LLM/OpenAI-client/MCP callers stay unchanged.
Co-authored-by: MervinPraison <[email protected]>
Address gateway API endpoint review findings (Issue #2715):
- P1: _extract_text now forwards the full labelled transcript when the
messages array carries assistant turns (stateless OpenAI-SDK history),
so externally-supplied context is never silently dropped on a fresh
session; single-turn calls still forward only the last user line.
- P2: OpenAI 4xx/5xx errors now use the spec shape
{"error": {message, type, code, param}} so the OpenAI SDK parses
error.message correctly.
- P2: unauthenticated callers without a session header now get a unique
per-request key instead of a shared "anon" session, preventing
cross-caller memory bleed when auth is disabled.
- Documented the buffered SSE streaming limitation (deliberately avoids a
hot-path regression).
Co-authored-by: Mervin Praison <[email protected]>
Add config-gated, additive Starlette routes on the running gateway app so
OpenAI-SDK and MCP clients reach the same stateful agents/sessions as chat
users — one process, one agent state, three protocols.
- core: add ApiConfig (openai/mcp toggles) to GatewayConfig + YAML parsing
- wrapper: GatewayApiEndpoints adapter dispatching /v1/chat/completions
(+SSE), /v1/responses, /v1/models and MCP /mcp JSON-RPC into the gateway's
register_agent agents, shared session store and admission gate
- routes gated by config.api.*, token-protected exactly like /info
- 3-way surface: CLI --openai-api/--mcp, YAML gateway.api.{openai,mcp},
Python WebSocketGateway(openai_api=True, mcp=True)
Co-authored-by: MervinPraison <[email protected]>
The TYPE_CHECKING block still imported AgentManager while the file's
type annotation and runtime code use Agents. Update the stale import
to Agents for consistency (Greptile review finding).
Co-authored-by: Mervin Praison <[email protected]>
auto_instrument_all referenced the name Agents at runtime but only
imported AgentManager, causing a NameError that was silently swallowed
by _init_telemetry's broad except. This made agent/workflow constructor
auto-instrumentation a silent no-op when telemetry was enabled.
Import Agents (alias of AgentTeam/AgentManager) so the runtime name
resolves and the intended instrumentation runs.
Co-authored-by: MervinPraison <[email protected]>
Pass auto_save explicitly in apply_cli_session_continuity and serialise the
module under xdist_group so parallel workers cannot drop persisted messages.
Co-authored-by: Cursor <[email protected]>
Add a dependency-free tracing-hook protocol seam to the core gateway so a
tracer plugin can wrap each pipeline stage (inbound/admit/agent.run/llm.call/
tool.call/outbox.enqueue/delivery) in an OTel span parented per turn, carrying
the existing correlation id as an attribute.
Core holds only the protocol + a zero-cost no-op default (NullGatewayTraceHook)
so there is no OpenTelemetry import and no hot-path overhead when disabled. The
heavy OTLP exporter lives in praisonai-plugins.
Co-authored-by: MervinPraison <[email protected]>
Move xdist-safe session redirection into tests/unit/cli/conftest.py so all
CLI tests avoid polluting ~/.praisonai/sessions; opt out via no_session_isolation.
Co-authored-by: Cursor <[email protected]>
Rename _isolated_session_stores -> isolated_session_stores and have the
two find_last_session tests unpack the fixture's pre-isolated stores
instead of recreating DefaultSessionStore instances and re-monkeypatching
the same tmp_path dirs. Removes redundant double-patching flagged by
Gemini and Greptile.
Co-authored-by: Mervin Praison <[email protected]>
Redirect session stores to per-test temp dirs and resolve helpers via the
module namespace so monkeypatch applies under parallel CI workers.
Co-authored-by: Cursor <[email protected]>
Patch praison_ai._get_auto_generator (not cli_main shim) so --init preflight
tests hold when CI sets API keys; guard .lower() on empty command/model ids.
Co-authored-by: Cursor <[email protected]>
Rich error panels split "pip install praisonai" across lines on narrow
CI terminals; match on "install praisonai" instead.
Co-authored-by: Cursor <[email protected]>
The early gate in run_main already exits before _run_prompt is reached,
so the second _require_wrapper_for_default_run call was dead code. Also
add the missing PEP8 blank line between top-level function definitions.
Co-authored-by: Mervin Praison <[email protected]>
Smoke CI failed because default standalone run exited on missing API key
before emitting the pip install praisonai hint. Also remove shadowed
apply_cli_session_continuity import that broke --output actions mode.
Co-authored-by: Cursor <[email protected]>
Complete the tool-output-limit consolidation by replacing the two
remaining hardcoded 16000 fallbacks in tool_execution.py (lines 783,
849) with the shared DEFAULT_TOOL_OUTPUT_LIMIT constant, eliminating
the last drift-prone literals flagged by Greptile.
Co-authored-by: Mervin Praison <[email protected]>
Write resolved performance symbols and PERFORMANCE_MONITORING_AVAILABLE
back into globals() so subsequent attribute-style access (e.g.
telemetry.PerformanceMonitor, dir(), inspect) bypasses __getattr__ and the
import machinery, and the flag no longer re-imports all three submodules on
every read. Addresses Greptile P1 review feedback on #2672.
Co-authored-by: Mervin Praison <[email protected]>
Source the 16000-char default from a single DEFAULT_TOOL_OUTPUT_LIMIT
constant in config/feature_configs.py so OutputConfig.tool_output_limit,
ToolConfig.output_limit, ToolConfig.from_dict, and the agent.py constant
all reference one value and cannot drift. Both public fields and existing
precedence/validation are unchanged.
Co-authored-by: MervinPraison <[email protected]>
Defer performance_monitor/performance_utils/performance_cli imports via a
module-level __getattr__ so get_telemetry()/MinimalTelemetry no longer parse
~68K of performance-monitoring code. All public symbols and
PERFORMANCE_MONITORING_AVAILABLE remain importable with identical semantics.
Co-authored-by: MervinPraison <[email protected]>
Addresses two valid P2 findings from Greptile review on the lazy gateway
registration path:
1. Thread-safety: guard the _gateways_registered check-then-act with a
module-level threading.Lock (double-checked locking) so concurrent first
callers register gateway providers exactly once.
2. Double registration: remove the redundant module-level auto-call in
gateways.py. Registration is now driven solely (and lazily) by
registry._ensure_gateways_registered(), reducing cold-start writes from
6 to 1 for the openrouter provider while preserving the import-cost
deferral this PR introduces.
Updated test_registry_only_imports_typing to allow stdlib threading import.
Co-authored-by: Mervin Praison <[email protected]>
_supported_backends() used list_registered() (canonical names only),
so PersistenceConfig.validate() rejected alias-named backends (neon,
xata, cockroachdb, libsql, motor, ...) that the registry resolves and
create() accepts. Switch to list_all_names() so validation matches
resolution. Fixes reviewer-reported alias regression on #2667.
Co-authored-by: Mervin Praison <[email protected]>
Consolidate the three hardcoded backend lists in persistence/config.py to
a single source of truth by deriving supported backends from the registry.
Fixes a latent false-negative where PersistenceConfig.validate() rejected
registry-supported backends (valkey, async_mongodb, gcs, etc.).
Co-authored-by: MervinPraison <[email protected]>
Move gateway provider registration off the praisonai.llm import path so
the config-only hot path (build_config_list) no longer pays the gateway +
registry + importlib.metadata entry-point scan cost (~22ms). Registration
now runs lazily on first get_default_llm_registry() access, guarded by a
run-once flag, so create_llm_provider("openrouter/...") still resolves with
no explicit import.
Co-authored-by: MervinPraison <[email protected]>
Mirror OpenAIClient.__init__ resolution for base_url and max_retries when
building the singleton cache key so an explicit argument matching the
OPENAI_MAX_RETRIES / OPENAI_API_BASE env value reuses the cached client
instead of spuriously recreating it. Also add the missing EOF newline.
Co-authored-by: Mervin Praison <[email protected]>
Replace the 27 direct litellm.completion/acompletion calls on the primary
agent turn, streaming turns, tool-iteration turns and reflection paths with
the existing _completion_with_retry/_acompletion_with_retry wrappers so every
LLM API call is rate-limited, failed over and retried with backoff.
Also route the native OpenAI-client path through the SDK's built-in retry
(honouring Retry-After + backoff) via an optional max_retries knob
(OPENAI_MAX_RETRIES env / get_openai_client(max_retries=...)), defaulting to
the SDK default so behaviour is unchanged when unset.
Co-authored-by: MervinPraison <[email protected]>
Addresses reviewer feedback on PR #2662:
- Normalise revoke_on_secret_rotation like other config booleans so string
"false"/"0" (env-substituted YAML) actually opts out (CodeRabbit, Major).
- Coerce non-string auth_token (unquoted YAML int) to str so os.environ
assignment no longer raises TypeError mid-reload (Greptile p2).
- Warn when auth_token is present but resolves to empty instead of silently
keeping the old secret (Greptile p2).
- Add 3 unit tests covering these paths.
Co-authored-by: Mervin Praison <[email protected]>
The file 'n' was an accidentally committed scratch file at the repo root.
It duplicated the existing, more complete LSPConfig at
src/praisonai-agents/praisonaiagents/lsp/config.py and the LSPClient / LSP
intelligence already exposed via praisonaiagents.lsp. It also carried a hard
unused pylsp import that would crash on load. No SDK value; removed.
Co-authored-by: Mervin Praison <[email protected]>
Stamp each authenticated WebSocket session with a fingerprint of the active
shared secret at connect time, and force-close every session carrying a stale
stamp when auth_token is rotated via config hot-reload. Reuses the existing
server-initiated close path plus the new GatewayCloseCode.CREDENTIALS_ROTATED
and ConnectRecoveryStep.REAUTHENTICATE so leaked/revoked credentials stop
working within one reload cycle without a full process restart. Defaults on;
opt out via gateway.revoke_on_secret_rotation: false.
Co-authored-by: MervinPraison <[email protected]>
Address reviewer feedback on PR #2659:
- run.py: move --restore early-exit before stdin ingestion so a piped
`... | praisonai run --restore last` no longer drains the pipe it won't use.
- run.py: extract shared case-insensitive _is_yaml_file() helper used by both
the stdin-merge gate and downstream file/prompt routing, fixing a case-sensitive
mismatch that could misroute uppercase YAML paths (e.g. AGENTS.YAML).
- stdin.py: bound sys.stdin.read() to 10 MB to prevent unbounded buffering;
skip the select-based read on Windows (select is socket-only there); replace
the blind except-pass with a debug-level log so piping issues stay diagnosable.
- tests: add coverage for Windows skip and the size cap.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Address reviewer feedback on PR #2660:
- find_last_session now reuses list_project_sessions' merged+de-duplicated
view so a session id living in both stores is counted once and list <-> continue
stay consistent; added a limit param for heavy-user windows.
- Redirect both stores to isolated temp dirs in the global-visibility and
child-skipping tests so assertions no longer contend with pre-existing
sessions on the machine or parallel runs.
- Add trailing newline at EOF.
Co-authored-by: Mervin Praison <[email protected]>
find_last_session() previously searched only the project-scoped store
(~/.praisonai/sessions/projects/<id>/), so `run --continue` never saw a
session created by a core Agent(session_id=...), chat, the gateway, or the
API (which land in the flat global store). It now searches the project +
global default stores and resolves to the most-recent root session,
skipping sub-agent/forked children. Adds list_project_sessions() and routes
`session list` through it so anything resumable is also listable. Core store
list_sessions() now surfaces parent_id/parent_session_id for root filtering.
Co-authored-by: MervinPraison <[email protected]>
Add a shared EOF-safe stdin helper (resolve_cli_input) and wire it into
the modern run/code/chat commands so they compose in Unix pipelines and
CI, matching the legacy bare-prompt path. The legacy read_stdin_if_available
now delegates to the same helper for a single implementation.
Co-authored-by: MervinPraison <[email protected]>
Addresses reviewer findings on PR #2654:
- P1: AutoGen v0.2 now catches ToolTimeoutError at the execution boundary and
returns an LLM-readable string instead of letting the exception abort the chat.
- P2: guard the shared user_proxy against duplicate register_for_execution so a
second same-named tool cannot silently overwrite the first callable.
- P3: recompute _max_leaked_workers when the timeout pool is (re)built so the
recycle threshold always matches the actual pool size.
Co-authored-by: Mervin Praison <[email protected]>
Gap 1: AutoGen v0.2 adapter now registers YAML-declared tools via
register_for_llm (assistant) and register_for_execution (user_proxy)
instead of silently discarding spec.tools. Non-callable/unnamed tools
are logged and skipped rather than dropped.
Gap 2: PraisonAIDB.close()/aclose() reset lifecycle state under the
init lock (idempotent, lock-safe), so a closed adapter cleanly
re-initializes on reuse instead of dispatching to a closed store.
Removed dead code trailer in on_run_start.
Gap 3: Instance-owned tool-timeout pool recycles once half its workers
leak to stuck sync tools, preventing permanent starvation. On timeout
the wrapper now raises ToolTimeoutError instead of returning a JSON
string, preserving the tool's declared return-type contract.
Co-authored-by: MervinPraison <[email protected]>
- test_kanban_host_injection: move pytest.importorskip('praisonai_bot._wrapper_bridge')
before importing the kanban_bridge shim. The shim eagerly runs
`import praisonai_bot...` at module load and ensure_praisonai_bot() never
raises, so a bot-tier-absent env previously errored instead of skipping.
- test_daemon_background: use popen.call_args.args[0] instead of
call_args[0][0] for clearer, signature-resilient assertion.
Co-authored-by: Mervin Praison <[email protected]>
Use pytest.importorskip for the private _wrapper_bridge module so the
integration test skips instead of erroring when praisonai_bot is not
installed, and add the missing trailing newline.
Co-authored-by: Mervin Praison <[email protected]>
Wire audit_hybrid_modules into smoke CI, assert jobs.server bridge resolution, and add a regression test that background daemon spawns praisonai_code.runtime.
Co-authored-by: Cursor <[email protected]>
The ^ anchors only matched top-level imports, silently passing
function-level (indented) `from praisonai_code` imports — the common
pattern in this codebase. Comment lines are still stripped by the
existing grep -v filter.
Co-authored-by: Mervin Praison <[email protected]>
Route host_app bridges through the wrapper bridge, correct jobs.server paths, lazy-load praisonai_code from bot via _code_bridge, and document or gate wrapper-required CLI paths with CI coverage.
Co-authored-by: Cursor <[email protected]>
UnifiedExecutionMixin is in the Agent MRO but no production path invokes
its methods; the live paths use chat_mixin/tool_execution methods with
similar names. Following the issue's non-breaking approach: emit a
DeprecationWarning from the public methods, note the deprecation in the
module/class docstrings and at the agent.py import. Hard removal from the
MRO is deferred to a later deprecation cycle to preserve the public method
names.
Co-authored-by: MervinPraison <[email protected]>
Wrap BEFORE_TOOL/AFTER_TOOL hook input construction in has_hooks()
guards to avoid a per-tool-call os.getcwd() syscall and dataclass
allocation when no hooks are registered. Behaviour is identical when
hooks exist; the AFTER_TOOL _last_normalized_result cleanup remains
unconditional.
Co-authored-by: MervinPraison <[email protected]>
- dependency_checker: cache list_available_sources() once (_resolver_sources)
so verifying N tools no longer triggers N full resolver scans
- tools_doctor: tighten fallback guard to `if "builtin"/"external" in buckets`
so TOOL_MAPPINGS/package fallback still fires when only non-relevant buckets
(e.g. "local") are populated
- cli/features/tools: apply the human-readable description overlay to
resolver-discovered tools via setdefault, restoring descriptions in
`tools list` instead of "No description"
Co-authored-by: Mervin Praison <[email protected]>
Defers the praisonai wrapper import and package_root computation into the
vision-model branch, the only path that needs a file path. The standard LLM
path uses `-m praisonai.train.llm.trainer` and no longer imports the wrapper
package on the common path. Addresses Greptile P2 review feedback.
Co-authored-by: Mervin Praison <[email protected]>
- executor.py: fix broken tick() docstring unpacking example (yields JobResult,
not a tuple); import inspect at module level (remove per-tick local import and
unused dataclasses.field); cast job.message to str to avoid TypeError; use
inspect.isawaitable for robust awaitable detection; offload blocking
_audit_output writes via asyncio.to_thread so the event loop is not stalled.
- condition_gate.py: on POSIX run the pre_run shell in a new session and kill the
whole process group on timeout so children are not orphaned; reap the process.
Co-authored-by: Mervin Praison <[email protected]>
Centralise tool-source enumeration on the canonical ToolResolver so the
template dependency checker, tools doctor, and CLI tools handler consult
the full local/builtin/external/registered resolution chain instead of a
partial subset. Install-hint tables and human descriptions are kept as a
presentation overlay; each site falls back to its legacy partial scan when
the resolver is unavailable.
Co-authored-by: MervinPraison <[email protected]>
Resolve wrapper training scripts via the praisonai wrapper package instead
of relative to the moved legacy dispatcher, run the standard LLM fine-tuner
through the canonical praisonai.train.llm.trainer module, and reduce the
stale top-level train.py to a thin re-export of the canonical trainer.
Co-authored-by: MervinPraison <[email protected]>
The storage/__init__.py note labeled the module "legacy" while
persistence.state.redis.RedisStateStore actively wraps and depends on
RedisStorageAdapter defined there — a direct contradiction. Clarify that
storage/ is the canonical low-level adapter layer and document why the
MongoDB/DynamoDB/Valkey state stores are not yet collapsed (differing
TTL/hash/counter contracts and DynamoDB's differing partition-key schema).
Co-authored-by: MervinPraison <[email protected]>
Move gateway scheduler executor to praisonai_bot with wrapper shims, compose
[claw] from praisonai-bot[gateway,bot], restore onboard CI coverage, and
document the four-tier architecture plus config-kernel substitute for Phase 0.
Point builtin platform loaders at praisonai_bot.bots so pip install
praisonai-bot works without the wrapper; extend CI smoke and boundary
tests, sync SDK docstrings, and map PYPI_TOKEN for uv publish.
find_packages(include=["praisonai_bot*"]) excluded
integration/bridges and integration/pages because they lacked
__init__.py, dropping kanban_bridge and bot_health from built wheels
(and breaking the praisonai.integration.* backward-compat shims on
installed, non-editable installs). pages/__init__.py guards the
optional bot_health import (praisonaiui) like the wrapper original.
Co-authored-by: Mervin Praison <[email protected]>
- Create praisonai_bot/cli/output/console.py providing get_output_controller.
Four bot CLI command modules (onboard, kanban, bot, gateway) imported
..output.console which never existed, breaking praisonai.bots / CLI
backward-compat shims at import time. The bridge lazily delegates to
praisonai-code's OutputController when co-installed (C9-compliant via
_code_bridge) and falls back to a minimal self-contained controller for
standalone praisonai-bot installs.
- Un-ignore src/praisonai-bot/praisonai_bot/cli/output/ (the bare `output`
.gitignore rule was excluding the new package dir; matches the existing
negations for the other packages).
- Fix wrong import paths in test_legacy_parse_args.py: dispatch_gateway /
argparse_builder live in praisonai.cli.legacy.dispatch (wrapper), not
praisonai_bot; correct ineffective mock (_wrapper_bridge -> _bot_bridge).
- Repoint daemon/gateway install tests to canonical praisonai_bot.daemon.* /
praisonai_code.cli._paths.* / praisonai_bot.cli.features.* targets so mocks
hit the module where the code actually lives (was silently missing).
- Make test_doctor_checks resilient to transient praisonai.__file__ nulling by
resolving the wrapper dir via __path__/find_spec, skip if wrapper absent.
praisonai-bot unit suite: 1096 passed, 10 skipped (was 11 failed).
C9 boundary + backward-compat: 47 passed. C9 import gates: 0 wrapper imports.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Split bots, gateway, channel CLI, and OS daemon into standalone praisonai-bot
package while preserving praisonai.* import paths via alias_package shims.
Adds four-tier PyPI publish workflow, CI gates, and migrated test suite.
Reviewer feedback (CodeRabbit, Greptile) flagged the original fix as partial:
error() and _render_command(cmd) were escaped, but the output arg plus
action(), list_items(), _render_summary(), narrative(), llm_indicator() and the
multi-agent prefix still interpolated raw dynamic text into Rich markup and
could raise MarkupError.
Add a shared _safe_markup() helper (safe_text + rich.markup.escape) and route
every rich-mode dynamic-text path through it, centralizing the protection.
Add regression tests for the remaining paths.
Co-authored-by: Mervin Praison <[email protected]>
Copilot is unreliable; proceed to @claude FINAL once CodeRabbit/Greptile
(or optional Qodo/Gemini wait) complete. Set REVIEW_CHAIN_SKIP_COPILOT=1.
Co-authored-by: Cursor <[email protected]>
Main's PermissionAllowlist now defaults to a durable, scoped SQLite store
(agent_id, tool_name, arg_signature) instead of the PR's original JSON
allow-list. Adapt the durable-approval tests to that merged API:
- isolate each manager's allowlist via a per-tmp_path allowlist_path so
tests don't touch (or leak into) the shared ~/.praisonai default store;
- use durable=False for the pure in-memory default cases;
- grant globally via scope_to_agent=False in the allow-always restart test
(the request has no agent identity, so a scoped grant is intentionally
skipped by the merged resolve()).
Pending-queue durability (store/rehydrate/_record_resolution) from this PR
is preserved unchanged. All 9 durable tests pass.
Co-authored-by: Mervin Praison <[email protected]>
- Prevent GC of fire-and-forget durable-resolve tasks by holding strong
refs in _bg_tasks and surfacing errors via a done callback (Greptile P1).
- Preserve original expiry on rehydrate() by deriving created_at from the
store's persisted expires_at, so a restart no longer extends an
almost-expired approval by a full TTL (Greptile/CodeRabbit P2).
- Mark durable rows 'expired' when in-memory entries are pruned so the
audit trail records the terminal state (Greptile/CodeRabbit P2).
- Replace flaky fixed sleep in tests with bounded polling / awaiting the
scheduled durable task; add tests for expiry preservation and prune.
Co-authored-by: Mervin Praison <[email protected]>
C8.4 moved AgentsGenerator loading to praison_ai.py; unit test mock target
must follow the implementation path for agent-file routing tests.
Co-authored-by: Cursor <[email protected]>
Back ExecApprovalManager with an optional ApprovalStoreProtocol store and a
persisted allow-always list so pending gateway approvals and allow_always
grants survive a restart. Persist on register()/resolve(), rehydrate
outstanding approvals in the gateway startup path, and keep the in-memory
zero-config default unchanged (opt-in via PRAISONAI_GATEWAY_DURABLE_APPROVALS).
Co-authored-by: MervinPraison <[email protected]>
- _snippet/_uri_to_display now reject server-returned paths outside the
workspace root (prevents reading files like /etc/shadow from a misbehaving
language server).
- _run_lsp transparently offloads to a worker thread when a loop is already
running, so tools work from async agent contexts instead of erroring.
- Flatten hierarchical DocumentSymbol.children so nested methods are surfaced.
- Send textDocument/didClose before shutdown (per LSP spec); centralized the
open/close lifecycle in the driver.
- Tests for containment, flattening, and outside-workspace refusal.
Co-authored-by: Mervin Praison <[email protected]>
Add built-in tools backed by the existing LSP client so a coding agent can
navigate code semantically (definition/references/hover/symbols) instead of
falling back to text grep:
- lsp_definition, lsp_references, lsp_hover
- lsp_document_symbols, lsp_workspace_symbols
Adds get_document_symbols/get_workspace_symbols to LSPClient and advertises
the corresponding capabilities. Tools are lazily imported, path-safe, and
degrade gracefully with a clear message when a language server is not
installed. Registered in TOOL_MAPPINGS for CLI/YAML/Python selection.
Co-authored-by: MervinPraison <[email protected]>
Addresses reviewer (Greptile) findings on PR #2623:
- server.py: call note_unauthorized() before should_log() so log-sampling
and count/suppressed values are accurate (was logging frame 2 spuriously).
- server.py: use distinct close code 4028 for unauthorized-frame flood so
clients can distinguish it from 4008 handshake rate-limiting.
- rate_limiter.py: exclude keys already in _lockouts from the overflow
fail-closed check so a returning client with an expired lockout can clear
its stale entry instead of being trapped until the next prune cycle.
- tests: add deterministic regression test for expired-lockout recovery
under saturation.
Co-authored-by: Mervin Praison <[email protected]>
Address reviewer (Greptile) findings on PR #2629:
- P1: threaded scheduled delivery built a throw-away DeliveryRouter per
call, so its idempotency LRU was always empty and a re-fired threaded
job could double-post. Now the shared router's public
is_duplicate_key/remember_key guard owns dedup while a one-off
thread-binding router (sharing the dead-target registry) handles the
actual send; the key is recorded only after a confirmed success.
- P2: removed private router._botos access — resolve the channel bot via
the new _resolve_channel_bot helper (case-insensitive), which also
de-duplicates the repeated fallback lookup.
- P2: _ThreadBindingBot now computes thread_id acceptance once in
__init__ instead of calling inspect.signature on every send.
Adds 2 regression tests: threaded re-fire is deduplicated, and a failed
threaded send stays retryable.
Co-authored-by: Mervin Praison <[email protected]>
- due.py: 'at' schedules now evaluate against the caller-supplied `now`
instead of wall-clock time, making one-shot jobs deterministic and
consistent with every/cron (CodeRabbit, Greptile).
- due.py: guard croniter parse/get_next so a single malformed cron
expression returns not-due instead of aborting the whole tick loop
(CodeRabbit).
- store.py: add/update/remove/remove_by_name now run under the same
cross-process `_file_lock()` with a reload, and `update` preserves an
active lease, so they can no longer clobber lease/last_run_at written
by a concurrent `claim_due` (CodeRabbit).
Co-authored-by: Mervin Praison <[email protected]>
Drop misleading "Safe" wording (execute_command runs arbitrary shell,
no sandbox) and document that apply_patch is the file-creation primitive
for new files while edit_file targets existing files.
Addresses Greptile review comments on PR #2614.
Co-authored-by: Mervin Praison <[email protected]>
Address reviewer feedback on PR #2609:
- Exclude bool from numeric tool_timeout guards (bool subclasses int, so
`tool_timeout: yes/true` no longer silently becomes a 1s cap on every tool)
- Add regression test for bool rejection (CLI + role level)
- Set self.logger in test _make_generator helper (avoids opaque AttributeError)
- Replace lambda assignment with named function (Ruff E731)
Co-authored-by: Mervin Praison <[email protected]>
- Iterate arun/achat independently so a sync arun no longer shadows an async achat
- Replace deprecated get_event_loop() with get_running_loop() in async context
- Add tests for achat-only and sync-arun/async-achat branches
Co-authored-by: Mervin Praison <[email protected]>
Detect Gemini in pull request reviews (not just issue comments), re-trigger
Copilot on Gemini review events, and advance the fallback/sweep to post @copilot
before @claude so PRs no longer stall on optional Gemini waits.
Co-authored-by: Cursor <[email protected]>
The gateway's scheduled-job and hook/webhook delivery paths called
bot.send_message() directly, bypassing the resilient DeliveryRouter used on
the interactive BotOS path. Route both through DeliveryRouter so they gain
per-platform token-bucket rate limiting, bounded-LRU idempotency dedup, and
self-healing dead-target suppression — preventing double-posts on
crash-and-resume, unthrottled 429 bursts, and forever-retries to dead chats.
- Add a thin _ChannelBotOS adapter over the gateway's {channel: bot} registry
(the router only needs get_bot/list_bots), with case-insensitive lookup.
- Lazily build a DeliveryRouter (sharing a DeadTargetRegistry) via a new
delivery_router property; falls back to the prior bare send if unavailable.
- Preserve threaded scheduled delivery via a _ThreadBindingBot proxy without
mutating the shared router.
- Derive a stable idempotency key from target + session + text digest so a
re-fired job/hook does not double-post; a failed send stays retryable.
Co-authored-by: MervinPraison <[email protected]>
Add an optional cross-process claim/lease primitive so a due job fires
at most once across tickers, processes and hosts.
- protocols.py: add optional `claim_due(now, owner_id, lease_seconds)`
and `complete(job_id, owner_id)` to ScheduleStoreProtocol.
- due.py: extract shared `is_due` used by runner and stores.
- store.py: implement `claim_due`/`complete` in FileScheduleStore under
an OS advisory file lock (fcntl/msvcrt); pre-advance `last_run_at` and
take a lease atomically; expired leases are reclaimable (crash-safe).
- models.py: round-trip lease metadata (lease_until/lease_owner).
- runner.py: add `claim_due_jobs`/`complete_run`/`supports_atomic_claim`
helpers; delegate due-check to shared `is_due`.
- executor.py: tick() atomically claims jobs when supported and releases
the lease after each run; falls back to non-atomic path otherwise.
Backward-compatible: new methods are optional and hasattr-guarded.
Co-authored-by: MervinPraison <[email protected]>
- PreauthConnectionBudget: cap concurrent unauthenticated WS connections per
source IP (default 32); acquired at accept, released on auth-success/close;
unresolved IPs share one bounded bucket; loopback exempt.
- UnauthorizedFloodGuard: per-connection counter that closes the socket after
N unauthorized frames (default 10) and log-samples the rest.
- AuthRateLimiter overflow fail-closed: reject new keys closed when the key
map is saturated while preserving existing lockouts.
- Declarative config: preauth_max_connections_per_ip / max_unauthorized_frames
on GatewayConfig (YAML + Python), with sane defaults.
Co-authored-by: MervinPraison <[email protected]>
Replace the gateway's in-memory Set[str] allow-list with a durable,
SQLite-backed ScopedAllowlistStore keyed by (agent_id, tool_name,
arg_signature). "Allow always" grants now survive gateway restart and
are scoped to the granting agent by default instead of leaking across
every agent/argument.
- Add ScopedAllowlistStore (WAL SQLite) mirroring bots/_approval_store.
- Rework PermissionAllowlist as a scoped, durable, fail-closed shim that
rehydrates persisted grants on startup while preserving the legacy
name-only API (add/remove/__contains__/list).
- Wire ExecApprovalManager.register/resolve to scoped, durable checks;
resolutions are scoped to the requesting agent by default (opt-in
widen via scope_to_agent=False, opt-in arg scoping via scope_to_args).
- Extend /api/approval/allow-list to accept optional agent_id and expose
scoped grants, backward-compatibly.
- Add tests for durability, scoping, arg-signature scoping and legacy API.
Co-authored-by: MervinPraison <[email protected]>
StatusOutput gained rich.markup.escape() in #2604 but EditorOutput still
interpolated dynamic error/tool text into Rich markup wrappers, crashing
the agent loop with MarkupError when messages contained sequences like
[/close]. Apply the same sanitisation to error() and _render_command, and
route the on_error callback through error() to avoid duplication.
Co-authored-by: Mervin Praison <[email protected]>
- Fix praisonai init hint to the working `praisonai run --agent/--command` form
- Route init, bare-TUI (app.py), chat, and setup implicit defaults through
resolve_default_model() so a user with only a non-OpenAI provider key is
defaulted to an appropriate model
- Add DEFAULT_FALLBACK_MODEL single source of truth in llm/env.py; resolver and
entry points reference it instead of re-declaring gpt-4o-mini
- Add grep-guard + resolver-behaviour tests
Co-authored-by: MervinPraison <[email protected]>
Add lazy TOOL_MAPPINGS entries for the diff-based file editor
(edit_file, apply_patch, create_edit_tools) and the agent-facing todo
tools (todo_add, todo_list, todo_update, create_todo_tools) so they
resolve by name from CLI (--tools edit_file), YAML (tools: [edit_file])
and Python, and appear in `praisonai tools list`.
Also add a curated `coding` prebuilt toolset that prefers the safe
diff-based editor over blunt write_file.
Co-authored-by: MervinPraison <[email protected]>
Gap #2: tool_timeout was accepted from CLI/YAML, written into every role,
then silently dropped -- _wrap_tool_with_timeout had zero call sites. Wire
the existing timeout stack into _build_tools_dict via a new
_resolve_effective_tool_timeout (CLI wins, else max declared per role/agent)
so the field actually enforces for every adapter.
Gap #1(c): remove ag2 from FrameworkAdapterRegistry.DEFAULT_PRIORITY. Its
adapter is an unimplemented stub not present in _BUILTIN_ADAPTERS, so
advertising it as a default dispatch target let pick_default route to a
NotImplementedError.
Co-authored-by: MervinPraison <[email protected]>
Dynamic message content (tool args like ['x'], error text containing
[/tag]) was interpolated into Rich markup and could raise MarkupError.
Escape via rich.markup.escape() after encoding sanitization.
Add high-fidelity strict cp1252 TextIOWrapper regression test plus a
MarkupError guard test (addresses CodeRabbit + Greptile review feedback).
Co-authored-by: Mervin Praison <[email protected]>
Add wrapper tui package init to merge app.py with praisonai-code widgets,
use relative timestamps in merge-gate conflict selftests, and skip PraisonAI
PR reviewer when GitHub App secrets are not configured.
Co-authored-by: Cursor <[email protected]>
The praisonai run LLM loop crashed on Windows cp1252 consoles because the
StatusOutput._emit_text Rich color branch (use_color=True, the production
default) called console.print() with raw U+25B8 (▸) / U+2192 (→) glyphs.
Rich's legacy Windows renderer encodes strictly with no fallback, so the
UnicodeEncodeError propagated to llm.py and aborted the run.
Prior fixes (#2563 plain-print branch, #2571 main.py display helpers) never
touched this path, and unit tests only exercised use_color=False.
Sanitize the message via the existing safe_text() helper before the Rich
print in both StatusOutput._emit_text and EditorOutput.llm_indicator, and
add tests that drive the production use_color=True path on cp1252.
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
- interactive/features __init__: drop fragile del _code_dir (NameError on
empty __path__) and narrow blanket except to ImportError so delegated
names (InteractiveConfig, WorkflowHandler, etc.) resolve reliably
- populate_from_module: install PEP 562 __getattr__ live proxy so wrapper
reassignments (test monkeypatching) stay visible while preserving
snapshot re-export of names like _LogCapture
Co-authored-by: Mervin Praison <[email protected]>
Replaces the process-wide PRAISONAI_ALLOW_LOCAL_TOOLS env mutation in
_load_user_module_safe (which leaked authorization across threads) with a
per-call skip_env_check kwarg on the canonical load_user_module. The gate
stays closed by default; explicit CLI --tools/--tools-dir loads authorize
themselves in-process without touching global state.
Addresses Greptile/CodeRabbit thread-safety findings on #2599.
Co-authored-by: Mervin Praison <[email protected]>
The custom-LLM self-reflection branch passed reflection_data as a positional
dict to the ReflectionOutput Pydantic model, which raises TypeError in
Pydantic v2 (BaseModel.__init__ takes no positional args). This caused
custom-LLM reflection to silently fail and retry every iteration. Unpack the
dict with ** so reflection actually works.
Co-authored-by: Mervin Praison <[email protected]>
Address Greptile review: the canonical praisonai_code._registry logger is
named 'praisonai_code._registry'. Re-exporting it directly would silently
break any log-level configuration targeting the historical 'praisonai._registry'
name. Create a local logger with the historical name instead.
Co-authored-by: Mervin Praison <[email protected]>
Allow Claude after Copilot timeout when prior reviewers finished, use
pull_request_target for fork secret access, and sweep open PRs for missing
FINAL @claude on schedule and workflow_dispatch.
Co-authored-by: Cursor <[email protected]>
When `Memory(config={"provider": "dakera"})` was constructed without the
optional `dakera` SDK installed, the registry caught the factory's
ImportError and returned None, and the explicit-provider retry path only
covered mem0/chroma/mongodb — so dakera silently fell through to the
sqlite/in_memory fallback instead of raising the helpful
`praisonaiagents[dakera]` install hint.
Add `dakera` to that factory map and cover the full `Memory` construction
path with a test that asserts the ImportError (with install hint) is
raised. (Greptile P1.)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Consolidate the duplicate file-loading path in
templates/tool_override.py onto the canonical
praisonai_code._safe_loader.load_user_module instead of a raw
importlib spec_load + exec_module. This removes the divergent
security gate (the wrapper previously exec'd user modules directly,
bypassing PRAISONAI_ALLOW_LOCAL_TOOLS) so there is one owner and one
trust model for user-module execution.
The wrapper's explicit-load contract (load_from_file/load_from_directory
work for user-provided paths, only remote URLs rejected) and the
implicit tools.py autoload opt-in (PRAISONAI_ALLOW_TEMPLATE_TOOLS) are
preserved as a thin wrapper-level policy layered on top of the canonical
loader. No call-site or return-shape changes.
Co-authored-by: MervinPraison <[email protected]>
Replace the 337-line drifted copy of PluginRegistry in the wrapper with a
re-export shim to praisonai_code._registry, mirroring the existing
tool_resolver/tool_registry shim pattern. This removes the duplicate base
class and the latent thread-safety race (the missing
`if self._loaders.get(canonical_name) is loader:` guard) that only existed
in the wrapper copy. Subclasses keep importing
`from praisonai._registry import PluginRegistry` unchanged.
Co-authored-by: MervinPraison <[email protected]>
The docs_runner package was a superseded implementation of the docs
code-execution feature. The shipped `praisonai docs` command is served
entirely by `praisonai.suite_runner`; docs_runner was imported nowhere
in product code and kept alive only by its own unit test.
Removes:
- src/praisonai/praisonai/docs_runner/ (7 modules)
- src/praisonai/tests/unit/cli/test_docs_runner.py (orphaned test)
CLI coverage for the shipped `docs` command remains via test_typer_cli.py
and test_cli_integration.py. No public API, CLI flag, YAML surface, or
Python import outside the removed package is affected.
Co-authored-by: MervinPraison <[email protected]>
Resolves reviewer-bot findings on the Dakera memory adapter:
- Strip reserved keys (session_id/tags) from the metadata copy
unconditionally so they never leak into the stored payload when the
same key is also passed via kwargs (the short-circuit `or` previously
skipped `meta.pop()` when the kwarg was truthy). kwargs still win.
- Chain the missing-dependency ImportError with `from exc` so the
original import failure is preserved for debugging (ruff B904).
- Include `agent_id` in the delete-failure warning for multi-tenant
traceability.
- tests: add a regression test for kwargs-vs-metadata reserved-key
precedence, and a gated real-SDK integration test that exercises a
live Dakera server (skipped unless DAKERA_URL is set).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Add praisonai-code wrapper re-export shims for modules moved to the wrapper
during C8, and update hybrid interactive/features package resolution so Core
Tests and Optimized Test Suite imports resolve correctly.
Adds a `dakera` memory provider that wraps the Dakera SDK to implement
`MemoryProtocol`, following the existing mem0/chroma/mongodb factory pattern.
Dakera is a self-hosted memory server offering decay-weighted vector recall
scoped by `agent_id`. Because Dakera has a first-class `memory_type` field,
the adapter maps PraisonAI's two tiers onto distinct Dakera types
(short-term -> `working`, long-term -> `episodic`), keeping scratch context
separate from durable knowledge.
- factories.py: `create_dakera_memory_adapter` factory + `DakeraMemoryAdapter`
(MemoryProtocol + DeletableMemoryProtocol + ResettableMemoryProtocol).
- adapters/__init__.py: register `dakera` factory (lazy-loaded).
- pyproject.toml: `dakera` optional extra (`dakera>=0.12.8`).
- tests: 18 unit tests with a fake `dakera` module (no server required).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Add pr-review-chain gating so @copilot waits for CodeRabbit and Greptile,
and @claude FINAL only posts after Copilot responds. Remove early Gemini
Claude trigger and cap stale-FINAL recovery via existing merge-gate guards.
Co-authored-by: Cursor <[email protected]>
- praisonai/adapters/__init__.py: restore lazy public exports
(AutoReader, ChromaVectorStore, BasicRetriever, FusionRetriever,
register_default_* etc.) alongside the new C8.5 ServeHandlerAdapter /
get_template_store helpers. Fixes Greptile P1 import-time regression for
downstream callers.
- serve.py: catch AttributeError alongside ImportError at every wrapper
bridge getattr() site so a missing export yields the friendly
"module not available" exit (4) instead of a raw traceback (Greptile P2).
Co-authored-by: Mervin Praison <[email protected]>
In a standalone praisonai-code install (no wrapper), LazyCommandGroup.list_commands
unconditionally advertised `app`, `standardise` and `standardize` even though their
implementations were repatriated to praisonai.cli.commands.* (C8.2). This surfaced
dead commands in `--help` whose get_command() returns None without the wrapper.
Gate those additions behind wrapper_available(), consistent with the rest of
_WRAPPER_RESIDENT_COMMANDS. Full installs are unaffected (all commands still
advertised). get_command_names() is left unchanged as it only runs in the wrapper
dispatch context where these commands always resolve.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Repatriate wrapper-heavy CLI commands and features to praisonai, route via
_WRAPPER_RESIDENT_COMMANDS, and normalise remaining touchpoints through
_wrapper_bridge so the standalone hot path stays PyPI-independent.
The "<prefix> *" bare-match fallback in PersistentApproval.matches
previously applied to any shell pattern ending in " *", including
user-authored globs like "bash:rm *". That silently changed fnmatch
semantics so bare "bash:rm" matched when it did not before.
Add a `derived` flag (persisted in to/from_dict) set only when the
manager auto-generalises a target into a reusable prefix glob. The
bare-prefix fallback is now gated on it, so explicit/user-authored
patterns keep exact fnmatch behaviour. Resolves the remaining Greptile
P1 (rules.py:241).
Co-authored-by: Mervin Praison <[email protected]>
Address reviewer (Greptile) security findings on reusable approval scopes:
- arity.py: a bare single-token command (e.g. `bash:git`) is now kept
literal instead of becoming `bash:git *`, honouring the documented
conservative contract (never auto-approve arbitrary subcommands).
- arity.py: commands containing shell control operators (&&, ||, |, ;,
&, $(), backtick, >, <, newline) are no longer generalised, so a
second command cannot be swallowed into the reusable glob tail.
- rules.py: the "<prefix> *" bare-match fallback is scoped to
bash:/shell: patterns only, so user-authored non-shell patterns
(e.g. read:/secret *) retain exact fnmatch semantics.
Adds tests covering bare-command, compound-command, and non-shell
bare-prefix scoping. Full permissions suite passes (103 tests).
Co-authored-by: Mervin Praison <[email protected]>
Add a command-arity table (permissions/arity.py) so a persistent
"always"/"session" shell approval can record a reusable prefix glob
(e.g. `git status` -> `bash:git status *`, `npm run build` -> `bash:npm run *`)
instead of the literal command, reducing approval fatigue. Opt-in via
`PermissionManager.approve(reusable_scope=True)`; default stays literal for
full backward compatibility. `suggest_scope_pattern()` surfaces the derived
scope so CLI/UI can display and override it.
Co-authored-by: MervinPraison <[email protected]>
Address reviewer findings on the MESSAGE_RECEIVED inbound gate:
- Redaction field mismatch (Greptile P1): HookResult exposes modified_input,
not modified_data, so redacted content was silently dropped. Read
modified_input (with modified_data fallback) via a shared helper. Applied to
both inbound and outbound (fire_message_sending had the same latent bug).
- Async fail-open (Greptile P1): execute_sync raises inside a running loop, so
gates fired from async handlers fell through to pass-through. Add
_run_hooks_blocking that runs the async execute on a worker thread when a
loop is running, so deny/redact decisions are honoured (no fail-open).
- Slack ignored rewritten content (CodeRabbit): route command parsing and
agent dispatch through the gated content instead of the raw Slack event.
- Tests: fix fake to real modified_input shape, add real-HookRunner redaction
and deny tests, and a running-loop test proving the gate does not fail open.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Address reviewer feedback on PR #2588:
- _registry.py: skip praisonai.channels entry points whose name collides with
an existing built-in/registered loader (Greptile P1); log discovery failures
with exc_info for diagnosability (CodeRabbit).
- _config_schema.py: reuse _BUILTIN_PLATFORMS as fallback single source of
truth and emit a debug signal on fallback (CodeRabbit).
- gateway.py: chain exception on registry load failure (CodeRabbit).
- Add tests/unit/bots/test_channel_registry.py covering new platform
registration, built-in override protection, and non-fatal discovery failure.
Co-authored-by: Mervin Praison <[email protected]>
- Point ToolResolver at the config directory's tools.py (Path(path).parent)
so config-local tools resolve regardless of process cwd (CodeRabbit).
- Preserve original tool list order when mixing string names and callables
instead of resolving all strings first then appending callables (Greptile).
- Import ToolResolver via the praisonai.tool_resolver shim so the
praisonai_code bootstrap runs in lean installs; update regression test
to match (Greptile).
Co-authored-by: Mervin Praison <[email protected]>
Make the inbound message pipeline a real control point, symmetric with the
outbound fire_message_sending. fire_message_received now returns a decision
dict ({"content", "drop"}): a hook/plugin can deny to drop an incoming
message before it reaches the agent, or return modified_data["content"] to
rewrite (e.g. PII redaction) it. All adapters (telegram, slack, discord,
whatsapp, email, agentmail) skip agent dispatch when drop is set. The core
hook contract is unchanged; this is a wrapper-only, backward-compatible fix.
Co-authored-by: MervinPraison <[email protected]>
- Registry now also scans the idiomatic `praisonai.channels` entry-point
group so a pip-installed connector self-registers with zero code.
- Config validator sources valid platforms from the registry
(`list_platforms()`) instead of a hardcoded set, so a channel registered
via `register_platform()` or an entry point is accepted in gateway.yaml.
- `praisonai gateway channels --available` lists all registered platforms.
Co-authored-by: MervinPraison <[email protected]>
- BotOS.from_config now resolves tool names via the shared ToolResolver
(local tools.py -> wrapper registry -> praisonaiagents -> praisonai-tools
-> plugins) instead of an ad-hoc importlib lookup against
praisonaiagents.tools, so custom/local/praisonai-tools/plugin tools work.
- GatewayConfigSchema now models the top-level gateway:/hooks: blocks the
running server reads, so a real gateway.yaml validates through one schema.
- Add regression tests for both.
Co-authored-by: MervinPraison <[email protected]>
Registries sharing one DeliveryControlStore must agree on ttl_seconds /
max_size because the dead-target TTL/bound sweeps are table-wide. Add
register_dead_config() to record the first registry's bounds and reject a
second registry attaching with divergent settings, turning the previously
documented-only invariant into an enforced runtime guard.
Co-authored-by: Mervin Praison <[email protected]>
A duplicate idempotency_key made DeliveryRouter.deliver() return True without
sending text, but BotOutboundMessenger.send() then continued into media upload,
so a re-fired proactive send with attachments could double-upload media.
The messenger now owns the whole text+media envelope: it checks for a duplicate
up-front (skipping both text and media) via the new public
DeliveryRouter.is_duplicate_key/remember_key, and records the key only after the
full envelope succeeds so a failed send stays retryable.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Addresses reviewer feedback on PR #2582:
- Telegram callback approvals now verify chat.id (not just message_id) so a
button with a colliding message_id in a different chat cannot resolve the
request; falls back to message-only match when no chat id is present
(backward compatible with existing payloads/tests).
- normalize_approvers now splits bare comma-separated strings and trims
whitespace, so allowed_approvers="999,111" is not treated as a char set.
- Added regression tests: cross-chat callback ignored + comma-string normalize.
Co-authored-by: Mervin Praison <[email protected]>
An adapter returning explicit False signals a failed delivery without
raising. Previously deliver() cached the idempotency key and cleared the
dead-target flag for such a send, suppressing legitimate retries. Normalise
it into a transient DeliveryNotAccepted so it flows through the existing
failure path (key not recorded, dead target untouched). None/void stays a
success signal to avoid regressing the dominant adapter convention.
Also isolate the dedup/retry tests from the real RateLimiter via a no-op
limiter stub in the test helper (CodeRabbit nitpick).
Co-authored-by: Mervin Praison <[email protected]>
Delivery-control state (rate-limit token bucket, penalty windows, dead-target
registry) was per-process in-memory / whole-file JSON, so multi-worker gateways
each kept their own bucket and exceeded platform rate limits (~N x ceiling).
Add an opt-in SQLite-backed DeliveryControlStore that both RateLimiter and
DeadTargetRegistry can share, matching the SQLite-first design already used by
the outbox/DLQ/journal. Atomic BEGIN IMMEDIATE token reservation means all
workers draw from one bucket; dead targets become atomic row upserts that
survive restart. Single-process deployments keep the fast in-memory/JSON path
by default (store is opt-in via a new store=/scope= param).
Co-authored-by: MervinPraison <[email protected]>
Wire the workspace-boundary gate into PermissionManager.check() for
file-mutating tool prefixes so a broad write_file:*/edit:* approval no
longer silently permits writes outside workspace_root. Out-of-workspace
paths emit an external_dir: ask (deny rules still win); in-workspace and
no-workspace_root behaviour is unchanged.
Closes the last remaining Greptile P1 (file tools bypass gate).
Co-authored-by: Mervin Praison <[email protected]>
Chat-native approval backends (Telegram inline buttons, Slack/Discord
keyword replies) resolved a decision from whoever responded, with no
allowed-approver check — so any member of a shared group could approve a
gated tool, bypassing the human-in-the-loop guardrail.
Add an optional allowed_approvers allowlist to TelegramApproval,
SlackApproval and DiscordApproval (also honoured via TELEGRAM_APPROVERS /
SLACK_APPROVERS / DISCORD_APPROVERS env vars). When set, an approve/deny
from a user not in the allowlist is ignored so the tool stays gated;
Telegram additionally shows the presser a "not authorized" alert. When
unset, legacy "any responder" behaviour is preserved (backward compatible).
Adds shared normalize_approvers()/is_authorized_actor() helpers and tests
covering authorized, unauthorized and no-allowlist paths for all three
channels.
Co-authored-by: MervinPraison <[email protected]>
Route the proactive path (DeliveryRouter.deliver) through the same
rate limiter and idempotency guard the reply path already uses so
scheduled/background sends can't trip platform 429s or double-post on
retry. Adapters already provide outbox/retry/DLQ via send_message;
this closes the remaining rate-limit and dedup gaps.
Co-authored-by: MervinPraison <[email protected]>
Addresses CodeRabbit nitpick on PR #2577:
- Use _completion_with_retry / _acompletion_with_retry for the bounded
finalisation call so it gets the same rate-limit/failover resilience as
every other completion path (avoids a transient 429 dropping straight to
the placeholder fallback).
- Extract the duplicated wrap-up prompt and step-limit fallback string into
STEP_LIMIT_WRAP_UP_PROMPT / STEP_LIMIT_FALLBACK_MESSAGE class constants,
matching the existing OLLAMA_*_PROMPT pattern.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Closes the remaining Greptile P1 bypass where a path operand joined to a
short getopt flag (e.g. `curl -o/tmp/outside`) skipped the external_dir
boundary check. Adds _extract_flag_path() to unwrap the attached value and
two regression tests.
Co-authored-by: Mervin Praison <[email protected]>
Prevents web_fetch/Claude-memory tools leaking into the tools-disabled
wrap-up call (tool_choice=none + tools is invalid). Also drops the now
meaningless tool_choice when no tools remain.
Co-authored-by: Mervin Praison <[email protected]>
Addresses CodeRabbit nitpick: parameterize the console builder by encoding
so the UTF-8 regression test reuses the shared helper instead of
re-implementing the TextIOWrapper/Console setup inline.
Co-authored-by: Mervin Praison <[email protected]>
On reaching max_iter, the tool-calling loop now performs one bounded
LLM call with tools disabled (tool_choice="none") and a wrap-up nudge to
summarise what was accomplished, what remains, and next steps, instead of
returning canned placeholder text. Falls back to the previous placeholder
if the extra call fails, so behaviour never regresses. Applied to the sync
responses/chat-completions paths and both async paths.
Co-authored-by: MervinPraison <[email protected]>
Addresses Greptile review findings on PR #2571:
- _sanitize_renderable now rebuilds Markdown renderables from safe_text()-
sanitized markup so Panel.fit(Markdown(...)) LLM responses are no longer
dropped on cp1252 consoles.
- Add _renderable_to_plain() so the last-resort fallback extracts real
text (str/Text/Markdown/Panel) instead of str(Panel).
- Route the final plain print() to the console's own file stream instead
of process-wide sys.stdout, preserving captured/redirected output.
- Add 3 tests covering the markdown response path, Markdown sanitization,
and console-stream fallback routing.
Co-authored-by: Mervin Praison <[email protected]>
Add a distinct workspace-boundary guardrail to the core permission engine.
When a PermissionManager is configured with a workspace_root, shell write
targets and path-like args (plus a file-tool helper) that resolve outside
the root now emit a dedicated external_dir:<parent>/* sub-target defaulting
to ask, reusing the shared path_safety containment logic. Safe-by-default
and fully backward compatible: with no workspace_root, behaviour is
unchanged.
Co-authored-by: MervinPraison <[email protected]>
Addresses reviewer (greptile) findings on PR #2567:
- call.py: wrap send_to_twilio per-event processing in try/except so a
malformed OpenAI message or handle_response_done error is logged and the
stream continues, instead of escaping into asyncio.gather() and cancelling
the sibling Twilio task (dropping the live call). gather stays inside the
openai_ws async-with (Gap 2 fix preserved).
- base.py: add best-effort __del__ that shuts down the per-adapter offload
ThreadPoolExecutor so idle worker threads do not outlive the adapter in
long-lived services that forget to call cleanup().
Co-authored-by: Mervin Praison <[email protected]>
praisonai run crashed on Windows cp1252 consoles because the LLM loop routes
through main.py Rich display helpers (display_tool_call, display_error,
display_interaction) which emit decorative glyphs (▸ U+25B8, ⚠ U+26A0) via
Rich Console.print() with strict encoding and no fallback. PR #2563 only
fixed the StatusOutput/TraceOutput plain-print paths.
Add a centralized _safe_console_print() helper that catches UnicodeEncodeError
and retries with safe_text()-sanitized renderables (str/Text/Panel), falling
back to a plain ASCII print as last resort. All display_* console.print sites
now route through it, so run degrades gracefully instead of crashing while
preserving Unicode glyphs on UTF-8 consoles.
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
The exit-code guard in AsyncTUI._execute_prompt used `not response`, which is
always False for a truthy AutonomyResult(output='') returned by agent.astart()
on auth failure, so `praisonai chat` exited 0 on an invalid API key.
- Add _extract_response_output()/_is_empty_agent_response() helpers that inspect
the wrapper's `output` attribute instead of object truthiness.
- Gate the auth-error guard on empty output and surface AutonomyResult.error /
success=False as failures.
- Add tests mirroring the production astart()->AutonomyResult path.
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
Gap 1: Wrapper cli_backend resolver now checks execute()/stream() (the real
CliBackendProtocol methods) instead of the nonexistent process_turn. Adds a
single shared _is_cli_backend_instance() predicate so the check lives in one
place; factory callables now flow through resolve_cli_backend_config.
Gap 2: Re-indent send_to_twilio + asyncio.gather back inside the
`async with openai_ws:` block in api/call.py so the OpenAI Realtime bridge
runs on an open socket. Swaps the swallowing bare-except for logger.exception.
Gap 3: Add opt-in SUPPORTS_ASYNC flag + bounded per-adapter ThreadPoolExecutor
in BaseFrameworkAdapter.arun. Sync-only adapters (CrewAI, AutoGen v0.2) declare
SUPPORTS_ASYNC=False and emit a warning; PraisonAIAdapter declares True.
Co-authored-by: MervinPraison <[email protected]>
Addresses CodeRabbit nitpick — test explicitly registers via _add_loader,
so the name now matches the behaviour under test.
Co-authored-by: Mervin Praison <[email protected]>
Address P1 review feedback (Gemini + Greptile): when a _ClientConn is
registered from a synchronous context, _ClientConn.start() returns early
because no event loop is running, leaving _task as None. Frames later
offered on a running loop would then queue with no drainer, causing silent
message loss and spurious slow-consumer eviction.
offer() now lazily calls start() when _task is None; since offer() always
runs on the event loop in production, this guarantees queued frames are
drained. Adds a regression test in test_gateway_gaps.py.
Co-authored-by: Mervin Praison <[email protected]>
Align CLI tests with C7 approval bridge and Code version branding,
update BotOS drain expectations for reliability presets, make gateway
client registration sync-safe without an event loop, scope workflow
caplog to the correct logger, and inspect serve Typer options directly.
- Remove overly-broad auth signatures (permission denied, access denied,
unauthorized, http 403, bare 401) that could misattribute unrelated
warnings as auth failures (Greptile P2 / CodeRabbit).
- Drop the broad stderr scan; rely on scoped log-record matching gated on
an empty response.
- Profiled chat path: wrap agent.start() so profiler.stop()/print_report()
always run, and no longer treat a successful empty response as a failure
(Greptile P1 / CodeRabbit).
Co-authored-by: Mervin Praison <[email protected]>
Addresses CodeRabbit review: prevent AttributeError when None/non-str is passed
to the print path, and cover StatusOutput.output() (via sys.stdout) and
TraceOutput.response() fallback paths plus UTF-8 preservation.
Co-authored-by: Mervin Praison <[email protected]>
praisonai chat previously returned exit code 0 even when the LLM call
failed with an authentication error (e.g. invalid OPENAI_API_KEY),
because AsyncTUI._execute_prompt swallowed the error into a string and
the CLI always exited 0.
Track a failure flag on AsyncTUI (raised exceptions and logged-then-
swallowed auth errors) and raise typer.Exit(1) from the chat command so
shell scripts and CI can detect authentication failures.
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
Add shared praisonaiagents/output/encoding.py helper (can_encode,
safe_symbol, safe_text) mirroring the doctor formatter's
_can_encode_unicode fallback. Sanitize the non-Rich print fallback paths
in status.py and trace.py so decorative glyphs (▸ → ✓ ✗ │ └ 📊) degrade to
ASCII instead of raising UnicodeEncodeError on Windows default cp1252
consoles.
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
The smoke step does `cd src/praisonai-code` before invoking
`bash scripts/check_c7_imports.sh`, so the repo-root-relative path
does not resolve from that CWD and bash exits 127. Use
"$GITHUB_WORKSPACE/scripts/check_c7_imports.sh" so the gate runs
regardless of working directory.
Co-authored-by: Mervin Praison <[email protected]>
optional_wrapper_attr now also catches AttributeError so recipe
creation (and any other optional wrapper lookup) falls back to the
default when an installed praisonai module is missing the requested
constant, not just when the wrapper is absent.
Co-authored-by: Mervin Praison <[email protected]>
- check_c7_imports.sh: enforce the allowlist (flag any non-allowlisted
praisonai-code file carrying wrapper imports) instead of only printing
its presence (Greptile P1).
- c7_wrapper_import_allowlist.txt: add managed.py, commands/app.py,
performance_checks.py (pre-existing lazy/guarded wrapper imports) so
enforcement passes at 47/47.
- Consolidate duplicated _bots_config_schema() into _wrapper_checks.py
(bots_config_schema); import as alias in bot_checks/gateway_checks
to avoid drift (CodeRabbit).
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
- check_c7_imports.sh: use POSIX whitespace and match bare
`from praisonai import X` (was undercounting); robust self-location
via BASH_SOURCE; raise regression baseline to accurate 225.
- app.py: route AgentOS through _wrapper_bridge to satisfy strict gate.
- main.py: guard both _fw_validators_module() calls and the capabilities
dispatch against ImportError so standalone praisonai-code fails cleanly.
- recipe_creator.py: load praisonai.auto constants via optional_wrapper_attr
so recipe creation degrades gracefully without the wrapper.
- C7.1_BOUNDARIES.md: dedupe heading, document new baseline/regex.
Co-authored-by: Mervin Praison <[email protected]>
The regression-baseline step ran `rg` under `set -euo pipefail`, so on
runners/subprocesses without ripgrep on PATH the pipeline died with exit 127
before printing the count — failing the PR's own test_c7_import_gate_script_passes
and the new CI smoke gate. Prefer rg when present, fall back to portable
grep -r, and guard the no-match case against pipefail.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Document three-tier ownership, route optional wrapper imports through
_wrapper_bridge, tighten the C7 import gate baseline to 211, and add
boundary tests plus doctor skip helpers for standalone praisonai-code.
Address reviewer feedback on PR #2557:
- Greptile P1 / CodeRabbit Major: ThreadPoolExecutor workers are joined by
its internal atexit handler at interpreter shutdown even after
shutdown(wait=False), so a hanging import could still stall CLI exit.
Replaced the pool with per-package daemon threads that are abandoned on
timeout and never block process termination.
- Greptile P2: non-ImportError failures (broken installs, e.g. broken C
extension) were masked as "missing". They are now reported in a separate
"broken" bucket, surfaced as WARN with a reinstall remediation, instead of
telling users to install an already-installed package.
- Added regression test for the broken-install case.
Co-authored-by: Mervin Praison <[email protected]>
praisonai doctor env --deep timed out when a single optional import (e.g.
chromadb/crawl4ai) hung, tripping the engine-wide 10s ThreadPoolExecutor
budget. check_optional_deps now probes each optional package concurrently
with an individual per-package timeout and abandons hanging import threads
via shutdown(wait=False), reporting slow imports as "slow/skipped" instead
of failing the whole check.
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
Add an invoke_without_command Typer callback so `praisonai doctor` with no
subcommand runs the comprehensive setup report instead of failing with
"Missing command". Adds --quick (env-only alias), --live, --deep, --strict
and --json passthrough options. `doctor env` stays fast/backward-compatible.
Add a numbered "Next steps:" footer to the text formatter listing remediations
for all warnings/failures (Hermes-style actionability), shown only when issues
exist and suppressed in --quiet mode.
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
Register a `fix` subcommand on the doctor Typer app that wires into the
existing DoctorHandler fix logic. Dry-run by default with --execute to
apply, matching the runtime/cleanup patterns. Resolves the
"No such command 'fix'" error.
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
vector-store.mdx is a dedicated feature page; DOC_FEATURE_CATEGORY_ALIASES
was folding it into Knowledge and reporting Vector Store as undocumented.
Co-authored-by: Cursor <[email protected]>
Add scripts/check_c7_imports.sh with strict hot-path and regression baseline;
extend CI smoke to run it and verify standalone run does not hit wrapper imports.
- tool_resolver.resolve_all_tools: use self._tools_py_path (not cwd) for
tools.py-vs-tools/ mutual exclusion so explicit/custom paths are honored (CodeRabbit)
- run.py: --approval help text now lists plan/accept-edits/bypass/secure/presentation
to match _approval_bridge backend list (CodeRabbit)
- tool_resolver.list_available: log praisonai-tools discovery failures instead of
silent pass (CodeRabbit S110)
Co-authored-by: Mervin Praison <[email protected]>
For explicit/config models, inject_credentials_into_env() only exports the
API key (not the stored base_url) for non-OpenAI providers, and the resolver
previously skipped the credential-store fallback entirely once the key was in
env. A stored OpenRouter/Groq/Gemini credential with a custom base_url could
pass the gate but send requests to the provider default URL.
Now recover the stored base_url (never overriding the user-requested model)
for explicit models, scoped to the resolved provider; env/config base_url still
wins. Inferred defaults keep adopting the stored model as before.
Co-authored-by: Mervin Praison <[email protected]>
- Set supports_streaming=False on o1/o1-mini fallback entries so
`models describe` reflects their "No streaming" notes (CodeRabbit)
- describe_model now matches provider-qualified ids (e.g. groq/llama...)
against bare fallback ids when litellm is unavailable (CodeRabbit)
Co-authored-by: Mervin Praison <[email protected]>
Route build_config_list through the credential-store aware resolver so a
stored credential's own model/base_url (e.g. a saved OpenRouter endpoint)
is used after inject_credentials_into_env exports only the API key. Falls
back to the env-only resolver if the credential bridge is unavailable;
environment variables still take precedence inside the resolver.
Addresses Greptile P1 (config.py:12 — stored endpoint dropped).
Co-authored-by: Mervin Praison <[email protected]>
When inject_credentials_into_env() exports a stored provider key (e.g.
OPENROUTER_API_KEY) into the environment, resolve_llm_endpoint saw the key
already set and skipped the credential-store fallback, so a stored credential's
custom model/base_url were dropped in favour of the hard-coded provider default.
For inferred (zero-config) default models only, consult the stored credential
of the resolved provider to recover its saved model/base_url. Explicit
MODEL_NAME/config models and env base_url still take precedence, so no
cross-provider mixing occurs. Addresses Greptile finding at credentials.py:88.
Co-authored-by: Mervin Praison <[email protected]>
- catalogue.py: use 'is not None' so zero-cost (free/local) models keep 0.0 cost instead of falling back to None (Gemini)
- catalogue.py: validate_model now does a single describe_model lookup instead of is_valid_model + describe_model double pass (Gemini)
- llm/__init__.py: bind all names advertised in __all__ so 'from praisonai_code.llm import *' and named imports resolve (Greptile)
Co-authored-by: Mervin Praison <[email protected]>
- Bind __all__ names in praisonai_code.llm/__init__.py so direct/star imports work
- Add OpenRouter to credential gate (env mapping, model-key vars, stored-provider
map, known_keys) and to env.py provider defaults + fallback scan
- Align _provider_from_model with is_configured for bare claude/gemini model ids
- Scope stored-credential fallback to the resolved provider for explicit/config
models; only scan all providers for inferred defaults
- Inject stored credentials before is_configured gate in app.py/run.py
- Guard None from import_wrapper_module in observability setup (standalone mode)
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Model listing and AutoGen config_list building no longer import the wrapper;
shims preserve praisonai.llm.* module identity for backward compatibility.
Credential checks and endpoint resolution no longer import the wrapper; shims
preserve praisonai.llm.* module identity and CI smoke verifies standalone import.
- cli_backends/registry: only rewrite ValueError for genuinely unknown IDs;
let real loader/constructor failures propagate with actionable messages
- kanban/sqlite_store: reuse _get_task_with_conn to remove duplicated inline
SELECT/Task.from_dict in update_task
- test_query_rewriter_cli: assert the rewritten prompt actually reaches the agent
- test_approval_interactive: autouse fixture resets callback + approval context
to remove test order-dependence
- test_security_fixes_core: assert second caller wakes strictly later (not equal)
- test_serve_unified: re-scope gateway docstring test to the unified-config
contract instead of a removed deprecation notice
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
- register_cli_backend now registers the factory directly instead of wrapping
it in a loader, so registry.create() returns a backend instance (not the
factory function). Fixes TypeError on backend.config for custom backends.
- resolve() guards the post-load cache write against concurrent
unregister/re-register, preventing resurrection of stale plugin classes.
- create_lazy_getattr captures the defining module name once at build time
instead of inspecting the call stack on every failed lookup (correct
attribution + no per-miss frame inspection).
Co-authored-by: Mervin Praison <[email protected]>
Wire post-C6 legacy suite into extended/nightly CI, add praisonai-code console
script and version helpers so the code package runs without the wrapper for
agentic commands while wrapper-only features stay behind optional imports.
Bring the 27 re-enabled legacy unit files to 401 passed (34 intentional skips) by updating tests for current APIs and fixing a kanban CAS read-before-commit bug. Vendors PluginRegistry into praisonai-code and resolves runtime version from the code package as the first C7 step toward standalone installs.
- profiler: only stop tracemalloc if this call started it (memory/memory_snapshot)
- profiler: add streaming_async async CM + ProfilerCompat delegate
- profiler: wrap export_json in {"summary": ...} for report-shape consumers
- bump_and_release: prioritize targeted upgrade over frozen; flow code_version
through validate_dependencies for --code/--code-pin; align use_frozen w/ release
- C6_VERIFICATION: state package-root cwd for pytest commands
Co-authored-by: Mervin Praison <[email protected]>
Resolve conflict in praisonai_code/cli/app.py. main already shipped a
fix for the same #2512 bug via a `_WRAPPER_COMMANDS` frozenset, which is
more robust than this branch's `_BOT_COMMANDS` registry (single source of
truth + graceful degradation when `praisonai` isn't installed).
- Adopt main's `_WRAPPER_COMMANDS` approach; drop redundant `_BOT_COMMANDS`
- Restore all 8 bot entries in `_LAZY_COMMANDS` so `--help` advertises them
- `get_command()` re-routes wrapper commands to absolute
`praisonai.cli.commands.*` paths at invocation time
- Update TestBotCommandRouting to the merged design; fix count docstring
(nine -> eight) and use immutable ClassVar frozenset (RUF012)
- Add regression test asserting wrapper commands import via absolute path
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
C6 integration gate found the 8 bot/channel commands (gateway, bot,
onboard, pairing, identity, kanban, claw, dashboard) were registered in
praisonai_code's _LAZY_COMMANDS with relative paths (.commands.gateway),
resolving to praisonai_code.cli.commands.* — but those files correctly
stayed in the main praisonai package. Result: praisonai gateway/bot/... all
raised ModuleNotFoundError.
Add a dedicated _BOT_COMMANDS registry with absolute praisonai.cli.commands.*
paths, imported lazily at invocation time so praisonai_code keeps no static
dependency on praisonai. Wire it into get_command(), list_commands() and
get_command_names(). Add TestBotCommandRouting regression guards.
Co-authored-by: Mervin Praison <[email protected]>
- bump_version.py / bump_and_release.py: add (?m) anchor so `--code` matches
the `[project]` version line (was silently no-op, leaving code pyproject stale)
- praisonai-code cli/app.py: hide wrapper-only commands from list_commands()
when the praisonai wrapper is absent, and reuse _wrapper_available() helper
Co-authored-by: Mervin Praison <[email protected]>
Removes the speculative astart(task) probe that caused double execution,
swallowed real TypeErrors, and re-materialised a dynamic astart child on
unspecced Mock agents. A genuine async entry point is now detected purely
via inspect.iscoroutinefunction, which is True for real async def methods
and AsyncMock, correctly routing sync Mock agents to the start() fallback.
Addresses review feedback from Greptile, CodeRabbit, Gemini, and Copilot.
Co-authored-by: Mervin Praison <[email protected]>
Route bot/gateway and other wrapper-local lazy commands through
praisonai.cli.commands, add missing C5 schema module shims, extend
backward-compat tests, and run C5 plus standalone praisonai-code checks
in the optimised smoke workflow.
Sync __version__ with package metadata, declare toml for config resolver,
remove duplicate praisonai-code dependency in the wrapper, and add C5
backward-compatibility regression tests.
- Bootstrap praisonai_code from sibling monorepo path when not installed
- Delegate python -m praisonai.cli.main via runpy to the moved module
- Revert test PYTHONPATH to historical agents+praisonai layout only
Co-authored-by: Cursor <[email protected]>
- Detect Rich via direct import in praisonai-code output controller
- Keep config.schema.json at praisonai.cli.configuration shim path
- Assert setup errors from stderr in missing-args test
- Un-ignore praisonai-code cli/output in .gitignore
Co-authored-by: Cursor <[email protected]>
Anchor package paths to GITHUB_WORKSPACE so cd into praisonai-agents
does not break subsequent installs of praisonai-code and praisonai.
Co-authored-by: Cursor <[email protected]>
Add a composite action that installs praisonai-agents, praisonai-code,
and the praisonai wrapper in order so CI no longer fails looking for
praisonai-code on PyPI during the extraction migration.
Co-authored-by: Cursor <[email protected]>
Use a static version in pyproject.toml instead of dynamic setuptools
resolution so the extracted package stays at 0.0.0 during migration.
Co-authored-by: Cursor <[email protected]>
Remove leftover conflict markers from pyproject.toml and praisonai_code/cli/__init__.py
that broke pip install and pytest collection on main.
Co-authored-by: Cursor <[email protected]>
main.py:4418 imports load_config from praisonai.cli.configuration.loader,
but only get_config existed. The import was wrapped in try/except, silently
swallowing the ImportError so auto-rules config (rules.auto / rules.max_chars)
was never honored. Add load_config as an alias for get_config and export it.
Note: The full 26-file move to praisonai_code/cli/ is deferred — it depends on
C2/C3/C4 (which create praisonai_code/cli/commands and features); those are not
present on this branch, so moving now would break CLI imports.
Co-authored-by: praisonai-triage-agent[bot] <272766704+praisonai-triage-agent[bot]@users.noreply.github.com>
Co-authored-by: MervinPraison <[email protected]>
Move the 80 agentic command modules from praisonai/cli/commands to the
new praisonai_code/cli/commands package, keeping the 9 bot-channel
commands (gateway, bot, onboard, pairing, identity, kanban, mint_link,
claw, dashboard) in main.
Because C0/C2 sibling packages (output/state/features/configuration/
utils) still live in praisonai.cli, moved modules' relative imports are
rewritten to absolute praisonai.cli.* / praisonai.* references. serve.py
keeps a lazy import of praisonai.cli.features.serve as specified.
Old command paths become sys.modules aliases to the moved modules so the
Typer registry (.commands.X), existing imports, and unittest.mock.patch
targets all remain transparent. Full unit/cli suite passes (1483 passed).
Co-authored-by: praisonai-triage-agent[bot] <272766704+praisonai-triage-agent[bot]@users.noreply.github.com>
Co-authored-by: MervinPraison <[email protected]>
* refactor: move interactive/execution/ui/output/state to praisonai-code (C2) (fixes#2515)
Move 30 CLI files across five sub-packages (interactive, execution, ui,
output, state) from praisonai/cli/ into the new praisonai_code/cli/ package
as part of the incremental praisonai-code extraction (parent #2512).
- Create minimal praisonai-code scaffold (pyproject + package inits) since
C0 is not yet on this branch; wire it as a path dependency of praisonai.
- Preserve all public import paths (praisonai.cli.{interactive,execution,ui,
output,state}.*) via identity-preserving shims at the old locations so that
app.py (kept in main until C5), conftest fixtures that mutate module
globals, and 150+ dependent tests keep working unchanged.
- Forward not-yet-moved relative siblings (configuration/utils/session/
features) from praisonai_code.cli back to praisonai.cli via a temporary
meta-path shim (to be removed in C5).
Relative imports in the moved files are unchanged. Interactive/TUI/ui tests
pass (351 passed locally).
Co-authored-by: MervinPraison <[email protected]>
* fix(cli/ui): add TYPE_CHECKING UIConfig import and drop dead select_backend branch
Addresses reviewer findings on PR #2530:
- Import UIConfig under TYPE_CHECKING so the string annotations in
select_backend/_get_backend_by_name/_auto_select_backend resolve (fixes ruff F821).
- Remove the unreachable `select_backend` branch in __getattr__ (select_backend is a
real module-level function; it also referenced a non-existent `.selector` module).
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
---------
Co-authored-by: praisonai-triage-agent[bot] <272766704+praisonai-triage-agent[bot]@users.noreply.github.com>
Co-authored-by: MervinPraison <[email protected]>
* refactor: move runtime/ + cli_backends/ to praisonai-code with shims (fixes#2514)
C1 of the praisonai-code extraction (#2512). Moves the 8 warm-runtime and
CLI-backend files into the new praisonai_code package and leaves PEP 562
shims at the old praisonai.runtime / praisonai.cli_backends paths for
backward compatibility.
- Create minimal src/praisonai-code scaffold (pyproject, __init__, README)
- git-move runtime/ (5) + cli_backends/ (3) into praisonai_code/
- cli_backends/registry.py: .._registry -> absolute praisonai._registry
- Shims re-export public surface + alias submodules and __main__ entrypoint
- Wire praisonai-code path dep + uv source into main pyproject
Co-authored-by: MervinPraison <[email protected]>
* fix: propagate shim submodule import errors instead of swallowing them
Address CodeRabbit/Greptile P2: the praisonai.runtime and
praisonai.cli_backends compat shims caught and suppressed all exceptions
while aliasing moved submodules. Since these submodules always exist in
praisonai_code, a genuine import failure was hidden and re-surfaced later
as a confusing missing-submodule error on the old import path. Let the
original error (and traceback) propagate.
Co-authored-by: Mervin Praison <[email protected]>
* fix: bind aliased shim submodules on package object + clarify README deps
- praisonai.runtime / praisonai.cli_backends shims: also assign each moved
submodule into the package namespace (globals()[name] = mod), not just
sys.modules. Pre-inserting into sys.modules does not set the parent-package
attribute, so `import praisonai.runtime.descriptor` followed by attribute
access could raise AttributeError (CodeRabbit Major).
- README: reconcile dependency section with pyproject.toml (third-party deps)
and document the deliberate C1 residual praisonai._registry back-import.
Co-authored-by: Mervin Praison <[email protected]>
* fix: stop aliasing runtime __main__ in shim (breaks python -m praisonai.runtime)
Pre-inserting praisonai_code.runtime.__main__ into sys.modules under the
old-path key made runpy see praisonai.runtime.__main__ as already loaded
under a foreign loader, raising a RuntimeWarning + ImportError and bypassing
the physical delegating shim. Exclude __main__ from the alias loop; the
physical praisonai/runtime/__main__.py already delegates to the moved
entrypoint. Fixes Greptile P1.
Co-authored-by: Mervin Praison <[email protected]>
---------
Co-authored-by: praisonai-triage-agent[bot] <272766704+praisonai-triage-agent[bot]@users.noreply.github.com>
Co-authored-by: MervinPraison <[email protected]>
* feat: scaffold praisonai-code package (C0) (fixes#2512)
C0 of the incremental praisonai-code extraction (issue #2512). Creates
the new sibling package with zero file moves:
- src/praisonai-code/pyproject.toml — depends on praisonaiagents only
(no praisonai dep) to avoid a PyPI cycle
- praisonai_code/__init__.py — exposes __version__
- README.md — migration overview + dependency rules
- praisonai/pyproject.toml — editable path dep on praisonai-code
No runtime behaviour changes; praisonai remains the user-facing install.
Co-authored-by: MervinPraison <[email protected]>
* fix: declare praisonai-code as praisonai dependency so uv source activates
The [tool.uv.sources] entry only redirects an already-declared dependency;
without praisonai-code in [project.dependencies] the editable path source was
inert and praisonai never installed the sibling package. Addresses Greptile
and CodeRabbit review findings.
Co-authored-by: Mervin Praison <[email protected]>
---------
Co-authored-by: praisonai-triage-agent[bot] <272766704+praisonai-triage-agent[bot]@users.noreply.github.com>
Co-authored-by: MervinPraison <[email protected]>
Address Greptile P1/P2 findings on background job delivery:
- deliver='all' now fans out to every home channel via a new
BotOS._deliver_job_text helper (DeliveryRouter does not resolve 'all')
- origin SessionSource now carries thread_id so a threaded origin is complete
- JobCompletedInput.to_dict() now serializes result so hook consumers
persisting via to_dict() keep the completed job payload
Adds focused wrapper tests for the delivery routing.
Co-authored-by: Mervin Praison <[email protected]>
* fix: remove import-time side effects and add ToolSourceRegistry (fixes#2510)
Gap 2a: persistence registries are now built lazily via
get_default_registry(kind); importing praisonai.persistence.registry no longer
walks entry points or registers ~40 builtin loaders as a side effect. Legacy
CONVERSATION_STORES / KNOWLEDGE_STORES / STATE_STORES still resolve via module
__getattr__. Factory functions accept an injected registry= for multi-tenant
isolation.
Gap 2b: adapters/vector_stores.py no longer auto-registers into the core-SDK
vector-store registry at import; callers invoke register_default_vector_stores()
explicitly.
Gap 3: new ToolSourceRegistry (praisonai.tool_sources entry-point group) mirrors
the other PluginRegistry-based registries; ToolResolver appends discovered
sources after the built-in chain and accepts source_registry= for DI.
All changes are backward-compatible.
Co-authored-by: MervinPraison <[email protected]>
* fix: harden ToolSourceRegistry opt-out and invalid-plugin handling
Addresses reviewer feedback on PR #2511:
- Add discover_entry_points flag to PluginRegistry (default True) and thread
it through ToolSourceRegistry so ToolSourceRegistry(discover_entry_points=
False) is a truly empty, isolated registry. Fixes the opt-out path that
previously still loaded installed third-party sources (CodeRabbit).
- _coerce_source() now raises TypeError on objects that are not (and do not
produce) a valid ToolSource, so create_sources() skips misbehaving plugins
instead of appending a broken source that raises on every lookup and
disables negative caching (Greptile P2 / CodeRabbit).
- Make empty-registry tests deterministic via discover_entry_points=False and
add a test for invalid-shape plugins being skipped.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
---------
Co-authored-by: praisonai-triage-agent[bot] <272766704+praisonai-triage-agent[bot]@users.noreply.github.com>
Co-authored-by: MervinPraison <[email protected]>
* feat: make core read_file coding-grade with offset/limit + line numbers (fixes#2524)
Add optional offset/limit windowed reads and line-numbered output to the
built-in read_file tool so a coding agent can bound large-file reads and
reference exact lines for subsequent edits. Includes a default line cap
with a paging hint and a per-line char cap. Backward compatible:
line_numbers=False with no window returns the exact legacy plain string.
Co-authored-by: MervinPraison <[email protected]>
* fix: harden read_file windowing (negative bounds, newline preservation)
Address reviewer findings on PR #2527:
- Reject negative `limit` (was silently expanding to DEFAULT_MAX_LINES);
now yields an empty window.
- Guard negative `max_line_chars` so it no longer truncates lines
backwards via line[:-n].
- Preserve within-window newlines in raw windowed reads via
splitlines(keepends=True); avoid a double newline before the paging hint.
- Rename shadowing local `offset` -> `line_index` in the render helper.
Co-authored-by: Mervin Praison <[email protected]>
---------
Co-authored-by: praisonai-triage-agent[bot] <272766704+praisonai-triage-agent[bot]@users.noreply.github.com>
Co-authored-by: MervinPraison <[email protected]>
* feat: add capped ripgrep-backed grep and glob built-in tools (fixes#2525)
Add tools/search_tools.py with two lazily-imported, capped, path-safe
built-ins for fast code search:
- grep(pattern, path, glob, case_insensitive, max_results): regex/literal
content search. Prefers ripgrep when present, falls back to a pure-Python
tree walk (zero external deps). Returns `path:line: match`, hard-capped
with a truncation hint.
- glob(pattern, path, max_results): file discovery via pathlib, honours
.gitignore, skips noise dirs, sorted by mtime, hard-capped.
Both reuse the existing workspace containment (path_safety.resolve_within_root)
so searches cannot escape the workspace. Registered as lazy built-ins in
tools/__init__.py, usable via CLI/YAML/Python. Additive and backward
compatible; ripgrep is an optional accelerator, never a hard dependency.
Co-authored-by: MervinPraison <[email protected]>
* fix: address reviewer feedback on grep/glob search tools
- gitignore: last-match-wins so negation re-includes (!keep.log) work
- gitignore: bare/dir-only rules (build/) now exclude files beneath them
- _iter_files: handle single-file root so grep(path=file) works on fallback
- glob(): honour ancestor dir-only ignore rules via ancestor walk
- ripgrep: drop per-file --max-count; total-cap in Python to match fallback
- python fallback: cap pattern length to reduce ReDoS exposure
- tests: force fallback for determinism, mock rg branch, multi-line leak check,
dir-only + negation gitignore, single-file grep (19 tests)
Co-authored-by: Mervin Praison <[email protected]>
---------
Co-authored-by: praisonai-triage-agent[bot] <272766704+praisonai-triage-agent[bot]@users.noreply.github.com>
Co-authored-by: MervinPraison <[email protected]>
Adds a proactive delivery path so a background subagent launched from a
chat can route its own result back to the originating conversation when it
finishes — no active turn, no polling — mirroring the scheduler's existing
deliver="origin"/"all"/"platform:chat_id" grammar.
Core (praisonaiagents):
- New JOB_COMPLETED HookEvent + JobCompletedInput for observability.
- BackgroundJobManager.start_job gains optional on_complete callback and
origin context; JobInfo carries origin. Callback fires on terminal state
(completed or failed) and is best-effort (never crashes the worker).
- spawn_subagent gains deliver/platform/chat_id/thread_id/session_id params
and an on_job_complete injection point; fully backward compatible —
without deliver it stays pull-only via subagent_result.
Wrapper (praisonai):
- BotOS.on_background_job_complete routes the result via the SAME
DeliveryRouter the scheduler uses, records delivery, and fires the
JOB_COMPLETED hook. fire_job_completed helper added to _protocol_mixin.
Tests cover on_complete success/failure, exception isolation, origin
capture, deliver echo, and the unchanged pull-only default.
Co-authored-by: Mervin Praison <[email protected]>
The no-config `praisonai gateway start` path resolved the reliability
preset for admission but hardcoded drain_timeout=None and dropped the
resolved drain, so `--reliability production` never applied its 15s
window and `--drain-timeout` was ignored on shutdown. Pass the explicit
drain through the resolver and use the resolved window in stop().
Co-authored-by: Mervin Praison <[email protected]>
Addresses two P1 issues from Greptile review on PR #2536:
- Reloads can interleave: add asyncio.Lock (_reload_lock) so overlapping
triggers (file watcher, SIGHUP, back-to-back SIGHUPs) apply one at a time
and cannot clobber _loaded_config / channel state across await points.
- Partial writes drop reloads: event-driven watcher now verifies the config
file's mtime has settled (_config_settled) after debounce; if a write is
still in progress it re-arms the change event instead of parsing a
half-written file and dropping the completed update.
Co-authored-by: Mervin Praison <[email protected]>
- Fix CRITICAL bypass: with lockout_seconds=0 (default), denial no longer
resets the window; the key stays denied until the window naturally expires.
- Chain re-raised ValueErrors in the config validators (B904).
- Use dataclasses.FrozenInstanceError in the frozen-decision test (B017).
- Add regression test asserting denials persist within the window when no
lockout is configured.
- Document lazy state reclamation / wrapper ownership for unbounded identities.
Co-authored-by: Mervin Praison <[email protected]>
- Add watchdog-based event-driven gateway.yaml watching with graceful
fallback to the existing mtime-polling loop when watchdog is
unavailable or inotify is exhausted.
- Add a SIGHUP handler that triggers the same diff-driven reload path,
giving operators/systemctl a deterministic "reload now" without a
process shutdown.
- Drain a channel's in-flight turns within a bounded window before a
reload-driven restart, reusing the bot drain machinery. The window is
configured via gateway.reload_drain_timeout (falls back to
drain_timeout).
- Log a concise summary of what a reload applied.
- Add optional watchdog dependency (new [gateway] extra + [all]).
Co-authored-by: MervinPraison <[email protected]>
Introduces a named `reliability` posture ("production" | "default" | "off")
that composes the already-existing lifecycle knobs — graceful drain and
inbound admission — in one switch across Python, YAML and CLI surfaces.
- New wrapper resolver `praisonai/bots/_reliability.py` maps the preset onto
the existing BotOS constructor args; explicit drain_timeout /
max_concurrent_runs / admission_policy always override the preset.
- Default posture now applies a sane 5s drain window so a restart no longer
cuts in-flight turns; "production" adds a 15s drain + CPU-scaled admission
ceiling with a bounded fair queue; "off" preserves immediate teardown.
- Wired into BotOS.__init__, BotOS.from_config (YAML), the WebSocketGateway
config path, and the `praisonai gateway start --reliability` CLI flag.
- Durable inbound journaling remains on by default at the session level.
Co-authored-by: MervinPraison <[email protected]>
Complete the gateway policy-protocol family with an injectable
RateLimitPolicyProtocol + RateLimitDecision in core, plus a config-driven
SlidingWindowRateLimitPolicy default. Developers can now inject custom
(per-tenant, distributed, cost-based) limiters symmetric with the send,
idle, drain and concurrency policies.
Co-authored-by: MervinPraison <[email protected]>
C0 of the incremental praisonai-code extraction (issue #2512). Creates
the new sibling package with zero file moves:
- src/praisonai-code/pyproject.toml — depends on praisonaiagents only
(no praisonai dep) to avoid a PyPI cycle
- praisonai_code/__init__.py — exposes __version__
- README.md — migration overview + dependency rules
- praisonai/pyproject.toml — editable path dep on praisonai-code
No runtime behaviour changes; praisonai remains the user-facing install.
Co-authored-by: praisonai-triage-agent[bot] <272766704+praisonai-triage-agent[bot]@users.noreply.github.com>
Co-authored-by: MervinPraison <[email protected]>
Mark agno and google_adk as handoff-compatible runtimes, pin
praisonai-frameworks>=0.1.8, and add wrapper e2e handoff tests.
Co-authored-by: Cursor <[email protected]>
2026-07-01 07:28:37 +01:00
1582 changed files with 153681 additions and 56466 deletions
<p>praisonai-mcp is Tier 2e — the heavy MCP server host: full <code>MCPServer</code>, tool/resource/prompt registries, recipe bridge, HTTP-stream auth, and MCP client config management. Depends only on <code>praisonaiagents</code>. v0.0.2. Protocol: MCP 2025-11-25.</p>
<figureclass="wp-block-table"><table><thead><tr><th>Registry</th><th>Commands</th><th>Gate</th></tr></thead><tbody><tr><td>MCP-resident Typer</td><td>list, add, remove, sync, test, serve, doctor</td><td><code>mcp_package_available()</code></td></tr><tr><td>MCPServerCLI host</td><td>serve, list-tools, list-resources, config-generate, tasks, auth</td><td>Direct via <code>praisonai-mcp <cmd></code></td></tr><tr><td>Code-resident light</td><td><code>serve mcp</code></td><td>Always available with code + agents</td></tr><tr><td>Wrapper-resident</td><td>dashboard, schedule, langfuse</td><td><code>wrapper_package_available()</code></td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:paragraph -->
<p>Standalone <code>praisonai-code</code> hides <code>mcp</code> from <code>--help</code> when <code>praisonai-mcp</code> is not installed. Host commands like <code>praisonai-mcp serve</code> skip the <code>mcp</code> prefix; <code>praisonai mcp serve</code> goes through the Typer group.</p>
<p>Heavy MCP hosting needs only <code>praisonaiagents</code> + <code>praisonai-mcp</code>. Full capability registry (workflows, deploy, extended tools) activates when the wrapper is co-installed via <code>_wrapper_bridge</code>.</p>
<!-- /wp:paragraph -->
<!-- wp:heading -->
<h2class="wp-block-heading">Bridges (no PyPI cycles)</h2>
<p>Wrapper pins <code>praisonai-mcp>=X</code> after mcp is on PyPI. CI: <code>.github/workflows/pypi-release.yml</code>. Import gate: <code>scripts/check_c12_mcp_imports.sh</code>.</p>
<p>praisonai is Tier 3 — the integration wrapper that wires <code>praisonaiagents</code> + five Tier-2 packages (<code>praisonai-code</code>, <code>praisonai-bot</code>, <code>praisonai-train</code>, <code>praisonai-browser</code>, <code>praisonai-mcp</code>) into one install, with dashboard, framework adapters, jobs API, serve orchestration, and backward-compat shims. v4.6.150.</p>
<p>Each Tier-2 package is installable standalone with its own console script. The wrapper pins all of them and preserves every old import path via shims. Use the wrapper when you need several surfaces together — HTTP serve, dashboard, framework YAML, jobs/recipe — without managing tier pins yourself.</p>
<p>Protocols live in <code>praisonaiagents/frameworks/</code>; adapter implementations in wrapper <code>framework_adapters/</code>. Entry-point group: <code>praisonai.framework_adapters</code>.</p>
<figureclass="wp-block-table"><table><thead><tr><th>Package</th><th>Status</th><th>Choose when…</th><th>Standalone install (planned)</th></tr></thead><tbody><tr><td><code>praisonai-sandbox</code></td><td><strong>C13 preferred</strong> (execution goal)</td><td>Isolated agent code execution — Docker/E2B/Modal/Daytona/Sandlock</td><td><code>pip install praisonai-sandbox[docker]</code></td></tr><tr><td><code>praisonai-deploy</code></td><td><strong>C13 alt</strong> (deploy goal)</td><td>Ship <code>agents.yaml</code> to Docker/AWS/GCP/Azure</td><td><code>pip install praisonai-deploy</code></td></tr><tr><td><code>praisonai-replay</code></td><td>Conditional C14+</td><td>Deterministic replay from run ledger / traces</td><td>After product sign-off</td></tr><tr><td><code>praisonai-serve</code></td><td>Deferred C15</td><td>HTTP API hosting without full umbrella (~14k LOC)</td><td>With recipe/jobs decision</td></tr><tr><td><code>praisonai-recipe</code></td><td>Deferred</td><td>NL → agent YAML without jobs UI</td><td>Tied to jobs + serve</td></tr><tr><td><code>praisonai-jobs</code></td><td>Deferred</td><td>Async long-running agent jobs API</td><td>Extract with serve or stay wrapper</td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:paragraph -->
<p>C13 decision rule: <strong>sandbox</strong> = where code runs; <strong>deploy</strong> = where the agent service lives. Ship one first after product sign-off.</p>
<figureclass="wp-block-table"><table><thead><tr><th>Criterion</th><th>Pass (browser/MCP)</th><th>Fail (example)</th></tr></thead><tbody><tr><td>Named user goal without full <code>praisonai</code></td><td><code>pip install praisonai-browser</code> → automate Chrome</td><td><code>pip install praisonai-persist</code> → ???</td></tr><tr><td>Coherent product boundary</td><td>One Typer group / one console script</td><td>Whole <code>cli/features/</code> orchestration layer</td></tr><tr><td>PyPI deps: <code>praisonaiagents</code> only</td><td>Tier-2 + lazy <code>_wrapper_bridge</code></td><td>Hard <code>import praisonai</code> at module level</td></tr><tr><td>Worth a separate install</td><td>User who never wants dashboard/serve/bots</td><td>Internal docs tooling</td></tr><tr><td>Extraction shape</td><td>C10 single PR (~3–11k LOC)</td><td>Mega-epic without product sign-off</td></tr></tbody></table></figure>
<!-- /wp:table -->
<!-- wp:heading -->
<h2class="wp-block-heading">When to use which tier</h2>
<!-- /wp:heading -->
<!-- wp:table -->
<figureclass="wp-block-table"><table><thead><tr><th>Scenario</th><th>Install</th></tr></thead><tbody><tr><td>Pure Python agents, minimal deps</td><td><code>pip install praisonaiagents</code></td></tr><tr><td>Terminal coding agent, CI, headless</td><td><code>pip install praisonai-code</code></td></tr><tr><td>Telegram/Discord gateway only</td><td><code>pip install "praisonai-bot[gateway,bot]"</code></td></tr><tr><td>Agent training (no GPU)</td><td><code>pip install praisonai-train</code></td></tr><tr><td>LLM fine-tuning (GPU)</td><td><code>pip install "praisonai-train[llm]"</code></td></tr><tr><td>MCP server for Cursor/Claude</td><td><code>pip install "praisonai-mcp[all]"</code></td></tr><tr><td>Browser automation (extension/CDP)</td><td><code>pip install "praisonai-browser[all]"</code></td></tr><tr><td>CrewAI/AutoGen YAML</td><td><code>pip install "praisonai[crewai]"</code></td></tr><tr><td>Full ops + channels + dashboard</td><td><code>pip install "praisonai[claw]"</code></td></tr><tr><td>Everything in one install</td><td><code>pip install praisonai</code></td></tr><tr><td>Library author / embedding</td><td><code>praisonaiagents</code> only</td></tr></tbody></table></figure>
Before implementing anything, answer these questions:
- CORE vs WRAPPER vs TOOLS ROUTING:
- CORE vs WRAPPER vs TOOLS 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 (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`.
3. Tools → MervinPraison/PraisonAI-Tools: agent-callable integrations at runtime (Composio, Slack, APIs). Do NOT add to praisonaiagents/.
'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.',
@@ -201,7 +269,7 @@ function finalClaudeCompletedOnSha(comments, headPushedAt) {
}
constFINAL_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: 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';
'@claude You are the FINAL architecture reviewer. If the branch is under MervinPraison/PraisonAI (not a fork), you are able to make modifications to this branch and push directly. SCOPE: Focus ONLY on Python packages (praisonaiagents, praisonai). Do NOT modify praisonai-rust or praisonai-ts. Read ALL comments above from Gemini, Qodo, CodeRabbit, and Copilot carefully before responding.\n\n**Phase 1: Review per AGENTS.md**\n1. Protocol-driven: check heavy implementations vs core SDK\n2. Backward compatible: ensure zero feature regressions\n3. Performance: no hot-path regressions\n4. SDK value: review in depth whether the change genuinely adds value to the SDK — never add features for the sake of adding them. It must strengthen the SDK (simpler, more user-friendly, robust, world-class, secure). If it does not clearly add value, request changes or recommend rejecting/closing rather than merging scope creep\n5. Do not bloat the Agent class with additional params — only if absolutely required; we already support many params.\n6. Repo routing: agent-callable tools → PraisonAI-Tools; lifecycle plugins → PraisonAI-Plugins; optional sandbox backends → PraisonAI-Plugins (`praisonai.sandbox` entry point) — request changes if wrongly added to praisonaiagents/\n\n**Phase 2: FIX Valid Issues**\n7. For any VALID bugs or architectural flaws found by Gemini, CodeRabbit, Qodo, Copilot, or any other reviewer: implement the fix\n8. Also independently identify and fix any gaps or issues you find in the changed code — do not rely only on prior reviewer feedback\n9. Push all code fixes directly to THIS branch (do NOT create a new PR)\n10. Comment a summary of exact files modified and what you skipped\n\n**Phase 3: Final Verdict**\n11. If all issues are resolved, approve the PR / close the Issue\n12. If blocking issues remain, request changes / leave clear action items';
core?.info?.(`PR #${prNumber}: skip Claude FINAL — ${gate.reason}`);
return{posted:false,reason:gate.reason};
}
awaitgithub.rest.issues.createComment({
owner,
repo,
issue_number:prNumber,
body:finalBody,
});
constnote=gate.requiredTimeout
?' (required reviewer timeout fallback)'
:gate.copilotSkipped
?' (Copilot timeout fallback)'
:'';
core?.info?.(`Posted Claude FINAL on PR #${prNumber}${note}`);
return{posted:true,reason:''};
}
constCOPILOT_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';
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'
body: '🤖 Auto-triaging with Claude. Analyzing issue and implementing a fix...\n\nClaude will:\n1. Route correctly: agent tools → PraisonAI-Tools; lifecycle plugins → PraisonAI-Plugins\n2. Search codebase for root cause\n3. Implement a minimal fix following AGENTS.md\n4. Create a PR with the changes'
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'
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'
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' +
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'
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'
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'
core.setOutput('started_at', new Date().toISOString());
await github.rest.issues.addLabels({
@@ -252,7 +262,6 @@ jobs:
- name:Claude merge gate assessment (read-only)
id:assess
continue-on-error:true
uses:./.github/actions/claude-code-action
env:
GH_TOKEN:${{ steps.app-token.outputs.token }}
@@ -279,12 +288,13 @@ 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 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.
3. Confirm SDK value gate: change strengthens praisonaiagents (no scope creep). The aim is a LIGHTWEIGHT AND POWERFUL package — BLOCK changes that add params/modules/exports duplicating existing Agent capabilities (instructions/backstory/tools/hooks/memory), or add knobs/fields with no live consumer, i.e. scope creep for the sake of it.
4. Repo routing: BLOCK if agent-callable tools (→ PraisonAI-Tools), lifecycle plugins (→ PraisonAI-Plugins), or optional sandbox backends (→ PraisonAI-Plugins via `praisonai.sandbox`) wereadded 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.
Do not approve or merge via GitHub UI/API — only post the verdict comment.
disallowed_tools:|
@@ -299,11 +309,12 @@ jobs:
Read
timeout_minutes:15
- name:Post fallback verdict if Claude did not comment
- name:Post fallback BLOCK if Opus did not comment
Read AGENTS.md to understand the architecture rules.
SCOPE: Focus ONLY on Python packages (praisonaiagents, praisonai). Do NOT modify or touch praisonai-rust or praisonai-ts packages.
AIM: This package must stay LIGHTWEIGHT AND POWERFUL. Do a CRITICAL REVIEW at each stage (issue triage, fix planning, PR review/implementation) and reject scope creep for the sake of adding features. Prefer existing Agent capabilities (instructions/backstory/tools/hooks/memory) over new params/modules/exports. Even when fixing a raised issue, keep the fix minimal and lightweight — do NOT expand the surface area or add knobs with no live consumer.
3. Tools → MervinPraison/PraisonAI-Tools: agent-callable integrations at runtime (Composio, Slack, API/DB connectors). Do NOT implement in praisonaiagents/ — use STEP 3-ALT.
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.
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).
- Does it duplicate existing functionality? Check if Agent already supports this via existing params (reflection, planning, tools, hooks, memory). If it does, reject the change as scope creep and prefer the existing capability — the aim is lightweight and powerful, not more knobs.
- Do not bloat the Agent class with additional params — only if absolutely required; we already support many params.
- 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.
-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"
@@ -7,4 +7,9 @@ You are working on the PraisonAI project.
- Be concise and helpful in responses
- Test implementation thoroughly
- Ensure backward compatibility with existing APIs
- Follow protocol-driven design: core protocols in praisonaiagents/, heavy implementations in praisonai/
- Follow protocol-driven design across the eight packages: core protocols in `praisonaiagents/`, agentic terminal CLI in `praisonai-code/`, bots/gateway in `praisonai-bot/`, LLM fine-tuning + agent training in `praisonai-train/`, browser automation in `praisonai-browser/`, MCP server host in `praisonai-mcp/`, sandbox backends in `praisonai-sandbox/`, integrations/serve/dashboard in the `praisonai/` wrapper
- Preserve old `praisonai.*` import paths via shims when moving code between packages (see §2.3 in `src/praisonai-agents/AGENTS.md`; shim helpers in `src/praisonai/praisonai/cli/_shim.py`)
- Package boundaries and dependency rules: `ARCHITECTURE.md` §2 (Tier 2 packages must never PyPI-depend on the wrapper; cross-tier access goes through lazy `_*_bridge` modules)
- 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.
| 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`) |
| 3 | `src/praisonai/` | `framework_adapters/`, serve, dashboard, async jobs API | — |
**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 |
This directory contains examples for integrating PraisonAI Agents with**Terminal-Bench 2.0** via the **Harbor framework** for AI agent benchmarking.
This directory contains adapters for benchmarking PraisonAI on**Terminal-Bench 2.1** via the **Harbor framework**. Everything lives in the examples/benchmark layer + CI — there are **zero changes to the `praisonaiagents` / `praisonai` SDK surface**.
## Overview
[Terminal-Bench 2.0](https://tbench.ai) is a Stanford/Laude Institute benchmark that has become the gold standard for evaluating AI coding agents in real terminal environments. The [Harbor framework](https://harborframework.com) is the official evaluation harness that abstracts container lifecycle, parallelization, and cloud providers.
[Terminal-Bench 2.1](https://www.tbench.ai/docs/run-terminal-bench-2-1) is the current release of the Laude Institute benchmark for evaluating AI coding agents in real terminal environments. The [Harbor framework](https://github.com/laude-institute/harbor) is the official evaluation harness that abstracts container lifecycle, parallelization, and cloud providers.
## Setup
Install Harbor and PraisonAI into **one venv** and run from the repo root with `PYTHONPATH=.` so the adapter import path resolves:
Loaded 100 of 1582 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.