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.
- 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]>
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>
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 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]>
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]>
* Add Google ADK framework integration to PraisonAI wrapper.
Wires probe, optional extra, doctor card, CLI hints, workflow validation, and live e2e tests against praisonai-frameworks[google-adk]>=0.1.7.
* fix: use registered probe key for google_adk doctor fallback
The doctor runtime card passed the dotted module path 'google.adk' as the
fallback package name to _runtime_usable. In the partial-install fallback
path this reaches is_available('google.adk'), which raises ValueError for an
unknown framework name instead of returning a bool, and could crash
`praisonai doctor`. Pass the registered probe key 'google_adk' to match every
other framework (openai_agents, agno, etc.).
Co-authored-by: Mervin Praison <[email protected]>
* fix(google_adk): doctor probe, availability, and test gating gaps
Fix doctor fallback probe key, robust ADK import detection, and accept
GEMINI_API_KEY for provider_google live test gating.
* fix(frameworks): tighten tests, accurate direct-prompt hints, e2e doc gates
- Remove openai-agents/agno/google-adk hints from handle_direct_prompt's
no-framework error path (that flow only dispatches PraisonAI/CrewAI/AutoGen)
- Assert exact adapter.run() call contract in Agno/Google ADK generator tests
- Tighten Agno e2e assertion to require explicit "Agno Output" marker
- Document PRAISONAI_LIVE_TESTS + API key gates for Agno/Google ADK e2e in README
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: Mervin Praison <[email protected]>
* fix: remove dead memory/utils.py and fix latent NameError in memory.py (fixes#2503)
Delete orphaned praisonaiagents/memory/utils.py (no importers; duplicated the
adapter-layer lazy-import guards). Fix three latent NameError call sites in
memory.py that referenced the now-removed helpers without importing them:
_get_chromadb() in _init_chroma() and _check_litellm()/_check_openai() in the
quality-metrics path, replacing them with inline lazy imports per AGENTS.md.
Co-authored-by: MervinPraison <[email protected]>
* fix: chain re-raised ImportError in _init_chroma (B904)
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 reviewer feedback (Greptile P1, CodeRabbit) on the three CLI
comma-separated tool-loading paths (_rewrite_query, _expand_prompt,
handle_research_command):
- Instantiate class-based tools returned by resolve_tool() before passing
to agents, mirroring resolve_all_from_yaml (tool_resolver.py:886). The
resolver chain reaches praisonai-tools which exposes BaseTool subclasses;
resolve_tool() defaults to instantiate=False so classes were leaking to
agents uninstantiated.
- Filter empty segments from the comma split to avoid resolving '' and
emitting a spurious "Unknown tool ''" warning on trailing/empty commas.
Co-authored-by: Mervin Praison <[email protected]>
Remove the superseded duplicate `bots/_auth.py` UnknownUserHandler and
repoint its lone legacy unit test at the canonical `bots/_unknown_user.py`
handler used by every bot adapter (Telegram, Discord, Slack).
The legacy test's assertions targeted the divergent `_auth.py` instance
contract, so they are rewritten to the canonical classmethod contract
(`UnknownUserHandler.handle(message, bot_ctx) -> bool` via `BotContext`),
covering deny/allow, already-paired, owner-DM and CLI-fallback pairing,
code-generation failure, and missing-sender drop.
Both modules are private (unexported), so no deprecation cycle is needed
and runtime behaviour for shipping bots is unchanged.
Co-authored-by: MervinPraison <[email protected]>
Replace three duplicated inline TOOL_MAPPINGS lookups in cli/main.py
(query-rewriter, prompt-expander, deep-research tools_path) with the
canonical resolve_tool() resolver, which walks the full resolution
chain and caches results.
Co-authored-by: MervinPraison <[email protected]>
Sanitise auto-filled task agent names for spaced role titles and add sequential/handoff PraisonAI.run live tests so handoff YAML works through the wrapper path.
enable_telemetry() no longer flips enabled=True on a lock-less instance; guard track/shutdown paths and fix the CLI telemetry test mock under xdist.
Co-authored-by: Cursor <[email protected]>
Use dict-style YAML, praisonai.run() API, and verify external adapter
resolution in full execution test with real OpenAI API calls.
Co-authored-by: Cursor <[email protected]>
Use dict-style YAML, praisonai.run() API, and verify external adapter module
in full execution test with real OpenAI API calls.
Co-authored-by: Cursor <[email protected]>
* fix: prune permission-denied tools from LLM-advertised set (fixes#2472)
Shape the advertised tool surface by the agent's effective permission tier
so the model is only ever offered tools it can actually call. Previously the
entire tool set was advertised and denial was enforced only at execution
time, causing denied-call loops that burn tokens and turns.
The new _prune_denied_tools helper mirrors the execution-time enforcement
(_perm_deny / _perm_allow) and runs at the end of _format_tools_for_completion.
It is a no-op when no deny set and no allow set are configured, preserving
backward compatibility. ask/allow tools remain advertised (approval still
happens at execution time as defence in depth).
Co-authored-by: MervinPraison <[email protected]>
* fix: prune permission-denied tools from system prompt enumeration
_build_system_prompt() enumerated raw self.tools and named every tool in
the prompt ("You have access to the following tools: ..."), so a denied
tool stayed advertised in natural language even after being pruned from
the function schema. Filter tool_names through a shared _tool_name_allowed
predicate so the prompt and schema advertise the same permission-shaped
surface. Refactors _prune_denied_tools to reuse the predicate (DRY).
Addresses CodeRabbit major (security) finding on PR #2475.
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]>
* fix: address three wrapper-layer architectural gaps (fixes#2470)
- Remove divergent _format_template overrides in autogen/praisonai
adapters; both now inherit the safe regex-scoped core implementation
(preserves JSON-like braces, no silent {topic} drop).
- Replace process-wide _TOOL_TIMEOUT_EXECUTOR singleton with an
instance-owned executor on AgentsGenerator, with deterministic
lifecycle (close()/context manager) for multi-tenant safety.
- Hoist duplicated YAML -> framework conversion into a single canonical
build_agent_specs() builder consumed by all three adapters, giving
consistent defaults for the same YAML.
Co-authored-by: MervinPraison <[email protected]>
* fix: address reviewer findings on wrapper-layer refactor
- agents_generator: capture executor inside lock and guard close() with the
same lock to remove the lazy-create/close() data race (CodeRabbit Major).
- crewai_adapter: raise on duplicate task names so context wiring can't bind
to the wrong task (CodeRabbit Major).
- praisonai_adapter: forward task_spec.tools into PraisonTask so per-task YAML
tools are honored, matching CrewAI (CodeRabbit Minor).
- autogen_adapter: drop the no-op tool-registration loop that misleadingly
implied tools were wired (CodeRabbit Major).
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]>
Co-authored-by: Mervin Praison <[email protected]>
* fix: surface registry and entry-point tools in tools list (fixes#2474)
ToolResolver.list_available() previously enumerated only local tools.py,
TOOL_MAPPINGS, and praisonai-tools, while the resolution chain also reached
the wrapper ToolRegistry and the core SDK tool registry (entry-point plugins
and runtime-registered tools). This made some resolvable tools invisible to
`praisonai tools list`/`validate`/`info`. Discovery now matches resolution by
also querying the wrapper registry and the core registry (triggering
idempotent entry-point discovery), with source attribution in the CLI output.
Co-authored-by: MervinPraison <[email protected]>
* fix: align tool discovery source attribution with resolution precedence
- Reorder list_available() discovery to match resolve()/default_sources
precedence (wrapper registry before built-in/external), so a name present
in multiple sources is attributed to the source resolve() actually returns.
- Add ToolResolver.list_available_sources() returning canonical source ids;
CLI now uses it instead of fragile substring matching on descriptions, so
a local tool whose docstring contains "Registered" is no longer mis-bucketed.
- Do not advertise default-chain tools (built-in/external/core registry) when
the resolver is built with a custom sources= chain it cannot resolve.
- Add regression tests for precedence attribution, docstring isolation, and
custom-source isolation.
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]>
Co-authored-by: Mervin Praison <[email protected]>
* feat: add BEFORE_TOOL_DEFINITIONS hook to transform advertised tool defs (fixes#2473)
Adds a first-class, additive extension point that receives the assembled
tool definitions for a request and may modify or filter them before they
reach the LLM, mirroring how BEFORE_LLM lets a hook mutate messages.
- New HookEvent.BEFORE_TOOL_DEFINITIONS + BeforeToolDefinitionsInput dataclass
- Fired in chat_mixin.py (sync + async paths) right after formatted_tools is
assembled; mutations are adopted, no-op when nothing registered
- New before_tool_definitions plugin protocol/base method for plugin consumers
- Unit tests for event, runner mutation, and agent integration
Co-authored-by: MervinPraison <[email protected]>
* fix: address reviewer findings on BEFORE_TOOL_DEFINITIONS hook
- deep-copy tool defs before hook to stop cache poisoning (CodeRabbit)
- add async helper using await execute so async chat runs the hook (Greptile P1)
- correct events.py docstring to only promise in-place mutation (Greptile P2)
- revert protocol method addition that broke HookPluginProtocol isinstance (backward-compat)
- add cache-reuse and async regression 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]>
Co-authored-by: Mervin Praison <[email protected]>
* fix: wire durable, actor-authorised approval backend for chat channels (fixes#2479)
Adds PresentationApprovalBackend, a thin ApprovalProtocol adapter wrapping the
previously-orphaned PresentationApprovalHandler + SQLite ApprovalStore. It
authorises the approver against allowed_actors, persists pending approvals and
rehydrates them on restart, binds decisions to an unguessable approval id, and
never routes the allow/deny decision through an LLM classifier.
Exposed via praisonai.bots and selectable through the CLI --approval secure
backend (allowed actors via PRAISONAI_APPROVAL_ACTORS).
Co-authored-by: MervinPraison <[email protected]>
* fix: harden secure approval backend (rehydration allowlist, fail-closed actors)
Address reviewer findings (Greptile P1, CodeRabbit) on PR #2482:
- Preserve actor allowlist across restart: rehydrate() now re-applies the
backend's configured allowed_actors to rehydrated pending approvals so a
restored approval cannot become unrestricted (security regression).
- Fail closed for --approval secure/presentation: require a non-empty
PRAISONAI_APPROVAL_ACTORS instead of silently allowing any actor.
- Add "presentation" to VALID_BACKENDS so the documented alias validates.
- Add regression tests: rehydration preserves allowlist, replay rejection.
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]>
* fix: wire portable presentation renderer into Telegram delivery (fixes#2481)
Bind the orphaned MessagePresentation renderers into the live delivery path.
The Telegram adapter now implements SupportsPresentation (render_presentation /
presentation_limits / truncate_presentation), delegating to
TelegramPresentationRenderer and sending a native inline keyboard. The inbound
handler (streaming + non-streaming) pops the agent-captured presentation and
renders it after the text reply, which serves as the plain-text fallback.
Co-authored-by: MervinPraison <[email protected]>
* fix(telegram): harden presentation rendering per reviewer feedback
- Truncate callback_data on UTF-8 byte boundary (Telegram 64-byte limit),
preventing non-ASCII payloads from silently dropping the inline keyboard
- Convert renderer web_app dicts to WebAppInfo objects (python-telegram-bot >=20)
- Pop the captured presentation immediately after chat() in both streaming and
non-streaming paths so cancel/return doesn't leave stale buttons next turn
- Strengthen tests: assert per-button encoded payloads + callback case, add a
64-byte callback_data truncation test
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]>
* fix: add self-healing dead-target registry for outbound delivery (fixes#2486)
Record a (platform, channel_id) as dead only on a confirmed permanent
failure (403 Forbidden, 404 chat-not-found), short-circuit future sends,
and automatically clear the flag the moment any send succeeds again.
- New DeadTargetRegistry (bots/_dead_targets.py): persistent, bounded
(TTL + max_size), thread-safe, default-OFF.
- New is_permanent_target_failure() in _resilience.py: classifies whole-
target deaths (403/404) while keeping transient errors and message-scoped
404s on the existing retry path.
- DeliveryRouter.deliver() consults the registry pre-send, marks dead on
permanent failure, and clears (self-heals) on success.
Co-authored-by: MervinPraison <[email protected]>
* fix: address dead-target reviewer findings (401 scope, self-heal, size())
- _resilience.py: drop 401 from permanent-target codes; 401 is an
account/token-level auth failure, not a per-channel death (greptile P1).
Keep 403/404/410.
- _dead_targets.py: add should_reprobe() + reprobe_seconds so a suppressed
target gets a single self-healing re-probe ~hourly instead of waiting the
full 30-day TTL (greptile P1). size() now prunes TTL-expired entries
before counting (greptile P2).
- delivery.py: deliver() lets one re-probe send through when due; success
self-heals, repeated permanent failure re-suppresses and resets the clock.
- tests: +9 cases (401 not permanent, 410 permanent, size prune, reprobe
lifecycle, router re-probe self-heal + re-suppress). 32 pass, 56 related pass.
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 out-of-process relay transport abstraction for gateway (fixes#2485)
Add core RelayTransport and CapabilityDescriptor protocols so a thin
connector process can own the platform socket and relay normalised events
to a headless gateway, enabling NAT-friendly hosting, one gateway fronting
many connectors, and lossless scale-to-zero. Wire a transport= seam on Bot
plus a generic RelayAdapter in the wrapper.
Co-authored-by: MervinPraison <[email protected]>
* fix(bots): route relay inbound to agent + add dormant forwarder (#2485)
Address blocking reviewer feedback (Greptile P1/P2) on the relay seam:
- RelayAdapter now builds a real BotSessionManager and routes relayed
inbound GatewayMessages through session.chat() to the configured agent,
relaying the reply back down the transport. Previously inbound only
fired manual on_message handlers, so a headless gateway never replied
and bypassed the BotOS-spliced admission gate / identity / delivery
wiring (which duck-types on adapter._session).
- Bot.go_dormant() forwarder so scale-to-zero controllers can ask the
out-of-process connector to keep buffering while the gateway sleeps.
- Add wrapper unit tests for inbound->agent routing, manual-handler
coexistence, session/admission splice point, and dormant forwarding.
🤖 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]>
Patch releases passed validation via --frozen --upgrade-package but then
called full uv lock --upgrade-package because use_frozen_lock was inverted.
Co-authored-by: Cursor <[email protected]>
uv lock --dry-run --upgrade-package re-resolves all requires-python splits
and fails on py3.14 win32 even when praisonaiagents is on PyPI. Use
--frozen --upgrade-package for patch validation and lock refresh instead.
Co-authored-by: Cursor <[email protected]>
* fix(release): use frozen uv lock upgrade for wrapper patch releases
Avoid re-resolving all optional extras during bump_and_release validation,
which fails on known crewai/flow/langfuse cross-extra conflicts. Patch
releases now run `uv lock --frozen --upgrade-package praisonaiagents` and
validate the default editable install instead of a full universal lock.
Co-authored-by: Cursor <[email protected]>
* fix(tests): stabilise test_version_show under xdist
Mock print_json instead of capsys — pytest stdout capture and capsys diverge
on CI xdist workers, leaving capsys.out empty despite JSON in failure logs.
Co-authored-by: Cursor <[email protected]>
* fix(release): stop pairing uv lock --frozen with --upgrade-package
--frozen forbids lockfile writes, so combining it with
--upgrade-package praisonaiagents could not refresh uv.lock. Validation
now uses --dry-run with a pinned target version; the release path drops
--frozen so the targeted upgrade actually writes uv.lock for commit.
Co-authored-by: Mervin Praison <[email protected]>
---------
Co-authored-by: Cursor <[email protected]>
Co-authored-by: praisonai-triage-agent[bot] <272766704+praisonai-triage-agent[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
The WhatsApp adapter wrapped deliver_outbound in a per-chunk
'except Exception' that logged and continued, so an exhausted/permanent
durable failure (after DLQ park) could still return a success-looking
BotMessage. Let the original exception propagate to match Telegram/
Slack/Discord and preserve the failure contract. Adds a regression test.
Co-authored-by: Mervin Praison <[email protected]>
Extract Telegram's proven outbound resilience (retry/backoff that honours
Retry-After + OutboundDLQ park) into a shared OutboundResilienceMixin and
wire it into Slack, Discord, WhatsApp, Email, Linear, and AgentMail so an
agent reply is no longer silently dropped on a transient send error on any
channel. Resilience state is lazily initialised from config, so adapter
constructors are unchanged and behaviour is backward-compatible.
Co-authored-by: MervinPraison <[email protected]>
Update test_unknown_framework_generic_hint to expect praisonai-frameworks[...]
after get_install_hint() switched from praisonai[...] during package split.
Unblocks test-core (root) on main and open PRs.
Co-authored-by: Cursor <[email protected]>
Addresses CodeRabbit nitpick: existing tests only validated Agent's
constructor contract, not handle_direct_prompt(). Adds a test that
exercises the CLI path and asserts Agent receives tool_config (with
timeout/retry mapping) and never the deprecated tool_timeout/
tool_retry_policy kwargs.
Co-authored-by: Mervin Praison <[email protected]>
handle_direct_prompt() passed the deprecated tool_timeout (and
tool_retry_policy) kwargs directly to Agent(), crashing with
TypeError on every `praisonai run` call. Agent now expects these via
tool_config=ToolConfig(timeout=..., retry_policy=...). Merge both into
a single ToolConfig at the call site, preserving the 60s default
timeout and retry behavior. Add regression tests.
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
The `praisonai serve gateway` Typer command discarded GatewayHandler.start()'s
sysexits-based return code, so a fatal-config (78) or transient (75) start
exited the command with 0 — a misconfigured gateway looked healthy to a
supervisor. Propagate the code via typer.Exit, matching the other serve
subcommands. Adds CLI tests for the fatal and clean-shutdown paths.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
A truthy 'agents' section containing a non-mapping item (e.g. agents: ["bad"])
previously raised AttributeError on .get(), routing through classify_exit_reason
to the restart code (75) and crash-looping forever. Validate that 'agents' is a
list and each entry is a mapping, raising FatalConfigError -> exit 78.
Co-authored-by: Mervin Praison <[email protected]>
Addresses reviewer findings on PR #2439:
- Greptile P1: a malformed/schema-invalid gateway.yaml (load_gateway_config
raises ValueError) now maps to GATEWAY_FATAL_CONFIG_EXIT_CODE (78) instead of
the restart code (75), so a misconfig no longer crash-loops under a supervisor.
- Greptile P1: a missing/malformed --agents file now raises FatalConfigError
(78) instead of being swallowed and starting an empty gateway that reports 0.
- CodeRabbit Major: the compatibility import fallback no longer catches a broad
ImportError (which masked genuine failures while importing
praisonaiagents.gateway). It imports the package first and only falls back on
ModuleNotFoundError (absent core) or AttributeError (older core missing the
new symbols); any other import error surfaces.
Adds tests/unit/cli/test_gateway_exit_codes.py covering the CLI integration
paths (missing/empty/schema-invalid gateway.yaml, broken --agents file).
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Gateway/bot exits previously returned a generic 1 for every failure, so a
transient blip and a fatal misconfiguration looked identical to a process
supervisor — causing crash-loops on misconfig under systemd/s6/Kubernetes.
Add a sysexits.h-based protocol:
- core (praisonaiagents/gateway/protocols.py): GATEWAY_OK/RESTART/FATAL_CONFIG
exit codes (0/75/78), FatalConfigError, and a pure classify_exit_reason().
- wrapper CLI (cli/features/gateway.py): start() now returns an int code;
fatal-config conditions -> 78, transient -> 75, clean -> 0.
- runtime entry point (runtime/__main__.py): classifies exit cause into codes.
Backward-compatible fallbacks added in the wrapper for older cores.
Co-authored-by: MervinPraison <[email protected]>
Greptile P1: check_code_skew lazy-imported praisonaiagents.gateway during the
/model hot operation. If an in-place update broke that import surface, the
guard's own import would raise, the broad except returned None, and the risky
switch proceeded — defeating the guard. Pre-resolve and cache the pure
detect_code_skew predicate at boot (capture_boot_fingerprint, run at startup
when imports are known-good); check_code_skew now uses the cached reference and
only falls back to a fresh import when none was cached.
Co-authored-by: Mervin Praison <[email protected]>
- _async_bridge: poison scope-owned bridges on shutdown so a leaked copied
ContextVar cannot resurrect an orphaned loop+thread (Greptile P2). Shared
default keeps non-permanent shutdown for backward compat.
- cli/main: _require_agents() now seeds flags via _ensure_availability_flags()
and reads globals().get(...) instead of a bare PRAISONAI_AVAILABLE, fixing
the NameError / Ruff F821 risk (CodeRabbit).
- framework_adapters: memoize resolved adapters in __getattr__ to avoid
re-importing on every access (CodeRabbit nitpick).
Co-authored-by: Mervin Praison <[email protected]>
- framework_adapters/__init__.py: stop eagerly importing every adapter;
use PEP 562 __getattr__ so registry.py lazy loaders stay effective and
optional framework deps are not pulled in on package import.
- _async_bridge.py: add ContextVar-scoped bridge with public current_bridge()
and scoped_bridge() so servers can isolate per-session loops; module-level
_BG default preserved for full backward compatibility.
- cli/main.py: collapse 17 duplicated PRAISONAI_AVAILABLE guard blocks into a
single _require_agents() helper (DRY).
Co-authored-by: MervinPraison <[email protected]>
Addresses Greptile P1: in an editable/source checkout, read_code_fingerprint returned the git SHA as soon as git rev-parse HEAD succeeded, so the mtime fallback never ran. An in-place file overwrite that did not advance HEAD left both boot and disk fingerprints identical, letting the /model guard miss the skew it is meant to catch. Now the newest .py mtime (ns) is combined with the rev (<sha>+mtime:<ns>); detect_code_skew still shortens the leading SHA.
Co-authored-by: Mervin Praison <[email protected]>
The /model hot path lazy-imports praisonaiagents.gateway.*, but the
fingerprint only covered the praisonai wrapper package. An in-place
update to only the SDK package was invisible to the guard. Now
read_code_fingerprint() (with no explicit dir) fingerprints both
packages and combines them fail-open, so an update to either is caught.
Extracts the per-directory logic into _fingerprint_dir() helper.
Addresses Greptile P1 (SDK updates remain invisible).
Co-authored-by: Mervin Praison <[email protected]>
- Keep protocols.py protocol-shaped: move concrete read_code_fingerprint
(subprocess + os.walk) to the praisonai wrapper; core retains only the
pure detect_code_skew predicate (CodeRabbit P1, AGENTS.md).
- Fingerprint the praisonai wrapper package (the hot-path owner that
lazy-loads praisonai.gateway.*), not praisonaiagents (Greptile P1).
- Capture the boot fingerprint at BotSessionManager startup via
capture_boot_fingerprint() so an in-place update before the first
/model is still caught (CodeRabbit/Greptile P1).
- Preserve sub-second precision via st_mtime_ns (CodeRabbit).
- Strengthen tests: assert non-None real fingerprint and deterministic
mtime fallback; add wrapper test suite for fingerprint/capture/guard.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Detect in-place code updates to a running gateway and refuse hot
operations (/model) with a clear "restart required" message instead of
crashing with a cryptic ImportError from a first-use lazy import.
- core: pure detect_code_skew predicate + best-effort read_code_fingerprint
- wrapper: capture boot fingerprint and pre-flight check in handle_model_command
- fail-open and opt-out via session_manager.code_skew_guard
Co-authored-by: MervinPraison <[email protected]>
Patch _estimate_min_call_cost separately so the pre-call guard does not
short-circuit the second async call, letting it reach _execute_unified_achat_completion's
post-call ceiling enforcement. Assert achat_completion is awaited twice.
Co-authored-by: Mervin Praison <[email protected]>
Address the two latest Greptile P1 findings:
- Dispatcher "run close retry drops": when _close_run_safe returned False the
run id was retained in _task_runs but its task had already left
running_tasks, so the per-process cleanup loop never revisited it and the
open run / stale current_run_id lingered until shutdown. Add
_retry_pending_run_closes(), run at the start of each cleanup cycle, to
close such orphaned runs (close_run already rejects finalized runs, so a
genuinely-completed run is not overwritten).
- Store "done task reblocked": record_failure() now no-ops on a terminal task
(done/archived) so a stale process-exit handler cannot increment the failure
counter or re-block a task that kanban_complete already finished.
Tests: TestTaskRunsAndRetry 15/15 pass (adds terminal-task no-op test);
dispatcher retry helper validated.
Co-authored-by: Mervin Praison <[email protected]>
Addresses 3 latest Greptile P1 findings:
- Drop pre-existing non-tenant-scoped idx_tasks_idempotency during migration
so two tenants on the same board can no longer collide on the same key.
- record_failure normalizes a persisted max_retries=0/negative to the board
default so legacy/migrated tasks are not auto-blocked on first failure.
- close_run rejects already-finalized runs (ended_at set) so a stale
current_run_id cannot rewrite a failed attempt as completed.
Adds 3 unit tests covering each fix.
Co-authored-by: Mervin Praison <[email protected]>
Address the remaining Greptile P1 findings on PR #2408:
- kanban_complete: move task to 'done' FIRST (durable terminal commit),
then persist the run/handoff. A move_task failure no longer leaves an
orphan completed run for a task that never reached done.
- dispatcher success path: move_task('done') before close_run('completed')
so a move_task failure cannot release an already-completed task for retry
(move_task clears claim_lock, so the guarded release_claim is a no-op on a
done task).
- dispatcher: stop popping _task_runs before close; peek and only forget the
run id once close persists, so a transient close failure is retried on a
later cycle / during _shutdown instead of leaving stale current_run_id.
- _close_run_safe now returns bool to drive the forget-on-success logic.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
- create_task: atomic idempotent create (catch IntegrityError + re-query),
tenant-scoped idempotency index/key, validate max_retries (<1/invalid ->
board default so a bad value can't auto-block on first failure)
- close_run: clear tasks.current_run_id (guarded WHERE current_run_id = ?)
so a closed attempt no longer reports as the active run
- move_task: clear claim_lock on terminal (done/archived) so finished tasks
are no longer shown as owned by a worker
- kanban_complete: close the active run instead of recording a duplicate;
fail-fast (no done transition) if the handoff run isn't persisted
- dispatcher: tri-state _record_failure_safe (None = persistence failure ->
do not blindly release for retry); skip spawn + release claim when
start_run fails (never spawn without a durable run); close open runs on
forced shutdown
- protocols: KanbanRunProtocol timestamps as ISO-8601 strings to match
TaskRun.to_dict()
- tests: tenant-scoped idempotency, invalid max_retries fallback,
move-to-done clears claim, close_run clears current_run_id
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
- Add task_runs table (one row per attempt) with start_run/close_run/get_runs
and tasks.current_run_id pointing at the active attempt.
- Add per-task circuit-breaker (consecutive_failures + max_retries with board
default) via record_failure(): auto-blocks a thrashing task instead of
respawning; counter resets on completion.
- Structured handoff: kanban_complete(summary=, metadata=) persists onto the
run; add kanban_runs tool + get_retry_context so retrying workers read prior
outcomes/summaries/errors.
- Idempotent create: create_task(idempotency_key=) / kanban_create dedups
repeated creates on the same board.
- Dispatcher opens a run per claim and closes it with outcome; uses
record_failure for auto-block-after-N.
- Backwards-compatible schema migration for existing SQLite DBs.
- Core: add KanbanRunProtocol/KanbanRunsProtocol + run/retry fields to task
protocol shape.
Co-authored-by: MervinPraison <[email protected]>
Addresses reviewer findings (CodeRabbit, Greptile) on PR #2407:
- reclaim_stale_claims: add version guard so a heartbeat racing between
SELECT and UPDATE is not clobbered (prevents double-run); clear
last_heartbeat_at on reclaim.
- release_claim: only revert to 'ready' when still 'running' so a late
cleanup release cannot requeue an already done/blocked task; clear
last_heartbeat_at.
- dispatcher: never release a claim once a worker is spawned (isolate
post-spawn hook errors) to avoid duplicate runs.
- protocols: type lease timestamps as ISO strings to match to_dict().
- tests: add legacy-claim, done-task release, and heartbeat-race cases.
Co-authored-by: Mervin Praison <[email protected]>
Adds durable claim leases and automatic reclamation so a dead or hung
worker no longer strands its task in 'running' forever.
- Store: claim_task gains ttl_seconds + worker_pid lease; new heartbeat()
and reclaim_stale_claims() (PID-liveness + heartbeat-staleness sweep).
- Model/schema: claim_expires, worker_pid, last_heartbeat_at fields with
idempotent ALTER-TABLE migration for existing databases.
- Dispatcher tick now reclaims stale/crashed claims before dispatch,
records the spawned worker PID, and fires KANBAN_TASK_RECLAIMED.
- kanban_heartbeat tool now consumes heartbeats via store.heartbeat().
- Core: KanbanReclaimProtocol + lease/heartbeat task-contract fields.
Backward-compatible (defaults preserve prior behavior); no new deps.
Co-authored-by: MervinPraison <[email protected]>
Move the worker-busy gate ahead of the conversation_history pop in
_handle_undo_command so a running turn can no longer leave history changed
while the workspace is untouched (closing the latest Greptile P1 race).
Co-authored-by: Mervin Praison <[email protected]>
Under shed_oldest, if every queued waiter is already signalled but still
mid-cleanup, _shed_oldest_waiter() freed nothing yet the caller still
enqueued the newcomer, growing the wait set past queue_depth. Now it
returns whether a live waiter was actually evicted; when none can be
shed, the newcomer is rejected instead of overfilling the queue.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
- Real subprocess.TimeoutExpired now always re-raises (no longer masked by a
recorded reader error) so execute_command's kill path terminates the still-
running child instead of leaking it (Greptile P1 "Timeout cleanup skipped").
- Inherited-pipe drain timeout (direct child already exited) now raises a
dedicated _StreamDrainTimeout; execute_command reports a timeout WITHOUT
killing an already-exited / recycled PID (Greptile P1 "Exited process killed").
- Guard psutil.NoSuchProcess in the kill path.
- Tests updated/added: drain-timeout-no-kill, real-timeout-kills, and
read-error-still-kills coverage.
Co-authored-by: Mervin Praison <[email protected]>
execution_queue.get(timeout=0.5) removed the task from the queue before
the worker took processing_lock, leaving a window where _worker_busy()
could observe qsize()==0 AND current_task is None and let a workspace
rollback (/undo, /revert) race a turn about to write files. Now the
dequeue uses a non-blocking get_nowait() inside processing_lock so the
item leaving the queue and the current_task publish are atomic w.r.t. the
rollback gate, while the lock is never held across the poll wait.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Adds a regression test verifying that Agent-level config-derived options
(web_search/web_fetch/max_iter) are captured with real resolved values in
_panel_llm_kwargs, not None. Documents that config parsing happens before
the panel branch, closing the latest Greptile P1 false positive.
Co-authored-by: Mervin Praison <[email protected]>
The SIGTERM/SIGINT handler set uvicorn's should_exit only after running
forensic logging and subprocess.Popen. Under memory pressure or on a stuck
host that spawn could block before the server was told to drain, risking a
missed supervisor stop window (Greptile P1, server.py:4731). Flip should_exit
first; forensics remain best-effort afterward.
Co-authored-by: Mervin Praison <[email protected]>
- resume.rehydrate_session now resolves usage via read_session_usage so a
resumed globally-stored session restores the real cumulative totals instead of
an empty project shadow record (Greptile resume.py stale-usage finding).
- session list now reads persisted usage for global/backend SessionMetadata rows
so the Tokens/Cost columns and JSON output aren't blank (Greptile global-usage
finding).
- Add regression test test_rehydrate_prefers_store_holding_usage.
Co-authored-by: Mervin Praison <[email protected]>
The POST /hooks/<path> delivery path in _run_hook() called agent.chat
directly, bypassing the gateway-wide admission gate. A burst of distinct
hook requests could therefore exceed max_concurrent_runs and recreate the
inbound overload the gate guards. Enter the shared gate around the hook
run (mirroring the direct-WebSocket _run_session_queue path); rejection
surfaces a clean busy ack instead of running.
Co-authored-by: Mervin Praison <[email protected]>
The worker removed a task from execution_queue before publishing
worker_state['current_task'], so _worker_busy() could observe an empty
queue AND no current task in that gap and let /undo or /revert restore
the workspace just before the turn wrote files. Both sides now share
processing_lock so the busy check sees a consistent snapshot.
Co-authored-by: Mervin Praison <[email protected]>
Replace the unbounded t_out.join()/t_err.join() with joins bounded by the
remaining timeout budget (plus a small drain grace). If a background child
inherits stdout/stderr and keeps the pipe open after the direct child exits,
a reader thread can stay blocked in readline() indefinitely; the tool now
falls back to a TimeoutExpired result instead of hanging (Greptile P1).
Co-authored-by: Mervin Praison <[email protected]>
The raw signal.signal() fallback runs in a C-signal context; the
"Received shutdown signal" logger.info() call previously ran before the
forensic guard, so the fallback could deadlock in logging if a handler
lock was held when the signal arrived, missing the drain window. Move it
behind the guard so the fallback only flips should_exit.
Co-authored-by: Mervin Praison <[email protected]>
A negative max_concurrent_runs short-circuited to None (gate disabled) before
ConcurrencyLimitPolicy could validate it, so a numeric config typo silently
removed overload protection. Only an explicit 0/None now disables the gate;
negative values fall through to the policy which raises ValueError (fail-fast).
Co-authored-by: Mervin Praison <[email protected]>
- Wire _record_session_usage into custom-agent, profiled, and file-based
run paths so session-backed runs persist token/cost totals (was only on
direct-prompt branches).
- Make _record_session_usage tolerate a None output controller (profiled
paths) by falling back to typer.echo.
- _resolve_usage_store now prefers the store whose record already carries
usage metadata, so resuming a globally-stored session keeps accumulating
into the original record instead of a project-side shadow record.
- Add test for the global-store-split scenario.
Co-authored-by: Mervin Praison <[email protected]>
Registered panel presets dropped extra LLM options (base_url, api_key,
temperature, etc.) because create_panel_llm only forwarded extras from
dict descriptors. resolve_panel_config now normalizes an 'options' map for
both preset and dict paths, and create_panel_llm forwards it to the
aggregator LLM. Fixes Greptile P1 'Preset options dropped'.
Co-authored-by: Mervin Praison <[email protected]>
Greptile P1 (re-flagged): a transient DB/network blip permanently disabled
persistence for the adapter's lifetime. Init failures are now memoized only
for a bounded cool-down window (configurable via init_retry_cooldown, default
30s) so persistence recovers automatically once the backend is back, while a
down backend is still not hammered on every callback. Success clears the
cached failure. The adapter-level option is popped before forwarding kwargs to
backend store factories.
Co-authored-by: Mervin Praison <[email protected]>
When _communicate_streaming times out, a read error already recorded by a
reader thread (e.g. UnicodeDecodeError on invalid output bytes) is now
re-raised instead of being masked by the generic TimeoutExpired, so the
caller reports the real, more actionable failure (Greptile P1).
Co-authored-by: Mervin Praison <[email protected]>
- Mark worker busy atomically with queue dequeue so /undo and /revert can no
longer restore the workspace in the window between get() and current_task
assignment (Greptile P1).
- Thread configured checkpoints.storage_dir through every CLI entry point that
builds CheckpointsHandler (standalone `checkpoint` command group and
`praisonai run` auto-checkpoint/restore) so all paths read the same store
(Greptile P1 split-store).
- Add a regression test asserting the standalone checkpoint command honors the
configured storage_dir.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
A previous fix moved os.makedirs() into ShutdownForensics.__init__ to keep
the shutdown signal path free of filesystem I/O. But __init__ runs
synchronously during gateway startup, so a stuck NFS/FUSE/automount path
could hang startup before the server binds (the except clause catches
errors, not hangs). Run the directory prep in a bounded daemon thread; if
it doesn't finish within prepare_timeout, forensics degrades to a no-op
instead of wedging startup. Non-blocking on both startup and shutdown.
Addresses Greptile P1 (startup-can-block).
Co-authored-by: Mervin Praison <[email protected]>
Greptile P1: gateway doctor and gateway channels --probe reached
_probe_channels() without loading ~/.praisonai/.env, so ${VAR} tokens
stored there (e.g. by `praisonai onboard`) resolved empty and falsely
failed the credential check. Moved the env-file load into the shared
_probe_channels() helper so all three paths (doctor, channels --probe,
start --preflight) use identical runtime token resolution, removing the
now-redundant load in gateway_start.
Co-authored-by: Mervin Praison <[email protected]>
Greptile P1: enable_telemetry() previously only set the instance enabled flag
but left _is_monitoring_disabled()'s cached default-disabled value untouched.
Instrumentation paths (instrument_agent/instrument_workflow) gate on that
cached value and bailed out early, so hooks were never installed despite a
successful enable. Introduce a programmatic _MONITORING_OVERRIDE that
enable/disable_telemetry set; _is_monitoring_disabled() honors it (after the
explicit env opt-out check), so downstream gates reflect the new state.
Co-authored-by: Mervin Praison <[email protected]>
Record selected bundles at every nesting depth in resolve() so a nested
bundle's 'instruction' reaches to_prompt(), not only top-level bundles.
Co-authored-by: Mervin Praison <[email protected]>
Resolves the two outstanding Greptile P1 findings on PR #2455:
- _admission.py: under shed_oldest, when a waiter's eviction signal and its
semaphore acquisition resolve in the same scheduler wakeup, the eviction now
takes priority (guard on shed_event.is_set() instead of acquire_task in
pending). Previously the both-completed race admitted an evicted waiter and
could leak a slot during overload.
- gateway/server.py: the direct WebSocket message queue (_run_session_queue)
now enters the shared admission gate around agent.chat, so the
`praisonai gateway start --max-concurrent-runs ...` ceiling is enforced even
without a gateway.yaml. Rejections surface a clean busy ack.
- test_botos_admission.py: adds test_shed_signal_wins_when_slot_also_free, a
regression that drives admit() through the both-completed race and asserts the
evicted turn sheds with no capacity leak (verified to fail pre-fix).
Co-authored-by: Mervin Praison <[email protected]>
The first _validate_runtime_capabilities definition was shadowed by a
later, divergent definition via Python's method-resolution order. The
dead copy raised ImportError (fail-fast) while the live copy silently
returns, creating a maintenance hazard. Removed the dead block; the live
definition remains the sole implementation. Preserved _run_verification_hooks.
Co-authored-by: Mervin Praison <[email protected]>
test_aiui_datastore unit tests require praisonaiui from the ui extra;
main job only installed crewai/autogen and hit maxfail=10.
Co-authored-by: Cursor <[email protected]>
Run unit tests only in optimised main job (integration covered by
test-core-integration). Raise timeout to 30m, use xdist loadfile, drop
noop api ignore and align langflow exclusion with test-core shards.
Co-authored-by: Cursor <[email protected]>
Only reset praisonai.cli.app state when the module is already loaded
instead of importing it inside the fixture, avoiding CLI import side
effects in unrelated tests and no longer swallowing real import errors
behind a broad except ImportError.
Co-authored-by: Mervin Praison <[email protected]>
Harden BasicSkillMutator so a pending dir carrying only a malformed
.proposal.json (no SKILL.md) can no longer be listed as approvable or
approved into place over an active skill. Mirrors the legacy no-proposal
guard. Adds a regression test for this edge.
Co-authored-by: Mervin Praison <[email protected]>
Addresses CodeRabbit: the env-var test mocked SQLiteKanbanStore, so
get_kanban_db_path() / PRAISONAI_KANBAN_BOARD handling was never exercised
and the test only proved the closure captured the mock. Now keep the real
store and mock only the path-resolution edge, asserting the constructed
store.db_path matches the env-var-derived path.
Co-authored-by: Mervin Praison <[email protected]>
- skill_manage: only surface/approve pending dirs that contain a real
SKILL.md so stray/temp directories cannot be approved into place
(greptile P1); align approve() malformed-proposal handling with
list_pending() (coderabbit).
- doctor CLI tests: keep portable [0,1,2] exit codes (typer vs argparse
fallback) but require real output so a silent crash still fails.
- live-LLM tests (acp, self_improving_loop): also require OPENAI_API_KEY
alongside PRAISONAI_LIVE_TESTS so live runs skip instead of hard-failing.
- add unit tests covering pending-skill safety and malformed proposals.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
The EnhancedScheduledAgentExecutor in the praisonai wrapper scheduler was
never imported, exported, tested, or instantiated anywhere. The canonical
path uses the base ScheduledAgentExecutor with the gateway's inline
_deliver_scheduled_result delivery. Removing the dead module resolves the
structural drift with no behavioural change.
Co-authored-by: MervinPraison <[email protected]>
CI typer lacks CliRunner(mix_stderr=). Reset module-level Rich Console
and output controller between tests to stop closed-stdout flakes when
async_tui and Typer capture compete on the same xdist worker.
Co-authored-by: Cursor <[email protected]>
Greptile P1: registry.is_available() called create() which only caught
(ValueError, TypeError). A registered adapter whose constructor imports a
missing optional dependency would leak a raw ImportError to callers
(CLI --framework validation, doctor checks, pick_default, assert_framework_available)
before the friendly install hint could be produced. Now also catch ImportError
(covers ModuleNotFoundError) and treat the framework as unavailable.
Adds regression tests covering the contained-ImportError path.
Co-authored-by: Mervin Praison <[email protected]>
- get_install_hint: also catch ImportError/ModuleNotFoundError so a missing
optional dependency surfaced during adapter creation falls back to the generic
hint instead of escaping before the friendly message (Greptile P1).
- CLI + doctor fallback framework lists now include the built-in ag2 adapter so
--framework ag2 and agents.yaml ag2 validation work in the degraded path
(CodeRabbit).
- Tests: force install_hint=None so the generic fallback branch is actually
exercised (CodeRabbit).
Co-authored-by: Mervin Praison <[email protected]>
Add list_framework_choices() and get_install_hint() helpers as the single
source of truth for user-facing framework lists. Wire argparse choices,
doctor agents.yaml validation, and validators install hints through the
adapter registry so entry-point adapters are discovered automatically.
Co-authored-by: MervinPraison <[email protected]>
The xdist flake-fix set the module-level output controller to JSON mode
without restoring it, leaking state into sibling tests on the same worker.
Snapshot and restore the previous controller in a finally block.
Co-authored-by: Mervin Praison <[email protected]>
Use JSON output mode and a fresh CliRunner to avoid Rich panel I/O
racing with parallel workers (closed file flake on CI).
Co-authored-by: Cursor <[email protected]>
Bot commit re-added outdated suites (~32 failures). Keep them out of
the merge gate until tests are updated for current APIs.
Co-authored-by: Cursor <[email protected]>
Removed the global --ignore for tests/unit/jobs and tests/unit/sandbox
and added both directories to the subdirs shard paths so they execute in
the required Core Tests gate. Previously they were collected but never
run, hiding regressions (including sandbox security tests).
Co-authored-by: Mervin Praison <[email protected]>
Restores the type assertion implied by the test name
test_enable_security_injection_id_is_string so a non-string hook id
from enable_security() is caught (CodeRabbit).
Co-authored-by: Mervin Praison <[email protected]>
Adapter install hints, registry fallback, CLI messages and AutoGen
resolver errors referenced the deprecated praisonai[<framework>] extras.
After the Frameworks package split these must point to
praisonai-frameworks[<framework>]. The native PraisonAI adapter now
surfaces `pip install praisonaiagents` via its install_hint.
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
- Drop module-level slow marker from test_rate_limiter_concurrency so the
concurrency suite runs in the bots-gateway core shard (was collected but
deselected by -m "not slow"). Keeps allow_sleep for the timing fixtures.
- test_auth_functionality: snapshot/restore every LLM env var cleared by
_clear_llm_env_for_defaults so deletions no longer leak into later tests.
Co-authored-by: Mervin Praison <[email protected]>
Unit test jobs no longer inherit OPENAI_MODEL_NAME from secrets (which could differ); auth and resolver tests isolate model env vars for stable defaults.
- Add tests/unit/_dev/ to subdirs shard so test_parity_tracker.py runs in CI
(was silently dropped: subdirs listed dev/ not _dev/, root glob is non-recursive)
- Mirror provider-marker exclusions + tests/fixtures ignore in collect-only gate
so the early-failure check matches what the shards actually execute
- Declare explicit least-privilege permissions (contents: read) flagged by zizmor
Co-authored-by: Mervin Praison <[email protected]>
Expand test_permissions_help_lists_subcommands to assert all eight
permissions subcommands (list/allow/deny/ask/remove/reset/export/
import-rules) appear, addressing CodeRabbit's nitpick. Skipped the
suggested exit_code==0 assertions: in this Typer version, `--help`
on a subgroup reports CliRunner exit_code 1 (Exit(0) exception),
which is unrelated to the fix and would falsely fail.
Co-authored-by: Mervin Praison <[email protected]>
The warm-runtime fast path (_try_attach_runtime) reuses a cached agent and
does not carry a per-call thinking budget, so attach-eligible
`run --thinking ...` invocations silently dropped the requested setting.
Exclude thinking_budget from runtime_eligible (mirroring tools/approval/memory
per-invocation overrides) so those runs stay in-process where the budget is
applied. Adds a regression test.
Co-authored-by: Mervin Praison <[email protected]>
Addresses CodeRabbit findings on PR #2455:
- from_config_file() now passes max_concurrent_runs/queue_depth/overflow_policy
through to GatewayConfig (was silently dropping them -> legacy unlimited path)
- extract _stamp_admission_gate() helper; call from both start_channels and
_start_single_channel so a channel restarted during hot-reload still enforces
the global concurrency ceiling
- test import skip narrowed to ModuleNotFoundError (real regressions surface);
admission primitives imported unconditionally
- replace fixed asyncio.sleep with explicit steady-state wait (CI-robust)
Co-authored-by: Mervin Praison <[email protected]>
Address 3 P1 review findings on gateway inbound admission control:
1. shed_oldest now actually evicts the oldest in-progress waiter (FIFO
ticket registry + per-waiter shed Event raced against slot acquisition),
so the wait queue can no longer grow unbounded past queue_depth.
2. CLI admission flags (--max-concurrent-runs/--queue-depth/--overflow-policy)
are now honoured in no-config (WebSocket-only) startup mode, not just
when --config is passed.
3. Invalid gateway admission config is now fail-fast (raises ValueError to
abort startup) instead of silently swallowing the error and starting the
gateway with no gate (unbounded inbound runs).
Adds regression test for shed_oldest oldest-waiter eviction.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
The lazy command loader always called typer.main.get_command() which
expects a Typer instance and accesses Typer-only attributes such as
_add_completion. The permissions command exports a plain Click group,
causing 'Group object has no attribute _add_completion' and making the
entire permissions command tree unreachable.
Special-case native click.Command/Group instances so they are returned
as-is instead of being passed through the Typer converter.
Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com>
Core (praisonaiagents): add GatewayConcurrencyPolicyProtocol, AdmissionDecision
and a config-driven ConcurrencyLimitPolicy; add max_concurrent_runs/queue_depth/
overflow_policy to GatewayConfig.
Wrapper (praisonai): add AdmissionGate (asyncio.Semaphore ceiling + bounded wait
queue) enforced in BotSessionManager.chat before the per-user lock; wire BotOS
constructor, YAML (gateway.*), and CLI (praisonai gateway) surfaces; thread the
shared gate into channel/bot session managers. Disabled by default (no gate)
so legacy immediate-dispatch behaviour is preserved.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: MervinPraison <[email protected]>
Collection gate fails fast; four unit shards run in parallel with -n 2
(no cov on PR); integration runs serially. Keeps a test-core summary job
for merge gate compatibility after the monolithic 45min timeout.
Co-authored-by: Cursor <[email protected]>
Use pytest-xdist (-n 4) and a 45-minute timeout so the full unit suite
completes; install pytest-cov for PraisonAIUI tests that inherit --cov addopts.
Co-authored-by: Cursor <[email protected]>
enable_telemetry() was gated by _is_monitoring_disabled(), which is an
opt-IN gate (disabled by default). That made programmatic enabling a
silent no-op unless PRAISONAI_TELEMETRY_ENABLED was set, contradicting
the docstring contract and disable_telemetry()'s symmetric behavior.
Add _is_telemetry_explicitly_disabled() that checks only opt-out flags
(DO_NOT_TRACK, *_DISABLED) and use it in enable_telemetry(), so telemetry
turns on unless the user explicitly opted out.
Addresses Greptile/CodeRabbit review feedback on #2447.
Co-authored-by: Mervin Praison <[email protected]>
enable_telemetry() referenced _is_telemetry_disabled() which is defined
nowhere, raising NameError on every call. Point it at the existing
_is_monitoring_disabled() gate used elsewhere in the module. No API or
behaviour change beyond restoring the intended capability.
Co-authored-by: MervinPraison <[email protected]>
The first definition (agent.py:4122-4138) was shadowed by the
comprehensive definition further down in the same Agent class, making
it unreachable dead code. Removed the shadowed copy; the live resolver
remains the sole implementation with identical behaviour.
Co-authored-by: MervinPraison <[email protected]>
- commands/deploy.py: use 'is not None' so --tag/--region/--project/
--resource-group preserve explicitly-empty values (CodeRabbit minor).
- features/deploy.py: tighten handle_deploy_command token parser to fail
fast on unknown flags and missing option values instead of silently
skipping them (CodeRabbit nitpick).
Co-authored-by: Mervin Praison <[email protected]>
Replace sys.argv mutation + legacy PraisonAI().main() dispatch in the
Typer deploy commands with a thin handle_deploy_command() wrapper over
the canonical DeployHandler, mirroring the serve command pattern. Avoids
importing the 7k-line cli/main.py and propagates real exit codes.
Co-authored-by: MervinPraison <[email protected]>
Delete four dead-code modules under cli/features/ that had zero inbound
references across src/ and tests/. Each was a near-duplicate of a
properly wired counterpart:
- recipes.py -> commands/recipe.py (recipe_optimizer, praisonai.recipe.*)
- plugin_cli.py -> commands/plugins.py (praisonaiagents.plugins.*)
- profiling.py -> cli_profiler.py (chat/code/run)
- profile.py -> features/profiler/ package (commands/profile.py)
No command, flag, or import changes; all user-facing commands
(recipe, plugins, profile, --profile flag) are unchanged.
Co-authored-by: MervinPraison <[email protected]>
- Move diagnostic dir creation to ShutdownForensics.__init__ so the SIGTERM/
SIGINT signal path never blocks on os.makedirs (slow/hung FS like NFS) before
drain (CodeRabbit/Greptile P1).
- Widen ShutdownForensicsProtocol.spawn_diagnostic log_dir to Optional[str] to
match the wrapper/server contract (CodeRabbit).
- Normalise gateway.forensics.enabled so env-substituted YAML strings like
"false"/"0"/"off" disable forensics instead of being truthy (CodeRabbit/
Greptile P2).
- Skip forensics (logging/subprocess) on the raw signal.signal fallback path to
avoid C-signal-context re-entrancy; only request exit there (Greptile P1).
- Add 3 wrapper tests: dir prepared in __init__, no makedirs on signal path,
no-op when dir unprepared.
Co-authored-by: Mervin Praison <[email protected]>
- CLI: wire 'praisonai skills bundle list/show' into the argparse skills
route (features/skills.py); the Typer commands were unreachable on the
legacy entrypoint (Greptile P1).
- Prompt: surface bundle-level 'instruction' via a <bundle_instructions>
block in SkillManager.to_prompt (Greptile P1).
- Agent: warn when a bundle member is not found instead of silently
dropping it (Greptile P1).
- Resolve: recursively expand nested @bundle members with cycle
protection (Greptile P2).
- Agent: honour discovery scope for bundle + member loading instead of
always include_defaults=True (Greptile P2).
- Tests: add coverage for nested bundles, cycles, and instruction
injection.
Co-authored-by: Mervin Praison <[email protected]>
Capture a fast, non-blocking forensic snapshot when the gateway receives
a termination signal so the next boot and the operator can see why the
previous instance died (OOM vs supervisor stop vs parent death).
- core: ShutdownForensicsProtocol + pure helpers (format_forensics_for_log,
is_supervised, drain_timeout_has_headroom) in praisonaiagents/gateway.
- wrapper: ShutdownForensics reads /proc, os.getrusage/getloadavg, and
spawns a detached diagnostic that survives SIGKILL on the process group.
- server: forensic SIGTERM/SIGINT handlers + startup headroom warning when
the supervisor stop-timeout is shorter than drain_timeout + headroom.
Co-authored-by: MervinPraison <[email protected]>
A fallback skill (fallback_for_tools/servers) must still satisfy its own
requires_* requirements before being offered, even in non-strict
enforcement. Previously the requires_* gate only ran in STRICT mode, so a
fallback whose required tool was absent could leak into the prompt. Use
has_critical_missing so missing required tools/servers hide the fallback
in WARN/non-strict modes too (where state is only DEGRADED).
Addresses CodeRabbit (Major) and Greptile (P1) review findings.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Introduce skill bundles — named, reusable sets of skills selectable as
one unit via a '@bundle' marker across Python, YAML, and the CLI.
Core (praisonaiagents):
- BundleManifest model + discover_bundles() in skills/bundles.py,
mirroring discovery.py (forgiving, precedence-with-logging).
- SkillManager.add_bundle/get_bundle/discover_bundles/resolve to expand
'@bundle' selectors into member skills, plus add_skill_by_name to
materialise members by name. Members flow through the existing budget
+ prompt-injection path unchanged.
- Agent skill-init expands '@bundle' selectors; plain paths/names
pass through exactly as before (backward compatible).
Wrapper (praisonai):
- 'praisonai skills bundle list|show' subcommand.
Tests: 18 new bundle tests; all 226 skills tests pass.
Co-authored-by: MervinPraison <[email protected]>
Skills can now declare fallback_for_tools / fallback_for_servers in
frontmatter. A fallback skill is surfaced only when its target capability
is absent, composing with the existing requires_* gates.
Co-authored-by: MervinPraison <[email protected]>
- _delivery: only penalise the rate limiter on a server-mandated wait, not
on generic backoff after ordinary transient errors (Greptile P1 + CodeRabbit)
- _rate_limit: anchor refill clock to a global penalty's end so queued sends
stay staggered instead of firing together when the hold-off expires (CodeRabbit)
- _outbox: persist the structured server Retry-After hint from the live
exception into the stored error text so the durable retry gate honours it
(Greptile P1 + CodeRabbit); surface transient drain failures via a
recoverable _TransientDeliveryError so the hint is not lost
- tests: assert deliver_with_retry sleeps the mandated 5s, fix RUF059 unused
var, add OutboundQueue.drain gating coverage
Co-authored-by: Mervin Praison <[email protected]>
Address reviewer findings on PR #2430:
- Greptile P1: load ~/.praisonai/.env before the start preflight so
${VAR} tokens stored there resolve (mirrors GatewayHandler.start),
preventing false credential rejection at launch.
- CodeRabbit: bound each probe with asyncio.wait_for in BotOS.probe_all
and CLI _probe_channels so a stuck adapter cannot hang preflight;
timeouts surface as failed ProbeResults.
- Tests: assert success identity in render output, add CLI coverage for
`start --preflight` fail-fast and `--no-preflight` skip.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Hardens test_async_records_cost_and_enforces_ceiling per CodeRabbit review:
ensures the pre-call projection (not just post-call accounting) blocks the
over-budget second call by asserting achat_completion was awaited exactly once.
Co-authored-by: Mervin Praison <[email protected]>
Outbound send/retry paths now extract the server-mandated wait
(Telegram parameters.retry_after / HTTP Retry-After header, with a text
fallback) via a new server_retry_after() helper and honour it over the
generic exponential backoff. The RateLimiter gains an adaptive penalise()
that widens the affected channel (or global) lane for the window so the
next sends do not immediately re-trip the platform's throttle.
Co-authored-by: MervinPraison <[email protected]>
Add operator-facing channel credential checks so bad/expired tokens fail
fast with a precise per-channel verdict instead of a silent reconnect loop.
- BotOS.probe_all(): concurrent aggregate over each bot's probe()
- gateway doctor: probe every configured channel, exit non-zero on failure
- gateway channels --probe: same verdict alongside the channel listing
- gateway start --preflight/--no-preflight: fast pre-flight gate that aborts
on invalid tokens before launch (default on, config mode only)
Wrapper-only; reuses the existing core BotProtocol.probe()/ProbeResult
contract. Lightweight Bot instances are built per channel (no agent needed),
building the adapter lazily to call the platform identity API.
Co-authored-by: MervinPraison <[email protected]>
The gateway hello handler built a features map and read the client's
advertised capabilities but never persisted the negotiated protocol
version or the client's capabilities on the session. Store both on the
GatewaySession (with read-only properties and to_dict/from_dict
persistence) so they survive resume and can be inspected server-side.
Co-authored-by: MervinPraison <[email protected]>
Bots and other async callers route through _execute_unified_achat_completion,
not sync _chat_completion. After PR #2370 fixed stream=None accounting on
the sync path, async astart/achat still bypassed pre/post budget guards,
so ExecutionConfig(max_budget=...) was not a hard cap for gateway bots.
Mirror the sync pre-call guard and post-call cost accounting on the async
unified dispatch path, and add regression tests.
Co-authored-by: Mervin Praison <[email protected]>
- Thread checkpoints.storage_dir through CheckpointsHandler -> CheckpointService
so a configured store is actually used by session checkpoints and code --revert.
- Fix revert() timeline trim off-by-one so repeated /undo walks back through
history instead of restoring the same checkpoint.
- Make _run() running-loop fallback safe by running the coroutine on a worker
thread instead of driving a second loop on a thread that already has one.
- Gate /undo and /revert workspace rollback while the execution worker is busy
or has queued tasks to prevent racing in-flight file writes.
- Tests: use monkeypatch for PRAISONAI_CHECKPOINTS, add storage_dir threading
test and a sequential-revert history-walk test.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Addresses CodeRabbit/Greptile review feedback on PR #2424:
- accumulate_session_usage now prices deltas from summary["by_model"] so
multi-model runs persist correct cumulative cost (falls back to CLI model).
- read/write usage now resolve the session via the same project->global store
search order as rehydrate_session, so usage for globally-stored sessions
accumulates into the right record after resume instead of splitting.
Adds 2 tests (12 total, all passing).
Co-authored-by: Mervin Praison <[email protected]>
Addresses reviewer findings from Greptile and CodeRabbit:
- shell_tools: join reader threads deterministically (no fixed 1s join) so
full output is captured before returning, matching communicate() semantics
- shell_tools: surface reader-thread exceptions instead of returning a partial
prefix as a successful result
- shell_tools: stop emitting progress on timeout; log swallowed emit/close errors
- events: tool_progress_channel(None) now clears any inherited sink so nested
no-callback execution cannot leak progress into a parent channel
- tool_execution: guard the per-attempt sink so a timed-out tool can no longer
emit TOOL_PROGRESS after the timeout result is decided
- tests: add stderr streaming, nested None-channel, and parent-restore coverage
Co-authored-by: Mervin Praison <[email protected]>
Accumulate per-call token usage (already aggregated by the core token
collector) into the active session, price it via the CLI cost tracker, and
persist cumulative cost/tokens in the session record so they survive resume.
- project_sessions.py: add accumulate_session_usage/read_session_usage/
format_usage_footer helpers that merge usage deltas into session metadata.
- run.py: record + display a compact usage footer after each prompt run.
- resume.py: rehydrate cumulative usage totals into RehydratedSession.
- session.py: add Tokens/Cost columns to `session list` and a usage line
to the resume panel.
Co-authored-by: MervinPraison <[email protected]>
Integrate the existing core CheckpointService into the `praisonai code`
session lifecycle so autonomous multi-file edits have an automatic,
turn-aware safety net:
- New SessionCheckpointManager (wrapper) maps session turns to checkpoints
via the existing CheckpointsHandler. Auto-checkpoints at session start
and before each file-mutating turn. Default-safe (disabled, zero overhead)
unless enabled.
- Interactive REPL: /undo now also rolls workspace files back to the
pre-turn checkpoint (with diff preview); new /revert [n] rolls back n turns.
- `praisonai code` gains --checkpoints (opt into auto-checkpointing) and
--revert <id|last> (one-shot restore with diff preview, then exit).
- Config-driven via a new `checkpoints` section (auto, storage_dir) plus
PRAISONAI_CHECKPOINTS env override; added to resolver known keys.
- Reuses checkpoints/service.py as-is — integration only, no new core deps,
no Agent changes.
Co-authored-by: MervinPraison <[email protected]>
Add a TOOL_PROGRESS stream event and a thread-local progress channel so
tools can emit incremental output while they run, instead of buffering
everything until they return.
- streaming/events.py: new TOOL_PROGRESS StreamEventType plus
emit_tool_progress() and tool_progress_channel() helpers (zero overhead
when no sink is active).
- agent/tool_execution.py: activate the progress channel around each tool
call and forward TOOL_PROGRESS emissions through the StreamEventEmitter.
- tools/shell_tools.py: read stdout/stderr line-buffered in background
threads, emitting progress per line while preserving the existing
buffered return value and truncation semantics.
Backward-compatible: no tool signatures change and behaviour is identical
when streaming callbacks are not registered.
Co-authored-by: MervinPraison <[email protected]>
The except handler only ever stores Exception (not BaseException) to avoid
poisoning init state on KeyboardInterrupt/CancelledError; tighten the field
annotation to match.
Co-authored-by: Mervin Praison <[email protected]>
- auto.py: restore sync _structured_completion working inside a running
event loop (FastAPI/Jupyter/async tests) instead of raising via run_sync;
offload blocking sync provider calls off the bridge loop via to_thread.
- db/adapter.py: narrow init memoization to Exception (no longer poison on
KeyboardInterrupt/CancelledError); route async init through the shared
_init_lock'd sync initializer to remove the sync/async init race.
- tool_resolver.py: key the tools/ directory scan cache by resolved path so
a reused resolver / cwd change resolves the correct tools tree.
Co-authored-by: Mervin Praison <[email protected]>
- auto.py: collapse duplicated sync/async structured-completion paths into a
single _completion_impl coroutine driven by _maybe_await; sync entry point
bridges via run_sync. Adds _get_async_openai_client mirroring the sync client.
- db/adapter.py: add async-safe _ainit_stores (asyncio.Lock + asyncio.to_thread)
so async callbacks never block the event loop on a threading.Lock or eager
sync I/O; extract _build_stores; memoize init failures via _init_failed.
- tool_resolver.py: cache the tools/ directory scan (mirrors tools.py cache)
with invalidate_local_tools_dir(); warn when local tools override chain hits.
Co-authored-by: MervinPraison <[email protected]>
Add missing toml dependency for config resolver imports, correct stale
praisonai.praisonai test import paths, and align provider/integration
tests with current registry and BaseProvider APIs.
Co-authored-by: Cursor <[email protected]>
An explicit AUTOGEN_VERSION/autogen_version pin of v0.4 or ag2 no longer
silently selects v0.2 when the requested adapter is unavailable. resolve_alias()
now raises an actionable ImportError so a workflow depending on v0.4/AG2 APIs
surfaces the mismatch rather than running under a different runtime. Auto
selection still falls back to the first available variant.
Co-authored-by: Mervin Praison <[email protected]>
- resolve_alias() now accepts config and prefers config/YAML autogen_version
over AUTOGEN_VERSION env so workflow-specified pins win
- is_available() uses registry selectability (registered AND available) instead
of raw v2/v4/ag2 probes, keeping router availability consistent with resolve()
Co-authored-by: Mervin Praison <[email protected]>
Reference models in a PanelLLM were built with only base_url/api_key/api_version, so a panel using subscription credentials (auth=claude-code) authenticated the aggregator but silently fell back to default credentials for references, making them fail and collapse into unavailable notes. Add auth to the reference connection-kwargs allowlist so advisory calls authenticate the same way as the aggregator.
Co-authored-by: Mervin Praison <[email protected]>
The AutoGenFamilyAdapter (registered as `autogen`) advertised
`praisonai-frameworks[autogen]`, a non-existent install target. Since
get_install_hint() consults the adapter's install_hint first, this
resurfaced the CodeRabbit non-existent-package finding for the autogen
framework. Align it with the v0.2 concrete adapter (praisonai[autogen]).
Co-authored-by: Mervin Praison <[email protected]>
The panel branch only forwarded base_url/api_key; normal model paths also
preserve auth, metrics, max_iter, web_search, web_fetch, prompt_caching,
and claude_memory. Capture these at construction (auth is not retained as an
Agent attribute) and forward them via _ensure_llm_instance so a panel
aggregator is configured identically to a regular model selection.
Co-authored-by: Mervin Praison <[email protected]>
- get_install_hint(): drop the non-existent praisonai-frameworks[...] target
and lead with the real praisonai[...] extra so users aren't pointed at an
uninstallable package.
- framework_option_help()/run._framework_help(): narrow blind `except Exception`
to `ImportError` so real registry bugs surface instead of silently reverting
to the static framework list.
- test_framework_entry_point_discovery: remove dead `_Prep` helper class.
Co-authored-by: Mervin Praison <[email protected]>
Reference models were created as LLM(model=model) with no base_url/api_key,
so a panel with a custom backend would route advisory reference calls to the
default provider (and silently fold auth failures into an 'unavailable' note).
PanelLLM now captures connection kwargs (base_url, api_key, api_version) and
applies them when building reference LLMs, matching the aggregator backend.
Co-authored-by: Mervin Praison <[email protected]>
Narrow the broad except in validate_workflow_framework() to ImportError so
genuine entry-point discovery / registry initialization failures propagate
instead of being masked as a generic unsupported-framework ValueError.
Co-authored-by: Mervin Praison <[email protected]>
- Include workflow name/file_path in the unsupported-framework ValueError
so included-recipe failures are traceable (CodeRabbit).
- Re-apply _validate_framework() in WorkflowManager.execute/aexecute, which
iterate steps directly and bypassed Workflow.run() (Greptile P1).
- Add tests for empty-string framework, error identity, and the manager path.
Co-authored-by: Mervin Praison <[email protected]>
- runtime_checks: _runtime_usable no longer falls back to raw package
probe for unregistered frameworks; only ImportError of the registry
module triggers the package fallback (CodeRabbit/Greptile P1)
- autogen_adapter: resolve_alias() gates v0.4/ag2 on registry
registration+availability, falling back to v0.2 or raising an
actionable ImportError instead of returning an unregistered alias
that made registry.create() fail opaquely (CodeRabbit outside-diff)
- tests: add entry-point discovery test that exercises the
importlib.metadata.entry_points path without manual register()
Co-authored-by: Mervin Praison <[email protected]>
- Detect panel descriptor inline in Agent.__init__ to avoid eagerly importing
the heavy llm.llm module on every Agent() (hot-path regression).
- Forward Agent-level base_url/api_key and dict-descriptor extras to PanelLLM
so custom endpoints/credentials reach the aggregator.
- Validate config value types (references/aggregator must be non-empty strings,
enabled must be a bool) before constructing LLMs.
- Bound the per-turn reference cache with FIFO eviction to prevent unbounded
memory growth on long-lived agents.
- Sanitize reference-failure notes: log exception type only, never inject raw
provider error text into the aggregator prompt.
- Clarify docstring: no presets are bundled; register panel:<name> first.
- Add tests for type validation, descriptor forwarding, and failure sanitization.
Co-authored-by: Mervin Praison <[email protected]>
- agents_generator: validate effective self.framework (CLI/Gradio), not just YAML field, in both async/sync workflow runners
- cli/main: add framework validation to `workflow validate` for run/validate parity; narrow registry-discovery fallbacks to ImportError so real discovery errors surface
- workflow_framework: derive remediation framework list from registry instead of hardcoding crewai|autogen
- config_checks: stop falling back to hardcoded framework list when registry unavailable
- tests: tighten AG2 built-in exclusion assertion
Co-authored-by: Mervin Praison <[email protected]>
AutoGen v0.4 and AG2 are intentionally unregistered from the CLI framework
choices (implemented=False). The doctor previously probed raw package
presence, so it could report these runtimes as "available" even though
`--framework autogen_v4/ag2` is rejected and the adapters raise
NotImplementedError. Doctor now consults the adapter registry's
is_available() (falling back to the raw probe only if the registry is
unavailable), keeping doctor in sync with runnable frameworks.
Co-authored-by: Mervin Praison <[email protected]>
Workflow YAML files parse `framework:` into Workflow.framework, but the
native execution engine never read it, silently ignoring values like
`framework: crewai`. Add a fail-fast `_validate_framework()` check at the
start of Workflow.run() that raises a clear ValueError for any non-praisonai
framework. Unset/empty/praisonai (case-insensitive) remain backward-compatible.
Co-authored-by: MervinPraison <[email protected]>
The router's run() omitted the keyword-only params (tools_dict, agent_callback,
task_callback, cli_config). Because is_router=True skips protocol validation, a
direct call with these kwargs raised TypeError instead of the intended actionable
RuntimeError. Match the protocol signature so the guard message is always shown.
Co-authored-by: Mervin Praison <[email protected]>
Introduces a PanelLLM (mixture-of-agents) provider selectable like any model
via panel:<name> or {"provider":"panel",...}. Reference models run first as
advisory, tool-free calls; their outputs are injected at the tail of the latest
user turn (preserving the cached system-prompt prefix) and a single aggregator
model acts in the normal agent tool loop. Includes per-turn reference caching,
partial-failure tolerance, strict-provider-safe trimmed view, recursion guard,
and enabled=false collapse to aggregator-alone.
Co-authored-by: MervinPraison <[email protected]>
- dispatcher: emit canonical hook id (kanban_task_moved) via HookEvent enum
instead of literal name so subscribers actually receive promotion events
- dispatcher: skip move event when promoted task can't be read back (no
empty {} payloads)
- store: guard recompute_ready promotion with conditional UPDATE
(status IN todo/blocked) to avoid overwriting concurrent newer state
- models: move recompute_ready off minimal KanbanStoreProtocol into a
separate KanbanPromotionProtocol extension
- tests: assert canonical event id + unreadable-task skip case
Co-authored-by: Mervin Praison <[email protected]>
Adds a dispatcher tick step that promotes dependency-driven tasks to
'ready' once all their parents reach a terminal state, making the
parent->child DAG self-driving.
- store: add recompute_ready() and get_ready_children() to SQLiteKanbanStore
- core: add KanbanPromotionProtocol extension (non-breaking, isinstance-safe)
- dispatcher: call recompute_ready() each tick before claiming and fire
KANBAN_TASK_MOVED hook events for promoted tasks
- tests: store promotion cases (linked-after-done, multi-parent, blocked,
parentless-ignored), dispatcher promotion tick, protocol check
Co-authored-by: MervinPraison <[email protected]>
Allow workflow_dispatch target_repo input so secrets can be copied to
PraisonAI-Plugins and other repos from the main PraisonAI secret store.
Co-authored-by: Cursor <[email protected]>
praisonai code resolved --agent (profile tools + permission scope) and
--thinking (reasoning effort) onto a local args namespace, then called
_run_chat_mode/_start_interactive_mode without persisting it. The agent
creation path reads these via self.args, so the flags were silently
dropped (Greptile P1). Persist the caller args at the start of both
entry points; run.py already set praison.args, so this is idempotent.
Co-authored-by: Mervin Praison <[email protected]>
Addresses reviewer feedback on PR #2400 (Issue #2393):
- whatsapp.py constructs HealthResult directly (not via _default_health),
so it omitted last_run_progress; an actively-streaming WhatsApp run with
active_runs>0 could still be classified STUCK and restarted mid-run
(Greptile P1). Now resolves it via _resolve_run_progress().
- StreamEventEmitter.emit/emit_async iterated _callbacks directly; the new
non-streamed progress callback is added from the event loop while the
agent emits from a worker thread, so concurrent add/remove could corrupt
iteration (CodeRabbit). Iterate over a list() snapshot.
Co-authored-by: Mervin Praison <[email protected]>
Address reviewer findings on PR #2401:
- Only refresh the cached/lock hash when the formatter exits 0, so partial
or corrupt output from a failed formatter is never adopted as the new
clean baseline (Greptile P1).
- Log non-zero formatter exits at debug level (CodeRabbit).
- Resolve project-local relative formatter commands (e.g.
./node_modules/.bin/prettier) relative to the edited file's dir
(Greptile P2).
- Run built-in prettier with --no-config --no-editorconfig so we never
execute repository-controlled config (CodeRabbit security).
- Make the missing-binary test deterministic via shutil.which stub and add
regression tests for hash refresh and failed-formatter baseline safety.
Co-authored-by: Mervin Praison <[email protected]>
- delivery.py: unwrap Bot wrapper to its adapter before dispatch so real
Telegram/Slack/Discord upload primitives are reached (Greptile P1); re-run
validate_media_delivery_path at the router boundary so direct callers cannot
bypass the denylist (CodeRabbit security).
- _outbound_messenger.py: stop re-captioning the first attachment with the
already-delivered body text to avoid duplicate text / caption-limit skips
(Greptile P2).
- _outbound_media.py: add credential-basename denylist (.env, id_rsa, .npmrc,
etc.) reachable from any directory (Greptile security); parse policy booleans
explicitly so quoted "false" disables uploads (CodeRabbit); preserve
@channelusername Telegram chat IDs instead of int()-casting (CodeRabbit);
inspect the send_media hook signature instead of swallowing TypeError
(CodeRabbit nit).
- tests: build missing-media path from tmp_path (S108), add coverage for
wrapper->adapter unwrap, basename denylist, quoted-bool parsing,
caption-less hook, and chat_id passthrough.
Co-authored-by: Mervin Praison <[email protected]>
- Replace shared _current_depth nonlocal with thread-local depth state so
concurrent/nested background subagents no longer corrupt each other's
max_depth budget (Greptile P1 / CodeRabbit). Parent depth is captured at
call time and restored on the worker thread.
- Catch worker/cancellation exceptions from get_result() in the wait=True
path of subagent_result and return the same clean failure dict the
non-blocking path returns, instead of re-raising (Greptile P2 / CodeRabbit).
- Add test for failed background job collection via wait=True.
Co-authored-by: Mervin Praison <[email protected]>
Address CodeRabbit review on PR #2397:
- current_epoch() now returns "" unless both boot_id and pid1_start are
present. A partial epoch (e.g. boot_id alone, unchanged across same-host
container restarts) could let a durable stale marker match a fresh
instance, breaking the restart-safe guarantee.
- DrainMarkerPolicy.drain_requested() now rejects markers with a non-string
action (e.g. {"action": 1}) instead of treating them as a drain request.
- Add regression test for the non-string action case.
Co-authored-by: Mervin Praison <[email protected]>
Prevents _find_project_config from walking to the filesystem root and
picking up unrelated ancestor configs (e.g. ~/.praison.json) in non-git
directory trees. Reuses the _MAX_WALK_UP_DEPTH=10 cap pattern from
integration/context_files.py for consistency. Adds a regression test.
Co-authored-by: Mervin Praison <[email protected]>
Wire the existing agent-control surface into the primary terminal commands:
- `praisonai code` gains `--agent/-a` (named profile with tools + permission/
mode scope) and `--thinking` (reasoning effort), mirroring `run` and `bot`.
- `praisonai run` gains `--thinking`, applied across the direct-prompt,
actions-mode, and custom-agent paths.
- Add `thinking_to_budget()` in features/thinking.py mapping
off/minimal/low/medium/high to the canonical token budgets, reused by both
commands; unknown values fail closed (consistent with MODE_RULES validation).
- Reasoning effort is applied via the core `Agent.thinking_budget` setter
(post-construction) since the constructor does not accept it as a kwarg.
Defaults are unchanged when the flags are omitted.
`--agent` resolution reuses the existing custom_definitions loader and
approval/permission engine, so a profile defined once in
.praisonai/agents/<name>.md behaves identically across code, run, and the
Python Agent(...) constructor.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: MervinPraison <[email protected]>
Add an opt-in post-edit formatting pipeline to EditTools, symmetric to the
existing post-edit diagnostics hook. After a successful edit_file/apply_patch
the touched file is optionally run through a language-appropriate formatter
(ruff format/black/prettier/gofmt/rustfmt), then re-read. Default is "off" for
byte-for-byte backward compatibility; a missing or failing formatter never
turns a successful edit into a failure. Formatter map is configurable via the
new `formatters` param and exposed through create_edit_tools.
Co-authored-by: MervinPraison <[email protected]>
Channel health's busy branch measured progress purely from inbound
transport activity, which never advances during a single agent turn. Any
run exceeding stuck_after (default 900s) was classified STUCK and the
channel restarted mid-run, dropping the in-flight turn.
Core: add HealthResult.last_run_progress and have evaluate_channel_health
use max(last_activity, last_run_progress) for the active-run branch, so a
run that is actively streaming/calling tools stays BUSY while a run that
emits nothing for stuck_after is still flagged STUCK.
Wrapper: BotSessionManager records note_run_progress() at run start, on
each streamed event, and on agent emitter events during non-streamed runs;
the protocol mixin surfaces it via last_run_progress in the health result.
Co-authored-by: Mervin Praison <[email protected]>
Trigger release gate on Core Tests success (push to main), with midnight
cron as backstop. Add dedupe, superseded-SHA guard, and PyPI idempotent
skip. Use pypi-auto for ci/nightly patch releases; keep pypi for manual.
Stop swallowing Core Tests failures so green runs are trustworthy.
Co-authored-by: Cursor <[email protected]>
Wire the existing BackgroundJobManager into spawn_subagent so an agent can
launch a long-running subtask, keep working, and collect the result later.
- spawn_subagent gains background: bool = False (default unchanged/sync)
- background=True enqueues on background.job_manager and returns a job_id
- new subagent_result(job_id, wait=False) companion tool to poll/await
- depth, allowed_agents, tools, llm, and permission_mode scoping preserved
off the main thread
Co-authored-by: MervinPraison <[email protected]>
The gateway's outbound binding silently dropped every media attachment, so an
agent could describe a generated file in text but never deliver it. Add real
outbound media delivery plus an exfiltration path guard.
- _outbound_media.py: validate_media_delivery_path() rejects credential/system
locations (/etc, ~/.ssh, ~/.aws, gateway secret/pairing dirs), resolves
symlinks first, enforces a size cap, and supports strict mode (allowlist
roots OR recent-mtime window); deliver_media_to_adapter() uploads via the
live adapter's native primitive (Telegram send_photo/send_document, Slack
files_upload_v2, Discord file send).
- DeliveryRouter.send_media(): resolve target + dispatch the upload.
- BotOutboundMessenger.send(): validate + upload each media path and report a
truthful delivered/skipped count instead of "media not attached".
- Core PlatformCapabilities.supports_media (protocol-only) for graceful
degradation.
Co-authored-by: MervinPraison <[email protected]>
- _should_attempt_auth_refresh now uses the pure classify_error_kind
classifier instead of resolve_failover_decision, which mutates the idle
timeout circuit breaker. resolve_failover_decision is still called for
the same exception inside _handle_retry_exception, so the previous code
double-counted idle timeouts (Greptile P1).
- _handle_retry_exception now re-applies refreshed_creds (api_key/base_url/
headers) after _build_completion_params rebuilds kwargs, so the failed
attempt's stale credentials no longer clobber the freshly refreshed ones
(CodeRabbit Critical).
- Refresh-failure log now includes auth provider context and a remediation
hint (CodeRabbit Minor).
Co-authored-by: Mervin Praison <[email protected]>
Adds a pure, testable DrainMarkerPolicy and a current_epoch() instantiation
helper beside ScaleToZeroPolicy/DrainTimeoutPolicy in gateway/protocols.py.
The policy honours a current-epoch drain marker and ignores missing,
epoch-less, or foreign-epoch markers so a marker surviving a reboot on a
durable volume never wedges a fresh instance in "draining" forever.
Co-authored-by: MervinPraison <[email protected]>
The flow integration code and `praisonai flow` CLI already shipped in the
wrapper and instructed users to run `pip install praisonai[flow]`, but the
[flow] extra was never declared in pyproject.toml. Add it with langflow +
requests so the documented install command works.
Co-authored-by: Mervin Praison <[email protected]>
HierarchicalConfig._find_project_config previously only checked the literal
working directory, so running the CLI from a subfolder lost the project's
.praison.json/praison.json living at the project root. Session scoping already
walks up to the git root (cli/utils/project.py), making config discovery
inconsistent with session discovery.
Add an opt-out `walk_up` flag (default True) that walks parent directories
until a config file is found, stopping at the git-root boundary so config never
leaks across unrelated projects. Nearest config wins. Backward compatible.
Co-authored-by: Mervin Praison <[email protected]>
The shared _handle_retry_exception performed the blocking credential
refresh (_refresh_subscription_creds -> provider.refresh network I/O)
inline, which blocked the event loop when called from
_call_with_retry_async. Split the blocking refresh out of the I/O-free
decision helper: the sync loop refreshes inline, the async loop offloads
via asyncio.to_thread, and both pass the result into
_handle_retry_exception. Behaviour is otherwise unchanged.
Co-authored-by: Mervin Praison <[email protected]>
Use the existing async execute_callback dispatcher in _call_with_retry_async
instead of execute_sync_callback. This runs sync callbacks in a thread executor
(never blocking the event loop) and also fires async-registered 'retry'
callbacks, which were previously skipped on the async path.
Addresses Greptile P1 and CodeRabbit major review findings.
Co-authored-by: Mervin Praison <[email protected]>
Addresses valid reviewer findings (CodeRabbit) now centralized in the shared
_handle_retry_exception helper so both sync and async paths benefit:
- Return immediately after a successful subscription credential refresh so the
refreshed profile is not subsequently marked failed and its fresh creds are
not overwritten by the rotate_profile branch.
- Set can_retry=False when rotate_profile has no alternate profile available,
avoiding wasted retries against the same failing profile.
- Add focused BLE001 noqa with rationale for the non-fatal refresh catch.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
The relocated test_db_integration.py returned booleans for pass/fail.
Now under the pytest-collected tests/ tree, pytest ignores return values,
so failing checks were silently reported as passing (Greptile P1).
Convert all checks to assert statements and update the __main__ script
runner to catch AssertionError, preserving script-style execution.
Co-authored-by: Mervin Praison <[email protected]>
Extract the shared, I/O-free retry/failover decision logic from
_call_with_retry and _call_with_retry_async into a single private helper
_handle_retry_exception. Each method keeps only its own await/sleep tail.
This removes ~95 lines of duplicated failover/error-handling logic that had
already drifted: the async path was missing the 'retry' callback the sync
path emits. The async path now fires execute_sync_callback('retry', ...),
closing the behavioural gap. All failover/retry policy and branches are
preserved verbatim.
Co-authored-by: MervinPraison <[email protected]>
email.py and agentmail.py read account via self._config.get("account", ...),
but _config is never assigned (config lives on self.config) and BotConfig is a
dataclass with no .get(). This raised AttributeError on every inbound message.
Aligns both adapters with the proven getattr(self.config, "account", "default")
pattern already used by the other five adapters.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Slack/Discord /new handlers (and pre-existing chat paths) read account via
self._config.get("account", ...), but these bots store config on self.config
and BotConfig is a dataclass with no .get() — a guaranteed AttributeError that
left the shared per_chat transcript active. Switch all three adapters to
getattr(self.config, "account", "default"), matching whatsapp's working
pattern, and add a regression test for the BotConfig account contract.
Co-authored-by: Mervin Praison <[email protected]>
chat() keys the shared per_chat session under the gateway account
({platform}:acct:{account}:chat:...), but the /new reset handlers in
telegram/slack/discord omitted account, so on a non-default account a
group reset computed the :acct:default: key and missed the live shared
session. Forward config["account"] into reset() in all three handlers
and add a test asserting a mismatched account is a no-op while the
matching account clears the shared transcript.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Addresses reviewer findings on PR #2377:
- CodeRabbit: include `account` in the shared per_chat storage key so two
gateway accounts on the same platform reusing a chat/thread id never collide.
- CodeRabbit: don't treat a missing chat_type as per_chat — _storage_key now
derives chat_type from chat_id via detect_chat_type so a DM reset(chat_id=...)
resolves to the sender key, not a shared key.
- Greptile: thread the chat route into /new reset() in telegram/slack/discord
so a group reset clears the shared per_chat session.
Adds tests for account namespacing, chat_id-only reset, and DM reset safety.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Group/channel chats can now route to a single shared session keyed by
(platform, chat_id, thread_id) so the agent follows one coherent
multi-party transcript, with each turn prefixed by the sender for
attribution. Defaults remain per_user (DMs always stay per_user), so
the change is fully backward compatible.
Co-authored-by: MervinPraison <[email protected]>
The cooldown bypass used finalClaudeCompletedOnSha which only checks the
FINAL trigger comment is present and not stale — not that Claude actually
finished and posted a verdict. This let the gate skip the 35-min cooldown
before the FINAL run was even visible. Switch to findMergeGateVerdict so the
cooldown is only skipped once a real MERGE_GATE_VERDICT exists on HEAD.
Co-authored-by: Mervin Praison <[email protected]>
- Remove redundant post-push buffer (CI green on HEAD is the gate)
- MAX_CANDIDATES=1 to avoid parallel merge race on main
- Retry merge up to 3x when base branch was modified
- Pipeline status sync dispatches merge gate for oldest merge-ready PR
- Align pipeline sync cron to every 10 minutes
Co-authored-by: Cursor <[email protected]>
Replace 4x-daily schedule with */10 cron so merge-ready PRs are picked up
within ~10 minutes. Do not block on 35min @claude cooldown once FINAL
review is complete on current HEAD. Disable cancel-in-progress so assess
jobs are not aborted by the next scan.
Co-authored-by: Cursor <[email protected]>
The websocket session drain in start_with_config received the full
drain_timeout again after stop_channels already consumed up to that
window, so total shutdown could reach 2 * drain_timeout. Track elapsed
across both phases and pass only the remaining budget to the websocket
session drain.
Co-authored-by: Mervin Praison <[email protected]>
- DrainTimeoutPolicy rejects non-finite (nan/inf) and non-numeric timeouts
- BotOS coerces YAML/env drain_timeout strings to float (_coerce_drain_timeout)
- drain() caps each sleep to remaining budget for fractional timeouts
- stop_channels() bounds TOTAL drain to drain_timeout across N bots
- server coerces drain_timeout_cfg once (string -> float, finite)
- CLI Ctrl-C preserves opt-in/disable semantics (no forced 10s)
- test import guard narrowed to ImportError
Co-authored-by: Mervin Praison <[email protected]>
Add a bounded, opt-in graceful-drain phase so SIGTERM/SIGINT (rolling
deploy, auto-update, host restart) no longer hard-cancels live agent
turns. Default off — preserves today's immediate-cancel behaviour.
Core (praisonaiagents): new lightweight, testable decision contract
DrainDecision / GatewayDrainPolicyProtocol with a config-driven
DrainTimeoutPolicy, mirroring the existing IdleDecision/idle-policy.
Wrapper (praisonai): BotOS gains a drain(timeout) coroutine that
quiesces ingress (accepting flag), polls _running_turns to zero up to a
deadline, flushes the outbox when supported, and returns the abandoned
turn count; stop() runs the drain first when a drain_timeout is set.
The gateway server drains channel bots + websocket sessions on shutdown
when gateway.drain_timeout is configured.
3-way surface: CLI `--drain-timeout`, YAML `gateway.drain_timeout`, and
Python `BotOS(drain_timeout=...)`.
Co-authored-by: MervinPraison <[email protected]>
Addresses reviewer findings (Greptile P1/P2, CodeRabbit Major/Minor):
- resolve_durable_store_dir() now scopes the canonical store by platform
(state/<platform>/) so bots on different platforms under the same
PRAISONAI_HOME no longer share one journal/DLQ. This prevents a DLQ
replay from consuming another platform's failed inbound events and
avoids dedup-tuple collisions across platforms.
- Sanitise platform into a filesystem-safe subdir.
- Drop the "Path" forward-ref annotation that Ruff flagged as F821.
- Tighten tests to assert exact journal/DLQ paths (override + default)
and per-platform isolation.
Co-authored-by: Mervin Praison <[email protected]>
The core send_message tool resolved an OutboundMessengerProtocol from
per-turn context but no concrete messenger was ever registered, so it
always returned "no gateway available". This adds the missing wrapper
binding:
- BotOutboundMessenger binds the gateway DeliveryRouter to the core
protocol, fulfilling send()/list_targets() by resolving symbolic
targets and dispatching through the live bot adapter.
- BotSessionManager registers/clears the messenger per agent turn,
scoped to the turn's origin, alongside existing session context.
- BotOS/Bot propagate the DeliveryRouter into each adapter's session
(lazy-built adapters spliced in Bot._build_adapter).
Additive and backward-compatible: with no router, behaviour is
unchanged.
Co-authored-by: Mervin Praison <[email protected]>
Wire the inbound journal (dedup + crash replay) and inbound DLQ by default
in build_session_manager, which every shipped adapter already calls, so a
gateway bot started via onboard/bot.yaml/Python survives crashes and webhook
redeliveries without losing or double-processing messages. All durable
components share one canonical per-agent SQLite store under ~/.praisonai/state/.
Adds a delivery.durable flag (default on) and delivery.store override to the
config schema. Backward compatible: durability falls back to in-memory if it
cannot be initialised, and can be disabled via delivery.durable: false.
Co-authored-by: Mervin Praison <[email protected]>
Addresses two Greptile P1 findings on the multimodal tool-output path:
- Message ordering: split format_tool_result_messages into
build_tool_result_message_pair returning (tool_message, followup_user_message)
so callers append all tool replies for an assistant turn consecutively and
flush media-bearing user messages only after the batch (provider contract).
Applied to llm.py (sync+async), openai_client.py (sync/async/stream), and
chat_mixin.py streaming history.
- Security/fencing: external/untrusted tool text parts are now wrapped with the
existing prompt-injection fence (wrap_if_external) via function_name, so
injected text cannot reach the model as unfenced user content.
Adds 5 regression tests (ordering + external/trusted fencing). All 23 pass.
Co-authored-by: Mervin Praison <[email protected]>
Address reviewer findings on PR #2369:
- Add apply_patch & edit_file to DEFAULT_DANGEROUS_TOOLS so they are
gated by default instead of running ungated.
- Consult the approval registry's is_required()/get_risk_level() in the
backend approval path so plugin/startup-registered requirements are
no longer skipped when an interactive ConsoleBackend is attached.
- Clear stale PRAISONAI_TOOL_SAFETY=off in `praisonai code` safe branch
so a prior --no-safe run cannot leave later safe-default agents
unguarded in the same process.
- Add regression tests for all three.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
- credentials.py: do not surface expired OAuth token after refresh failure
(return None so callers re-authenticate instead of injecting stale bearer)
- llm/credentials.py: stop leaking OAuth internals (refresh_token, token_url,
client_id) through the resolver boundary; rely solely on refreshed token for
OAuth env injection
- oauth.py: start a short-lived local HTTP callback server for the auth-code +
PKCE flow so wait_for_callback can unblock; graceful error when optional
'requests' is missing
- auth.py: emit a single JSON document on OAuth login, reject invalid --method
values, include expires_at in all-provider status, and validate OAuth with a
freshly-refreshed token
- tests: add coverage for refresh-failure -> None, no-secret-leak lookup, and
the callback server forwarding (state, code)
Co-authored-by: Mervin Praison <[email protected]>
When stream=None and the streaming-first attempt succeeded, _chat_completion
returned before post-call budget accounting. Cumulative max_budget enforcement
never advanced _total_cost, so agents could exceed their configured ceiling.
Fall through to the shared post-call budget/trace/hook path instead of
returning early, and add regression tests for the streaming auto-detect path.
Co-authored-by: Mervin Praison <[email protected]>
Safe-by-default: when an agent runs with no explicit approval kwarg
(approval=None) on an interactive TTY, dangerous built-in tools (shell
exec, file writes, etc.) are now routed through an interactive approval
prompt instead of running unguarded or being silently hard-denied.
- Core (agent.py): add Agent._is_interactive_session() TTY check and,
in the approval=None branch, default-attach the core ConsoleBackend
when interactive so dangerous tools reach the backend (ask). Non-
interactive (pipe/CI) keeps the existing deny-by-default preset.
Bypass remains PRAISONAI_TOOL_SAFETY=off / approval="bypass";
explicit approval=False keeps deny-by-default without prompting.
- Wrapper (code.py): flip --safe default to ON, add --no-safe and
--dangerously-skip-approval bypass (the latter also exports
PRAISONAI_TOOL_SAFETY=off so the core ask path is skipped).
- Tests: cover interactive ask, non-interactive deny, env bypass,
explicit approval=False, and read-only tools staying ungated.
Read-only built-in tools remain ungated. Fully backward compatible:
explicit --approval/permissions are unchanged; only the default for
dangerous tools flips from run/deny to ask when interactive.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: MervinPraison <[email protected]>
Add a structured, opt-in multimodal tool-result contract so tools (screenshot,
chart renderer, PDF rasteriser, MCP image tools) can hand images/files back to
the model as model-visible message parts instead of degrading to text.
- tools/base.py: add ToolResult.content + is_multimodal, and
multimodal_content/text_part/image_part/file_part helpers.
- agent/tool_execution.py: add format_tool_result_messages() which detects
multimodal results (ToolResult.content, bare part lists, single part dicts,
MCP image blocks), reuses data-URI/image_url encoding, bounds image size, and
emits a tool message + follow-up user message with image_url parts. Skip
trust-wrap/truncation for multimodal results.
- llm.py & openai_client.py: route tool results through the multimodal-aware
formatter; plain text/JSON tool returns are unchanged (no regression).
- chat_mixin.py streaming path made multimodal-aware.
Co-authored-by: MervinPraison <[email protected]>
The prior delegate-forwarding approach only overrode 4 protocol methods, but
HostedAgent inherits ~25 more members from AnthropicManagedAgent (_execute_sync,
update_agent, interrupt, retrieve_session, list_sessions, archive_*,
session_id/managed_session_id properties, vaults, ...). Those resolve on the
wrapper before __getattr__, so a delegated foreign backend still ran them against
uninitialised Anthropic state (Greptile P1).
Use __new__ to return the resolved backend instance directly for non-Anthropic
providers. Python skips __init__ when __new__ returns a non-cls instance, so the
caller gets the real backend and no inherited member can shadow it. Anthropic and
its subclasses still initialise in-place as HostedAgent via super().__init__.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
- Bind lazy availability flags in parse_args/handle_direct_prompt/
create_gradio_interface so bare-name reads no longer NameError when these
are reached without main() (e.g. `praison run`, library callers, tests).
- Widen ToolSource.lookup() return type to include _ResolveResult and add a
TYPE_CHECKING import for ToolRegistry so annotations type-check cleanly.
Co-authored-by: Mervin Praison <[email protected]>
- _media.py: add resolve_max_inbound_media_bytes() so an operator-set
max_inbound_media_bytes (incl. 0 to disable) carried via config.metadata
actually reaches the runtime adapters (core BotConfig has no such field).
- telegram.py / whatsapp.py: resolve the cap via the shared helper instead
of getattr on a field BotConfig never carries.
- telegram.py: for unknown-size inbound media, stream to disk via
download_to_drive() so the path-based cap rejects oversized files before
buffering them in RAM.
- _session.py: forward attachments to agents whose chat() accepts **kwargs,
matching the streaming astart() path (no longer silently dropped for
wrapper agents).
- tests: resolver override/disable, kwargs-wrapper attachment forwarding.
Co-authored-by: Mervin Praison <[email protected]>
Address reviewer (Greptile) findings on PR #2363:
- HostedAgent: foreign (non-AnthropicManagedAgent) backends now have their
ManagedBackendProtocol surface (execute/stream/reset_session/reset_all) and
arbitrary attribute access forwarded to the delegate, so inherited methods
never run against uninitialised state. Backend resolution uses an
issubclass() check so AnthropicManagedAgent subclasses are initialised
in-place via super().__init__ instead of taking the delegate path.
- ToolResolver: a source that raises during lookup now marks the negative
result non-cacheable, so a transient import/registry failure no longer
hardens into a persistent cached miss.
Co-authored-by: Mervin Praison <[email protected]>
The web-mode message path used self._config.get("account", ...), but
self._config is never assigned (only self.config exists) and BotConfig
is a dataclass without a .get() method, so the call raised AttributeError
before any agent turn. Align with the cloud media path's pattern.
Co-authored-by: Mervin Praison <[email protected]>
- whatsapp: use self.config (was undefined self._config) so media-bearing
messages reach the agent (CRITICAL)
- telegram/whatsapp: default media cap to DEFAULT_MAX_INBOUND_MEDIA_BYTES so
inbound media is enabled by default (core BotConfig lacks the field)
- _session: gate streaming astart() attachments on signature for backward compat
- _media: disambiguate RIFF families (WEBP/AVI/WAVE) via offset-8 tag; revalidate
redirect targets against the SSRF guard
- _config_schema: reject negative max_inbound_media_bytes (ge=0)
- telegram/whatsapp: clean up cached temp files after each turn
- whatsapp: check Graph metadata HTTP status and enforce a bounded download
- telegram: short-circuit oversized files via file_size; route native videos
- tests: RIFF disambiguation + negative-limit rejection
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
WhatsApp's custom health() omitted last_activity/active_runs and Linear's
webhook path bypassed fire_message_received, so both adapters could not feed
the new passive inbound-liveness signal to the health evaluator (Greptile P1s):
- _active_run_count() now resolves the session from _session OR _session_mgr,
so adapters using _session_mgr (WhatsApp/Linear) report in-flight runs DRY-ly.
- WhatsApp.health() surfaces last_activity (seeded from start time) and
active_runs, so a reachable-but-deaf receive path is subject to the
stale-socket check instead of being HEALTHY forever.
- LinearBot._handle_agent_message() refreshes inbound liveness via _note_inbound().
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
resolve_default_model() now only persists user-chosen models (explicit
--model and MODEL_NAME/OPENAI_MODEL_NAME env overrides). Provider-inferred
defaults and the gpt-4o-mini fallback are no longer written to the recency
store, so a later run re-evaluates credentials when the user's available
providers change instead of locking in a stale inferred default.
Addresses Greptile P1 "Stale Recent Model Wins" and CodeRabbit's matching
major finding. Also updates the docstring precedence to include the
MODEL_NAME/OPENAI_MODEL_NAME env step.
Co-authored-by: Mervin Praison <[email protected]>
Address reviewer findings on PR #2359:
- CodeRabbit (Major): the late rewrite in parse_args() turned any non-.yaml
positional into args.direct_prompt, so .yml paths and existing non-YAML files
were misclassified before main() could preserve them. parse_args() now keeps
existing files and .yaml/.yml paths as args.command.
- Greptile (P1): case-sensitive YAML suffix check let uppercase .YAML/.YML fall
through to a one-shot run. Both sites now use a case-insensitive check.
- CodeRabbit (nitpick): added tests that drive the real parse_args() path
(bare prompt, uppercase .YAML, .yml, existing file) instead of mocking it.
Co-authored-by: Mervin Praison <[email protected]>
Address reviewer findings (Greptile P1s, CodeRabbit) on PR #2360:
- protocols.py: a busy run with no inbound timestamp no longer stays BUSY
(non-recoverable) forever; falls back to uptime so STUCK can fire.
- _protocol_mixin.py: seed last_activity from start time so a reachable-but-deaf
channel is still subject to the stale-socket check instead of HEALTHY forever.
- health_monitor.py: parse numeric thresholds defensively (unresolved env
placeholders / empty / non-numeric fall back to defaults; clamp to lower bound
so negative stuck_after can't restart healthy long-running turns).
Co-authored-by: Mervin Praison <[email protected]>
- Per-stream adaptive interval (_current_min_interval) instead of mutating
shared StreamingConfig, so a flood in one stream cannot slow others
(Greptile P1 / CodeRabbit Major).
- finalize() now always edits the placeholder first (avoiding a stale
"Thinking..." draft) and falls back to a fresh send so the completed
answer is always delivered, even when progressive edits were disabled
(Greptile P1 x2).
- Sync StreamingConfigSchema with the new flood-control/reasoning fields
(CodeRabbit Major).
- Pass platform="telegram" at the production telegram callsite so
Telegram-specific recoverable errors are classified (Greptile P2).
- Annotate intentional broad adapter-boundary catches with noqa BLE001.
- Add tests for cross-stream isolation, placeholder reuse, and edit-fail
fallback.
Co-authored-by: Mervin Praison <[email protected]>
Three wrapper-layer extensibility gaps:
1. Framework auto-selection is now centralised on FrameworkAdapterRegistry
via pick_default(), so entry-point adapters can participate in default
selection. _entrypoint.run()/arun() delegate to it instead of a
copy-pasted hardcoded probe tuple. cli/main.py resolves the optional-dep
availability flags lazily so plain import (e.g. --help) no longer walks
the meta-path with find_spec() for every optional dependency.
2. ToolResolver now honours the sources= extension point: the default
resolution chain is built as an ordered list of ToolSource objects and
resolve() walks self._sources. Passing a registry auto-wires cache
invalidation (registry.set_resolver) so register_function() no longer
leaves stale caches silently.
3. HostedAgent provider->backend resolution is delegated to a new
ManagedBackendRegistry (praisonai.managed_backends entry-point group),
so third-party managed runtimes can be added without editing
hosted_agent.py. Existing anthropic-only ValueError semantics and
LocalAgent provider= DeprecationWarning are preserved for backward
compatibility.
All changes are local to the wrapper and backward-compatible.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: MervinPraison <[email protected]>
Gateway bot adapters dropped user-sent photos/documents/video instead of
passing them to the core Agent's existing vision support. WhatsApp even
substituted literal placeholder strings.
- Add shared praisonai.bots._media.cache_inbound_media() helper with
size cap, magic-byte content-type validation, and SSRF-safe URL fetch.
- Thread an `attachments` parameter through BotSessionManager.chat() to
agent.chat(prompt, attachments=...) (skipped when the agent's chat()
doesn't support it).
- Add max_inbound_media_bytes to the bot config schema (3-way surface).
- WhatsApp: download media by id (Graph API) and forward instead of
emitting "[Image received]" placeholders.
- Telegram: register a PHOTO/Document handler and download+validate
inbound media via the shared helper.
Co-authored-by: MervinPraison <[email protected]>
A bare positional argument that is not an existing file, not a
.yaml/.yml path, and not a known subcommand is now routed to
handle_direct_prompt so `praisonai "summarise this folder"` Just Works
instead of failing with "File not found". Existing agent-file and
subcommand behaviour is preserved.
Co-authored-by: MervinPraison <[email protected]>
Channel health previously relied almost entirely on an active outbound probe
(e.g. Telegram getMe), so a reachable-but-deaf channel was reported HEALTHY
forever and the BUSY/STUCK health reasons were never produced.
- core: evaluate_channel_health now takes stuck_after_seconds and inspects
HealthResult.active_runs — busy channels return BUSY (non-recoverable, never
restarted mid-run), busy-with-no-progress beyond stuck_after returns STUCK,
and idle channels with stale inbound activity return STALE_SOCKET.
- core: add HealthResult.active_runs field (in-flight agent turns).
- wrapper: MessageHookMixin refreshes a _last_inbound_activity timestamp on
every inbound message and _default_health reports last_activity + active_runs,
so all adapters gain passive inbound liveness DRY-ly.
- wrapper: HealthMonitorConfig gains a stuck_after threshold (Python/YAML/CLI)
wired into the monitor's evaluate call.
Co-authored-by: MervinPraison <[email protected]>
Core (praisonaiagents): Agent._get_default_model() now resolves a
provider-appropriate default from present credentials (OPENAI_MODEL_NAME >
first provider whose key is set > gpt-4o-mini) instead of always falling
back to gpt-4o-mini.
Wrapper (praisonai): new model_resolver with recency store under
~/.praison/state/model.json and a one-line first-run notice; chat.py now
resolves explicit > recent > provider-aware default.
Co-authored-by: MervinPraison <[email protected]>
Surfaces the existing load_context_files() loader in the unified CLI
interactive runtime so chat/run/code/tui agents auto-discover project
context (AGENTS.md/CLAUDE.md) via walk-up, bounded by a token budget and
opt-out via --no-context / PRAISON_NO_CONTEXT. Also fixes host_app to
walk-up auto-discover when no explicit context paths are given.
Co-authored-by: MervinPraison <[email protected]>
DraftStreamer now handles edit failures gracefully: recoverable
flood/429/transient errors widen the edit interval (capped) and, after
N consecutive failures, disable progressive edits and deliver the
completed answer as a single final send. <think>/<reasoning> spans are
stripped from streamed and final output so internal reasoning never
leaks to users. Reuses is_recoverable_error from _resilience.py.
Co-authored-by: MervinPraison <[email protected]>
<p><strong>v4.6.113</strong> — three Python packages, strict dependency direction, and <strong>C8 complete</strong>: reverse wrapper imports eliminated from <code>praisonai-code</code>. Tap the diagrams; tables hold the rules.</p>
<!-- /wp:paragraph -->
<!-- wp:quote -->
<blockquoteclass="wp-block-quote"><p><strong>One rule (unchanged):</strong><code>praisonai-code</code> never lists <code>praisonai</code> in <code>pyproject.toml</code>. Wrapper access is <strong>bridge-only</strong> via <code>_wrapper_bridge</code> — zero direct <code>from praisonai</code> imports remain.</p></blockquote>
<blockquoteclass="wp-block-quote"><p><strong>C8 change:</strong> 30+ commands are <code>_WRAPPER_RESIDENT</code> — implementation lives only in the wrapper; code package routes lazily with no duplicate modules.</p></blockquote>
<figureclass="wp-block-table"><table><thead><tr><th>Gate</th><th>Check</th><th>Status</th></tr></thead><tbody><tr><td>Hot path</td><td>No module-level wrapper import in main/app/run/chat/code</td><td>✅</td></tr><tr><td>Strict</td><td>Zero wrapper imports in app/run/chat/code</td><td>✅</td></tr><tr><td>Baseline</td><td>≤ <strong>50</strong> direct import lines (current: <strong>0</strong>)</td><td>✅</td></tr><tr><td>Allowlist</td><td>Any offender must be listed (list is empty)</td><td>✅</td></tr><tr><td>Smoke</td><td>Standalone <code>praisonai-code run</code></td><td>✅</td></tr></tbody></table></figure>
<figureclass="wp-block-table"><table><thead><tr><th>Milestone</th><th>State</th><th>Release</th></tr></thead><tbody><tr><td>C7 hot path</td><td>✅ Complete</td><td>—</td></tr><tr><td>C7.1 boundaries</td><td>✅ Complete</td><td>v4.6.110</td></tr><tr><td><strong>C8 reverse imports</strong></td><td>✅ Complete</td><td><strong>v4.6.113</strong></td></tr><tr><td>C8.4 full main.py split</td><td>⏳ Deferred</td><td>Future</td></tr><tr><td>PyPI splits (bot / frameworks)</td><td>⏳ Out of scope</td><td>—</td></tr></tbody></table></figure>
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: '@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'
-m "not slow and not network and not local_service and not provider_anthropic and not provider_google and not provider_ollama and not provider_grok_xai and not provider_groq and not provider_cohere"
-q --tb=line --disable-warnings
--timeout=60 -n 2 --dist loadfile
--ignore=tests/fixtures
--ignore=tests/e2e
--ignore=tests/live
--ignore=tests/unit/test_langflow_components.py
)
if [ "${{ matrix.root_glob || 'false' }}" = "true" ]; then
-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" \
-m "not provider_anthropic and not provider_google and not provider_ollama and not provider_grok_xai and not provider_groq and not provider_cohere" \
python -m pytest tests/unit/ \
-m "not slow and not network and not local_service and not provider_anthropic and not provider_google and not provider_ollama and not provider_grok_xai and not provider_groq and not provider_cohere" \
@@ -7,4 +7,5 @@ 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: core protocols in `praisonaiagents/`, agentic terminal CLI in `praisonai-code/`, bot/channel and heavy implementations in `praisonai/`
- Preserve old `praisonai.cli.*` import paths via shims when changing moved CLI code (see §2.3 in `src/praisonai-agents/AGENTS.md`)
| 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) |
| 3 | `src/praisonai/` | `framework_adapters/`, train, 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 |
The **agentic terminal CLI** (`run`, `chat`, `code`, warm runtime, CLI backends, Typer app) lives in **`praisonai-code`** (`praisonai_code`). **Bots, gateway, and channel CLI** live in **`praisonai-bot`** (`praisonai_bot`) after C9. The **`praisonai`** wrapper keeps integrations, framework adapters, train, serve, and dashboard. **`pip install praisonai`** still installs everything; old import paths are preserved via shims.
| Product | Location | Stays out of |
|---------|----------|--------------|
| Agentic terminal CLI | `src/praisonai-code/praisonai_code/` | Must **not** depend on `praisonai` (PyPI cycle) |
| Bot/channel runtime | `src/praisonai-bot/praisonai_bot/` | Must **not** PyPI-depend on `praisonai` or `praisonai-code` |
| `features/__init__.py``__path__` extension | Extracted features; five bot features stay local | Handler imports via `__getattr__` |
**Typer lazy-load:** `praisonai_code.cli.app._WRAPPER_COMMANDS` lists bot/channel commands that resolve via **`praisonai.cli.commands.*`** (wrapper), not `praisonai_code.cli.commands.*`.
**Monorepo bootstrap:** `praisonai._bootstrap.ensure_praisonai_code()` adds sibling `src/praisonai-code` to `sys.path` so `PYTHONPATH=src/praisonai-agents:src/praisonai` works without an explicit code entry.
**Install order (CI and local dev):** `praisonai-agents` → `praisonai-code` → `praisonai` (see `.github/actions/install-monorepo-packages`).
**Invocation paths that must remain valid:**
```bash
praisonai … # console script → praisonai.__main__
python -m praisonai …
python -m praisonai.cli.main …
python -m praisonai.runtime …
from praisonai.cli.main import PraisonAI
from praisonai.cli.commands.run import …
unittest.mock.patch("praisonai.cli.commands.run.X") # same module object
```
**Regression gate:** `src/praisonai/tests/unit/test_c5_backward_compat.py` (also run in optimised CI smoke). Standalone `pip install praisonai-code` requires declared deps (e.g. `toml` for config resolver).
system_prompt+=f"\n\nYou have access to the following tools: {', '.join(tool_names)}. Use these tools when appropriate to help complete your tasks. Always use tools when they can help provide accurate information or perform actions."
system_prompt+="\n\nExplain Before Acting: Before calling a tool, provide a brief one-sentence explanation of what you are about to do and why. Skip explanations only for repetitive low-level operations where narration would be noisy. When performing a batch of similar operations (e.g. searching for multiple items), explain the group once rather than narrating each call individually."
@@ -520,6 +524,12 @@ Your Goal: {self.goal}"""
cleaned_tools=sort_formatted_tools(cleaned_tools)
# Prune permission-denied tools from the advertised set so the model is
# only ever offered tools it can actually call. Mirrors the execution-time
# enforcement in tool_execution.py (_perm_deny / _perm_allow). No-op when
# no deny set and no allow set are configured (backward compatible).
# Display functions - lazy loaded to avoid importing rich at startup
# These are only needed when output=verbose
_display_module=None
@@ -191,6 +191,15 @@ class LLM:
# Ollama-specific prompt constants
OLLAMA_TOOL_USAGE_PROMPT="Please analyze the request and use the available tools to help answer the question. Start by identifying what information you need."
OLLAMA_FINAL_ANSWER_PROMPT="Based on the tool results above, please provide the final answer to the original question."
# Step-limit finalisation constants (used when the tool loop hits max_iter)
STEP_LIMIT_WRAP_UP_PROMPT=(
"You have reached the step limit for this task. Do not call any tools. "
"Provide a final answer that summarises what you accomplished, what "
"remains unfinished, and any suggested next steps, using the tool "
"results already gathered."
)
STEP_LIMIT_FALLBACK_MESSAGE="Reached the step limit before finishing this task."
# Force tool usage prompt template (used when model ignores tools)
FORCE_TOOL_USAGE_PROMPT="""You MUST use one of the available tools to answer this question.
@@ -923,6 +932,156 @@ Respond with ONLY a valid JSON tool call in this format:
Loaded 100 of 1103 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.