Lazy-load A2AClient via package __getattr__ so server-only A2A usage no
longer pulls in aiohttp at import time. Public surface unchanged:
A2AClient remains importable and listed in __all__.
Co-authored-by: MervinPraison <[email protected]>
Greptile P1: the instance-wide _draining flag turned every concurrent
failed-reply enqueue into a no-op during boot recovery, silently dropping
genuinely-new obligations and violating the durable-outbound contract.
Replace the boolean drain guard with a per-entry signature match so only the
nested re-park of the exact reply currently being redelivered is suppressed;
any other concurrent failure is still persisted. Also add the missing
typing.Any import (would NameError under get_type_hints).
Co-authored-by: Mervin Praison <[email protected]>
The reply path is already durable-by-default (permanent/exhausted sends are
parked in a canonical per-platform OutboundDLQ), but its crash-recovery had no
boot caller — the missing counterpart to the inbound journal's
_replay_inbound_journal sweep. A parked reply sat until an operator manually
replayed it.
- OutboundDLQ.redeliver(send): drains parked replies oldest-first through a raw
send, keeping + re-attempting on failure, with a _draining guard so a
re-failed send never double-parks.
- WebSocketGateway._replay_outbound_dlq(bot): outbound counterpart to
_replay_inbound_journal; redelivers via the adapter's own send_message with a
visible "recovered — may be a duplicate" marker, scheduled after the channel's
transport connects, on full boot and hot-reload.
Minimal and safe-by-default: no new params/modules/exports, nothing added to
core, success path unchanged.
Co-authored-by: MervinPraison <[email protected]>
The replay-safety gate only ran inside the overloaded/idle_timeout
branch of resolve_failover_decision. Post-dispatch failures whose
messages lack a timeout/overloaded/5xx substring (bare connection
reset, remote protocol error, incomplete/chunked read) classify as
"unknown" and were retried without the gate, risking double-execution
of side-effecting tool turns. Move the gate ahead of all retry
branches so any post-dispatch failure on a tool turn surfaces
provider_outcome_unknown instead of replaying.
Co-authored-by: Mervin Praison <[email protected]>
Post-dispatch transient failures (read timeout, connection reset
mid-response) may mean the provider already produced output and already
ran side-effecting tool calls in that turn. Blindly replaying such a
failure on a tool-calling turn can double-execute the turn.
Adds is_replay_unsafe() to error_classifier.py to distinguish
pre-dispatch failures (DNS/connect/TLS — safe to replay) from
post-dispatch failures (read timeout / connection reset — replay-unsafe),
and gates resolve_failover_decision so a post-dispatch failure on a
side-effecting turn (one exposing tools) surfaces a typed
'provider_outcome_unknown' outcome instead of silently retrying.
Default-safe and additive: tool-less turns and pre-dispatch failures
retry exactly as before; no new user-facing config surface.
Co-authored-by: MervinPraison <[email protected]>
Address reviewer P1 findings on the coherent undo/revert path:
- Interactive /undo /revert now rewind the reused warm Agent's own
chat_history in lockstep with the auxiliary conversation view, matched
by user-message count so interleaved system/tool messages don't skew the
boundary. Applies to both the modern REPL and the default AsyncTUI, which
each cache and reuse a single Agent across turns.
- praisonai run --revert rewinds a named session's conversation by real
user-turn boundaries instead of a fixed 2*n assumption, so turns that
persisted tool/system/assistant messages no longer strand orphaned
messages or drop an unrelated earlier turn.
- Make run --revert honest that file checkpoints are workspace-wide and may
not correspond exactly to a named session; point users to interactive
/undo for fully per-session coherence.
- Add unit coverage for agent chat_history rewind (with interleaved tool
messages), clear-to-preamble, and best-effort no-agent safety.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Wire the existing SessionCheckpointManager into the modern/default CLI
surfaces so /undo and /revert roll back files + conversation together,
and reconcile the split checkpoints.auto default.
- session_checkpoints: add a `default` param to from_config so one
checkpoints.auto key drives both file checkpoints and coherent revert
(interactive surfaces pass default=True; programmatic default stays False).
- modern repl.py: register /undo + /revert, checkpoint each turn, and rewind
the in-memory conversation via a thin store adapter.
- default AsyncTUI: register /undo + /revert and checkpoint file-mutating turns.
- run.py: add --revert (files + conversation) distinct from file-only --restore.
- tests: assert /undo restores both file and conversation on the default path.
Co-authored-by: MervinPraison <[email protected]>
- db/adapter.py: sync on_agent_start now passes an async-safe create_session
dispatcher so an async-mode store actually persists a new session instead of
dropping an un-awaited coroutine (Greptile P1).
- db/adapter.py: aon_agent_start/aon_agent_end now share the agent:{session_id}
state key and the end transition updates the started record in place, keeping
agent_name/metadata context instead of writing a disconnected record
(Greptile P2).
- api/agent_invoke.py: AgentRegistry.unregister uses a sentinel so a removal is
reported correctly even when the stored value is None/falsy (Greptile P2).
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
- api/agent_invoke.py: introduce thread-safe AgentRegistry (atomic pop
removes unregister TOCTOU 500) with optional per-embedding scoping via
a `registry=` kwarg; public functions stay backward-compatible.
- observability/hooks.py: memoize discover_observability_sinks() so the
importlib.metadata entry-point walk no longer runs on every run/request;
add reset_observability_sink_discovery() escape hatch.
- db/adapter.py: align PraisonAIDB.aon_agent_start/aon_agent_end with the
AsyncDbAdapter protocol (correct arg names/order, persist user_id instead
of dropping it); guard the sync on_agent_start against async-mode-store
coroutines via _resolve_sync to avoid the opaque 'coroutine not iterable'
crash.
Co-authored-by: MervinPraison <[email protected]>
Replace the duplicated public-callable extraction walk in the legacy
recipe/workflow tools.py path with the canonical owner in
praisonai_code.tool_resolver. Adds extract_functions_from_loaded_module
(single authority over the walk) and reuses it from both
ToolResolver.load_functions_from_module and workflow_commands, preserving
the PRAISONAI_ALLOW_LOCAL_TOOLS gate, safe-loader, own-module-origin
filter, and "Loaded N tools" notice.
Co-authored-by: MervinPraison <[email protected]>
- Core: FleetSupervisionPolicy no longer records events while cooling down and
clears its event window when the cooldown elapses, so the breaker re-arms
without an external reset() and held (non-)restarts cannot re-trip it.
- Monitor: evaluate the failing-channel fraction on every sweep (note_fleet_state
now has a live caller) and re-evaluate/clear the breaker inside the monitor
loop, so the degraded-owner fact clears without a status read.
- Monitor: a disabled per-channel restart budget (max_restarts_per_hour=0) no
longer counts idle channels as failing.
- Tests: fix CI regression in test_gateway_health_block_matches_runtime_consumer
(new fleet keys are genuinely consumed by from_dict), add monitor-loop cooldown
recovery coverage, tighten failing-fraction assertion, correct misleading comment.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Address reviewer findings on PR #3842:
- core: migrate_config_with_doctor now rejects a config stamped by a newer
build (version > GATEWAY_CONFIG_VERSION) and a malformed/boolean stamp via
new ConfigVersionError, so an older binary can never downgrade a newer
config or coerce config_version: true to 1. Exported from gateway package.
- wrapper: doctor guards the migration import (ImportError-safe for older core),
surfaces the version error as a warning without migrating, and rewrites
gateway.yaml atomically (tmpfile + fsync + os.replace) so an interrupted
--fix can't truncate the config. --fix help/docs document config migration.
- tests: newer-version rejection, boolean/non-integer stamp rejection, export.
Co-authored-by: Mervin Praison <[email protected]>
Per-channel restart budgets are blind to a systemic fault that restarts
every channel at once. Add a pure core FleetSupervisionPolicy breaker
(praisonaiagents/gateway) enforced by the wrapper ChannelHealthMonitor,
which holds restarts and records ONE gateway degraded-owner fact on the
shared DegradedCapabilityRegistry when the fleet thrashes. Wire the
gateway's shared registry into the single supervision owner and expose
gateway.health.fleet_* YAML thresholds.
Co-authored-by: MervinPraison <[email protected]>
Add a canonical GATEWAY_CONFIG_VERSION stamp and a declarative
LegacyConfigRule contract in core (praisonaiagents/gateway/config.py) with a
single migrate_config_with_doctor executor. Wire `gateway doctor` to detect an
out-of-date config and `--fix` to migrate it forward once and stamp the new
version, so cross-release upgrades no longer silently drift.
Co-authored-by: MervinPraison <[email protected]>
Address reviewer feedback on PR #3839:
- When litellm is installed but a capability helper is missing or raises,
preserve litellm as authoritative (return False) instead of overriding
with the static heuristic. Heuristics now run only when litellm is
genuinely absent (litellm is None), preventing unsupported params
(response_format / web_search_options) from reaching provider requests.
- supports_parallel_function_calling now uses a dedicated, narrower
heuristic instead of the general function-calling one, so serial-only
tool-calling families are not over-reported as supporting parallel calls.
- Add tests for installed-litellm error path and serial-vs-parallel.
Co-authored-by: Mervin Praison <[email protected]>
Capability helpers (supports_structured_outputs / function_calling /
parallel_function_calling / web_search / prompt_caching) delegated entirely
to litellm and returned False for every model when litellm was absent,
silently disabling structured-output / tool-calling / caching / web paths on
lean provider-native installs. Each helper now falls back to a small,
conservative, pattern-based static heuristic only when litellm is
unavailable. litellm remains authoritative when installed (no behaviour
change for existing deployments); no new deps, no network, no config knobs.
Co-authored-by: MervinPraison <[email protected]>
The before-tool injection hook allowed a tool call whenever no scanned
string matched. Because _extract_strings bounded the walk by bytes /
cardinality and returned a partial result, an attacker controlling
tool_input could pad benign strings ahead of an injection payload so the
payload was never reached, and the gate allowed the call (Greptile P1).
_extract_strings_bounded now reports whether the walk was truncated, and
create_hook blocks (fail-closed) on truncated, un-vettable input instead
of allowing it. _extract_strings is kept as a back-compat shim.
Co-authored-by: Mervin Praison <[email protected]>
- security/injection.py: _extract_strings walks iteratively bounded by bytes+
cardinality instead of a depth-4 cutoff, scans dict keys, and is cycle-safe so
nested tool inputs can no longer bypass the scanner.
- db/adapter.py: share one _serialize_tool_call helper across sync/async paths
that persists the full tool result verbatim (no silent [:1000] truncation).
- persistence/orchestrator.py: add aretrieve_knowledge/aadd_knowledge that
offload sync stores via asyncio.to_thread (and await native async stores) so
RAG no longer blocks the event loop.
Co-authored-by: Mervin Praison <[email protected]>
Wrapper BaseFrameworkAdapter._resolve_llm now delegates the spec->model-name
precedence to core's super()._resolve_llm instead of re-implementing it, then
derives base_url/api_key locally to build the PraisonAIModel provider object.
Removes duplicate logic and drift risk. Behaviour-preserving.
Co-authored-by: MervinPraison <[email protected]>
Move the workflow_runs page implementation into praisonai_bot where its
consumer (host_app.py) expects it, following the established bot_health
C9 shim pattern. The wrapper module becomes a backward-compat shim.
Co-authored-by: MervinPraison <[email protected]>
- Use self._hook_runner (agent-scoped registry) instead of the process-wide
default so agent-registered MODEL_FALLBACK hooks actually fire.
- Add async _aemit_model_fallback that awaits the runner; the async recovery
path no longer drops hooks via execute_sync() inside a running loop.
- Sync fallback now forwards the active stream callback so streaming consumers
receive the MODEL_FALLBACK stream event.
- Async fallback honours emit_events (no stream leak when emit_events=False).
- Extract shared _build_model_fallback_input / _emit_model_fallback_stream.
- Tests: agent-registry isolation, async hook dispatch, suppression.
Co-authored-by: Mervin Praison <[email protected]>
When the primary model becomes unavailable mid-turn and the runtime switches to
the next entry in fallback_models, the switch was invisible except for a log
line. Add HookEvent.MODEL_FALLBACK (+ typed ModelFallbackInput) and
StreamEventType.MODEL_FALLBACK, emitted at both switch sites in chat_mixin.py
via a guarded _emit_model_fallback helper. Zero overhead when nothing is
subscribed; provider internals stay redacted (only the failure class is
exposed). Follows the existing SESSION_PERSIST_FAILED observability precedent.
Co-authored-by: MervinPraison <[email protected]>
Resolve the shared background runner once and inject it into
BackgroundHandler in _handle_tasks_command so a single /tasks
invocation uses one instance, and harden the test fixture to own the
process-wide singleton lifecycle (bypassing the package's lazy
__getattr__) so a cross-file daemon can't empty the dedicated runner.
Co-authored-by: Mervin Praison <[email protected]>
Address greptile P1 review findings on tools list MCP discovery:
- Naming mismatch: the run path registers MCP tools by their bare __name__
(e.g. `read`), but `tools list` previously fabricated `<server>_<tool>`
names that would not resolve. Discovery now lists tools under the exact
runtime callable name, with cross-server duplicates disambiguated as
`name (server)` so nothing is hidden.
- Timeout blocking: each server is now discovered in a worker thread under a
hard per-server cap (_MCP_DISCOVERY_TIMEOUT_S), so a slow/unreachable server
is abandoned and reported as unavailable instead of stalling the listing.
Updated + added unit tests (runtime names, duplicate disambiguation, timeout).
Co-authored-by: Mervin Praison <[email protected]>
Add an `mcp` source to `praisonai tools list` so the terminal shows the
tools exposed by configured MCP servers alongside built-in, local,
external, and registered tools. Discovery is lazy (only when the default
view or `--source mcp` is requested) and fail-soft: an unreachable or
misconfigured server is reported as unavailable with a reason instead of
blocking the rest of the listing. Reuses the existing guarded
`MCPHandler.create_mcp_from_server` and `_collect_mcp_servers_from_config`
so no new dependencies or Agent params are introduced.
Co-authored-by: MervinPraison <[email protected]>
The two failing CLI tests (test_tasks_detail_and_cancel,
test_tasks_lists_background_tasks_in_repl) are a pre-existing cross-worker
flake unrelated to this PR's todo-streaming changes: under
`pytest -n 2 --dist loadfile` a background daemon from another file mutates
the process-wide BackgroundRunner singleton mid-assertion, so the handler
resolves a different runner than the test pinned.
Add an optional `runner=` dependency-injection seam to
`_handle_tasks_command` (None in all production call sites, so behaviour is
unchanged) and have the tests inject their dedicated runner directly. This
bypasses global-singleton resolution entirely, making the assertions immune
to the race. Verified deterministic across 500 iterations under active
singleton chaos.
Co-authored-by: Mervin Praison <[email protected]>
The async tool-execution path (_execute_tool_async_impl) never entered
tool_progress_channel, so emit_todo_update / emit_tool_progress always
returned False during async single-Agent runs — live TODO_UPDATED events
were invisible to subscribers. Mirror the sync execute_tool path: build a
progress sink from the existing stream emitter and activate the channel
around both the coroutine await and the run_in_executor call (set before
copy_context_to_callable so the contextvar propagates into the worker
thread). Zero overhead when no stream callbacks are registered.
Adds regression tests covering the TODO_UPDATED event, TodoTools emission,
the single-in_progress invariant, and async-path channel propagation.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Adds StreamEventType.TODO_UPDATED plus a small emit_todo_update() helper
that reuses the existing thread-local progress sink (already active around
every tool call). The built-in todo tool now publishes the full ordered
list on each add/update, so any frontend can render a live checklist.
Enforces the "exactly one in_progress" convention and consolidates the
planning TodoItem status vocabulary to include cancelled.
Co-authored-by: MervinPraison <[email protected]>
Per-request AgentsGenerators borrow the cached generator's tool-timeout
executor but previously kept leak accounting local, so a stuck sync tool
never incremented the owner's _leaked_workers and the shared pool was
never recycled — eventually starving all requests (Greptile P1).
Delegate _get_tool_timeout_executor/_note_leaked_worker/_timeout_owner_key
to the cached owner so leaks are counted on, and recycling happens on, the
generator that actually owns the pool.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
- serve agents: build a per-request AgentsGenerator that borrows the cached
generator's warm immutable pieces (adapter, config_list, tool resolver,
tool-timeout pool) instead of mutating shared cli_config under a global
lock, so concurrent requests are no longer serialised.
- persistence: add scan_prefix() to StateStore with a prefix-scoped default;
override with non-blocking SCAN on Redis and Upstash so get_runs/get_traces
never issue KEYS praison:* across the whole keyspace.
- security: split strict jailbreak patterns (auto-HIGH) from soft role-play /
imperative patterns (MEDIUM alone, HIGH only when combined), so benign
prompts like "You are now analyzing..." are not blocked while real attacks
("ignore all previous instructions", DAN) still block on a single hit.
Co-authored-by: MervinPraison <[email protected]>
Pre-existing xdist race (unrelated to PR #3810's agent/knowledge changes):
under -n 2 --dist loadfile a persistent worker's process-wide background
runner could interleave with these tests, surfacing as "No background tasks"
and an uncancelled RUNNING task. Add an explicit xdist_group so the file's
tests stay serial on one worker, and reset the module-level _shared_runner on
fixture teardown so no dedicated instance leaks into a later module. Assertions
are unchanged.
Co-authored-by: Mervin Praison <[email protected]>
cleanup_launch_registration filtered the shared FastAPI router by path
only, so closing an agent launched at a path that also serves a
different verb (e.g. built-in GET /health, GET /) removed that unrelated
route too. Filter on path AND POST method since launch() always
registers via .post(path).
Co-authored-by: Mervin Praison <[email protected]>
- Agent.close() now tears down the routes launch() actually registered
(module-level state in execution_mixin.py) instead of a disconnected,
never-populated ServerRegistry — closes the launched-endpoint handle leak.
- Remove dead Agent.checkpoints/Agent.background pass-through state and
properties that were never consumed by any code path (no live consumer),
keeping the surface lightweight.
- MongoDBKnowledgeAdapter.add/search now honor user_id/agent_id/run_id
scoping, matching SQLite/Chroma adapters, fixing the silent cross-tenant
data leak.
Co-authored-by: MervinPraison <[email protected]>
The /tasks CLI resolves the runner via ``from praisonaiagents.background
import get_background_runner``, which reads the name off the package (whose
__init__ exposes it through a lazy __getattr__), imported at call time by
BackgroundHandler._get_runner. The fixture only patched the ``runner``
submodule binding, so once another worker file materialised the package
attribute, the CLI bypassed the patch and resolved the process-wide shared
runner instead — letting a concurrent cross-file daemon race the two tests
under ``pytest -n 2 --dist loadfile`` (the observed "No background tasks"
and uncancelled RUNNING failures). Patch the name on the package too so the
CLI deterministically sees the dedicated per-test runner. Test-only; no
assertions weakened and no SDK behaviour changed.
Co-authored-by: Mervin Praison <[email protected]>
Move the byte-for-byte-identical _deliver_result and _build_delivery
methods from AgentScheduler and AsyncAgentScheduler up to the shared
_BaseAgentScheduler, mirroring the existing consolidation of
_should_suppress_delivery, _budget_exceeded and _build_stats. Removes
~55 lines of duplication and the drift risk on a safety-relevant
delivery path. No behaviour change: both methods are plain synchronous
methods referencing only attributes both subclasses set identically.
Co-authored-by: MervinPraison <[email protected]>
The two failing CLI tests (test_tasks_detail_and_cancel,
test_tasks_lists_background_tasks_in_repl) are a pre-existing shared
BackgroundRunner singleton isolation flake, unrelated to this PR's
gateway-only changes. Port the already-authored isolation fixture that
pins a dedicated runner per test and monkeypatches get_background_runner
to return it, so no other file's daemon loop can reach the instance
_handle_tasks_command resolves. Assertions (list, detail, real CANCELLED
status) are unchanged.
Co-authored-by: Mervin Praison <[email protected]>
The two test_interactive_tasks_command.py tests fail intermittently under CI's
pytest -n 2 --dist loadfile because the process-wide BackgroundRunner singleton
(and a live daemon loop another file left running on it) races these tests,
surfacing as "No background tasks" and an uncancelled RUNNING task. This is
unrelated to the scheduler-delivery changes in this PR.
The prior fix only reset _shared_runner, still sharing a process-wide object
other threads could touch. Instead, pin a dedicated BackgroundRunner per test
and monkeypatch get_background_runner to return exactly it, so no other file's
daemon can reach the instance _handle_tasks_command resolves. No assertions
weakened: list, detail, and the real CANCELLED status are all still verified.
Verified passing even with a pre-polluted singleton.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Addresses reviewer findings on PR #3806:
- Greptile P1 (blocking): the pin no longer permanently mutates the shared
registry Agent. _check_model_drift now returns a run-scoped restore callable
applied in a finally around the whole run (gate/policy/turn/exception paths),
so a scheduled job's model never leaks into attended turns on the same agent.
Extracted the run tail into _run_pinned for a single guaranteed restore point.
- CodeRabbit (Major): provider/model are normalized (embedded "openai/gpt-4o"
prefix lifted out) before comparison, so a provider-qualified pin and a bare
resolved model no longer false-drift; split provider snapshots compare equal
to embedded agent strings too.
- CLI: clarified --pin/--no-pin help — pinning enforces a snapshot only once one
exists via an explicit --model; kept the CLI lightweight (no agent/LLM
instantiation at creation time) per AGENTS.md.
Tests: added run-scoped-restore and provider-prefix-normalization cases; core
scheduler (127) and wrapper scheduler (274) suites pass.
Co-authored-by: Mervin Praison <[email protected]>
A NaN/inf budget or rss slipped past the `rss <= target` check (all NaN
comparisons are False) and spuriously evicted every warm cache. Guard with
math.isfinite and safely coerce headroom_ratio, falling back to 0.9.
Co-authored-by: Mervin Praison <[email protected]>
Unattended scheduled runs resolved their model at fire time, so a job created
against a cheap/local default silently inherited whatever the default later
became — an invisible cost/behaviour drift for automations run without
supervision.
- core: additive provider/model/pin_model snapshot fields on ScheduleJob with
backward-compatible round-trip serialisation (unset = today's behaviour).
- wrapper executor: before the model turn, compare the resolved agent's model
with the snapshot; on drift fail closed (recorded error + optional fail-closed
delivery) with no model turn; otherwise pin the run to the snapshot.
- CLI: `schedule add --model` / `--pin/--no-pin`.
Co-authored-by: MervinPraison <[email protected]>
Add a pure, import-free core planner plan_pressure_evictions plus a
WarmSession fact and a MemoryPressureProtocol (cgroup limit + anon RSS)
to the gateway protocols. Under memory pressure the gateway can now name
the coldest (LRU) rebuildable warm session-agent caches to soft-evict
before the kernel OOM-kills the whole process, guarding against evicting
in-flight or not-yet-flushed sessions. Mirrors the existing
MemoryPressurePolicy admission contract; the decision is testable in
isolation and the gateway enacts the returned plan.
Co-authored-by: MervinPraison <[email protected]>
- failure.py: map validation/invalid_output -> format_error and OR in the
source outcome's is_retryable() so invalid_output stays retryable
- presentation.py: DegradedDelivery now uses immutable Tuple fields; report
derived from the same conversion/selection as adapt_presentation (select->
buttons, priority/cap truncation), passes callback_store, and reports
callback shortening only for genuinely lossy payloads (reply actions & select
options), never plain callbacks or store-backed lossless refs
- tests: pytest.raises for frozen checks; add invalid_output-retryable,
long-callback-with-store, plain-callback, and long-select-option coverage
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Addresses reviewer feedback on #3800/#3802:
- P1: Build SchedulerDelivery eagerly in the (sync/async) scheduler __init__
when a deliver token is set, via a shared _build_delivery() helper. The
creation-time pre-flight warning/preview now fires the moment the scheduler
is created instead of lazily at fire time (after the first run). Fire-time
lazy build is retained as a fallback; self-heal remains the second line.
- P2: Move optional validate_target/preview_target off the runtime_checkable
DeliveryResolverProtocol into a separate DeliveryPreflightProtocol so
existing resolve()-only resolvers still satisfy the base protocol under
isinstance. Export DeliveryPreflightProtocol from praisonaiagents.gateway.
Backward-compatible, no new deps, no Agent params. Scheduler suite: 262
passed, 7 skipped; preview suite: 9 passed.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Add a creation-time pre-flight so a scheduled/agent-initiated send's
DeliveryTarget is checked and previewed the moment it is created, not only
when the job fires. Core gains a thin protocol seam (DeliveryValidation,
ScheduleTargetError, DeliveryResolverProtocol.validate_target/preview_target)
and a pure DeliveryTarget.preview(); the wrapper SchedulerDelivery validates
its target on construction, logging an actionable warning for unroutable
tokens and a preview otherwise. Fire-time self-heal is kept as second defence.
Co-authored-by: MervinPraison <[email protected]>
Add two pure, dependency-free core primitives so every bot turn ends in a
visible, actionable outcome and presentation downgrades are never silent:
- praisonaiagents/bots/failure.py: FailureReply + render_failure_reply maps an
AgentRunOutcome or PraisonAIError to typed, user-facing next-step copy keyed
by failure class (expired auth, missing key, rate limit, timeout, budget,
doom-loop, needs-help), reusing the existing remediation_hint.
- praisonaiagents/bots/presentation.py: DegradedDelivery + a backward-compatible
adapt_presentation_with_report returning the adapted presentation plus a typed
record of dropped controls with machine-readable reason codes. Existing
adapt_presentation signature is unchanged.
Mirrors classify_final / resolve_ingress_admission so no adapter re-decides the
failure/degradation outcome. No new deps, no config knobs.
Co-authored-by: MervinPraison <[email protected]>
Auto-compaction previously read a static per-model context-window table with a
flat 128k default, mis-estimating the trigger point for any non-hardcoded model
(overflow for small windows, premature compaction for large ones).
get_model_limit()/get_output_reserve() now consult litellm's model_cost registry
(max_input_tokens/max_output_tokens) first, then fall back to the static tables.
Lazy-imported: zero overhead and fully backward compatible when litellm is absent.
Co-authored-by: MervinPraison <[email protected]>
- auto.py: _rewrite_agent_refs now rewrites route target strings
(route: {label: [agent_id]}) and loop/branches payload refs including
feedback_to, so renamed _auto_N agents are no longer routed to the
pre-existing colliding agent.
- orchestrator.py: PRAISONAI_SESSION_CACHE_MAX now falls back to the
documented default (1024) on empty/non-numeric values instead of raising
ValueError during persistence init.
Co-authored-by: Mervin Praison <[email protected]>
- Gap 1: PersistenceOrchestrator._session_cache is now a bounded LRU
(OrderedDict + PRAISONAI_SESSION_CACHE_MAX, default 1024) to prevent
unbounded memory growth in long-running serve/jobs/bot processes.
- Gap 2: flow.start_flow() now joins LANGFLOW_COMPONENTS_PATH with a comma
(pydantic list separator) instead of ';', matching the CLI helper so a
pre-existing env value no longer silently disables all components.
- Gap 3: merge_with_existing_workflow now tracks agent renames and rewrites
step agent references (including nested parallel/route/loop/branches) so
merged steps dispatch to the freshly-generated agent, with a log line on
each rename.
Co-authored-by: MervinPraison <[email protected]>
- Move PresentationRendererProtocol to bots/protocols.py (core extension-point
convention), re-exported from presentation.py for backward-compatible imports.
- Normalize platform ids (case-insensitive) in the renderer registry to match
the channel registry, so mixed-case plugin ids resolve their renderer.
- Validate registrations at the API boundary: reject empty platform ids and
renderers missing get_limits/render, failing loudly at registration.
- Make wrapper built-in registration older-core safe (swallow ImportError) and
non-destructive (only fill empty slots, so plugin overrides win).
- Case-insensitive older-core fallback in get_renderer.
- Fix pre-existing CLI test failure: _execute_prompt fake stub now accepts the
read_only kwarg the production signature passes.
Co-authored-by: Mervin Praison <[email protected]>
The async TUI review path calls self._execute_prompt(prompt, read_only=...),
but the parity test's fake stub only accepted `prompt`. The resulting TypeError
was swallowed by the broad except in _execute_in_background, so the fake never
recorded the prompt (KeyError: 'prompt'). Align the fake with production.
Co-authored-by: Mervin Praison <[email protected]>
Aligns resolve_ingress_admission with the live BotConfig.group_policy
default (mention_only) instead of respond_all, so an adapter forwarding
an unset policy fails safe (mention-gated) rather than replying to all
group traffic. Clarifies allowlist/pairing docstring contract.
Co-authored-by: Mervin Praison <[email protected]>
Add PresentationRendererProtocol + register_presentation_renderer /
get_presentation_renderer to praisonaiagents.bots.presentation so any
channel — built-in or a pip-installed plugin — can register a native
renderer the same way it already contributes config via ChannelDescriptor.
The wrapper now registers the four built-in renderers through this seam and
resolves via the core registry, so plugin channels no longer silently
degrade interactive UI to plain text.
Co-authored-by: MervinPraison <[email protected]>
Add pure, zero-dependency resolve_ingress_admission(...) -> IngressDecision
in praisonaiagents.bots.admission, mirroring evaluate_channel_health and
BotLoopGuard. Unifies channel/user allowlist, block-list, group policy
(respond_all/mention_only/command_only/observe) and pairing into one typed
verdict with a machine-readable reason_code so drops are recordable instead
of logger.debug-only and cannot drift per transport. Adds no new user knob.
Co-authored-by: MervinPraison <[email protected]>
Addresses the reviewer-flagged P1 that ``--pure`` / ``--no-plugins`` leaked
across in-process invocations: the flag set PRAISONAI_NO_PLUGINS with no
restore, so a later run/chat/code call in the same process inherited the
plugin-free baseline it never asked for.
- Add scoped_no_plugins() context manager + scopes_no_plugins() decorator in
cli/utils/env_utils.py that set PRAISONAI_NO_PLUGINS for the duration of one
command and always restore the prior value on return (mirrors the existing
--allow-local-tools save/restore pattern). functools.wraps preserves the
Typer signature so the real option set is unchanged.
- Apply the decorator to run/chat/code callbacks and drop the unscoped env
mutation from each body.
- Add regression tests for set/restore, prior-value restore, no-op, exception
safety, and decorator signature preservation.
Also repair pre-existing (present on main) test stubs broken by the earlier
read_only review-agent change: async-TUI stubs must accept the read_only
kwarg. Source is correct; only the stubs were stale.
Not implemented (out of scope per issue #3773): suppressing praisonai.tools /
praisonai.tool_sources entry-point discovery. The issue scopes suppression to
the praisonai.plugins group; tool suppression is explicitly optional/secondary
and would be scope creep.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Add an ephemeral, per-run flag to suppress discovery/loading of external
plugins without mutating persisted enable/disable state.
- Core: PluginManager honours a PRAISONAI_NO_PLUGINS env var and an optional
`disabled` constructor param (Python parity for plugins=False) that
short-circuits discover_entry_points()/auto_discover_plugins(), emitting a
single "running without external plugins" notice.
- Wrapper: add --pure / --no-plugins to run/chat/code, which set
PRAISONAI_NO_PLUGINS for the process only.
- Surface reproduction hint / suppression status in `plugins doctor`.
Co-authored-by: MervinPraison <[email protected]>
Addresses reviewer findings on PR #3747:
- Route /code-review and /security-review through a cached read-only review
agent whose tool set excludes write/command-execution tools, enforcing the
advertised read-only boundary at the capability level (not just by prompt).
- Skip @file expansion for generated review prompts so a `@../../secret`
token inside an untrusted diff cannot exfiltrate files.
- Distinguish diff-collection failures (raise ReviewDiffError) from a
successful empty diff, so errors are no longer masked as "Working tree clean".
- Parse review args with shlex so quoted paths survive; reject multiple
positional paths with a usage message.
- Derive slash-command autocomplete from _BUILTIN_COMMANDS so code-review /
security-review are discoverable and stay in sync with registration.
- Tests: option forwarding, multi-path rejection, diff-error vs clean tree,
ReviewDiffError propagation, and read-only tool filtering.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <[email protected]>
Under CI's `pytest -n 2 --dist loadfile`, a worker process persists across
files, so the process-wide BackgroundRunner singleton (and any tasks other
files scheduled on its daemon loop) leaked into these REPL /tasks tests. The
old fixture only cleared `_tasks`, which a concurrently-mutating daemon task
could repopulate — surfacing as "No background tasks" and an uncancelled
RUNNING task. Fully reset the singleton so `_handle_tasks_command` resolves the
same fresh instance we seed. No assertions weakened; behaviour still fully
verified (list, detail, actual CANCELLED status).
Co-authored-by: Mervin Praison <[email protected]>
Resolve two P1 correctness gaps from Greptile's second review pass:
- session fork: the store persists sessions under a *sanitized* filename
(non [alnum-_] -> _), so a global-only id containing e.g. '.' or ':' was
found by session_exists_anywhere but missed by the literal '{id}.json'
store-dir check, forking an empty history from the project store. Sanitize
identically when resolving the store dir.
- REPL /branch: replace the inline, unlocked busy check with the shared
_worker_busy() helper, which reads current_task and queue size under the
worker's processing_lock. This closes the dequeue/publish race where a turn
is in-flight but momentarily invisible.
Tests: sanitized-global-id fork resolution; /branch busy state read under lock.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
When the legacy REPL is launched with --approval plan the backend already
enforces PermissionMode.PLAN, but session_state['plan_mode'] defaulted to
False so the first no-arg /plan re-enabled an already-active mode instead of
exiting it (Greptile P1). Seed plan_mode from backend.permission_mode on first
use, mirroring the async TUI's _sync_plan_mode_from_backend. Adds a source
regression test.
Co-authored-by: Mervin Praison <[email protected]>
The Enter handler expanded @file mentions before _queue_or_execute, but
_execute_in_background already expands canonically. Since
_process_file_mentions is not idempotent, the second pass re-interpreted
@tokens embedded in attached file contents and appended unrequested files.
Removes the redundant enter-handler expansion so all paths expand once.
Strengthens the parity test to drive the real _execute_in_background path
and assert _process_file_mentions runs exactly once.
Co-authored-by: Mervin Praison <[email protected]>
Address three P1 review findings on the mid-session forking feature:
- session fork CLI now resolves the store directory from the same
canonical CLI stores session_exists_anywhere() searched (project then
global), so forking a global-only session copies its real history
instead of producing an empty fork from the project-scoped store.
- session fork rejects an out-of-range --at-message (negative or >=
message count) up front rather than silently wrapping via Python slice
semantics while reporting success.
- REPL /branch refuses to fork while a turn is still executing (worker
busy or queue pending), preventing the async worker from saving the
parent's in-flight turn onto the freshly-switched fork.
Adds tests for out-of-range indices, global-only forking, and
branch-while-busy rejection.
Co-authored-by: Mervin Praison <[email protected]>
Address two Greptile P1s on the headless `praisonai code -p` path:
- Profiling no longer bypasses headless output. The `-p` branch now runs
before the `--profile` branch and rejects the `-p` + `--profile/--profile-deep`
combination, so the machine-readable JSON/text contract and status exit code
are always honored instead of a human report exiting 0.
- `-p` fails closed on options the minimal headless agent cannot honor
(`--tools`, `--agent`, `--plan`, `--no-acp`, `--no-lsp`) rather than silently
dropping the caller's tools/profile/scope. Options that ARE honored
(`--model`, `--thinking`, `--verbose`, `--workspace`, `--resume/--session/--continue`)
remain allowed. Chose fail-closed over re-implementing the heavy ACP/LSP
wiring to keep the command lightweight (AGENTS.md).
Adds tests for both guards.
Co-authored-by: Mervin Praison <[email protected]>
Address four valid P1 findings from Greptile review of the /tasks feature:
1. Bot command dispatch was missing: wire handle_tasks_command into the
telegram, discord and slack adapters (was advertised but never invoked).
2. Task registries were disconnected: arun_background now submits to the
process-wide get_background_runner() so /tasks and `background list` see
the same tasks (previously each call built its own runner).
3. Ownerless tasks bypassed isolation: _owned now fails closed — a bot user
only sees/acts on tasks whose metadata.user_id matches exactly.
4. Cancellation left execution running: add BackgroundRunner.cancel_task_sync
(hops to the background loop) and use it so the underlying future is
actually cancelled instead of merely marking the record.
Tests updated for fail-closed ownership + new ownerless-isolation case.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Addresses Greptile P1: the TUI lazily built the checkpoint manager only on
the first /diff or /undo, so its "session start" snapshot captured an
already-modified workspace — /diff omitted prior edits and /undo restored the
post-edit state. Now on_mount() eagerly establishes the baseline before any
turn, and each submission records a pre-turn checkpoint so /undo can roll back
individual turns. Both paths are best-effort and no-op when checkpointing is
disabled. Adds two lifecycle regression tests.
Co-authored-by: Mervin Praison <[email protected]>
resolve_session accepts legacy-only ids via the deprecation-window
fallback, but rename_session did a canonical-only lookup so `session
rename` failed on a pre-existing session the CLI otherwise lists,
resumes, deletes and exports. Add a _legacy_rename fallback mirroring
_legacy_delete, backed by a new SessionManager.rename that persists the
title in the existing metadata `name` field (empty clears it, matching
canonical store semantics). Adds regression tests for the store and CLI
legacy paths.
Co-authored-by: Mervin Praison <[email protected]>
Addresses Greptile P1 findings and CI failures on PR #3750:
- async_tui/legacy: exiting PLAN now restores the launch-time policy
(accept-edits/bypass) instead of forcing DEFAULT.
- async_tui: sync config.plan_mode from a live PLAN backend at startup so
the [PLAN] indicator shows and the first /plan toggles off, not on.
- Fix collision with pre-existing planning tests: /plan <task> message keeps
"Creating plan" substring; /plan no-arg test updated for the intentional
persistent-toggle behavior.
- Add regression tests for restore-on-exit and startup sync.
Co-authored-by: Mervin Praison <[email protected]>
Address two Greptile P1 issues in the interactive shell escape:
- attach !!cmd output even when the command fails (do not silently
drop context the user explicitly requested)
- retain _pending_context until agent.start() succeeds so a model
error does not lose the attached shell output
Applies to both the REPL and TUI paths; adds regression tests.
Co-authored-by: Mervin Praison <[email protected]>
Cap build_recap() at RECAP_MAX_CHARS (3500) so an unbounded persisted/
compaction summary can never exceed Telegram's 4096-char message limit or
flood a terminal. Only the older summary is trimmed; the recent tail is
always preserved. max_chars<=0 disables the cap for full-block callers.
Fixes the valid unbounded-output concern raised in review; keeps the fix at
the core layer so every channel (CLI + all bots) benefits without per-adapter
plumbing.
Co-authored-by: Mervin Praison <[email protected]>
Address reviewer findings on the custom-command bridge (#3729):
- Telegram: register a catch-all MessageHandler(filters.COMMAND, ...) after
the built-in/registered CommandHandlers so file-based and entry-point
slash commands (never registered as explicit handlers) actually reach
handle_command and render, instead of being silently dropped.
- Resolver: require BOTH deployment opt-in and the command's own
allow_shell frontmatter before live !`cmd` substitution runs, so a
deployment enabling shell can't upgrade a command that declared it off.
- Telegram custom-command reply now routes through the normal delivery
pipeline (pop_last_presentation, fire_message_sending,
_send_response_with_media, render_presentation, fire_message_sent) and
sanitizes errors via safe_log_message/extract_root_cause_from_error/
safe_error_message instead of leaking raw exceptions to chat users.
- /help and native command-menu entries now merge resolver-discovered
file/entry-point commands (builtin/registered precedence preserved).
- Tests: add per-command allow_shell gating coverage.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
- Record SKILL.md mtime when a LoadedSkill enters the index (discover/reload/
add_skill) so an edit made between discover() and the first reload() is
detected instead of silently masked.
- Track discovery-owned skill names so reload() no longer reports skills
registered via add_skill() as removed (backward compatible).
- Refresh the change-detection baseline after manager-owned telemetry writes
so a telemetry-only SKILL.md update is not misread as a content change and
does not drop an activated skill.
- Make `praisonai skills reload` an honest scan report (no live-session
consumer exists cross-process) instead of an always-empty reload diff.
- Add tests: edit-before-first-reload, add_skill preservation, telemetry-only
no-false-change.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
- Force stream=False so a TTY streaming generator is not str()'d into a repr;
defensively materialize any generator that slips through.
- Explicitly set autonomy=False on the side agent so it can never trigger the
tool-using autonomous loop regardless of ambient config (read-only contract).
- Parse --keep only as the leading option so '/btw what does --keep mean?' is
preserved as question text.
- Replace the unconditional 'or True' rendering assertion with a real stdout
capture; add tests for leading-only --keep, autonomy=False, and generator
materialization.
Co-authored-by: Mervin Praison <[email protected]>
Address blocking reviewer findings on #3728 plugin runtime:
- discovery: trust gate now judges a plugin file by its own location, so a
repository-controlled symlink under .praisonai/plugins can no longer bypass
PRAISONAI_ALLOW_PROJECT_PLUGINS by pointing its target outside the project.
- discovery: track only tools a module actually contributed (pre-exec registry
snapshot) so unload never unregisters tools owned by another plugin; guard
_loaded_plugin_tools with a lock.
- loader: preserve boolean `plugins.enabled: true` (no longer collapses to an
empty allow-list that disables everything); refuse ambiguous disable.
- loader: fail fast when writing a TOML config without tomli_w instead of
writing an ignored YAML sidecar (config split-brain).
- CLI plugins: reload unloads stale single-file modules before rediscovery and
reports real counts + the discovery gate; disable unloads single-file plugin
tools; doctor checks registered-plugin hooks; plain-text output (no Rich
markup via typer.echo); chained exceptions; logged swallowed errors.
- tests: add symlink-gate, boolean-enabled, and isolated unload cases.
Co-authored-by: Mervin Praison <[email protected]>
Give the flagship `code` command a scripting-grade headless mode at parity
with `run --output json`/`chat --json`. `-p/--print` routes around the
decorated interactive path (which prints `Chat mode:`/`Prompt:` diagnostics
+ a profiling block and always exited 0), builds the code agent directly,
and emits a clean machine-readable envelope {result, session_id,
usage:{in,out,cost}, status} on stdout. Exit code reflects status (0 ok,
nonzero on error/empty result). `--resume <id> -p` composes for scripted
multi-turn. Usage is read from the existing token collector + cost tracker,
so no new Agent params, exports, or wrapper wiring are introduced.
Co-authored-by: MervinPraison <[email protected]>
Expose mid-session conversation forking that previously only existed as a
startup flag. Adds:
- /branch [title] [--at N] in the interactive REPL, forking the live
UnifiedSession, switching onto the fork, and recording parent/child lineage.
- praisonai session fork <id> [--at-message N] [--title ...] mirroring the
existing run --fork HierarchicalSessionStore substrate.
- Fork lineage (parent_id/children_ids) on UnifiedSession, merge-safe, shown
in /session and session list.
Wrapper-only change; no praisonaiagents core edits, no new deps.
Co-authored-by: MervinPraison <[email protected]>
Sessions were addressable only by opaque id. Add a lightweight,
metadata-only rename:
- DefaultSessionStore.rename_session() stores metadata["title"] via the
existing locked read-modify-write path; empty title clears it.
- list_sessions() surfaces "title"; _session_title() prefers it.
- session_resolver.rename_session() targets the canonical store holding
the id (mirrors delete_session).
- `praisonai session rename <id> <title>` subcommand; `session list`
Name column prefers the title, falling back to agent name.
Backward compatible: un-renamed sessions still resolve/resume by id.
Co-authored-by: MervinPraison <[email protected]>
Surfaces the already-complete background-task substrate on the interactive
surfaces so users can check on backgrounded work without leaving the session.
- interactive_legacy: /tasks (list), /tasks <id> (detail), /tasks cancel <id>
via the existing BackgroundHandler renderer
- bots/_commands: register `tasks` + handle_tasks_command with compact,
per-user-scoped rendering (owner via task metadata user_id)
- background: add lazy process-wide get_background_runner() so all surfaces
share one runner view
- fix latent crash in the CLI detail renderer when duration_seconds is None
Co-authored-by: MervinPraison <[email protected]>
Adds a working /diff to the coding REPL showing all file changes made this
session (session baseline -> working dir), with --turn and <file> scopes,
reusing the existing CheckpointService engine via SessionCheckpointManager.
Replaces the wrapper TUI's dead /diff and /undo stubs (which only printed
"use git diff" help text) with real handlers backed by the same engine as
the legacy REPL. Default-safe: when checkpointing is disabled, /diff reports
how to enable it rather than failing silently.
Also verifies and regression-tests the wrapper-resident `context` command
mapping (resolves via praisonai.cli.commands.context, not the placeholder
local module).
Co-authored-by: MervinPraison <[email protected]>
Reuses the existing gated executor (PRAISONAI_ALLOW_SHELL env-gate, 30s
timeout, 100KB output cap) from custom_definitions.py via a new shared
run_shell_escape helper. Wires !cmd / !!cmd handling into the legacy REPL
and async TUI input paths before command/prompt dispatch, rendering output
inline without consuming a model turn. !!cmd attaches output as context for
the next turn.
Co-authored-by: MervinPraison <[email protected]>
Wire the existing PermissionMode.PLAN enforcement into the REPL /plan slash
command instead of a one-shot prompt template. /plan toggles read-only mode
(set_permission_mode(PLAN)), /plan off exits, /plan <task> plans under
enforcement; async TUI shows a [PLAN] status indicator. Also list plan,
accept-edits and bypass in the --approval help text.
Co-authored-by: MervinPraison <[email protected]>
Reuse the existing summariser purely to inform the user with a 'where were we' block, without mutating the transcript or triggering compaction. Adds a core build_recap() primitive, the /recap bot command, and a --recap flag on session show.
Co-authored-by: MervinPraison <[email protected]>
Re-scan skill sources into a running session without restart. reload()
diffs discovery against the current registry, swaps the index atomically
(index changes take effect on the next turn, not mid-turn), preserves
activation/telemetry for unchanged skills, and deactivates removed ones.
Adds a thin `skills reload` CLI parity command reporting +added/~changed/-removed.
Co-authored-by: MervinPraison <[email protected]>
Ask a side question in a parallel, throwaway context so the main task's
history and momentum stay untouched. Builds a fresh read-only agent, runs
the question in isolation, renders a distinct block, and never appends to
the main conversation history. Opt-in `/btw --keep` records a one-line note.
Co-authored-by: MervinPraison <[email protected]>
Address reviewer findings on $1..$n interpolation:
- preserve literal out-of-range dollar-number text (e.g. legacy "$100")
instead of erasing it
- tokenize with non-POSIX shlex so Windows paths (C:\Users\alice) survive
- merge $ARGUMENTS + positional substitution into a single pass so injected
tokens that look like $1/$ARGUMENTS are never re-scanned
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Bots were a command island: .praisonai/commands/*.md and plugin-bundle
commands surfaced only in the code REPL/TUI, never in Telegram/Slack/etc.
This adds a shared CustomCommandResolver in praisonai-bot that consumes the
existing custom_definitions loader/interpolator (via the sanctioned
_code_bridge seam — no cross-tier hard dependency, no reimplementation) with
a bot-appropriate safety posture:
- Shell (!`cmd`) substitution OFF by default regardless of frontmatter;
opt in per deployment via bots.commands.allow_shell. When off, live
segments render inert instead of executing or failing the command.
- Project-scope only by default (user ~/.praisonai excluded unless
include_user_scope), allow-listable via expose.
- Builtin precedence preserved; file commands merged into /help and the
command menu through the shared ChatCommandMixin (all adapters benefit).
- praisonai.bot_commands entry-point group lets installed packages add bot
commands without forking; file/project commands win on collision.
Telegram is wired as the reference adapter (catch-all dispatch renders the
command and submits it via the normal session.chat path). Adds config schema
(BotCommandsConfigSchema) and TDD tests covering project command execution,
shell-off-by-default, allow-list, user-scope exclusion, builtin precedence /
help merge, and entry-point resolution.
Co-authored-by: MervinPraison <[email protected]>
Defect 1 (config split-brain / fake CLI): add core get_plugin_registry()
returning real entry-point, registered and single-file plugins with
provenance and enabled state; the praisonai-code plugins CLI now lists/
inspects/enables against it and deletes the hardcoded four-plugin list.
enable/disable write the plugins.enabled allow-list in .praisonai/config.yaml
(the same file the runtime reads) via a new config-loader write path — no
more ~/.praisonai/config.json split-brain.
Defect 2 (trust-gate bypass): route project-local single-file plugin exec
in plugins/discovery.load_plugin through an opt-in gate
(PRAISONAI_ALLOW_PROJECT_PLUGINS / plugins.allow_project_plugins, default
false), mirroring the project-tools gate. A cloned repo's
.praisonai/plugins/*.py no longer runs until the user opts in. User-global
plugins remain trusted.
Defect 3 (leaky unload / unreachable reload): unload_plugin now unregisters
the tools a module harvested (tracked per module) instead of leaking them;
new `praisonai plugins reload` forces rediscovery + hook rewire so plugins
take effect without a restart.
Co-authored-by: MervinPraison <[email protected]>
Adds the minimal, self-contained slice of the plugin-bundles proposal that
strengthens existing local commands without a new subsystem: parse
argument-hint/model/tools (allowed-tools) frontmatter onto CustomCommand and
support shell-style positional $1..$n interpolation alongside $ARGUMENTS.
Positional tokens are split with shlex, injected before $ARGUMENTS (so user
input is never re-scanned), and escaped identically so they can never
introduce shell substitution. Legacy two-field command files are unchanged.
Co-authored-by: MervinPraison <[email protected]>
Address reviewer feedback on PR #3726 (Greptile/CodeRabbit):
- Never silently drop unsupported delivery targets: _can_deliver now always
dispatches a configured target so _dispatch_delivery raises and the caller
records delivery_error (matches PR's stated contract).
- Reuse chunk_message so Telegram/Slack/Discord size limits are not exceeded.
- Fall back to the gateway's persisted HomeChannelRegistry state file when
{PLATFORM}_HOME_CHANNEL env is unset, so gateway-registered home channels
resolve out of process.
- Preserve Discord threaded context via message_reference (parity with the
Telegram/Slack thread_id handling).
- Tests updated to lock the new contract (chunking, discord thread reference,
registry fallback, unsupported-target error recorded).
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Scheduled jobs could only deliver their `deliver:` target when a live,
in-process bot adapter was running in the same process as the scheduler.
Running `praisonai schedule tick` out of process (OS cron, CI, serverless,
scale-to-zero) computed the result but silently dropped delivery because the
executor's injected `delivery_handler` was None.
This adds a stateless, token-authenticated standalone sender per platform
(Telegram/Slack/Discord) in the praisonai-bot wrapper. When no live
`delivery_handler` is wired, the executor falls back to a single HTTP call
using the same `{PLATFORM}_BOT_TOKEN` env the gateway uses, with a bare
`deliver: telegram` home-channel target resolving its chat id from
`{PLATFORM}_HOME_CHANNEL`. Delivery failures are recorded as `delivery_error`
exactly as the live-adapter path does.
Wrapper-only change (no core edits, no new Agent params, zero new deps —
stdlib urllib). The live-adapter path is unchanged.
Co-authored-by: MervinPraison <[email protected]>
A fast-exiting command (e.g. sys.exit(3)) could be mis-reported as a
[exit 124] timeout on a loaded CI runner: after the reader thread hit
stdout EOF, proc.poll() could transiently return None before the exit
status was reaped, and the old logic inferred a timeout purely from
process state and SIGKILL'd the already-exiting process. Timeout is now
decided solely by the wall-clock deadline, with a brief grace wait to
reap fast exits, so the real exit code is preserved.
Co-authored-by: Mervin Praison <[email protected]>
- DeferredResolver.resolve now buffers values that arrive before the handle
is registered (early completion) instead of discarding them; the buffered
value is delivered when register()/register_if_absent() runs.
- Add register_if_absent() so the LLM run loop registers atomically and a
gateway-owned or concurrent-turn callback is never clobbered.
- LLM._register_deferred_if_any uses register_if_absent (no check-then-set race).
- cancel() clears any buffered early resolution.
- Add tests for early-resolution buffering, atomic no-clobber, cancel-clears-buffer.
Co-authored-by: Mervin Praison <[email protected]>
Move max_rpm<=0 validation ahead of the explicit-limiter check in both
Agent.__init__ and yaml_parser so ExecutionConfig(max_rpm=0, rate_limiter=obj)
raises ValueError as required by the behaviour matrix. Add regression test.
Co-authored-by: Mervin Praison <[email protected]>
Address reviewer feedback on PR #3720:
- Catch UnicodeDecodeError alongside JSONDecodeError so corrupt/binary
session files are quarantined instead of propagating an uncaught
decode error (both DefaultSessionStore and HierarchicalSessionStore).
- Make _quarantine_corrupt() pick a collision-free destination so two
corrupt files in the same millisecond are each preserved distinctly.
- Strengthen tests: assert exact byte-for-byte preservation, add an
invalid-UTF-8 regression, and add a HierarchicalSessionStore
corruption-recovery smoke test verifying the SESSION_PERSIST_FAILED
hook metadata.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
max_rpm was stored on the Agent but never wired to a runtime RateLimiter,
so ExecutionConfig(max_rpm=N) did not throttle requests. Now the Agent
auto-builds RateLimiter(requests_per_minute=N) when max_rpm is set and no
explicit rate_limiter is provided. Explicit rate_limiter still takes
precedence; max_rpm<=0 raises ValueError. YAML max_rpm now also wires a
live limiter.
Co-authored-by: Dhivya-Bharathy <[email protected]>
Add a lightweight DeferredResolver registry in tools/call_executor.py that
links a deferred handle_id to a resolution callback. The run loop now registers
any deferred tool result (via LLM._register_deferred_if_any) so that a later
resolve_deferred(handle_id, value) — fired on background completion — re-injects
the result instead of it being lost. Exposes register_deferred/resolve_deferred/
get_deferred_resolver/DeferredResolver from praisonaiagents.tools.
Co-authored-by: MervinPraison <[email protected]>
A malformed session JSON file was read as an empty session and then
overwritten by the next write, permanently destroying a user's history
with only a log line. Now the corrupt file is quarantined aside
(<file>.json.corrupt-<ts>) before starting fresh, preserving recoverable
bytes, and the event is surfaced via the existing SESSION_PERSIST_FAILED
hook rather than silently dropped. Applied to both the default and
hierarchical stores.
Co-authored-by: MervinPraison <[email protected]>
Extend the existing import-free header parser so a single-file plugin can
declare its capability surface (Channels, Provides/Tools, Config,
Auto Enable When Configured) statically. discover_plugins()/list_plugins
already surface this metadata to onboarding/doctor without importing the
plugin's runtime, enabling capability enumeration and config validation
before runtime loads.
Co-authored-by: MervinPraison <[email protected]>
Address reviewer findings on plugins/tools add commands:
- uv pip install now pins --python sys.executable so the package lands in the
interpreter running the CLI rather than an env uv auto-discovers.
- Add importlib.invalidate_caches() after install so a just-installed
distribution is importable before discovery runs.
- tools add now builds a fresh ToolResolver after install so instance-level
availability caches (e.g. praisonai_tools importability) are re-evaluated;
invalidate() only clears the per-name resolve cache.
- Add --global option to both commands (uv --system / plain pip) for installing
into the ambient system environment.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Concurrent callers of discover_plugins() could both pass the _discovered
check before either set it, double-loading entry points and double-counting.
Wrap the first scan in the re-entrant registry lock so concurrent callers
block, then observe _discovered and return 0 without rescanning. The get()
path already holds the RLock, so re-acquisition is deadlock-safe.
Co-authored-by: Mervin Praison <[email protected]>
Adds one-command install-and-verify for plugin/tool packages from the CLI.
Installs via the resolved installer (uv pip or python -m pip), triggers
discovery, and reports exactly which plugins/tools registered (name, type,
hooks/source) or exits non-zero with a clear message if nothing was
discovered. Supports --dry-run (verify only) and --upgrade.
Co-authored-by: MervinPraison <[email protected]>
The core tool registry now discovers the documented canonical
`praisonai.tools` entry-point group in addition to the legacy
`praisonaiagents.tools` and `praisonai.tool_sources` groups (kept as
deprecated aliases with a one-cycle DeprecationWarning). Because the CLI,
YAML and Python surfaces all defer to the core registry, an installed
third-party tool package is now resolvable by name across all three
surfaces automatically. The canonical group wins on a name collision and
the lazy, cached entry-point loading pattern is preserved.
Co-authored-by: MervinPraison <[email protected]>
- run_sync_or_offload now holds the in-flight bridge future and cancels it
when the worker join times out, so stuck async work stops instead of
lingering on the bridge loop (CodeRabbit).
- A2U _MAX_SUBS/_QUEUE_MAX now fall back to defaults on non-positive or
unparsable env values, preventing asyncio.Queue(maxsize<=0) from silently
becoming unbounded (CodeRabbit).
Co-authored-by: Mervin Praison <[email protected]>
- glob/grep: PRAISONAI_WORKSPACE confinement is now opt-in; without it set,
no sandbox is enforced (cwd was never a real boundary and enforcing it
broke all callers passing an explicit directory + 15 CI tests).
- a2u_server: fail-closed guard now reads the launcher-set
PRAISONAI_CALL_BIND_HOST (with PRAISONAI_A2U_BIND_HOST override) and
rejects unauthenticated non-loopback/unknown binds, matching the existing
call-server security contract (fixes Greptile P1).
Co-authored-by: Mervin Praison <[email protected]>
- _async_bridge: add awaitable arun_sync_or_offload() sibling and bound the
run_sync_or_offload() worker join with a timeout so a stuck coroutine can no
longer pin the caller (and, when the caller is the loop thread, the loop)
indefinitely.
- tools (glob/grep/multiedit): confine to PRAISONAI_WORKSPACE root, filter
security.is_protected paths from results/reads, refuse edits to protected
files, and cap per-line length in grep to bound ReDoS cost.
- endpoints/a2u_server: refuse fail-open auth on non-loopback binds, cap
subscriptions, and bound per-subscription queues with drop-on-full so a
slow/stalled consumer or subscription flood cannot OOM the process.
Co-authored-by: MervinPraison <[email protected]>
- Reset task status/result/retry_count/variables before each batch item so
run_task/arun_task actually re-execute (previously items 2..N returned the
first item's stale "completed" result).
- Snapshot task run state before the batch and restore it in finally, keeping
the public Task API backward-compatible.
- Validate each input is a dict: fail_fast raises ValueError, continue records
a per-item error instead of crashing.
- Add regression tests: two-item lifecycle reset, invalid-input handling.
Co-authored-by: Mervin Praison <[email protected]>
Match the injected manager_task by its actual task id (manager_task_id)
instead of its name in both sync and async hierarchical paths. A legitimate
user task named "manager_task" is no longer incorrectly rejected/hidden.
Also state 0-based ids in the manager schema and add regression coverage.
Co-authored-by: Mervin Praison <[email protected]>
The hierarchical manager could delegate to the injected manager_task because
validation only checked membership in self.tasks (which includes it) and the
schema field described task_id as "1-based" while runtime IDs are 0-based. As a
result the manager would self-select task_id=1 and the real user task never ran.
- manager_schema.py: task_id description now references the exact 0-based ids
and forbids selecting manager_task.
- process.py (sync + async): reject unknown ids AND manager_task; re-prompt with
only delegable (non-manager) task ids in the error context.
Co-authored-by: Dhivya-Bharathy <[email protected]>
When a Task sets output_pydantic (or output_json) but the agent returns
freeform prose, the task no longer silently succeeds with pydantic=None.
- default_completion_checker: structured output that was requested but not
produced is now treated as NOT complete, driving the existing retry loop
and leaving the task "failed" after retries instead of succeeding on prose.
- _process_task_result: use model_validate, include the schema name and the
underlying error in the warning, and return TaskResult(success=False) with
a clear error when structured output could not be produced. Raw output is
preserved for debugging.
Co-authored-by: Dhivya-Bharathy <[email protected]>
Adds a lightweight map-over-inputs primitive that runs the same team once
per input dict, reusing the existing {{key}} `variables` interpolation
(per-run task copy => template immutability + isolation). Returns an
aggregate dict with per-item outcomes, ordered results, continue/fail_fast
error policy, and a batch token-usage rollup. No new heavy classes/modules.
Co-authored-by: Dhivya-Bharathy <[email protected]>
FunctionTool.get_schema was a near-verbatim copy of the inherited
BaseTool.get_schema producing byte-identical output. Since __init__
always sets _schema_override and parameters, the parent's hasattr guard
is always satisfied, making the override dead duplication. Removed it so
FunctionTool inherits BaseTool.get_schema, and dropped the now-unused
copy import.
Co-authored-by: MervinPraison <[email protected]>
Replace triplicated eight-field mapping in Agent.context_manager with a
single _manager_config_from_context_config helper. Pure DRY clean-up,
identical output for all input types (ContextConfig, preset string, dict).
Co-authored-by: MervinPraison <[email protected]>
- await awaitable on_event results so async callbacks actually run
- when a progress sink is passed, route through subprocess so SDK mode
never silently drops progress events
- replace vacuous stream test assertion with exact event equality
- add async-callback coverage
Co-authored-by: Mervin Praison <[email protected]>
ClaudeCodeIntegration.stream() built `claude -p --output-format stream-json`
without --verbose, which the CLI rejects, so it could never run -- and nothing
added --include-partial-messages, so no token/tool-use deltas were emitted.
_build_command now forces both flags for stream-json.
execute() gains an optional on_event/on_progress callback: when supplied, the
run is streamed and every parsed event (system/init, tool-use, text deltas,
result) is forwarded live so a caller or UI can show progress, while the final
result text is still returned. A failing progress sink never breaks the run.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Prefer as_dict(), fall back to dataclasses.asdict for a bare
GatewayStatus-shaped snapshot, and use json.dumps(default=str) so a
partially-populated or non-standard snapshot never dead-ends.
Co-authored-by: Mervin Praison <[email protected]>
Adds a lightweight, read-only introspection tool so an agent can inspect
and report its own live gateway state (run status, active-session
inventory, delivery/DLQ backlog, degraded owners) from inside a turn.
Mirrors the send_message / OutboundMessengerProtocol pattern: core ships
GatewayStatusProtocol + GatewayStatus snapshot shape and the built-in
gateway_status tool, which resolves the live source from the per-turn
session context (register_gateway_status). When no gateway is running,
the tool is simply not offered and fails cleanly. No Agent-class changes,
no new heavy dependencies; the concrete live binding belongs in the
praisonai-bot wrapper.
Co-authored-by: MervinPraison <[email protected]>
A non-numeric persisted command_timeout (e.g. a hand-edited "five") raised
ValueError at float(...) outside the command error handler, escaping the ticker
loop and breaking every other job. Normalise defensively in the executor:
non-numeric, non-positive, or non-finite values fall back to 60s and the job
still runs. Adds a regression test.
Co-authored-by: Mervin Praison <[email protected]>
- Fix CI: test_command_action.py used the deprecated
asyncio.get_event_loop().run_until_complete() which raises RuntimeError
on Python 3.10+ with no current loop. Switch to asyncio.run() to match
the sibling test_condition_gate.py / test_run_policy.py convention.
- Address Greptile P1 (memory): stream and cap command output as it is read
(bounded to _MAX_COMMAND_OUTPUT_CHARS) instead of buffering the full
stream via communicate(), so a chatty/runaway command in the long-running
gateway cannot exhaust memory. Timeout still kills the process group.
Co-authored-by: Mervin Praison <[email protected]>
Adds a model-free command action to scheduled jobs: a job carrying a
`command` runs that shell command on its schedule and delivers stdout
verbatim to its DeliveryTarget — no agent resolved, no model turn taken.
- core: `command`/`command_timeout` fields on `ScheduleJob` with
backward-compatible serialisation (persisted only when set).
- wrapper (praisonai-bot): executor runs the command off the event loop,
bounded by `command_timeout` and killed with its process group on POSIX
(mirrors ShellConditionGate); non-zero exit surfaces exit code + output;
output is bounded before verbatim delivery via the existing path.
- CLI: `praisonai schedule add --command/--script [--command-timeout]`.
Additive and backward-compatible: agent-driven jobs are untouched.
Co-authored-by: MervinPraison <[email protected]>
Non-numeric, non-finite (NaN/inf), or non-positive timeout_s values now
fall back to the 120s default and are clamped to a 3600s upper bound so a
model-controlled timeout can never undermine the tool's bounded-wait
"never a silent hang" guarantee (Greptile review, PR #3691).
Co-authored-by: Mervin Praison <[email protected]>
Adds an agent-callable request/reply tool that sends a prompt to a symbolic
target and awaits that target's next correlated reply, returning it into the
agent's turn. Unlike fire-and-deliver send_message, it always resolves to a
typed outcome (reply | timeout | undelivered | no_route) so a path never hangs
silently.
Core-only surface mirroring the existing send_message pattern:
- ConversationReply + ConversationReplyStatus + ConversationRequestProtocol in
gateway/protocols.py (protocol seam bound by the running gateway/bot).
- register/get/clear_conversation_requester context slot in session/context.py.
- ask_conversation tool in messaging_tools.py, reusing the send-policy guard
and cross-thread loop scheduling used by send_message.
Co-authored-by: MervinPraison <[email protected]>
Close two gaps where the read-only --plan contract silently downgraded:
- run --plan --profile: thread the resolved approval backend
(PermissionMode.PLAN) into _run_prompt_profiled and
_run_from_file_profiled so profiled runs stay permission-gated.
- code --plan REPL: the interactive worker's _build_agent now unpacks
args.agent_approval into Agent(**agent_extra_kwargs), matching the
single-prompt path, so REPL sessions deny mutating tools instead of
falling back to the legacy approval prompt.
Adds regression tests for both paths.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
- run.py: move config + recorded-session model resolution ahead of the keyless
local-first credential gate so a resumed session on a reachable local endpoint
is no longer silently switched to the local model (Greptile/CodeRabbit).
- store.py: SessionData.from_dict now folds legacy top-level model/llm (mirrored
by to_dict) back into metadata so historical sessions recover their recorded
model on resume (CodeRabbit backward-compat).
- tests: add latest-turn-wins and legacy top-level normalization coverage.
Skipped async get_session_model accessor: no async consumer exists; only the
synchronous CLI resume path calls it, so it would be dead API surface (AGENTS.md).
Co-authored-by: Mervin Praison <[email protected]>
Add a first-class `--plan` boolean flag to `praisonai run` and `praisonai code`
that selects the existing read-only planning permission mode
(PermissionMode.PLAN). It maps onto the same approval plumbing as
`--approval plan` rather than a bespoke deny-set, so the agent may
explore/read/search while every mutating tool is denied.
Wrapper-only change: core (Agent(approval="plan")) and YAML (mode: plan)
already thread onto PermissionMode.PLAN, so no new enforcement logic or Agent
params are introduced. Contradictory permission flags fail closed.
Co-authored-by: MervinPraison <[email protected]>
Resume restored conversation history but not the model the session was
running: with no explicit --model, it re-resolved the current default, so a
change to the user's default between runs silently switched the model
mid-conversation. Now --continue/--session restore the recorded model
(explicit --model still wins and updates it going forward).
- core: add DefaultSessionStore.get_session_model() reading the session-level
metadata model, falling back to the latest turn's recorded model.
- wrapper: resolve and restore the recorded model in run_main before dispatch
when no model was chosen, with an info line noting the restored model.
Co-authored-by: MervinPraison <[email protected]>
install_hook()/_resolve_backend() were never called by any runtime path
(Greptile P1). Approval is already enforced via the adapter's
_resolve_agent_approval() -> Agent(approval=ApprovalConfig(...)) core gate,
so these methods were a redundant, untested, second before_tool enforcement
that would double-gate. Removing per AGENTS.md (no dead code with no live
consumer / duplicate behaviour). Tests unchanged and green.
Co-authored-by: Mervin Praison <[email protected]>
- praisonai_adapter: resolve YAML/CLI handoff {to:[roles],...} into core
Agent.handoffs after all agents exist, mapping timeout/max_depth/
max_concurrent/detect_cycles onto HandoffConfig. The --handoff family is
no longer silently dropped.
- _approval_spec.install_hook: replace the two TODO no-op stubs with real
backend dispatch (prompt via resolved backend, deny on rejection, fail
safe when no backend is available).
- auto.py: drop the duplicated LEGACY TOOLS block on the resolver-available
path (it repeated the installed tool list for zero information gain).
Co-authored-by: MervinPraison <[email protected]>
PR #3681 fixed the Windows cp1252 UnicodeEncodeError only in docker.py, but
the same bare-emoji print() crash remained in the other Python-API deploy
paths that also bypass the CLI's _configure_stdio() bootstrap:
- api.py foreground serve (🚀📁🔗💬)
- providers/aws.py, azure.py, gcp.py deploy/destroy (📦📝🚀🛑🗑️🔧🌍)
Replace them with ASCII [api]/[aws]/[azure]/[gcp] prefixes. cli/features
uses Rich console.print (encoding-safe) and is left unchanged.
Co-authored-by: Mervin Praison <[email protected]>
The praisonai-deploy Python API path (Deploy.deploy) bypasses the CLI's
UTF-8 stdio bootstrap from #3605, so bare emoji print() lines in docker.py
raised UnicodeEncodeError on default Windows cp1252 consoles and were masked
as "Docker build failed". Replace the six emoji status prints with portable
ASCII "[docker]" prefixes so both CLI and Python API paths are encoding-safe.
Co-authored-by: Dhivya-Bharathy <[email protected]>
Addresses the two P1 findings from Greptile on PR #3679:
1. Secret-context capture reuse: definition_hash excluded env values, so a
capture whose setup baked env-derived state into the filesystem could be
reused across provisions with the same env names but different values. Add
capture_key() which binds env *values* and use it as the docker image tag /
reuse key; keep the value-free definition_hash only as a non-sensitive
display label in the registry.
2. Concurrent registry lost-update: registry.json read-modify-write was
unlocked. Add a best-effort fcntl file lock (no-op on non-POSIX) and route
capture/touch/prune through an atomic _update_registry() so concurrent
writers don't drop each other's entries. Image deletion in prune moved
outside the critical section.
Tests: +4 core capture_key tests, +3 docker tests (secret isolation, concurrent
preservation, definition label). 28 core + 10 docker tests green.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Amortise environment provisioning: after packages + setup: run once, the
docker backend commits the prepared container to a local image keyed by a
stable definition hash, and subsequent provisions of the same definition
start from that capture — skipping pull + install + setup.
Core (praisonaiagents):
- definition_hash(config): stable sha256 over image/packages/setup/env-names/
resources (env values excluded so secrets never enter the key).
- SupportsCapture optional protocol (default: backends without it stay ephemeral).
- Loader accepts refresh: and capture: keys (carried in metadata; no new
ComputeConfig field without a typed consumer).
Docker compute (praisonai wrapper):
- capture()/has_capture() via docker commit -> praisonai-env:{hash12}.
- Reuse-on-match; edited definition -> new hash -> full rebuild + new capture.
- refresh: runs only on capture-based starts (cheap incremental step).
- Registry ~/.praisonai/environments/registry.json + list_captures()/
prune_captures() age-based GC.
- Failed commit degrades to today's ephemeral behaviour with a log line.
managed_local.provision_compute carries capture metadata (refresh/capture)
from the definition to the backend.
Tests: definition-hash stable/sensitive + env-value exclusion; docker
capture+reuse skips setup; definition change invalidates; refresh on hit
only; capture failure degrades ephemeral; non-capturing no-op; registry GC.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
On a second .start()/.run() of the same AgentTeam, the prior run's
synthetic manager_task remains last in self.tasks, so the return-task
snapshot picked it and returned the previous Manager output instead of
the final user-task result. _last_hierarchical_task_id now walks tasks
in reverse and skips any manager_task, selecting the real final task.
Addresses Greptile P1 review on agents.py.
Co-authored-by: Mervin Praison <[email protected]>
- process="hierarchical": track the last real (user-supplied) task id before
the synthetic manager_task is injected, so .start()/.astart()/.run() return
the final delegated task's result instead of the Manager's own output.
- FileMemory/LearnManager: re-read the on-disk state before each write so two
instances sharing the same user_id no longer clobber each other's entries
(last-writer-wins fixed for short/long-term, entities, auto-promotion, learn
stores).
- Approval registry: scope the "already approved" cache key to the requesting
agent so one agent's approval can't silently pre-authorize an identical call
from a different, stricter agent in the same context.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: MervinPraison <[email protected]>
Multi-agent gap review of the release pipeline surfaced 17 findings across
the gate, the workflow, and the bump script. This fixes the confirmed ones.
Gate (release-gate.js + nightly-release-gate.yml):
- A pypi-release run stuck in `waiting` (environment approval) blocked all
auto-releases with no age limit -- GitHub only auto-fails unapproved
deployments after 30 days, and exactly this stalled releases for 16h this
week. Waiting runs older than 6h are now ignored (with a warning linking
the run); in_progress/queued runs still block unconditionally, and the
stuck run is never cancelled -- if later approved, the concurrency group
serializes it and pypi_exists no-ops anything already published.
- PACKAGE_PATHS covered 3 of 9 published packages, so a change landing only
in e.g. praisonai-bot was never auto-released. Now lists all nine.
- The 3-day dedupe counted any successful run -- including dry_run=true,
which publishes nothing -- blocking real releases for 3 days after a dry
run. Dedupe now keys on v* GitHub releases, which only real releases create.
- The nightly path required last-green-Core-Tests SHA == HEAD exactly; a
docs-only or [skip ci] commit at the tip (the release safety-net commit
itself!) stalled the cron path indefinitely. A green ancestor with no
package-path changes since is now accepted.
- Replaced the dead 'Bump praisonai' workflow_run filter (those commits no
longer exist) with 'chore(release):', and wired the previously never-run
release-gate selftest into the gate as a step.
Workflow (pypi-release.yml):
- New `only` input: a dedicated single-package release is one dropdown pick
instead of eight skip flags. A "Resolve effective skips" step is the single
source of truth; the 19 publish/wait conditions are untouched (they gate
via the seeded pypi_exists outputs).
- PyPI existence probes now distinguish 404 (publish) from outages (fail the
run before anything is published) instead of treating any error as "not
published" and then dying on duplicate upload midway.
- The token-refresh step is continue-on-error and its consumer only swaps
tokens on success -- a failed re-mint previously overwrote a possibly-valid
token with an empty string, guaranteeing the safety net could not push.
- The safety net now (a) reverts uncommitted wrapper-side rewrites when the
wrapper release didn't complete, so a wrapper version that never reached
PyPI is not committed and then skipped past forever, and (b) retries
rebase+push 3x preferring the version bumps on conflict -- losing that
commit deadlocks future auto-releases ("already published" forever).
- "Verify PyPI versions" actually probes PyPI now; it previously echoed
checkmarks derived from input flags for publishes that may never have run.
- Dedicated releases get per-package tags (praisonai-mcp-v0.0.12) since the
wrapper's "Release v..." tag doesn't exist on that path; only packages
confirmed on PyPI are tagged.
- uv publish --check-url makes re-uploads idempotent (partial-upload
recovery); dry-run summary now includes sandbox/deploy and the effective
skip plan.
Bump script (bump_and_release.py):
- A conflicted `git pull --rebase` previously printed "trying to continue"
and proceeded from a mid-rebase detached HEAD -- tagging the wrong commit
and cascading conflict markers into the safety net. It now aborts, retries
preferring the release commit, and exits before pushing if that fails.
- Commit failures are no longer swallowed (only "nothing to commit" is
tolerated), so the tag can't silently point at the previous HEAD.
- `gh release create` is idempotent (view-then-create), unblocking reruns
after a transient failure between push and wrapper publish.
- Tag push is scoped to the release's own tag; `git push --tags -f` could
force-move historical tags.
Verified: node selftest (10 checks incl. new stale-waiting/dry-run-dedupe
cases), py_compile, YAML parse, actionlint (same 3 pre-existing SC2129
style warnings as main, nothing new), and the rebase-conflict retry logic
exercised against a scratch remote (bump wins, upstream commit preserved).
Co-Authored-By: Claude Fable 5 (1M context) <[email protected]>
Two existing tests asserted PraisonAIAgents raises ImportError (a v4
"removed" decision). Issue #3674 (owner) explicitly restores it as a
root-level silent alias for AgentTeam, which PR #3675 implements. Update
those tests to assert the restored back-compat contract instead.
Co-authored-by: Mervin Praison <[email protected]>
PraisonAIAgents was exported from praisonaiagents.agents but omitted from
the root lazy-import map and __all__, so `from praisonaiagents import
PraisonAIAgents` raised ImportError. Added it alongside its siblings and a
regression test covering all back-compat aliases.
Co-authored-by: MervinPraison <[email protected]>
Add functools.lru_cache to the pure model-capability predicates so the
same (model_name -> bool) result is computed once per process instead of
on every request/turn. Behaviour is unchanged: guards, litellm-None
fallbacks and except paths are all preserved, and keys are hashable
strings.
Co-authored-by: MervinPraison <[email protected]>
Addresses the three Greptile P1 findings on PR #3671:
- Wire load_environment_definition() into provision_compute() as an opt-in
baseline (explicit kwargs/instance config still win) so a committed
.praisonai/environment.yaml is actually applied.
- Validate nested config shapes in the loader so malformed values raise a
contextual ValueError (with file path) instead of incidental
AttributeError/TypeError.
- Tear down the just-started Docker container if package install or setup
fails, so a failed provision no longer leaks an unreachable container +
registry entry.
Co-authored-by: Mervin Praison <[email protected]>
Add the missing repo-committed environment definition joint: a lightweight
loader that maps .praisonai/environment.yaml onto the existing ComputeConfig
schema, plus the one new field (setup) with a live consumer.
- Core (praisonaiagents): ComputeConfig.setup: List[str]; load_environment_definition()
and find_environment_definition() (walks up like config discovery). YAML is
lazy-imported; unknown top-level keys raise ValueError with the file path.
network/backend preferences carried in existing typed fields (no new surface).
- Live consumer: docker compute provider runs setup commands once post-provision,
streamed to logs, with failures surfaced (raised) rather than swallowed.
- No file -> byte-identical behaviour (loader returns None; callers keep defaults).
Deliberately minimal per AGENTS.md (lightweight & powerful): no new CLI group,
no dispatcher rewrite, no SandboxConfig bridge, no RuntimeConfig remap.
Co-authored-by: MervinPraison <[email protected]>
Fix two valid reviewer (Greptile) findings in the research-writer example:
- The roles-file loader (praisonai_adapter._build_agents_and_tasks) sequences
tasks by declaration order and does not consume the top-level dependencies:
block; annotate agents.yaml and README to state this accurately.
- praisonai.run() is the Python API equivalent of the positional
`praisonai <agent_file>` CLI form (there is no `praisonai run <yaml>` CLI
subcommand for roles files); correct the README prose accordingly.
Docs/example only — no core SDK changes.
Co-authored-by: Mervin Praison <[email protected]>
The example passed topic via team.start(inputs=...), but AgentTeam.start
swallows **kwargs and never consumes `inputs`; interpolation reads only
task/team `variables` and replaces double-brace {{key}} placeholders.
Switch to AgentTeam(variables=...) with {{topic}} so the example actually
substitutes the topic instead of emitting a literal placeholder.
Co-authored-by: Mervin Praison <[email protected]>
Documents that Task.on_task_complete 2-param callbacks receive
(task_output, metadata) — NOT (task, task_output) like AgentTeam's hook.
Adds a copy-pasteable sequential human-review example and a regression
guard test. No new API surface (lightweight per AGENTS.md).
Co-authored-by: Dhivya-Bharathy <[email protected]>
Adds examples/yaml/teams/research-writer/ with a runnable single-file
agents.yaml (roles + nested tasks + dependencies) and README documenting
the existing praisonai run / AgentsGenerator loader path. No new core
modules or wrapper exports — examples/docs only, per the narrow scope.
Co-authored-by: Dhivya-Bharathy <[email protected]>
Addresses Greptile finding: the lint guard scanned examples/ + internal
CLI but omitted src/praisonai-agents/tests, where this PR migrated an
AgentTeam(...) call (crewai-tools-example.py). Reintroducing verbose=
there would previously go undetected.
Adds a scoped _SCAN_FILES list covering the specific migrated file rather
than sweeping the entire tests tree (which holds ~40 unrelated legacy
scripts still using verbose=/max_iter= that are out of scope for this PR).
Co-authored-by: Mervin Praison <[email protected]>
- flat layout (drop src/) so `python -m <slug>.main` works from the
project root without an install step
- switch YAML placeholders to double-brace {{topic}} and pass
variables= to AgentTeam (start(inputs=) was silently dropped)
- reject Python keywords in slugify to avoid invalid imports
- update pyproject packages.find to flat include and extend tests
Co-authored-by: Mervin Praison <[email protected]>
The delegate_task tool documented a `timeout` param but discarded it,
letting delegated work block past its deadline (Greptile P1). Run the
synchronous subagent spawn on a bounded ThreadPoolExecutor and return a
structured timeout failure on expiry. timeout<=0 disables the bound.
No new params/exports; reuses existing create_subagent_tool runtime.
Co-authored-by: Mervin Praison <[email protected]>
The rich.table import was executed before the console None-check, so an
ImportError bubbled to the outer handler and showed the static model list
instead of the real catalogue. Guard the import so the plain-text table
renders the live catalogue when Rich is absent (greptile P2).
Co-authored-by: Mervin Praison <[email protected]>
- Reject empty/whitespace query in forget() to prevent broad LIKE '%%'
deletion of unrelated short-term and long-term records (Greptile P1)
- Scope query-based forget() to long_term to mirror remember()/recall()
- Fix test fixture to use sqlite provider (was 'none' -> in_memory)
- Add empty-query guard test
Co-authored-by: Mervin Praison <[email protected]>
AgentTeam.__init__ accepts neither verbose= nor **kwargs, so passing
verbose= raised TypeError at construction. Verbosity is controlled via the
consolidated output= param (silent/minimal/verbose) and iteration limits via
execution=. Migrated all internal CLI, template generators, and examples off
the removed verbose=/max_iter=/session= kwargs, and added an AST lint guard
that scans examples + internal CLI for invalid AgentTeam kwargs to prevent
regression.
Co-authored-by: Dhivya-Bharathy <[email protected]>
Replace the always-failing stub in delegate_task with a call into the
existing create_subagent_tool runtime. A lightweight sub-Agent is spawned
(role derived from agent_type) to execute the task, preserving the JSON
response schema and the @require_approval(risk_level="medium") gate.
Co-authored-by: Dhivya-Bharathy <[email protected]>
praisonai models list crashed on Windows cp1252 consoles when Rich rendered
emoji (U+1F527 wrench) in the Capabilities column. Added stdout_supports_unicode()
helper and _capabilities_label() ASCII fallback, routed the table through the
encoding-aware output console, and applied the same guard to models describe/validate.
Co-authored-by: Dhivya-Bharathy <[email protected]>
Adds thin, intuitive aliases over existing long-term store/search/delete
methods and makes Memory(config=...) optional so Memory() works standalone.
No new modules, dataclasses, exports, or backend changes; fully backward
compatible.
Co-authored-by: Dhivya-Bharathy <[email protected]>
Define ManagerInstructions once at module level with extra="forbid" so the
generated JSON schema includes additionalProperties: false, which OpenAI's
strict structured-output API requires. Removes two duplicate inline class
definitions in process.py and adds a regression test.
Co-authored-by: Dhivya-Bharathy <[email protected]>
resolve_output and resolve_execution live in param_resolver.py, not
feature_configs.py. Split the test imports so resolver functions come
from param_resolver while config dataclasses stay in feature_configs.
Unblocks the entire praisonai-agents unit suite collection.
Co-authored-by: Dhivya-Bharathy <[email protected]>
Address reviewer feedback on the local-first session mirror:
- set_chat_history now mirrors the replaced transcript (Session save_state
and bot session manager persist via this path, not add_message, so those
histories were never reaching the mirror). Records carry stable ids so a
re-append is idempotent last-writer per id — the append-only shape the
mirror expects.
- _SessionMirrorWriter.enqueue drops-to-log after close() instead of
silently queuing records with no running consumer.
- The mirror writer thread is now started AFTER os.makedirs so a failed
construction on an unusable session dir never leaks an idle daemon thread.
Adds tests for all three; full session suite (123 tests) green.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Adds the core, unblocked half of session sync: a tiny SessionMirrorProtocol
(append/load/list_sessions) plus an optional mirror= on DefaultSessionStore
that appends persisted records to the mirror on a bounded background thread.
Local-first: the local write always happens first and a mirror outage never
blocks, fails, or corrupts a turn. Zero overhead / no thread when unset.
Heavy backend adapters + `session sync` CLI are deferred to the wrapper and
depend on the store-unification sibling (#3645, not yet merged).
Co-authored-by: MervinPraison <[email protected]>
- protocols.py: LocalTurnLock.release() now identity-checked via a per-key
holder map so a stale/duplicate token can never release a waiter's lease
(fixes concurrent-turns-on-one-session race). Add type-only asyncio import
under TYPE_CHECKING to resolve the 'asyncio.Lock' annotations (F821).
- config.py: reject NaN ttl (not self.ttl > 0); from_dict tolerates explicit
YAML nulls for backend/ttl via the 'or default' idiom.
- tests: add stale-token release regression, NaN-ttl rejection, null-tolerant
from_dict; make same/distinct-session concurrency tests deterministic with
asyncio.Event barriers (3.10-safe, no Barrier).
Co-authored-by: Mervin Praison <[email protected]>
UnifiedSessionStore hardcoded ~/.praison/sessions/ while every other CLI
surface reads/writes the canonical ~/.praisonai/sessions/ root via
paths.get_sessions_dir(). This made `praisonai code` conversations live in
a separate directory, invisible to `session list/show/export` and the
dashboard.
Resolve the default sessions directory from the canonical paths helper
(with a safe fallback to the legacy path if the helper is unavailable), so
code sessions land under the shared root. Explicit session_dir overrides
and the backward-compatible DEFAULT_SESSION_DIR attribute are preserved.
Co-authored-by: MervinPraison <[email protected]>
Turn serialisation is enforced by an in-process asyncio.Lock (LockMap) that
cannot see across processes/hosts, so a horizontally-scaled gateway
(replicas > 1) runs concurrent turns on one session — corrupting the shared
transcript, tripping provider strict-alternation, duplicating replies and
double-billing.
Add a pure, dependency-free TurnLockProtocol + TurnLeaseToken and a zero-cost
in-process LocalTurnLock default to core gateway protocols, plus a TurnLockConfig
selector wired into GatewayConfig (Python + YAML). The "local" default reproduces
today's asyncio.Lock behaviour byte-for-byte (single-replica unchanged, no new
dependency); "redis" is the seam for a distributed lease implementation in the
wrapper/bot package (reusing RedisConfig + the scheduler's owner+TTL pattern).
Mirrors how every other gateway robustness knob (drain, admission, rate-limit,
liveness, dead-letter) is a swappable pure protocol in core.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Address reviewer findings (CodeRabbit, Greptile) on PR #3641:
- Plumb the gateway's real bind host into enable_shell_tools via a new
keyword-only gateway_bind_host param. Production callers construct a
per-channel BotConfig that never carried the bind host, so an externally
bound (0.0.0.0) gateway was invisible to the exposure downgrade. The
gateway now passes self.config.bind_host / self._host explicitly.
- Treat every configured group_policy (including command_only) as a
multi-user surface: a group member's command can reach execute_command,
so blanket auto-approve must downgrade to approval.
- Call callable host accessors (e.g. GatewayServer.host) before
stringifying so is_loopback classifies loopback deployments correctly.
- Add tests for command_only downgrade, explicit external/loopback
gateway_bind_host, and callable host accessor resolution.
Co-authored-by: Mervin Praison <[email protected]>
- Keep DegradedCapabilityProtocol unchanged (mark/clear/list_degraded); move
find() onto new DegradedCapabilityLookupProtocol so existing external
registries retain structural isinstance conformance.
- Module guard falls back to list_degraded() when a legacy registry lacks both
the guard and find(), failing-closed instead of raising AttributeError.
- Export DegradedCapabilityLookupProtocol.
- Fix Ruff B015 pointless-comparison in no-op guard tests (assert ... is None).
- Add legacy-registry coverage for base/lookup protocol conformance and the
list_degraded() fallback.
Co-authored-by: Mervin Praison <[email protected]>
Complete the degraded-owner contract's read side (Issue #3518) by adding
the core fail-closed primitive so a request targeting a degraded owner
short-circuits to a typed, redacted "unavailable — next action" outcome
instead of proceeding and failing silently.
Adds to praisonaiagents/gateway/degraded_state.py:
- OwnerUnavailable exception carrying only DegradedOwner's redacted fields
- DegradedCapabilityRegistry.find() point read
- DegradedCapabilityRegistry.assert_owner_available() guard
- module-level assert_owner_available() no-op-safe for an optional registry
Exports both from praisonaiagents.gateway and covers with unit tests.
Co-authored-by: Mervin Praison <[email protected]>
Blanket auto_approve_shell now degrades to explicit approval when the
gateway binds to a non-loopback interface or the channel is a
multi-user/group surface, mirroring the auth token's assert_external_bind_safe
posture. Operators can opt back into blanket auto-approve with
auto_approve_shell_acknowledge_exposed: true.
Co-authored-by: Mervin Praison <[email protected]>
Address reviewer findings on PR #3638:
- Loop detection & loop guard now observe the FULL tool result; the compact
model-facing view is swapped in only right before return, so a stable
compact summary no longer masks progressing polls as no-progress loops.
- The compact override now passes through wrap_if_external, so external/
untrusted model_output cannot bypass the prompt-injection fence.
- Model-facing annotations (AFTER_TOOL additional_context, loop-guard notices)
are re-applied to the compact view so they always reach the model.
- Unified the to_model_output hook contract (receives the output value in both
safe_run and the executor) and guarded it in safe_run so a failing builder
degrades to full output instead of corrupting a successful result.
- Corrected resolve_model_output docstring to match implementation.
Co-authored-by: Mervin Praison <[email protected]>
Address reviewer findings on PR #3637:
- save_session_state now returns the SessionStore bool result so a silent
False (serialisation/disk failure) no longer reports success to callers.
- Tests use pytest tmp_path (no leaked temp dirs) and verify cross-instance
durability via a separate DefaultSessionStore over the same directory.
Co-authored-by: Mervin Praison <[email protected]>
Add an optional, backward-compatible way for tools to feed the LLM a
compact summary while keeping the full result for display/hooks/tracing:
- ToolResult(model_output=...) compact view + resolve_model_output helper
- BaseTool.to_model_output(result) override, wired through safe_run
- @tool(to_model_output=fn) on the decorator / FunctionTool
- tool_execution.py resolves the compact view and swaps it in for the
model only after tracing + AFTER_TOOL hooks saw the full payload
No override => unchanged behaviour (model sees full output).
Co-authored-by: MervinPraison <[email protected]>
Route AgentTeam.save_session_state/restore_session_state through the
durable, project-scoped SessionStore (the same store single-Agent
--continue uses), keyed deterministically by session_id via
update_session_metadata/get_session. This removes the fuzzy
search_short_term(limit=10) lookup and the memory-gating: multi-agent
runs now resume deterministically even with memory=False.
shared_memory is demoted to optional enrichment, and the legacy
semantic lookup is kept only as a fallback for pre-existing sessions,
preserving full backward compatibility. No new params or exports.
Co-authored-by: MervinPraison <[email protected]>
- _resolve_framework gains require_workflow: WorkflowAutoGenerator now
rejects an installed-but-non-workflow default (e.g. when native praisonai
is absent) instead of emitting a workflow YAML AgentsGenerator would later
reject (Greptile/CodeRabbit P1).
- PushClient.supports_publish now also requires an active connection, closing
the disconnected/pre-connect false-positive that let gated producer calls
proceed and fail with ConnectionError (Greptile P1).
- Tests: _fake_registry sets SUPPORTS_WORKFLOW; framework-resolution tests now
assert exact create()/pick_default() interactions (CodeRabbit).
Co-authored-by: Mervin Praison <[email protected]>
- agents_generator: let FileNotFoundError bubble from _load_config and drop
the `if config is None: return` guards so run/arun/serve raise instead of
returning a silent None for a missing agent file.
- auto: add BaseAutoGenerator._resolve_framework and route both AutoGenerator
and WorkflowAutoGenerator through registry.pick_default() with adapter
validation, so defaults and validation are a single source of truth.
- push: PollingTransport.send now raises NotImplementedError for unmapped
message types (publish/create/presence) instead of silently dropping them;
PushClient logs the capability loss on fallback and exposes supports_publish.
Co-authored-by: MervinPraison <[email protected]>
- read-only policy preset now denies real dangerous tool names
(tool:write_*/delete_* + *_write*/*_delete* globs) instead of
patterns that never match the SDK's own tools
- LLMGuardrail._llm_validate recognizes the SDK's LLM.get_response
interface, so string/LLM-instance guardrails validate instead of
silently failing closed on every call
- iter_stream warns when an output guardrail is attached, since
token-level streaming cannot retroactively validate output
Co-authored-by: MervinPraison <[email protected]>
Strengthen test_generate_dockerfile_includes_gunicorn_timeout per reviewer
feedback (Greptile/CodeRabbit): bare "--timeout" substring also matched the
HEALTHCHECK --timeout=10s option, so a malformed CMD could pass. Now assert
the exact option-value pairs in the generated Gunicorn command.
Co-authored-by: Mervin Praison <[email protected]>
Agent LLM workloads (cold import + first model round-trip) routinely
exceed Gunicorn's 30s default worker timeout, causing the worker to be
SIGKILL'd mid-request on the first /chat call. Add explicit --timeout 120
and --graceful-timeout 30 to the generated CMD.
Co-authored-by: Dhivya-Bharathy <[email protected]>
Collapsing the release to a single commit (#3572) removed the per-package
"Bump praisonai-mcp to 0.0.12" commits. On a full release that is fine --
the wrapper's "Release v<x.y.z>" commit describes the whole batch. But a
single-package release (every other skip_* set, including skip_wrapper)
never reaches the wrapper, so its bump lands via the safety-net step and
was labelled with the generic "persist version bumps".
Derive the package list from the staged paths so that case reads
"chore(release): bump praisonai-mcp [skip ci]" instead. Falls back to the
generic message when nothing under src/ is staged.
Verified the extraction against a scratch repo: single package, multiple
packages, and the no-src fallback.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Address reviewer feedback on the memoisation:
- Replace unbounded functools.lru_cache with a WeakKeyDictionary so cached
bound methods no longer keep their owning Agent alive (memory leak in
long-lived processes creating many short-lived agents).
- Fall back to direct signature inspection for unhashable/unweakreferenceable
callables (e.g. objects with __hash__ = None) instead of raising TypeError,
preserving correct 3-arg tool dispatch.
- Drop now-unused functools import.
Co-authored-by: Mervin Praison <[email protected]>
Cache the inspect.signature() on_progress-kwarg probe on the callable so it
runs once per dispatcher callable instead of once per tool call. execute_tool_fn
is invariant per agent, so the memoised result is identical. The
(TypeError, ValueError) fallback for non-introspectable built-in/C callables is
preserved. No behaviour or API change.
Co-authored-by: MervinPraison <[email protected]>
Addresses the three P1 reviewer findings on PR #3624:
- telegram.py: streaming branch now calls _maybe_send_voice_reply after
finalize, so voice.mode always/match_inbound is honoured when streaming
is enabled (previously text-only).
- gateway/server.py: the Telegram handler serving VOICE|AUDIO now threads
inbound_was_voice into _send_response_with_media, so match_inbound fires
for gateway-owned channels.
- tools/audio.py + bots/_tts.py: forward configured voice.speed through
tts_tool -> AudioAgent.speech (previously dropped).
- tests: update fake tts_tool signature and add speed-forwarding regression.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Adds a first-class, opt-in outbound voice-reply path mirroring the existing
inbound `stt` setting. New `bots/_tts.py` provides TtsConfig,
resolve_tts_config, should_voice_reply and synthesize_voice_reply (reusing
tools.audio.tts_tool so heavy TTS deps stay in tools). A TtsConfigSchema +
`voice` channel field give tri-surface (YAML/metadata) parity with `stt`,
carried through config.metadata by the gateway. Telegram wired as the
reference consumer: replies are spoken per the off/always/match_inbound mode,
degrading cleanly to the already-sent text on failure.
Co-authored-by: Mervin Praison <[email protected]>
Address the three Greptile P1 findings on PR #3622:
- Channel system_prompt_hint now reaches the model: inject into
backstory + rebuild system_prompt (the consumed prompt fields) instead
of the construction-only `instructions` attribute, which the runtime
prompt builder never re-reads. Idempotent and cache-stable.
- empty_final override is now reachable via the existing BotConfig.metadata
seam (no new typed knob); direct attribute still honoured for callers.
- Inbound crash-replay docstring/log corrected to state its real effect
(unblock stranded claimed rows so redelivery / session-resume can
reprocess) rather than over-claiming standalone re-drive, which is
already owned by _resume_interrupted_turns (#3379).
Co-authored-by: Mervin Praison <[email protected]>
Gap 1 — visible-outcome guarantee for empty finals: add a dependency-free
core classifier praisonaiagents.bots.silence.classify_final() that separates
deliberate silence from a blank/whitespace/[tool_calls: …] final, and resolve
"empty, not silence" once at the shared fire_message_sending funnel with a
recorded, typed fallback (operator log; optional empty_final_message config).
Every adapter (Slack/Telegram/etc.) now delivers a visible outcome instead of
silently dropping, DLQ-parking an empty send, or leaking a machine placeholder.
Gap 2 — wire the dead system_prompt_hint seam: inject a channel's declared
system_prompt_hint into the per-channel cloned agent's instructions via the
already-existing get_channel_system_prompt_hint() helper (the missing caller),
bounded and prompt-cache-stable. No-op for built-ins; third-party channels can
now teach the agent their constraints as the ChannelDescriptor contract states.
Gap 3 — invoke un-wired inbound crash-replay: call InboundJournal.replay() at
channel start-up (and hot-reload) so an inbound stranded 'claimed' by a crash
is re-driven exactly-once on the default-durable path, honouring the module's
own durability contract. Replayed counts are logged for operator visibility.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
- Fix NameError regression: _run_custom_agent referenced an undefined
`instructions` var (broke test-core cli suite). Add the param and thread
merged_instructions from the caller so the --agent path injects declared
instruction sources into the backstory.
- Forward merged_instructions on the --command path and skip the warm-runtime
attach when instructions are present (per-invocation context can't cross the
runtime process boundary), so declared instructions are no longer dropped.
- SSRF guard for remote instruction URLs: reject private/loopback/link-local/
reserved destinations and re-validate redirects, matching the existing
managed-config resolver policy. Opt-out via PRAISONAI_INSTRUCTIONS_ALLOW_LOCAL_URLS.
- Add regression tests for custom-agent instruction injection and the SSRF guard.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
PraisonAI.main() replaces args via parse_args() and previously carried
over only session fields, silently dropping the approval/permission
settings that the YAML run path threads onto praison.args. YAML workflows
launched with --approval/--approve-all-tools/--approval-timeout or
--allow/--deny (which derive a console backend) therefore bypassed the
approval gate. Preserve these fields unconditionally across the reparse,
independently of session flags (--no-save sets approval but not
cli_project_sessions). Adds regression tests exercising the real main()
preservation boundary.
Co-authored-by: Mervin Praison <[email protected]>
Adds a wrapper-level `instructions` config key and repeatable
`run --instructions <path|glob|url>` flag so teams can declare extra
instruction/context sources (files, globs, ~ paths, http(s):// URLs)
loaded alongside the convention-only AGENTS.md/CLAUDE.md auto-load.
- context_files: resolve_instruction_sources() expands globs/~, reads
local files, and lazily fetches remote URLs (stdlib urllib, size-bounded,
best-effort with warnings) — no new heavy deps.
- run.py: resolve config `instructions` (layer-concat via the existing
ConfigResolver list-merge), merge repeatable --instructions on top, and
inject up front into the agent backstory via the existing subtree-context
hook (passed as already_loaded to dedup nested files).
- schema: add `instructions` to config_hierarchy CONFIG_SCHEMA and the
resolver's KNOWN_TOP_LEVEL_KEYS (so it validates and is not warned).
Wrapper-only; no core changes, no new Agent params.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: MervinPraison <[email protected]>
Thread the existing --approval/--approve-all-tools/--approval-timeout and --allow/--deny/--permissions rules from praisonai run workflow.yaml into the YAML engine args so YAML agents are permission-gated exactly like single-agent run. Session continuity and structured --output already flow through _run_from_file; no new engine wiring or core changes are added.
Co-authored-by: MervinPraison <[email protected]>
The write tools resolved the symlink only inside is_protected() but then
open()'d the original path, so (a) an in-workspace symlink to an out-of-
workspace file bypassed the workspace-confinement check and (b) a symlink
retargeted between check and write could redirect an approved edit into a
protected file. Add security.resolve_real_path() and resolve abs_path once
in write_file/append/apply_diff/search_replace so the protection check,
workspace-confinement check and the actual read/write all act on the same
real target. New files are unaffected (realpath returns the path unchanged).
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
1. Call/agent-invoke server auth: constant-time token compare via
hmac.compare_digest; media-stream no longer carries the shared server
token in the URL (mints a one-shot, TTL-bound session token instead);
guard active_connections/client_ips mutations with asyncio locks to
close the async TOCTOU on the rate/connection limits.
2. Protected-path guard: resolve symlinks in is_protected /
get_protection_reason so a same-directory symlink to .env is no longer
treated as harmless (bypass fix; centralised so all writers inherit it).
3. run_python: pass code as an argv list to subprocess.run instead of
building a shell-quoted string re-tokenised by shlex, preserving the
caller's exact source (newlines/backslashes/quotes) on POSIX and Windows.
Co-authored-by: MervinPraison <[email protected]>
Cloud providers shell out to external CLIs and never generate the API
server, so the parsed deploy.api config had no live consumer and was
silently ignored. Remove it to avoid misleading config (per reviewer
feedback from CodeRabbit/Greptile). Docker path already honors the
sibling api block; add runtime tests asserting PRAISONAI_API_AUTH is
propagated when auth_enabled: false and omitted otherwise.
Co-authored-by: Mervin Praison <[email protected]>
Docker deploy ignored the optional sibling `deploy.api` block, so
`auth_enabled: false` was silently dropped and the generated server
defaulted auth on, returning 401 on /chat.
- schema.py: parse optional `deploy.api` for type docker and cloud
- main.py: pass PRAISONAI_API_AUTH=disabled to container when disabled
- add unit test + update example agents-docker.yaml
Co-authored-by: Dhivya-Bharathy <[email protected]>
The @require_approval decorator now keys approvals on the tool's effective
arguments, so a bare mark_approved(name) no longer unlocks arbitrary-value
positional calls (the sticky-cache bypass this PR closes). Update the test to
mark/check with the same args the calls use, preserving the mark->call intent
without weakening the security guarantee.
Co-authored-by: Mervin Praison <[email protected]>
The require_approval decorator only keyed approvals on kwargs, so
directly-invoked decorated tools called positionally (read_file("/a"))
all collapsed to the same empty-argument key — one approval unlocked any
later positional value. Bind positional + keyword args into a normalized
dict via the function signature so the approval key reflects the actual
call. Falls back to an __args__ tuple for C-callables without a bindable
signature. Adds regression tests.
Co-authored-by: Mervin Praison <[email protected]>
- approval: thread `arguments` through mark_approved/is_already_approved,
the require_approval decorator wrappers, and the Agent tool path so a
single approval no longer permanently unlocks a tool for any later
attacker-chosen arguments (safe-by-default).
- agent memory: stop defaulting to the shared constant "praison"/"default";
when memory is enabled without an explicit user_id, generate a per-instance
non-shared id and warn, preventing silent cross-session memory merges.
Co-authored-by: MervinPraison <[email protected]>
- _kill_pid now returns result.returncode == 0 on Windows and catches OSError
- _destroy_api returns success=False with failed PIDs when any kill fails
- tests: mock zero returncode, add nonzero-taskkill and partial-kill cases
Co-authored-by: Mervin Praison <[email protected]>
Add regression coverage for the separate _configure_stdio() helper in
__main__.main(), asserting utf-8 + errors="replace" are applied before app
dispatch, plus a no-op safety case. Also assert errors=="replace" in the
existing deploy._configure_stdio test.
Co-authored-by: Mervin Praison <[email protected]>
Replace Unix-only lsof in _destroy_api() with cross-platform port
discovery (netstat -ano on Windows, lsof on Unix) and cross-platform
process termination (taskkill on Windows, SIGTERM on Unix). Prevents
orphaned Flask API servers on Windows after destroy.
Co-authored-by: Dhivya-Bharathy <[email protected]>
Windows default consoles use cp1252, which cannot encode the emoji banner
prefixes emitted by the deploy CLI, causing UnicodeEncodeError on every
command. Reuse the established _configure_stdio() pattern from
praisonai-browser to reconfigure stdout/stderr to UTF-8 (errors=replace)
at both the praisonai-deploy console entry and the praisonai deploy bridge
entry (handle_deploy_command).
Co-authored-by: Dhivya-Bharathy <[email protected]>
Replace NamedTemporaryFile + os.unlink pattern with pytest tmp_path fixture
to avoid PermissionError [WinError 32] on Windows where the file handle
cannot be deleted while still open inside the context manager.
Co-authored-by: Dhivya-Bharathy <[email protected]>
The generated Flask API server was spawned with a bare 'python' which
resolves to a different interpreter than the parent venv on Windows and
multi-Python setups, causing ModuleNotFoundError: No module named 'flask'.
Spawn with sys.executable and preserve the parent environment so the child
inherits Flask and OPENAI_API_KEY for /chat.
Co-authored-by: Dhivya-Bharathy <[email protected]>
Extract the triplicated "build a ToolResolver, bucket its
list_available_sources(), then fall back to a TOOL_MAPPINGS +
praisonai_tools scan" helper into one internal module
templates/_tool_sources.py. tools.py, tools_doctor.py and
dependency_checker.py now call the shared helper; per-module
presentation stays local so output is byte-identical.
Co-authored-by: MervinPraison <[email protected]>
Add a pure next_fire_time(cron_expr, base) helper in
praisonaiagents.scheduler.due as the single owner of the "compute next
cron fire from a base" step. Core is_due and the wrapper's
ScheduleTicker.is_due now both call it, removing the duplicated
croniter().get_next(float) logic from the wrapper. No behaviour or API
change; croniter-missing graceful degradation preserved.
Co-authored-by: MervinPraison <[email protected]>
Address final-review findings on the durable-write salvage path:
- spill filenames add a random token so consecutive same-ms/PID
failures no longer overwrite each other (no lost turns)
- _reingest_spill runs _enforce_window before persisting so recovered
turns respect the retention policy like any ordinary write
- guard spill payload shape (non-object root / non-list messages /
non-object message) so a malformed spill can't block recovery
Co-authored-by: Mervin Praison <[email protected]>
When a durable session write fails (disk-full / SQLite-FTS corruption /
permission OSError) the store previously collapsed it to a silent False that
no caller reads, so the already-produced turn lived only in the in-memory
cache and was permanently lost on the next shutdown/crash — with no operator
signal.
This adds a safe-by-default, stdlib-only salvage path in the core session
store that triggers only on the failure path (no hot-path regression):
- New SESSION_PERSIST_FAILED lifecycle hook + SessionPersistFailedInput so a
failed durable write is observable (metrics/alerting/forensics), mirroring
the outbound MESSAGE_UNDELIVERED posture.
- On write failure, spill the unpersisted turn to an atomic fallback file
(~/.praisonai/state/session_spill/*.json, 0600, temp-file + os.replace +
best-effort dir fsync) instead of holding it only in memory.
- On session load, re-ingest any spilled turns (dedup on role+content+
timestamp), persist them back, and delete each spill file only after a
successful re-insert.
No new Agent params, no new dependencies, no behaviour change on the happy
path. Adds 6 unit tests.
Co-authored-by: MervinPraison <[email protected]>
Flipping SessionConfig.persist to True (durable-by-default) exposed two
latent bugs that the prior off-by-default masked:
1. CLI opt-out ignored: the multi-bot CLI builds WebSocketGateway with a
default session config, so __init__ selected a persistent store before
start_with_config loaded the YAML — and that loader never read the
session: block. A documented session.persist: false was silently ignored.
Fix: start_with_config now re-reads the session: block and re-selects the
store (unless an explicit store was passed to the constructor).
2. Startup crash on unwritable home: SqliteTranscriptStore/DefaultSessionStore
eagerly makedirs(~/.praisonai/sessions); on a read-only/absent home both
the primary store and its fallback raised, escaping __init__ and crashing
the gateway. Fix: the store selection now degrades to in-memory sessions
instead of aborting startup.
Store-selection logic extracted into a shared _build_session_store helper so
__init__ and start_with_config pick identically. Adds regression tests for
the opt-out, the explicit-store precedence, and from_dict persist defaults.
Co-authored-by: Mervin Praison <[email protected]>
fallback_text() unconditionally imported table_to_markdown/chart_to_text
and referenced BlockType.TABLE/CHART, which raised ImportError/AttributeError
on praisonaiagents releases predating those symbols (bot pins >=1.6.152) —
breaking fallback delivery even for text-only presentations.
- Import the two new helpers defensively with inline fallbacks
- Dispatch table/chart blocks via string block-type values (getattr-guarded)
Co-authored-by: Mervin Praison <[email protected]>
Add TABLE and CHART members to the portable presentation vocabulary so
agents can describe tabular and chart data once and have each channel
render it natively where supported or degrade deterministically to a
markdown table / compact text summary elsewhere.
- BlockType.TABLE / BlockType.CHART + PresentationBlock.make_table/make_chart
- columns/rows/chart_kind/series fields with dict round-trip
- PresentationLimits: supports_tables/supports_charts + max_table_rows/cols
- adapt_presentation clamps and degrades table/chart per capability flags
- table_to_markdown/chart_to_text helpers (exported) + fallback_text branch
Co-authored-by: MervinPraison <[email protected]>
Flip SessionConfig.persist from False to True so an out-of-box gateway
remembers conversations across restarts via the existing SQLite
transcript store. Updates the two YAML-parse fallbacks to match. Users
who want ephemeral sessions can still opt out with session.persist: false.
Co-authored-by: MervinPraison <[email protected]>
The previous approach replaced the bash:<command> approval target with a
shell:external-path:<path> target for out-of-workspace commands. This both
broke command-specific deny rules (a hard `deny: bash:rm *` no longer matched,
regressing DENY -> ASK and letting a user approve a denied command) and
collided with the existing `shell:` shell-prefix namespace in
PermissionManager.check().
The out-of-workspace boundary is already fully implemented in
PermissionManager (`_check_shell_command` + `_external_dir_target`): with a
workspace_root configured, a broad `bash:*` allow escalates external paths to
ASK while a command-specific deny still fires. build_permission_target now
keeps the verbatim `bash:<command>` target so that machinery applies and
command identity is preserved.
- approval/utils.py: drop the redundant shell:external-path namespace and the
_shell_external_paths helper; shell tools map back to bash:<command>.
- tests: replace target-mangling assertions with boundary-enforcement tests
that verify (a) broad bash:* does not cover external paths, (b) in-workspace
stays allowed, (c) command-specific deny still fires on external paths, and
(d) backward-compat with no workspace_root.
Fixes the deny-bypass flagged by Greptile and CodeRabbit.
Co-authored-by: Mervin Praison <[email protected]>
- share: atomic temp-file write + os.replace so a failed/interrupted
write never truncates a previously published transcript
- share/unshare: guard OSError (unwritable/full data home) as clean
command errors with chained context instead of raw tracebacks
- unshare: unlink unconditionally, treat FileNotFoundError as an
idempotent no-op (fixes concurrent-deletion race + ruff B904)
- soften overstated 'always redacts secrets' copy to accurate
best-effort wording (redactor #3426 is high-signal/best-effort)
- test: isolate test_share_path_is_stable_per_session via project fixture
Co-authored-by: Mervin Praison <[email protected]>
Adds `praisonai session share <id>` / `unshare <id>` to the CLI. `share`
reuses the existing session resolver + transcript redactor to publish a
single self-contained HTML transcript to ~/.praisonai/shares and returns a
file:// link; `unshare` revokes it. Zero external infrastructure or new
dependencies; sharing is opt-in and always redacts secrets first.
Co-authored-by: MervinPraison <[email protected]>
Make build_permission_target emit a distinct shell:external-path:<path>
target when a shell command touches a path outside the workspace root,
so a broad bash:*/"allow shell"/session grant cannot silently authorise
out-of-workspace access. Reuses the existing command_parser and
path_safety.resolve_within_root primitives (no new modules/params).
Default-on and fail-closed, with a PRAISONAI_SHELL_WORKSPACE_BOUNDARY
opt-out for trusted sandboxed/CI runs.
Co-authored-by: MervinPraison <[email protected]>
_TimeoutBoundTool was a plain proxy that did not subclass the wrapped tool's
class, so isinstance(proxy, BaseTool) returned False. Downstream executors
(praisonaiagents tool_execution, CrewAI/LangChain) route on that isinstance
check to call .run; the proxy failed it and, being non-callable, also skipped
the generic callable branch -> a BaseTool plugin tool with tool_timeout set
silently executed nothing.
Build the proxy as an instance of a dynamic subclass of type(inner) so
isinstance keeps passing (verified for praisonaiagents BaseTool and pydantic
CrewAI/LangChain-style tools) while still never mutating the shared inner
object and still routing _run/run through the per-generator timeout wrapper.
Added regression tests for isinstance/schema preservation and cross-generator
executor isolation.
Co-authored-by: Mervin Praison <[email protected]>
Gap 1: create_unified_app now enforces the api_key via the shared
_api_auth middleware (previously accepted but never referenced), and
serve unified refuses to bind a non-localhost host without an API key,
mirroring serve agents.
Gap 2: tool timeout wrapping no longer mutates shared framework tool
objects in place; each generator installs its own _TimeoutBoundTool
proxy that delegates schema attributes but routes _run/run through its
own executor. Owner identity is a stable per-generator uuid compared by
value, so the idempotency guard actually matches.
Gap 3: resume_or_create_session accepts a create_session dispatcher; the
orchestrator routes it through _sync so async conversation stores have
their create-session coroutine awaited instead of silently dropped.
Co-authored-by: MervinPraison <[email protected]>
Replace the fixed .tmp.<pid> temp filename with tempfile.mkstemp so
concurrent writers in the same process (multiple threads or FileMemory
instances writing the same file) cannot clobber each other's temp file,
where the first os.replace would remove the temp the sibling still needs.
Matches the atomic-write convention used elsewhere in the SDK
(storage/base.py, session/store.py). Addresses Greptile P1.
Co-authored-by: Mervin Praison <[email protected]>
- agents.py: add _gather_with_isolation so a failing async task no longer
orphans its siblings; siblings are awaited to completion and the first
exception is re-raised (return_exceptions=True) across all 3 gather sites.
- handoff.py: guard finally:_pop_handoff() with a `pushed` flag at all 5
call sites so a _check_safety rejection (which raises before _push_handoff)
no longer pops an ancestor's entry and corrupts the cycle/depth guard.
- file_memory.py: write JSON to a temp file + fsync + os.replace so a crash
mid-write can no longer truncate the persisted memory store.
Co-authored-by: MervinPraison <[email protected]>
Delete create_autogen_tool_function() and the 17 generated autogen_<Tool>
symbols plus their __all__ from inbuilt_tools/autogen_tools.py. Their only
consumer, ToolRegistry.register_builtin_autogen_adapters(), was removed in
the #1590 fix; the canonical AutoGen path is framework_adapters/autogen_adapter.py.
Keeps the praisonai_tools availability probe that feeds PRAISONAI_TOOLS_AVAILABLE
and the release `import *` smoke test.
Co-authored-by: Mervin Praison <[email protected]>
Address the three P1 findings on the declarative webhook channel:
- Gateway wiring: add a `webhook` branch to WebSocketGateway._create_bot and
recognize WebhookBot in the supervisor's own-aiohttp-server start branch, so a
`type: webhook` YAML channel actually constructs and starts (was silently
skipped by the hardcoded adapter switch). Guard _inject_routing_handler for
ingress channels without an on_message hook.
- Fail-loud dispatch: the HTTP handler now returns 500 when agent dispatch
raises, so providers retry instead of receiving a false 200 ack.
- Durable ingress id: fall back to a deterministic sha256(path + raw_body) when
no delivery header is present, keeping ingress journaling/dedup active for
generic senders (an empty message_id disabled both).
Adds tests for 500-on-failure, delivery-header and body-hash message ids, and
gateway construction of the webhook adapter.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Add a first-class `webhook` gateway channel that turns any HTTP event into an
agent trigger through configuration alone — no bespoke adapter code.
- core (praisonaiagents.bots): a small, import-light declarative filter
(WebhookFilter + evaluate_webhook_filter/resolve_field) with all/any/not
combinators and exists/equals/contains/in/regex leaf operators. Pure logic,
no HTTP, fail-safe on malformed input.
- wrapper (praisonai-bot): WebhookBot HTTP-ingress adapter + WebhookRoute,
reusing existing HmacWebhookVerifier / enforce_webhook_verification, the
aiohttp server pattern, and BotSessionManager (durable dedup ingress). Adds
prompt templating ({{ dotted.path }}) and a silent (ack-only) route.
- register `webhook` as a built-in loader in the channel registry.
Reuses existing primitives and ChannelRouteConfig.metadata; no core config
schema change or new Agent params.
Co-authored-by: MervinPraison <[email protected]>
Route WebSocketGateway._create_bot through the shared platform registry
(resolve_adapter) instead of a hardcoded if/elif over the seven built-ins.
Any channel the registry resolves — built-in, register_platform(), or a
praisonai.channels entry point — is now instantiated and started, with
ch_cfg passed through generically (mirroring probe_channels). An
unresolvable or unconstructable platform is recorded as a degraded channel
(visible in health/doctor) rather than a silent skip. Per-platform token
env-var fallbacks (email/AgentMail/Linear) are preserved.
Co-authored-by: MervinPraison <[email protected]>
- agent_invoke: _apply_agent_config now rejects overrides on non-isolatable
(shared registry) agents instead of mutating a shared object, preventing
config from leaking across concurrent/subsequent requests. Drop the
never-settable `temperature` key from the whitelist so the surface is honest.
- vector_stores: warn (once per collection) when an existing/legacy Chroma
collection uses a non-cosine distance metric, surfacing incomparable scores
without a destructive auto-migration of stored vectors.
Co-authored-by: Mervin Praison <[email protected]>
- ChromaVectorStore: centralise namespace->collection resolution so ad-hoc
namespaces keep the cosine metric; return the stored embedding (not the
query vector) from query(); scope delete_all to the caller's namespace.
- agent_invoke: honour agent_config overrides against the isolated clone
via a whitelist, rejecting unknown/unsettable keys with 400 (route) or a
clean error dict (standalone) instead of silently ignoring them.
- a2u_server.publish_sync: use get_running_loop, track the scheduled task
(WeakSet) so it can't be GC'd and surface its exceptions; expose
last_publish_task() for the real delivered count.
- push/client + acp/server: replace deprecated asyncio.get_event_loop()
with get_running_loop()/to_thread inside coroutines.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Both create-github-app-token steps omitted `repositories:`, so the run log
read "creating token for all repositories for given owner MervinPraison" --
the token carried write access to every repo under the account while the
job only ever touches PraisonAI.
Verified single-repo before narrowing: the workflow makes no cross-repo
call, and bump_and_release.py's only gh invocation is `gh release create`
against this repo, with all git operations on origin.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
- failover.py: snapshot next_profile + callbacks under the RLock, then
invoke callbacks after releasing it. User callbacks may block or take
external locks; running them under the shared credential-pool lock could
stall every concurrent agent and risk lock-ordering deadlocks (Greptile).
- chat_mixin.py: _run_id -> _current_run_id at the second tool-output
truncation call site the PR missed; previously passed None into
get_tool_output_store, defeating the run isolation Gap 3 restores.
Co-authored-by: Mervin Praison <[email protected]>
- LLMGuardrail.__call__ now fails closed on ambiguous LLM replies, matching
_llm_validate() and the class's documented fail-closed contract.
- FailoverManager gains a threading.RLock guarding all _profiles reads/mutations
and AuthProfile status transitions, since it is a shared credential pool by design.
- Fix _run_id -> _current_run_id typo that silently disabled tool-output
run-scoped isolation; harden get_tool_output_store so a None run_id no longer
adopts the first run's process-wide store.
- Add regression tests asserting both guardrail entry points fail closed.
Co-authored-by: MervinPraison <[email protected]>
- docker-compose: require POSTGRES_PASSWORD (no known fallback) and bind
Postgres to loopback by default (POSTGRES_BIND) — resolves GitGuardian
finding and the default-credentials exposure P1.
- railway starter: ship a Dockerfile so railway.json's DOCKERFILE build
and start command reference real files (fixes missing-file P1).
Co-authored-by: Mervin Praison <[email protected]>
A full pypi-release run landed 9 separate commits and 9 pushes on main --
one per package -- all authored as MervinPraison.
Collapse to one commit: delete the 8 per-package "Commit <pkg> version
bump" steps. They were redundant, since the wrapper step already calls
bump_and_release.py --no-add-all whose release_files list is a superset
of every file those steps staged, so all bumps now land in the script's
single "Release v<x.y.z>" commit. That needs --force, because the tree is
now dirty when the script runs and its pre-flight guard exits 1 under
--no-add-all; --no-add-all still restricts staging to the explicit list.
A trailing "Persist any uncommitted version bumps" step covers what the
single commit cannot: a package failing midway (those versions are
already live on PyPI, so the bumps must not be lost), skip_wrapper, or a
wrapper failure. It is a no-op on a clean run, and uses `git add -u` so
untracked build output stays out.
Attribute the release to praisonai-triage-agent[bot] by minting an
installation token from CLAUDE_APP_ID/CLAUDE_APP_PRIVATE_KEY -- the app
already behind that bot in claude.yml, claude-merge-gate.yml and
pipeline-status-sync.yml. GH_TOKEN is exported via $GITHUB_ENV, so the
commit, the tag push and gh release create are all the bot's;
secrets.GH_TOKEN is no longer referenced.
Two details this depends on: app tokens expire after 1h and this job can
exceed that (9 packages x up to a 600s PyPI wait each), so the token is
re-minted immediately before the wrapper release. And checkout sets
persist-credentials: false -- otherwise its http.extraheader would
outrank the remote URL and pin the first, expiring token.
Requires the App to be able to push to main (contents: write, plus a
ruleset bypass entry if main requires PRs).
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Add the same overlap-guard assertion used in eval/tokens.py so RAG-specific
extras can never silently shadow canonical MODEL_LIMITS entries and re-introduce
the drift that #3567 fixed.
Co-authored-by: Mervin Praison <[email protected]>
Deduplicate the canonical tool-argument identity scheme
(json.dumps(sort_keys=True, default=str) -> sha256[:16]) that was
independently re-implemented in the approval registry and doom-loop
detector. Both safety subsystems now delegate to a single
hash_tool_args() helper in approval/utils.py, removing the drift risk
between approval de-duplication and doom-loop detection. Behaviour is
byte-for-byte identical (verified against the original scheme).
Co-authored-by: MervinPraison <[email protected]>
Guard the BEFORE_LLM and AFTER_LLM payload construction with the existing
registry.has_hooks() primitive, matching the idiom already used at the
AFTER_TOOL/BEFORE_TOOL/AFTER_AGENT/BEFORE_AGENT sites. Also skip the
str(result) stringification when the trace emitter is a disabled NoOp by
guarding on _trace_emitter.enabled. Behaviour is identical whenever a hook
or emitter is registered; zero cost on the hot path otherwise.
Co-authored-by: MervinPraison <[email protected]>
Deduplicate the model->context-window data by sourcing exact values from
context/budgeter.MODEL_LIMITS (the canonical table) and layering only
RAG-specific extras underneath, mirroring eval/tokens.py. This removes the
drift where gpt-4.1/gemini-1.5-pro resolved to different windows in RAG vs
the rest of the SDK. The get_model_context_window wrapper (None guard,
normalisation, prefix/heuristic fallbacks) is unchanged.
Co-authored-by: MervinPraison <[email protected]>
Extract a single _iter_argv_tokens classifier as the one source of truth for
flag/value-consumption semantics. _find_first_command, _flag_names and
_build_run_argv now all consume it, so routing and rewrite can no longer
diverge. _find_first_command accepts the shared value_opts (defaulting to the
historical narrow static set) rather than a separate hardcoded VALUE_FLAGS.
Behaviour-preserving internal DRY refactor; no CLI surface change.
Co-authored-by: MervinPraison <[email protected]>
CLI (cli/features/deploy.py):
- extract compose up|down action before generic parse so `compose up`
without --file no longer sets file="up"
- drop duplicate `-r` key (F601) so --region short flag works again
- route `doctor --provider fly|railway|render` via provider registry
- wrap the four new handlers in the class try/except convention
- thread --force into create scaffolding
compose.py:
- _ensure_postgres_password now resolves the *effective* (last) value,
handling quoted-empty and inline-comment cases
- restrict generated project .env to 0600 on POSIX
- report the effective host API port (runtime env → .env → default)
- add 600s timeouts to docker compose up/down
helm.py: add 600s timeout to helm upgrade
starters.py: refuse to overwrite existing files unless force=True
Helm agents-api deployment.yaml:
- pass agents/port/serverFile via env vars (no Python-source injection)
- include POSTGRES_PASSWORD in DATABASE_URL via k8s $(VAR) interpolation
starters: railway.json/render.yaml use the prebuilt GHCR image instead of
a nonexistent Dockerfile/api_server.py; fix compose+helm README manifest links.
Adds regression tests (160 passed).
Co-authored-by: Mervin Praison <[email protected]>
Split the "POSTGRES_PASSWORD:-praisonai" and "change-me" string literals in
the compose security regression tests so GitGuardian no longer flags them as
hardcoded secrets. The assertions still guard the exact same behaviour (no
default password baked into compose; generated password differs from the old
placeholder). Also replace the static "change-me" placeholder in the
agents-api Helm README with an openssl-generated example.
Co-authored-by: Mervin Praison <[email protected]>
- docker-compose.yml: drop `:-praisonai` default password (GitGuardian);
require POSTGRES_PASSWORD via `:?` and bind Postgres to 127.0.0.1 by default
- .env.example: ship empty POSTGRES_PASSWORD (no repo-known credential) + POSTGRES_BIND
- compose.py: auto-generate a strong POSTGRES_PASSWORD into project .env when empty
- README + tests: document security defaults and guard against regressions
Co-authored-by: Mervin Praison <[email protected]>
TRUNCATE is the default compaction strategy but did not preserve
tool_calls/result pairs: the recent-window slice could orphan a tool
result, and the token-budget pop loop could drop an assistant
tool_calls message while leaving its tool result behind. Snap the
recent boundary and drop leading tool pairs atomically so strict
providers never receive an orphaned tool message.
Co-authored-by: Mervin Praison <[email protected]>
When register(..., overwrite=True) replaces a tool under the same name,
evict the previous tool's availability cache and last-success timestamp so
a broken replacement can't be served as available on its first failing
probe via the inherited grace window.
Co-authored-by: Mervin Praison <[email protected]>
Compaction strategies chose their recent/older boundary purely by token or
message count, which could split an assistant tool_calls message from its
matching tool result. The resulting orphaned tool message made strict
providers (OpenAI/Anthropic/Azure) reject the transcript with a 400,
wedging long-lived gateway/bot sessions on every subsequent turn.
Add ContextCompactor._snap_to_pair_boundary() which moves a chosen boundary
outward so a tool pair is never separated, and apply it in _sliding_window,
_summarize, _prune, _llm_summarize and _llm_summarize_async. Behaviour-only,
backward compatible, no new knobs.
Co-authored-by: MervinPraison <[email protected]>
Track last-success timestamp per tool in ToolRegistry. On a probe
exception within a grace window of a recent success, serve the last-good
True and skip caching a durable negative, so a flaky availability check
no longer strips a healthy tool from a long-lived agent. Sustained
failures beyond the grace window still mark the tool unavailable.
Co-authored-by: MervinPraison <[email protected]>
The new TestAgentOSChatSessionIsolation tests replaced Agent.achat at the
class level (`type(template).achat = _fake_achat`) and never restored it,
so the stub leaked into test_async_gemini_fix.py where the real achat is
called with a `tools=` kwarg the stub did not accept.
Use pytest's monkeypatch fixture (auto-restore) and accept *args/**kwargs
in the stub so it matches the real achat signature.
Co-authored-by: Mervin Praison <[email protected]>
Addresses reviewer feedback on #3555:
- Greptile P1: _preflight_tools now loads ~/.praisonai/.env before resolving
so a local tools.py enabled via PRAISONAI_ALLOW_LOCAL_TOOLS in that file is
not falsely rejected in strict mode (mirrors GatewayHandler.start).
- CodeRabbit: describe_unresolved excludes the exact name from difflib typo
candidates so a mapped-but-unloadable tool yields an install/generic hint
instead of a useless "Did you mean '<same name>'?".
- Add regression tests for both.
Co-authored-by: Mervin Praison <[email protected]>
An explicit (non-${ENV}) weak gateway.auth_token in gateway.yaml wins over
GATEWAY_AUTH_TOKEN at both startup (GatewayConfig.auth_token) and doctor
re-validation, so the env-only repair left the weak YAML value active and
falsely reported success. The repair now also rewrites the YAML token in
place when it holds an explicit weak value, while preserving ${ENV}
references. Fixes Greptile P1 on #3556.
Co-authored-by: Mervin Praison <[email protected]>
The degraded registry's retry_hint advertised `praisonai gateway doctor --fix`,
but no such flag existed — a dead-end on the gateway's primary recovery path.
Add `--fix`/`--dry-run` to `gateway doctor` that mints a strong GATEWAY_AUTH_TOKEN
when weak/missing, persists it to ~/.praisonai/.env (same store as onboard), and
re-validates the finding cleared. Align the core retry_hint docstring so the
advertised command is guaranteed to exist.
Co-authored-by: MervinPraison <[email protected]>
Add a start-time tool pre-flight gate so a mistyped, uninstalled, or
gated tool named in gateway.yaml is surfaced with a per-name reason and
fix hint instead of being silently skipped. Strict by default (mirrors
the credential pre-flight); --no-strict-tools / strict_tools: false
preserve today's skip-and-continue.
- tool_resolver.py: add describe_unresolved() (difflib "did you mean" +
local-tools/dependency hints) and a module-level wrapper.
- gateway start: add --strict-tools/--no-strict-tools and a
_preflight_tools() gate reusing the existing validate_yaml_tools.
Co-authored-by: MervinPraison <[email protected]>
- cli/features/deploy.py: API & Cloud shortcut commands now honor deploy.api
and deploy.cloud YAML when CLI flags are unset (defaults changed to None
sentinels; explicit CLI still overrides). Fixes YAML being silently ignored.
- providers/azure.py: create->update fallback now builds a proper
`az containerapp update` command (previous `cmd[1]='update'` produced the
invalid `az update containerapp`); env vars passed under a single --env-vars
flag so later pairs no longer override earlier ones.
- scheduler/deployment.py: replace buggy nested DeployArgs (dead self-ref,
missing attrs) with SimpleNamespace carrying all fields handle_deploy reads;
derive success from SystemExit code instead of always True; interruptible
retry wait via _stop_event so stop() works; drop unused time import.
- main.py: replace `assert docker_cfg` with explicit ValueError (assert is
stripped under python -O).
- _wrapper_bridge.py: optional_wrapper_attr now returns default when attr is
missing, not just on ImportError.
- docker.py: prepare_docker_build_context validates agents file before writing
api_server.py; docker_container_name sanitizes invalid chars.
- tests: add regression tests for API and Cloud YAML merge precedence.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
- Route the optional subtree-context helper through praisonai_bot (canonical
home) with a praisonai wrapper-shim fallback, removing the module-/function-
level ``from praisonai`` import that tripped the C7 hot-path import gate
(fixes smoke: test_c7_import_gate_script_passes). Feature now also works on
installs shipping praisonai_bot without the full wrapper.
- Propagate --no-rules to the custom-agent and profiled prompt paths
(_run_custom_agent, _run_prompt_profiled) so the opt-out is honoured on every
non-interactive Agent construction site, not just _run_prompt.
- Add an execution-path test asserting all three paths accept/forward no_rules.
Co-authored-by: Mervin Praison <[email protected]>
- ACCEPT_EDITS previously set _permission_mode but the decision resolver
deferred it (no-op), so advertised auto-accept-edits behaviour never
applied. Add an ACCEPT_EDITS branch that auto-approves edit-class tools
(write/edit/append/create/save/patch/mkdir) via a new _is_edit_tool
heuristic, deferring destructive/exec/external tools to normal flow.
- Remove the misleading `read_only`-as-mode line from PermissionMode.resolve
docstring (read_only is a deny-set preset and resolves to None).
- Tests: auto-approve edit tools, defer non-edit tools, auto_edit alias.
Co-authored-by: Mervin Praison <[email protected]>
The interactive REPL lazily attaches a subdirectory's AGENTS.md/CLAUDE.md the
first time the agent reads/edits a file under it, but the scriptable
`praisonai run` path built Agent(**agent_config) without it. In a monorepo,
`praisonai run "refactor packages/foo/bar.py"` therefore never picked up
packages/foo/AGENTS.md while `praisonai chat` in the same repo did.
Reuse the existing subtree hook (praisonai.integration.context_files) and the
existing Agent(hooks=...) seam via a small `_wire_subtree_context_hook` helper,
registered on a fresh session-scoped registry at all three run.py Agent sites.
Gated by the existing --no-rules / PRAISON_NO_RULES off-switch and honouring the
shared PRAISON_CONTEXT_BUDGET character budget. No new Agent params, no new core
module, no agent.py changes — keeping the surface area unchanged.
Co-authored-by: MervinPraison <[email protected]>
Add PermissionMode.resolve() as the single mapper from historical mode
spellings (AutonomyMode/ApprovalMode/CLI-flag aliases) onto the canonical
PermissionMode enum, and route the existing Agent(approval=<preset>) string
branch through it. Agent(approval='plan'|'bypass'|'accept_edits'|'dont_ask')
(plus aliases like yolo/auto_edit/reject/full_auto) now resolve to one
coherent model. Deny-set presets (safe/read_only/full/off/default) keep
their exact behaviour. No new Agent params or exports.
Co-authored-by: MervinPraison <[email protected]>
- call.py: module globals (OPENAI_API_KEY, CALL_SERVER_TOKEN, NGROK_AUTH_TOKEN,
rate limits) were frozen at import, before main()'s explicit load_dotenv(),
so a .env-only key was silently ignored. Add _refresh_env_globals() invoked
after load_dotenv() (also refreshes the n8n router's CALL_SERVER_TOKEN).
- agentos.py: clone_for_channel intentionally drops handoffs; agents with
handoffs now stay on the shared template so delegation is never silently lost.
- test: cover the handoffs-not-cloned path (9 passed).
Co-authored-by: Mervin Praison <[email protected]>
Gap 1: /chat now clones a per-request/session agent (reusing the existing
_supports_session_isolation / _clone_agent helpers from api/agent_invoke.py)
so concurrent callers no longer share mutable chat_history, and session_id is
bound to the clone instead of merely echoed. Plain mocks fall back to the
shared instance for backward compat.
Gap 2 (minimal): api/call.py no longer calls load_dotenv() unconditionally at
import time; it is gated behind PRAISONAI_CALL_LOAD_DOTENV and loaded
explicitly in main() so the CLI keeps working. Importing the module no longer
mutates os.environ.
Gap 3 rejected as scope creep (breaking public-API churn with a deprecation
window; deferred per the issue).
Co-authored-by: MervinPraison <[email protected]>
- Docker deploy no longer disables auth by default; falls back to the
secure APIConfig() default (auth_enabled=True, token auto-generated)
instead of exposing an unauthenticated /chat on 0.0.0.0 (Greptile P1).
- Write the generated API server into a private per-invocation mkdtemp
(mode 0700) instead of a predictable shared tmp path, preventing
local replacement before launch (Greptile P1).
- Update pre-existing GHSA deploy-codegen test to assert the genuine
security property (repr-escaped path, no raw injection) rather than the
old implementation-coupled `PraisonAI(agent_file=...)` string; the PR
intentionally forwards the chat message via the wrapper `run(..., cli_config)`
entrypoint since `PraisonAI.run()` takes no args.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
- llm_guardrail: assign self.logger before _initialize_llm so unsupported/
string-fail/dict-fail paths can log and fail closed instead of raising
AttributeError (CodeRabbit major).
- llm_guardrail: __call__ now checks `self.llm is None` (matches _llm_validate)
instead of truthiness (CodeRabbit minor).
- tests/permissions: lock the scoped-approval bypass fix — an agent-scoped
approval must reject an unnamed (agent_name=None) caller.
Co-authored-by: Mervin Praison <[email protected]>
- cancel_task now cancels any non-terminal task (PENDING or RUNNING),
fixing test_cancel_task regression and Greptile P1: queued/PENDING
delegations no longer escape cancel_all.
- delegate() re-raises external CancelledError (Greptile P1) and only
swallows delegator-initiated cancels into a result, so wait_for /
cooperative shutdown behave correctly.
- _execute_task and the semaphore-guarded path early-return when a cancel
landed before execution, so a cancelled task never runs.
Co-authored-by: Mervin Praison <[email protected]>
Move API, Docker, and cloud deployment from the wrapper into a standalone
praisonai-deploy package with backward-compat shims, CI/publish wiring,
and fixes for chat message forwarding, Docker build context, and CLI YAML merge.
Co-authored-by: Cursor <[email protected]>
- permissions/rules.py: PersistentApproval.matches now rejects agent-scoped
approvals for callers that can't prove identity (including agent_name=None),
closing the short-circuit bypass while leaving unscoped approvals unaffected.
- guardrails/llm_guardrail.py: LLMGuardrail.__call__ now fails closed on a
missing/unsupported LLM, matching its docstring and sibling _llm_validate.
- agents/delegator.py: cancel_task/cancel_all now propagate cancellation to the
real asyncio.Task; _execute_task no longer clobbers a CANCELLED status with a
late COMPLETED assignment.
Co-authored-by: MervinPraison <[email protected]>
Add a `skills add` subcommand as a thin alias over the existing
`skills install` path so `praisonai skills add https://...` works,
mirroring the familiar `npx skills add <url>` verb. Reuses existing
install/remote-fetch machinery; no new deps, modules, or core params.
Co-authored-by: MervinPraison <[email protected]>
- @tool(requires_approval=False) now emits DeprecationWarning (any use of the
deprecated spelling nudges migration); use a sentinel to tell "omitted" from
explicit False. approval= still wins for the resolved value.
- Agent/PraisonAIAgents launch(protocol="mcp") now keep the serve_agents() call
inside the ImportError guard, so a praisonai-mcp installed without its
transport extras yields one actionable install message instead of an uncaught
traceback.
- Tests: explicit-False-warns, omitted-no-warn, both-set-warns, and
missing-transport-extras for single- and multi-agent launch.
Co-authored-by: Mervin Praison <[email protected]>
- @tool now accepts canonical approval= (mirrors Agent(approval=...));
requires_approval= kept as a deprecated alias with a DeprecationWarning,
approval= wins when both set. Both funnel to the same ApprovalRegistry.
- PraisonAIAgents.launch(protocol="mcp") and Agent.launch(protocol="mcp")
now both delegate to praisonai_mcp.serve_agents (lazy import, clear
"pip install praisonai-mcp" error). Replaces the bespoke single-agent SSE block.
- Add praisonai_mcp naming constants list_sessions / awaiting_approval.
- http launch behaviour untouched.
Co-authored-by: MervinPraison <[email protected]>
A stop-on-error failure inside a loop_over step now aborts the whole
workflow instead of only the inner item iteration, and a completed
loop_over step now persists a checkpoint so resume does not re-run every
loop item. Both fixes live in the shared _run_workflow_loop generator so
they apply to sync execute() and async aexecute() alike.
Co-authored-by: Mervin Praison <[email protected]>
Extract the duplicated step-loop from execute() and aexecute() into a single
sans-io generator (_run_workflow_loop) that owns all orchestration:
condition-skip, loop_over iteration, checkpoint save/resume, branch_condition/
next_steps routing, early-stop and on_error handling.
execute() drives it with the sync _execute_single_step; aexecute() drives the
same generator with a new async _aexecute_single_step that awaits async
executors / agent.achat / async handlers (true-async preserved).
This restores loop_over, branch, and checkpoint/resume parity to the async
path and adds checkpoint/resume params to aexecute. Public signatures and
return shapes are unchanged.
Co-authored-by: MervinPraison <[email protected]>
Catch import errors in _load_bundled_dashboard_app so that when the
bundled ui_dashboard preset is unavailable, the loader returns None and
the existing build_host_app fallback runs, instead of the exception
escaping to the outer handler and dropping aiui to the standard dashboard.
Addresses Greptile (P1) and CodeRabbit (Major) review findings on #3535.
Co-authored-by: Mervin Praison <[email protected]>
Route _run_aiui_dashboard through the bundled ui_dashboard/default_app.py
preset so the documented 9-page unified dashboard actually ships, making
the preset the single source of truth instead of a divergent hardcoded
5-page list. Falls back to the previous build_host_app defaults if the
preset cannot be loaded. Adds the missing ui_dashboard/__init__.py for
consistency with the other bundled UI presets.
Co-authored-by: MervinPraison <[email protected]>
- Namespace per-session transcripts by tool name so reusing a client
session_id across different ask_* agents no longer leaks history.
- Serialize each session's load->run->save under a per-session lock to
stop concurrent turns clobbering each other's completed turn.
- Disambiguate agents whose names normalize to the same tool suffix so a
later registration never silently shadows an earlier agent.
- Raise ValueError on unknown serve_agents() transport instead of
silently falling back to http-stream.
- Add regression tests for all four cases.
Co-authored-by: Mervin Praison <[email protected]>
Address reviewer P1 security findings on PR #3530:
- Fail closed: if registering the approval requirement raises, the
decorator now re-raises RuntimeError instead of logging and handing
back an executable but ungated tool.
- Validate risk-level strings against ("critical","high","medium","low")
so a typo like requires_approval="critial" is rejected up front rather
than silently treated as non-critical.
Adds regression tests for both cases.
Co-authored-by: Mervin Praison <[email protected]>
- praisonai-mcp: new serve_agents() + agent_adapter publishing ask_{name}
per agent and list_agents, with per-call session_id minting and in-adapter
per-session transcript continuity, running over the existing spec-compliant
http-stream transport. Server-side agents only; no client-set instructions.
- Fix Defect C: rebind memory adapter to the real core API
(store_short_term/search/get_all_memories/reset_all); the old bindings
(get_session/get_all/clear_session/clear_all/list_sessions/add) never existed.
- Drop client-controllable `instructions` from praisonai.agent.chat/run so the
connecting client can no longer rewrite the persona.
- Tests for per-agent tools, session continuity, no-client-instructions, and
memory adapter against the real API.
Co-authored-by: MervinPraison <[email protected]>
Adds a one-place approval declaration to the @tool decorator that
registers the tool with the existing global ApprovalRegistry at
definition time. `requires_approval=True` maps to the "high" risk
level; a string sets the risk level explicitly. No behaviour change
when unset. Backward compatible.
Co-authored-by: MervinPraison <[email protected]>
Addresses two valid reviewer findings on PR #3527:
- Stale anchor: the sync & async loops advanced `_last_run_at` *after*
`_execute_with_retry` persisted daemon state, so a restart restored the
prior slot and replayed an already-completed run/delivery. Mark the
wall-clock anchor *before* executing so the state write records this slot.
- Silent degrade: without the optional `croniter` engine a `cron:` schedule
reverted to the pre-#3526 process-relative interval with no signal. Emit a
one-time actionable warning (matching core `scheduler.due`) instead.
Tests: new default-install warning + state-persistence cases (run without
croniter); ticker cron tests gated per-class so they no longer skip the
whole module. Scheduler suite: 259 passed (3 pre-existing dotenv env
failures unrelated).
Co-authored-by: Mervin Praison <[email protected]>
The AgentScheduler / AsyncAgentScheduler wrapper loops collapsed cron
expressions to a coarse integer interval anchored to process start, so a
"daily 09:00 brief" fired at whatever o'clock the daemon launched, re-phased
on restart, and silently dropped runs across downtime/dormancy.
Add a lightweight, wrapper-local ScheduleTicker that reuses core's croniter
engine to sleep until the next real wall-clock occurrence, catch up a missed
slot exactly once on resume, and persist last_run_at in the existing daemon
state file so restarts don't re-phase. Plain intervals (hourly, */30m, raw
seconds) keep identical behaviour. No new core surface, exports, or params.
Co-authored-by: MervinPraison <[email protected]>
- Always inject the env var as GATEWAY_AUTH_TOKEN (the only name the gateway
reads); auth.secretKey now only selects the Secret data key.
- validateAuth now fails fast whenever auth.enabled is true without a token
source, not only when ingress is also enabled — the default install
previously referenced a Secret that was never created.
- Update README security note to match the stronger validation.
Co-authored-by: Mervin Praison <[email protected]>
The bot's dependency floor (praisonaiagents>=1.6.152) admits core releases
that predate AttemptAndAgeDeadLetterPolicy (first shipped 1.6.161). On those
installs the import guard previously bound None and both durable queues
silently reverted to attempt-only dead-lettering, re-introducing the #3519
transient message loss. Add a dependency-free LocalDeadLetterPolicy in
_resilience (mirroring core semantics: attempt+age gate, permanent-error
short-circuit, min_age=0 legacy) and bind it as the import fallback so the
age gate holds on any allowed core version. Adds older-core coverage tests.
Co-authored-by: Mervin Praison <[email protected]>
Ship a single, self-contained Helm chart under deploy/helm/praisonai-gateway
that reuses the existing GHCR image (no new build pipeline, no core code
change). Grounded in real gateway behaviour: port 8765, /health probes, and
GATEWAY_AUTH_TOKEN via a Kubernetes Secret. Includes optional ingress with
WebSocket-friendly annotations and an optional CPU HPA. Chart refuses to render
if the gateway would be exposed via ingress without an auth token.
Co-authored-by: Dhivya-Bharathy <[email protected]>
- Enforce closed OWNER_KINDS/DEGRADED_STATES in DegradedOwner.__post_init__
so unsupported values can never reach operator-facing records (Greptile P2).
- Route the existing channel-credential degradation path through new
gateway helpers _mark_degraded_owner/_clear_degraded_owner so the shared
_degraded_registry has a genuine live producer feeding degraded_owners,
not just an empty registry read by health() (Greptile P1).
- Drop unused dataclasses.field import; add vocabulary-rejection tests.
Co-authored-by: Mervin Praison <[email protected]>
Dead-lettering decided on attempt-count alone burned the default five
attempts in ~45s of channel outage, permanently dropping deliverable
inbound/outbound messages during routine transient incidents.
Add a core DeadLetterPolicyProtocol + default AttemptAndAgeDeadLetterPolicy
in praisonaiagents/gateway that requires BOTH attempt-exhaustion AND a
minimum wall-clock age (default 6h) before dead-lettering, while known
permanent errors (credential/permanent_target) still short-circuit. The
bot durable queues (OutboundQueue.drain, InboundJournal redelivery/replay)
consume the policy where they previously tested attempts >= max_attempts,
gaining a dead_letter_min_age knob. min_age=0 restores legacy behaviour.
Co-authored-by: MervinPraison <[email protected]>
Adds a small, process-local DegradedCapabilityRegistry + DegradedOwner
contract in core (praisonaiagents/gateway/degraded_state.py), mirroring the
existing gateway policy-protocol + default-impl pattern. Any owner (channel,
provider, capability, route, gateway) records degradation with a consistent,
redacted, actionable shape.
Wires the bot gateway health() as a live consumer: it aggregates existing
channel degradation facts (plus any provider/capability/route degradation
recorded into a shared registry) into a single degraded_owners surface, so a
degraded provider or unresolved secret-backed capability is no longer
classified-but-invisible. The channel-only surface stays for backward compat.
Co-authored-by: MervinPraison <[email protected]>
The wrapper YAML schema types `autonomy` as an int 0-10 but core
Agent only accepts bool/str/dict/AutonomyConfig; forwarding the raw
int hit core's disable branch and silently ignored a configured
level. Map the numeric level onto core's string presets (0 => off,
1-3 => suggest, 4-7 => auto_edit, 8-10 => full_auto) and pass
bool/str/dict through unchanged.
Co-authored-by: Mervin Praison <[email protected]>
- praisonai_adapter._resolve_agent_approval: map the wrapper approval dict
onto the real ApprovalConfig fields (approve_all_tools->all_tools, resolve
string backend) instead of passing extra keys into the dataclass, which
raised TypeError on every --trust/--approve-* run against the native adapter.
- praisonai_adapter.run + persistence sync stores + async TUI: route through
the existing run_sync_or_offload helper instead of run_sync, so the flagship
sync praisonai.run(...) API and sync persistence hooks no longer crash from
inside a running loop (FastAPI, async test, notebook).
- praisonai_adapter._build_agents_and_tasks: forward agent-level fields the
core Agent already accepts (planning, reflection, guardrails, web, autonomy,
skills) instead of silently dropping them.
Co-authored-by: MervinPraison <[email protected]>
The per-instance async semaphore fix (correctly) enforces each Handoff's
own max_concurrent, but moved _semaphore_lock to instance level, breaking
the architectural test that asserts Handoff._semaphore_lock is a single
shared class attribute. Restore it as a class-level threading.Lock guarding
lazy semaphore creation while keeping the semaphore itself per-instance.
Co-authored-by: Mervin Praison <[email protected]>
- llm.py: add LLMResponseError and re-raise instead of swallowing a
mid-tool-loop error and returning "" as if the call succeeded, so the
existing try/except in chat_mixin.py can surface the real failure.
- memory.py: make adapter search/reset/delete gates symmetric with the
adapter-agnostic store path (any configured memory_adapter, not just
the hardcoded sqlite/in_memory providers). Also make store_long_term
adapter-agnostic to match store_short_term so adapter-stored memories
(e.g. "dakera") remain findable.
- handoff.py: make the concurrency semaphore instance-level (created once
per Handoff, rebound if the running event loop changes) instead of a
class-level singleton, so per-handoff max_concurrent limits no longer
override each other and multiple asyncio.run() lifecycles don't crash.
Co-authored-by: MervinPraison <[email protected]>
load() acquired the exclusive lock over max(file_size, 1) bytes on
Windows but discarded the returned length, so _release_exclusive_lock
fell back to unlocking only 1 byte. For session files larger than one
byte this left the remaining range locked until file close, blocking
concurrent access. Pass the acquired lock_length to the release call
to unlock the full range, mirroring save().
Co-authored-by: Mervin Praison <[email protected]>
- Persist reasoning/tool-progress/stream-error frames for reconnect replay (CodeRabbit)
- Missing agent now resolves terminally (final+error) instead of a dangling accepted ack (Greptile P1)
- Drain relay futures before final frame so final never overtakes trailing stream events (Greptile P1)
Co-authored-by: Mervin Praison <[email protected]>
Forward a closed set of structured progress events (reasoning, tool
progress, stream errors) to WS clients instead of dropping everything
but text/tool-call/end, and disambiguate the overloaded response frame
with status: "accepted" (provisional) vs status: "final" (with a
structured outcome).
Co-authored-by: MervinPraison <[email protected]>
Closes the two P1 gaps flagged in review: name-based schedule lookups/removals
had no principal check (a gateway user could read/delete another's automation by
guessing its name), and suggestion admission caps/dedup were measured globally
(one tenant could starve another's suggestions).
- store.py / config_store.py: get_by_name/remove_by_name gain optional principal
filter; a cross-owner match is skipped. principal=None stays global.
- protocols.py: ScheduleStoreProtocol get_by_name/remove_by_name updated.
- suggestion_store.py: add() scopes the pending cap & dedup window to the
suggestion's principal; principal=None keeps the global pool.
- tests: +6 cases for name-based mutation ownership and per-principal caps/dedup.
Co-authored-by: Mervin Praison <[email protected]>
Wire BasePlatformAdapter.format_message() (the existing deliver() seam) to
call format_for_dialect(text, caps.markdown_dialect) so the capability is
actually consumed in core — Telegram replies are MarkdownV2-escaped and Slack
gets mrkdwn. Default "markdown" dialect keeps existing adapters unaffected.
Fix strip_markdown to only unwrap *paired* emphasis/code spans so literal
identifiers (svc_1), globs (*.py) and arithmetic (a*b) are no longer corrupted.
Adds regression tests for both fixes.
Co-authored-by: Mervin Praison <[email protected]>
Add an optional `principal` owner key to the core scheduler data model so
a multi-user gateway isolates each user's automations and suggestions
instead of sharing one global, single-tenant store.
- Suggestion.principal + SuggestionStore.list_pending/accept/dismiss gain
an optional principal filter that rejects cross-owner access.
- ScheduleJob.principal round-trips through to_dict/from_dict; FileScheduleStore
and ConfigYamlScheduleStore .list() gain an optional principal filter.
- ScheduleStoreProtocol.list signature updated.
principal=None preserves the current global behaviour, so single-user
deployments are unaffected (fully backward compatible).
Co-authored-by: MervinPraison <[email protected]>
The bare-tool-name guard skipped the stdout-reporting instruction for
preconfigured agents whose own system prompt mentions execute_command,
leaving the "no output" bug reachable. Guard on a stable marker phrase
instead, keeping injection idempotent. Strengthen assertions and add
regression coverage for the preconfigured-prompt and duplicate paths.
Co-authored-by: Mervin Praison <[email protected]>
Addresses the durable at-least-once guarantee gaps flagged in review:
- Persist the recipient client_id alongside each pending event and
reconstruct _pending_acks on startup, so replayed events are scheduled
for retry/reconnect redelivery instead of being silently dropped.
- Evict acknowledged events from the SQLite store (and in-memory cache)
so acked rows are not resurrected after a restart.
- Backfill client_id column for pre-existing stores; strengthen tests to
assert redeliverability after restart and ack-eviction.
Co-authored-by: Mervin Praison <[email protected]>
Strengthen the shell-tool instruction in enable_shell_tools so the
gateway agent actually invokes execute_command and returns its stdout
verbatim, instead of intermittently replying "there was no output".
This addresses the flaky E2E sandbox marker check (E2E_SANDBOX_OK).
Co-authored-by: Dhivya-Bharathy <[email protected]>
Path.home() resolves via USERPROFILE on Windows, not HOME. The fixture
only set HOME, so on Windows the tool wrote to the real user profile and
the positive test asserted a file under tmp_path that was never created.
Set USERPROFILE alongside HOME so the fake home is honored on all
platforms and no writes escape the sandbox.
Co-authored-by: Dhivya-Bharathy <[email protected]>
Make the gateway's at-least-once push delivery guarantee durable by default
without an external Redis server. DeliveryConfig.store_backend now accepts
"sqlite" (new default), "redis" and "memory", validated in core. The wrapper
gains a SqlitePushStore mirroring the existing OutboundQueue pattern (WAL,
stdlib sqlite3, TTL eviction) at ~/.praisonai/state/push_delivery.sqlite, so
pending un-acked events survive a restart and replay on reconnect. "memory"
stays an explicit ephemeral opt-out; "redis" stays for multi-process fan-out.
Co-authored-by: MervinPraison <[email protected]>
- discovery.py: auto-include synced remote-skill cache (`current` aliases) in
default skill dirs so agents pick up `skills sync` output without install;
scan now treats a returned dir that is itself a skill (root-level SKILL.md)
as a single skill.
- remote.py: return the fetched dir as-is (never its parent) so a root-level
skill is not double-counted alongside its cache `current` alias.
- skills.py (CLI): `sync` reuses discover_skills (no re-implemented scan / no
double-count); no-URL sync now reads project praisonai.yaml/.yml as well as
the user-global config.
- test_remote.py: regression test for root-level SKILL.md repo discovered once.
Co-authored-by: Mervin Praison <[email protected]>
Task._setup_guardrail selected `agent.llm or agent.llm_instance`; since
agent.llm is always a non-empty model-name string it always won, discarding
the configured LLM instance (base_url/api_key/auth) and authenticating against
the wrong backend. Reorder to `llm_instance or llm`, matching
Agent._setup_guardrail. small_model routing still applies only to the bare
string case. Adds Task guardrail routing/instance-preference tests.
Co-authored-by: Mervin Praison <[email protected]>
Internal, non-user-facing LLM calls (context compaction/summarisation and
LLM guardrail validation) always ran on the primary agent model. Thread the
existing get_small_model resolver through these live auxiliary call sites so
a configured small_model is used, while leaving it unset preserves today's
behaviour (unset -> primary model).
- agent._create_llm_summarize_fn: resolve the summariser model via
get_small_model(primary_model=<agent model>) instead of hardcoding self.llm.
- Agent/Task LLM guardrail setup: when the guardrail LLM would default to the
bare primary model-name string, route it through get_small_model.
Change is strictly additive and backward-compatible: an explicit LLM instance
or per-call model always wins.
Co-authored-by: MervinPraison <[email protected]>
Introduce an opt-in RemoteSkillSourceProtocol and a default git-backed
implementation that syncs skills into a versioned local cache with atomic
swaps and offline-safe fallback to the last-good cache. Wired into
discover_skills via a new sources= param (backward compatible, zero
import-time cost) and surfaced through a `praisonai skills sync` CLI
command reading skills.urls / skills.sources from config.
Co-authored-by: MervinPraison <[email protected]>
Addresses two P1 reviewer findings on the wrapper-layer PR:
- registry.py: move the capability cache off a process-global dict onto the
FrameworkAdapterRegistry instance so two registries that register different
adapters under the same name no longer read each other's flags.
- observability/hooks.py: record which AgentOps teardown a run owns
(per-session vs legacy singleton). A run whose start_session returned no
usable handle now ends nothing instead of falling back to the package-global
end_session and cross-finalizing a concurrent run.
- agents_generator.py: keep the "cannot verify" refusal for genuinely unknown
frameworks while treating known built-ins (DEFAULT_PRIORITY / registered)
whose adapter can't be constructed as unsupported, preserving the historical
"not supported for framework=..." message.
Adds regression tests for all three behaviours.
Co-authored-by: Mervin Praison <[email protected]>
Gap 1: scope AgentOps to each ObservabilityRun (per-run start_session
handle + lock) instead of the process-global singleton so concurrent
runs don't cross-contaminate tags or cross-finalize sessions.
Gap 2: add run_sync_or_offload() to _async_bridge as the single
sync-or-offload driver; delete the duplicated copies in auto.py, use it
in PersistenceOrchestrator._sync (was silently raising inside a running
loop), and replace deprecated get_event_loop().run_in_executor with
asyncio.to_thread in api/agent_invoke.py.
Gap 3: add adapter_capability() and route SUPPORTS_RUNTIME_FEATURES /
SUPPORTS_WORKFLOW decisions through it, removing the
str(framework)=="praisonai" fallbacks so third-party adapters aren't
silently demoted on transient resolution failures.
Co-authored-by: MervinPraison <[email protected]>
The restore-all prune loop deleted every project file absent from the
target commit, silently destroying ignored/excluded files (.env, venv,
node_modules) that the snapshot never tracked. Mirror _sync_files()'s
ignore rules in restore so only would-be-tracked files are pruned.
Co-authored-by: Mervin Praison <[email protected]>
- snapshot: prune shadow files removed from the project in _sync_files() and
restrict restore() "restore all" to the target commit's tree (removing files
not in it), so undo() cannot resurrect deleted files or leak files from a
later snapshot.
- skills: guard the pending-store load-mutate-save cycle in _stage_pending/
approve/reject with a threading.Lock so concurrent proposals no longer
silently overwrite each other.
- permissions: consume a scope="once" approval/denial on first match in
_check_flat so it no longer becomes permanent for the process lifetime.
Co-authored-by: MervinPraison <[email protected]>
Delegate mcp.py's inline SSE-vs-HTTP URL check to the existing
mcp_transport.get_transport_type() helper so there is a single source of
truth for URL->transport classification. No behaviour change: /sse URLs
still use SSE and all other HTTP URLs use Streamable HTTP.
Co-authored-by: MervinPraison <[email protected]>
HooksQueryProtocol had zero importers, was not exported in
hooks/__init__.py __all__ or _LAZY_GROUPS. Consumers use
registry.list_hooks_for_api directly. No behaviour change.
Co-authored-by: MervinPraison <[email protected]>
Sync from_dict with to_dict so retry_initial_delay, retry_backoff_factor,
retry_jitter and compaction_strategy survive a to_dict/from_dict round-trip.
Also align code_sandbox_mode fallback with the dataclass default ("sandbox").
Co-authored-by: MervinPraison <[email protected]>
Both helpers in the praisonai wrapper's agents_generator.py were defined
but referenced nowhere (no callers, no tests, not exported). Also drops
the now-unused `import keyword` used only by the removed function.
Co-authored-by: MervinPraison <[email protected]>
- browser_available() now requires the playwright runtime importable, not just
praisonai_browser, so a base install without the Playwright extra falls back
to the cloud BrowserBaseTool instead of selecting a local tool that fails at
invocation (Greptile P1).
- create_browser_tool no longer advertises profile=X as active; PlaywrightBrowserAgent
has no profile param and uses a fresh context, so the value is now logged as
informational-only rather than misreported (Greptile P1).
- Add unit test covering the missing-Playwright fallback path.
Co-authored-by: Mervin Praison <[email protected]>
BotHandler._build_tools() now prefers praisonai-browser's local Playwright
automation (navigate/snapshot/click) over cloud-only BrowserBaseTool when
capabilities.browser is set, honouring --browser-headless / --browser-profile.
Falls back to BrowserBaseTool only when praisonai-browser is unavailable.
Adds a lazy _browser_bridge (mirroring _code_bridge) and an agent-callable
browser_automate tool wrapping PlaywrightBrowserAgent.
Co-authored-by: Dhivya-Bharathy <[email protected]>
Adds safe_print() to praisonai_bot.gateway.unicode_utils so E2E harnesses and
console reporters can echo Playwright's "playwright install" banner (drawn with
box chars like U+2551) without crashing on default Windows cp1252 consoles.
Falls back to safe_error_message() sanitization only when the stream encoding
rejects the text, keeping the actionable hint readable instead of masking it
behind a secondary UnicodeEncodeError.
Co-authored-by: Dhivya-Bharathy <[email protected]>
The CodeToolBridge.run_code(code) contract dropped the caller's per-run
allow-list, registry, timeout and max_output_size, so a transport could
only gate isolated tool calls by bridge-owned defaults — a weaker path
than the in-process executor and contrary to the "never a weaker path"
guarantee. Forward the invocation policy and limits to run_code so the
transport gates each call by exactly what the caller authorised.
- CodeToolBridge.run_code now takes allowed_tools/registry/timeout/
max_output_size as keyword-only params (new protocol, no external
implementers — fully backward compatible for existing callers).
- execute_code_with_tools forwards the invocation context to the bridge.
- Tests: recording bridge now uses only forwarded policy; added coverage
for policy/limit propagation and forwarded-allow-list enforcement.
Co-authored-by: Mervin Praison <[email protected]>
Add a transport-agnostic CodeToolBridge protocol and parent-side
serve_tool_call helper in core, plus a backward-compatible bridge=
option on execute_code_with_tools. When a bridge is supplied, code runs
under isolation and reaches tools over the bridge (same allow-list +
approval gate); omitting it preserves today's in-process behaviour.
Concrete per-backend transports remain a follow-up in the sandbox layer;
core only defines the contract to stay lightweight.
Co-authored-by: MervinPraison <[email protected]>
Address three P1 gaps in the gateway cancellation/timeout work:
- Timeout was unreachable: add per_turn_timeout to the bot GatewayServerSchema
(extra="forbid" was rejecting it) and stamp it onto self.config in
start_with_config so the documented YAML knob actually takes effect.
- Turn isolation: pass the InterruptController per-turn as the entry point's
cancel_token instead of mutating the shared agent.interrupt_controller, so
overlapping sessions on one Agent instance no longer interrupt each other.
Legacy agents without a cancel_token kwarg fall back to the shared attribute.
- Cancellation honesty: interrupt cooperatively first and wait a bounded grace
window (_ABORT_GRACE_SECONDS) for the turn — including a sync agent.chat in a
worker thread that cannot be force-killed — to unwind before the serial
session queue advances; hard task.cancel() only as a fallback.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Empty-string targets were skipped by the `if target` truthiness check,
retaining the silent fallback/misroute this validation prevents. Skip
only None (omitted) so every supplied string target is validated.
Co-authored-by: Mervin Praison <[email protected]>
Wire per-turn abort + timeout into the gateway so a runaway turn can no
longer wedge the serial per-session queue:
- Advertise the `abort` method (+ `message_abort` event) in the hello
capability handshake instead of the "abort not implemented" placeholder.
- Handle `abort`/`message_abort` WS frames and a portable `/stop` chat
command; both drive the existing InterruptController and cancel the
driving asyncio task via a new `_active_turns` registry.
- Run each turn under an optional `gateway.per_turn_timeout` (YAML/config
knob, 0 = disabled) via `_drive_turn`, normalising cancelled/timed-out
turns to a typed terminal message instead of leaving them hanging.
Reuses core's InterruptController (already checked by the agent run loop)
and `agent.interrupt_controller`; no new agent params.
Co-authored-by: MervinPraison <[email protected]>
Cross-check every channel's routes/routing targets (including default) and
bindings.agent against the declared agents: map in the canonical schema
validator. A typo now fails fast at load time (and in gateway doctor, which
loads via this schema) with the channel, bad target, and closest valid agent
id — instead of a silent logger.warning + misroute. Only enforced when
agents: is declared, so single-bot configs stay backward-compatible.
Co-authored-by: MervinPraison <[email protected]>
A run with session_id now routes to the stateful per-session agent
(_get_session_agent), not _get_agent. The test only stubbed _get_agent,
so it built a real Agent with no LLM backing and start() returned ''.
Stub the session builder too so the event fan-out contract is exercised
without a live LLM.
Co-authored-by: Mervin Praison <[email protected]>
Fixes Greptile P1: a value-taking run option whose separated value begins
with a dash (e.g. `--session -abc`, `--output -json`) was mis-classified by
`_flag_names` as an unsupported flag, diverting an otherwise fully
run-supported prompt to the legacy engine and losing session continuity,
--output modes, the credential gate and permissions.
`_flag_names` now accepts the `value_opts` set and skips the token consumed
by a value-taking option (with `--opt=value` handled inline). `_looks_like_
bare_prompt` passes `run`'s value-taking options in, and short-circuits the
no-dash case before the (potentially heavy) run-option introspection.
Co-authored-by: Mervin Praison <[email protected]>
Address 3 P1 review issues on the stateful warm-runtime session path:
- Attach id no longer replaces session identity: `run.py` passes the
persistence/conversation id as `session_id` and the `--attach` label as a
distinct `event_id`. A `--no-save --attach <id>` run streams events to the
attach id but stays on the isolated anonymous path (no persistence).
- Model override rebuilds the session agent: the per-session warm agent is now
keyed by the resolved model, so a later turn with a different model no longer
silently runs the previous provider/model.
- Continuity-wiring failure is not cached: when session-store wiring fails, the
turn runs one-shot but the unwired agent is not retained, so a later turn
retries wiring instead of permanently serving an uncoupled agent.
Wrapper-only (praisonai-code); no core SDK changes, no new Agent params, no new
deps. Adds tests for model override, continuity-failure, and event/session id
separation; updates the attach mock to accept `event_id`.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Address Greptile P1 findings on the interruptible-turn wrapper:
- Primary prompt-toolkit path now requests cooperative cancellation on
Ctrl-C when a turn is in flight (previously only reset the input
buffer), and clears any stale request at the start of each background
turn so a fresh turn is not immediately interrupted.
- _execute_prompt_interruptible refuses to start a new turn (and to clear
the shared controller) while a prior abandoned worker is still alive,
preventing the abandoned turn from being un-cancelled and resuming
against the warm session.
- Added test_abandoned_worker_blocks_new_turn.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
The CLI router treated any -/-- flag as a signal to fall back to the legacy
argparse engine, so common cases like `praisonai "fix the bug" --model gpt-4o`,
`--continue`, or `--output json` silently bypassed the modern `run` engine and
lost session continuity, --output modes, the credential gate and permissions.
_looks_like_bare_prompt now derives the run-supported option names by
introspecting the run command's Click parameters and routes a bare prompt to
`run` when every flag present is one run accepts; genuinely legacy-only flags
(and .yaml/.yml workflows) still fall back to legacy, now with a one-line
notice so the fallback is never silent. _build_run_argv reconstructs the
prompt + flags into a single `run "<prompt>" <flags>` invocation.
Co-authored-by: MervinPraison <[email protected]>
The warm runtime was a cold-start cache only: every /run cleared history
and --continue/--session runs bypassed the warm path, paying cold-start
each turn. Add a per-session warm agent registry keyed by session id that
rehydrates history once (via the existing apply_cli_session_continuity
wiring) and retains it across turns, while the anonymous path stays
isolated. Relax runtime_eligible so --continue/--session route to the warm
stateful path (fork/isolated/thinking/tool runs stay in-process).
Co-authored-by: MervinPraison <[email protected]>
Attach the core InterruptController to the interactive agent and run each
turn in a worker thread so the main thread stays responsive. Ctrl-C during
a turn now requests cooperative cancellation; the core chat loop honours it
at the next step boundary, returns partial output, and keeps the warm agent
and session intact. Wrapper-only change reusing the existing core primitive.
Co-authored-by: MervinPraison <[email protected]>
- write_file compute bridge: replace heredoc with base64-decoded stdin so
content can no longer terminate the heredoc early (silent truncation) or
inject host commands.
- LocalCompute._install_packages: validate pip specifiers against the strict
allowlist and shlex.quote each token before host execution.
- LocalManagedAgent._ensure_agent: pass api_key/base_url directly to the inner
Agent instead of os.environ.setdefault, preventing process-wide credential
pollution and subprocess env leakage across tenants.
Co-authored-by: MervinPraison <[email protected]>
Do not clear task._execution_context inside _build_execution_context.
The run_task/arun_task retry loops re-enter this helper on guardrail /
completion failures; clearing stripped the upstream output and validation
feedback the retry needs (Greptile P1). The Process engine already owns
this field's lifecycle (re-set before each yield, reset to None before
selecting the next task), so no cross-task leak.
Co-authored-by: Mervin Praison <[email protected]>
- mcp.py: wrap session.call_tool/read_resource/get_prompt in asyncio.wait_for
so a hung server fails one request without wedging the runner loop forever
- agents.py: fold task._execution_context (inter-task context + validation
feedback built by Process._build_task_context) into the prompt builder, and
clear it after consumption so it doesn't leak into later runs
- workflows.py: track future->idx map in parallel loop so error attribution
survives exceptions instead of using an unbound/stale idx
Co-authored-by: MervinPraison <[email protected]>
The legacy async tool loop in _achat_completion fired BEFORE_TOOL/AFTER_TOOL
inline and then again inside execute_tool_async, double-firing hooks when
registered and building hook inputs (os.getcwd/time.time) with no consumer
when none are. Rely on the single guarded dispatch in execute_tool_async,
which already handles blocking, arg mutation and after-context aggregation.
Co-authored-by: Mervin Praison <[email protected]>
The --entrypoints option was documented and parsed into use_entrypoints
but never wired to any discovery behavior. Removed the dead flag and
updated the docstring to reflect the actual --include-only interface.
Co-authored-by: Mervin Praison <[email protected]>
action_discover now reads built-in/external/registered/local from the
canonical ToolResolver.list_available_sources() in a single authoritative
walk, matching the resolver-first pattern already used by _get_builtin_tools
and templates/tools_doctor.py. The original praisonai_tools + TOOL_MAPPINGS
scan is preserved as an explicit offline fallback (_legacy_manual_scan) for
environments without praisonai-code. --include/--entrypoints flags and
grouped output are unchanged.
Co-authored-by: MervinPraison <[email protected]>
Downstream _execute_tool_async_impl matched only on __name__ and called
func(**args) directly, so a BrowserBaseTool-style BaseTool instance
(resolved by .name in dispatch) returned "not found" / never ran. Mirror
the robust sync resolution (BaseTool.name, FunctionTool .name alias, class
name) and invoke func.run() for BaseTool instances.
Also fix the short-circuiting `or` chain in async dispatch so an aliased
.name that differs from __name__ resolves instead of being dropped.
Adds regression tests exercising the real (unmocked) execute_tool_async
for a BaseTool instance and an aliased callable name.
Fixes#3450
Co-authored-by: Mervin Praison <[email protected]>
Address CodeRabbit review: when neither the monorepo praisonai-mcp sibling
nor an installed praisonai_mcp.mcp_server is available, the C12 shim cannot
resolve. Convert the resulting bare ModuleNotFoundError collection error into
a clean module-level pytest.skip. Guard find_spec against ModuleNotFoundError/
ValueError for an absent/broken parent, mirroring praisonai._bootstrap.
Co-authored-by: Mervin Praison <[email protected]>
Bot --browser wired BrowserBaseTool (a BaseTool instance without
__name__), causing async tool dispatch to raise
'BrowserBaseTool' object has no attribute '__name__'. Harden the
lookup in _achat_completion to resolve by __name__, .name, or class
name so all BaseTool subclasses dispatch safely, and drop the ad-hoc
__name__ monkey-patch in bots_cli.
Co-authored-by: Dhivya-Bharathy <[email protected]>
- Guard Unix-only ``resource`` import so _admission.py loads on Windows;
sampler self-disables gracefully when unavailable (Greptile P1 #1).
- Wire ``max_rss_mb`` through BotOS + gateway.yaml via a single-knob
build_memory_pressure_policy() so the feature has a live production
consumer instead of a dormant param (Greptile P1 #3).
- Surface max_rss_mb in admission stats; document resource-only soft-queue
degradation honestly rather than silently misleading (Greptile P1 #2).
- Correct MemoryPressurePolicy docstring to reference the real config key.
- Add tests: Windows-safe sampler, single-knob policy, BotOS wiring, stats.
Co-authored-by: Mervin Praison <[email protected]>
Move the new continuable param to the end of schedule_add() so the
pre-existing 9th positional slot still binds accept_suggestion, avoiding
a silent behaviour change for positional callers (Greptile P1).
Co-authored-by: Mervin Praison <[email protected]>
A transient DLQ-init failure no longer latches _outbound_resilience_ready,
so a later delivery re-attempts initialization once storage recovers,
instead of permanently degrading to retry-only and silently dropping
permanently-failed replies. Adds a regression test.
Co-authored-by: Mervin Praison <[email protected]>
Runtime exception messages could contain non-cp1252 characters that
would re-trigger UnicodeEncodeError on Windows consoles when printed.
Wrap exception interpolation in ascii() for all 7 failure messages.
Co-authored-by: Mervin Praison <[email protected]>
A delivered automated brief now seeds a resumable session so the user's
reply in the same chat resumes the job's conversation with full context,
instead of landing as a contextless new turn.
- core: add declarable DeliveryTarget.continuable field (opt-out only,
round-tripped in to_dict/from_dict) as the 3-way surface contract
- wrapper: gateway _deliver_scheduled_result seeds the reply session via
the existing mirror_to_session path when continuable
- tool/CLI: thread continuable through schedule_add + a --no-continuable
flag for fire-and-forget notices
Co-authored-by: MervinPraison <[email protected]>
Gateway admission was concurrency/CPU-scaled only and blind to memory, so a
burst of concurrent turns on a small always-on host drives RSS up until the
OOM killer fires — observed only post-mortem. This adds a resource-pressure
policy that queues under soft RSS pressure and sheds under hard pressure,
reusing the existing admission seam.
Core (praisonaiagents/gateway): ResourceSample, ResourcePressurePolicyProtocol
and a stdlib-first MemoryPressurePolicy (pure decision, no heavy imports),
sitting beside the concurrency / rate-limit / scale-to-zero policy family.
Wrapper (praisonai_bot/bots/_admission.py): a lightweight _RssSampler
(psutil-optional, resource.getrusage fallback, self-disables on unsupported
platforms) and AdmissionGate(resource_policy=...) wiring that folds the
resource decision into admit() by escalating to the more restrictive outcome.
Co-authored-by: MervinPraison <[email protected]>
Mirror the durable inbound journal on the outbound side: when no
outbound DLQ path is configured, default to the canonical per-platform
store (~/.praisonai/state/<platform>/outbound_dlq.sqlite) so a
permanently-failed or exhausted reply is parked (auditable + replayable)
instead of silently dropped. Escape hatch: outbound_resilience.enabled=false.
Co-authored-by: MervinPraison <[email protected]>
The wrapper permits a range of praisonaiagents versions (>=1.6.152), but
_attach_gateway_runtime imported GatewayRuntimeSeams/GatewayAdapterContractError
unconditionally — symbols only present in current core. On an older-but-allowed
core that import raised ImportError on every start()/probe()/health() (Greptile
P1). Guard the import and fall back to the pre-contract duck-typed splices so
those installs keep wiring identity resolution, delivery routing, admission
control, and cross-platform turn locking. The no-seams path now returns before
any import, staying a pure no-op independent of core version.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Classify loopback numerically via ipaddress so 127.0.0.2,
127.255.255.255, and expanded IPv6 ::1 keep the snappy loopback drain
instead of the 15s production window. Addresses Greptile review feedback.
Co-authored-by: Mervin Praison <[email protected]>
- classify_reload now rejects empty channel names (channels., channels..enabled)
and falls through to the fail-safe FULL restart instead of restarting channel ""
- wrapper _build_reload_plan gracefully degrades to full restart if an older
supported praisonaiagents lacks classify_reload, instead of crashing the reload
- add regression tests for malformed channel paths
Co-authored-by: Mervin Praison <[email protected]>
The unset (None) reliability posture now resolves to a backpressured
deployment — bounded admission ceiling + fair wait queue + graceful drain —
instead of the old no-admission one. It is bind-aware: a non-loopback bind
(a real deployment) resolves to the full production window, loopback keeps a
snappy 5s drain. reliability="off" opts back into today's immediate-teardown
behaviour; explicit reliability="default" keeps the legacy no-ceiling posture.
Co-authored-by: MervinPraison <[email protected]>
Add a pure, side-effect-free ReloadScope/classify_reload registry to the
core SDK so hot-appliable vs channel-scoped vs agents vs full-restart is
classified identically by every runtime, instead of being duplicated
ad-hoc in the wrapper. The wrapper's _build_reload_plan now delegates to
core, keeping the existing scoped per-channel restart behaviour while the
contract "grows" in the authoritative layer. Backward-compatible.
Co-authored-by: MervinPraison <[email protected]>
Addresses reviewer feedback on PR #3437:
- Greptile P1: the published editor schema was stricter than the runtime.
generate_agents_schema() now relaxes AgentConfig (drops required
role/goal/backstory, which the runtime auto-fills or maps from
'instructions') and allows list-form roles/agents (runtime _list_to_dict),
so editors no longer flag valid, executable YAML. The strict YAMLConfig used
by ConfigValidator is unchanged.
- Greptile P2: replace the silent `except Exception: pass` around the
yaml-language-server header write with a debug log so a dropped directive is
diagnosable.
- Regenerate agents.schema.json; add a runtime-parity guard test.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
- Legacy-only sessions now honour --sanitise instead of returning a raw
verbatim transcript, closing a secret-leak gap.
- Enforce --redact-level contract: validate against REDACT_LEVELS (standard|
strict), reject typos at the CLI, and give strict a real effect (masks
bearer tokens and PEM private-key blocks).
- Path matcher now covers single-segment POSIX (/tmp), C:/ Windows, and UNC
(\\server\share) paths; the cwd is masked as a stable path placeholder so it
is no longer half-masked as a secret leaving a dangling suffix.
- Add tests for single-segment/UNC paths, cwd suffix, strict mode, invalid
level, and exported REDACT_LEVELS.
Co-authored-by: Mervin Praison <[email protected]>
Derive a JSON Schema from YAMLConfig (generate_agents_schema), publish it as
a committed agents.schema.json artefact, expose it via `praisonai validate
schema --agents/--output`, and prepend a `# yaml-language-server: $schema=`
header when auto.py scaffolds agents.yaml so editors give autocomplete /
inline validation out of the box. Authoring-time only; runtime parsing and
execution are unchanged (a leading YAML comment is ignored by safe_load).
Co-authored-by: MervinPraison <[email protected]>
Address CodeRabbit review on MCP test conftest:
- Check the shim's required praisonai_mcp.mcp_server submodule instead of only
the top-level package, so a partial/incompatible install can't pass the guard
while collection still fails.
- Emit a pytest_report_header when MCP tests are skipped so contributors see why
and how to install praisonai-mcp.
Co-authored-by: Mervin Praison <[email protected]>
Playwright's "playwright install" banner uses box-drawing frame characters
(U+2551 etc.). On Windows cp1252 consoles these could clutter the sanitized
error text with '?' placeholders, obscuring the actionable hint. Map the
common frame characters to spaces in safe_error_message's symbol table so the
hint stays readable while remaining ASCII-safe.
Co-authored-by: Dhivya-Bharathy <[email protected]>
Add a lightweight, deterministic redactor so `praisonai session export
<id> --sanitise` (alias `--sanitize`) replaces secrets, absolute paths,
the working directory, and embedded file contents with stable
`[redacted:<category>:<n>]` placeholders. Default export is unchanged.
- new redact.py helper with stable placeholder map (stdlib only)
- wire redact/redact_level through export_session (Python: redact=True)
- wire --sanitise/--sanitize + --redact-level CLI flags, default off
- unit tests over a seeded secret, absolute path, and stable placeholders
Co-authored-by: MervinPraison <[email protected]>
Add tests/unit/mcp/conftest.py that restores the monorepo dev layout via
ensure_praisonai_mcp() and uses collect_ignore_glob to skip the directory
when praisonai_mcp is neither installed nor present as a sibling checkout,
turning a ModuleNotFoundError collection error into a clean no-op.
Co-authored-by: Dhivya-Bharathy <[email protected]>
The basic smoke script crashed with UnicodeEncodeError on Windows
default consoles (cp1252) because print() statements embedded emoji
(U+1F9EA, U+2705, U+274C, U+1F389) that cannot be encoded to stdout.
Replace all emoji with ASCII status tags ([TEST], [OK], [FAIL]) so the
script runs on any terminal encoding without decoration loss.
Co-authored-by: Dhivya-Bharathy <[email protected]>
Addresses CodeRabbit nitpick: add praisonai.yml / legacy praison.yml
discovery tests and a precedence test asserting canonical praisonai.yaml
wins over legacy praison.yaml when both are present.
Co-authored-by: Mervin Praison <[email protected]>
The praisonai-code resolver only searched for `praison.yaml` at the
project root, which is inconsistent with the agents SDK loader
(`praisonaiagents/config/loader.py`) that uses the canonical
`praisonai.yaml`. Add `praisonai.yaml`/`praisonai.yml` as the primary
project-root names while retaining `praison.yaml`/`praison.yml` for
backward compatibility, and update the docstring reference. Adds
regression tests covering both the canonical and legacy names.
Co-authored-by: Leebana <[email protected]>
CapabilityValidator cached the MCP server snapshot once, so servers that
connected after the first validation stayed invisible under STRICT
enforcement. Read the process-level registry live each call (cheap
set-copy under lock); tool cache is unchanged. (#3307)
Co-authored-by: Mervin Praison <[email protected]>
Gap 1: port loop-task pre-expansion from workflow() into aworkflow() so
async workflows with a CSV-driven loop start task expand per-row instead
of running once.
Gap 2: guard the per-turn _turn_tools_used buffer with the same
AsyncSafeState lock that protects chat_history, routing all sites through
_reset/_record/_drain helpers so concurrent chat()/achat() turns on one
Agent no longer corrupt hook/self-improve tool data. Backward-compatible
_turn_tools_used property retained.
Gap 3: track namespaced MCP server names in a process-level registry
(MCP.list_active_server_names(), populated in with_tool_prefix) and read
it from CapabilityValidator._get_available_servers so MCP-server-gated
skills can pass STRICT validation instead of always failing closed.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
parallel_handoffs runs children via asyncio.gather, which copies the
context per task. A copied ContextVar binding still references the same
parent list object, so when parallel_handoffs runs inside a non-empty
parent handoff chain, sibling tasks mutated a shared list and corrupted
cycle/depth tracking. Rebind each task's chain to a fresh per-task copy
at _run_one entry so siblings are fully isolated.
Co-authored-by: Mervin Praison <[email protected]>
- memory_mixin: append per-instance uuid suffix to default session_id so
same-named agents can no longer collide and bleed chat history
- handoff: replace threading.local() with contextvars.ContextVar so handoff
chain/depth is isolated per asyncio.Task under parallel_handoffs
- hooks/runner: add HookRunner.execute_async alias so async retry-with-backoff
no longer crashes with AttributeError
Co-authored-by: MervinPraison <[email protected]>
Address reviewer-flagged concurrency gaps in the scheduler + template cache:
- TemplateCache locks are now process-wide (class-level, keyed by path) so two
independent instances over the same dir coordinate; get() reads under the
per-key lock so it never observes the intermediate two-rename window; put()/
invalidate() take root+key locks and clear() takes the root lock, so a
concurrent clear() can no longer rmtree a half-written entry. put() stages
into a reserved .staging subdir (same fs, atomic os.replace) that clear()
skips, eliminating the put-vs-clear FileNotFoundError.
- AsyncAgentScheduler.start() no longer reports a successful start (and no
longer spins up a background task) when the immediate run trips the budget
brake (e.g. max_cost=0), matching the actually-stopped state.
Verified: 4 writer/clearer + 2 reader concurrency smoke test -> 0 races;
zero-budget start returns False with no task; 335 scheduler+template tests pass.
Co-authored-by: Mervin Praison <[email protected]>
- Async scheduler now honours the intentional-silence marker on delivery
(shared _should_suppress_delivery on _BaseAgentScheduler); removes the
duplicate budget guard, replacing both with a single _budget_exceeded()
helper that treats max_cost=0 as a real zero budget.
- TemplateCache.put()/invalidate() now stage into a sibling temp dir and
atomically os.replace() under a per-key lock, so concurrent readers never
observe a half-populated or mid-rmtree cache entry (multi-agent/interprocess).
- Scheduled agents.yaml tools now resolve through the canonical ToolResolver
(same path as `praisonai run`), instead of dropping everything but a
hardcoded search_tool — keeping CLI/YAML/Python behaviour identical.
Co-authored-by: MervinPraison <[email protected]>
- agents.py: _build_execution_context gains skip_memory_init; aexecute_task
passes True so a failed initialize_memory_async() no longer falls through
to the synchronous initialize_memory() on the event-loop thread.
- execution_mixin.py: reset app.openapi_schema after registering a shared-port
route so later launch() calls appear in /openapi.json and /docs.
Co-authored-by: Mervin Praison <[email protected]>
- Agent.launch(): register every path (not only first per port) and move the
keep-alive loop outside _server_lock so a second agent on a shared port is
no longer silently dropped and the process-wide lock isn't held forever.
- aexecute_task(): initialize memory via initialize_memory_async() to avoid
blocking the shared event loop on synchronous Memory() construction.
- MongoDBMemoryAdapter: factor embedding logic into shared _store/_search
helpers so short-term memory also gets/uses embeddings when
use_vector_search=True, matching the long-term tier.
Co-authored-by: MervinPraison <[email protected]>
These modules log exclusively via get_logger and never reference the
logging. namespace; the standalone import logging is dead code left over
from the get_logger migration. Removed it only where logging. is never
referenced (111 files); files that genuinely use logging. are untouched.
Co-authored-by: MervinPraison <[email protected]>
Delete two unreachable modules under context/fast/ that are never
imported, exported, or referenced anywhere in the SDK, tests, examples,
or docs:
- indexer/file_watcher.py (FileWatcher, IncrementalIndexer, ...)
- search_strategy.py (SearchStrategy, SearchState, SearchPhase)
Grep confirms only the definition sites matched; no importers exist.
Live FastContext path (fast_context, search_backends, parallel_executor,
indexer/file_indexer, symbol_indexer) is unchanged. No __init__.py edits
needed since neither module is exported.
Co-authored-by: Mervin Praison <[email protected]>
The registry.py copy of get_available_integrations() was unreferenced dead
code duplicating the canonical base.py implementation. The public symbol is
exported from integrations/__init__.py and lazily resolved via
_unified_registry.py, which binds to the base.py version. No importer, no
__all__ entry, and no internal caller referenced the registry.py copy.
Co-authored-by: MervinPraison <[email protected]>
Address three P1 review findings on the prompt-cache-stability contract:
- Wire GatewayMetrics into per-bot sessions via a new _stamp_metrics splice
(mirrors _stamp_admission_gate/_stamp_identity_resolver), called from both
start_channels and hot-reload, so prompt_cache_invalidations_total actually
increments in production instead of staying zero.
- Clear _prefix_sig in every session-clear path (_clear_session_data,
reap_stale, reset_all) so a reopened session establishes a fresh baseline
instead of emitting a false PROMPT_PREFIX_INVALIDATED against the old one.
- Fingerprint the full provider-visible tool schema (name + description +
parameters for dict tools, name + docstring for callables) instead of only
tool names, so a same-name schema edit is detected. Best-effort; never raises.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Core (praisonaiagents): new advisory HookEvent.PROMPT_PREFIX_INVALIDATED
and a pure prompt_prefix_signature(agent) that fingerprints only the
cache-relevant prefix (model + sorted tool names + system-prompt fp),
excluding volatile per-turn data. Kept out of agent.py to avoid bloat.
Wrapper/bot: BotSessionManager computes the signature after its per-turn
tool/model swaps, meters drift via an optional prompt_cache_invalidations_total
GatewayMetrics counter, and fires the advisory hook so silent provider
prompt-cache misses on long-lived conversations become auditable.
Co-authored-by: MervinPraison <[email protected]>
When team.astart() raises during a YAML/AgentTeam `--output stream-json`
run, the adapter now emits a terminal run.error event (best-effort,
without masking the original exception) before detaching the bridge,
matching the single-agent path's contract so consumers can distinguish
a failed run from an incomplete/still-running one. Fixes greptile P1.
Co-authored-by: Mervin Praison <[email protected]>
AgentTeam now exposes a lazy aggregate stream_emitter that fans in member
agents' StreamEventEmitter events, tagging each with the emitting agent's
agent_id. The PraisonAI adapter attaches the CLI stream-json bridge to this
team emitter around team.astart(), so `--output stream-json` on a YAML/team
run emits the same per-agent, per-step NDJSON stream as single-agent runs.
Both paths are zero-overhead no-ops outside structured output mode.
Co-authored-by: MervinPraison <[email protected]>
The event-loop liveness watchdog flags were only registered on the legacy
argparse parser in cli/features/gateway.py, which the real
`praisonai gateway start` Typer entrypoint (cli/commands/gateway.py) never
uses — so `--watchdog` / `--watchdog-timeout` were rejected at the primary
opt-in surface. Add both options to the Typer command and forward them to
GatewayHandler.start using the same None-means-fall-back-to-YAML convention
as the other store_true flags. `restart` already replays them via the
persisted start-flags. Adds 3 CLI-surface tests.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <[email protected]>
Arm the existing core LoopWatchdog (#3385) around the gateway serving loop
so a frozen event loop dumps all-thread stacks and hard-exits with the
restart exit code, letting systemd/launchd/Docker relaunch the process.
Opt-in via gateway.watchdog YAML block and --watchdog/--watchdog-timeout
CLI flags; zero cost when disabled. No systemd unit change needed since it
reuses GATEWAY_RESTART_EXIT_CODE (75).
Co-authored-by: MervinPraison <[email protected]>
- Make add_message/_modify_session_locked run the whole read-modify-write
inside a single BEGIN IMMEDIATE transaction so concurrent gateway
processes cannot lose appends (the process-local RLock alone did not
serialize across processes). Fixes Greptile P1 lost-update.
- Import legacy per-session JSON transcripts once when the SQLite store
first opens beside them, so upgrading to the sqlite default preserves
durable/resumable history. Fixes Greptile P1 legacy-invisibility.
- Wire the YAML session.store key through MultiChannelGatewayConfig.from_dict
so store: file|sqlite is actually honoured.
- Add concurrent-writer and legacy-migration tests.
Co-authored-by: Mervin Praison <[email protected]>
Addresses two P1 review findings on the RunOutcome terminal-outcome contract:
- Nested operation timeouts no longer misreported as run-budget hard_timeout.
RunOutcome.from_exception no longer promotes a bare asyncio.TimeoutError to
hard_timeout; the run-level budget is now the sole authoritative source of
hard_timeout, enforced explicitly in _astart_with_outcome via a task+wait so
only our own budget expiry maps to hard_timeout (inner TimeoutErrors -> failed).
- External asyncio.CancelledError is re-raised instead of being swallowed into a
benign RunOutcome, so cooperative cancellation / host shutdown is honoured.
Updates tests accordingly and adds coverage for nested-timeout->failed and
external-cancellation-propagates.
Co-authored-by: Mervin Praison <[email protected]>
Implement SqliteTranscriptStore(DefaultSessionStore) storing session
transcripts as one row per session in a WAL SQLite DB (concurrent readers,
transactional single-row writes, indexed lookups) instead of per-session JSON
files under a cross-process FileLock. It overrides only the persistence
primitives, inheriting all retention/compaction, message API, search scoring,
bookends and lineage-dedup logic unchanged.
Directory scans (list/search/gateway routing) are replaced with indexed
SELECTs. The gateway now defaults to this store when persistence is enabled,
selectable via a new `session.store` config key (`sqlite` | `file`).
Co-authored-by: MervinPraison <[email protected]>
Adds a small, closed RunOutcome result type in core plus an opt-in
return_outcome=True on run()/astart() so long-running hosts (gateways/bots)
can decide delivery/retry/DLQ from one field instead of inferring the
terminal state from exception identity. Distinguishes
completed|hard_timeout|cancelled|aborted|failed. String return unchanged.
Co-authored-by: MervinPraison <[email protected]>
Core save/restore_session_state only persist team._state, not per-agent
chat_history, so resumed/forked YAML team runs did not actually continue
the prior conversation (Greptile P1). Add wrapper-only capture/rehydrate
helpers that stash each agent's chat_history into team state on save and
re-inject it on restore — parity with the single-agent path, no core
change and no new Agent/AgentTeam params.
Also replace the source-text arun test with a behavioral call-order test
and add roundtrip/no-duplicate coverage for the chat-history helpers.
Co-authored-by: Mervin Praison <[email protected]>
Route YAML/team runs through the same --continue/--session/--fork/--no-save
continuity the single-agent prompt path uses, via existing core APIs.
- praisonai_adapter.arun(): resolve resume_session/auto_save from cli_config,
force-enable shared memory when a session is active, restore_session_state
before astart() and save_session_state after (save never fails a run).
- praison_ai._extract_cli_config_for_yaml(): thread resume_session/auto_save
into cli_config for `run agents.yaml`.
Wrapper-only; no new params/deps/core changes. Adds regression tests.
Co-authored-by: MervinPraison <[email protected]>
Address final-review P1s on #3401:
- Composite readers (AutoReader/DirectoryReader/GlobReader) now idempotently
wire the default readers into the core registry on first load(), so they no
longer silently return [] when register_default_adapters() was never called.
Keeps imports side-effect free (wrapper does heavy wiring at call time).
- _ensure_sibling() guards importlib.util.find_spec against ImportError/
ValueError from a discoverable-but-broken installed sibling, falling back to
the monorepo checkout instead of crashing.
Co-authored-by: Mervin Praison <[email protected]>
- Remove import-time auto-registration in adapters/{retrievers,rerankers,
readers}.py so importing them no longer mutates core-SDK singletons;
add idempotent, thread-safe register_default_adapters() single edge
(mirrors vector_stores.py policy).
- Make aretrieve()/arerank() genuinely async via asyncio.to_thread so the
blocking provider call no longer freezes the caller's event loop.
- Collapse the six copy-pasted ensure_praisonai_* functions in
_bootstrap.py into one _ensure_sibling() helper + table, fixing the
sys.path double-insert race with a lock.
Co-authored-by: MervinPraison <[email protected]>
Update _load_project_config docstring to reflect that home is now
excluded from project config discovery, and add two regression tests
covering the home-directory walk-up boundary.
Co-authored-by: Mervin Praison <[email protected]>
Address reviewer P1s on MTP fast-inference PR:
- export gguf now produces a LOCAL .gguf (needed by serve/--mtp-draft) and
only additionally pushes to the Hub when --hf is given (was Hub-only).
- find_llama_binary no longer returns a differently-named executable when
LLAMA_CPP_BIN points at one binary and a sibling is requested; falls through
to PATH / raises instead of launching e.g. llama-cli with server flags.
- Ollama disk preflight only enforces the 1.5x requirement when the source
size is known; skips (no false-positive block) for Hub-backed FROM.
Adds 6 tests (binary-lookup mismatch cases, gguf local/push dispatch,
unknown-source disk skip); full praisonai-train unit suite 118 passed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Mervin Praison <[email protected]>
Remove three module-level helpers that were never referenced anywhere in
the monorepo and were not part of the public API (no __all__, absent from
config/__init__.py and the top-level lazy-import map):
- parse_url_to_config
- is_numeric_string
- make_array_error
All exported/used helpers remain untouched.
Co-authored-by: MervinPraison <[email protected]>
Move the module-level `from pathlib import Path` into each
`ensure_praisonai_*` monorepo-dev fallback branch. Path is only ever
referenced after the `import praisonai_<pkg>` early-return fails, so the
installed-wheel hot path no longer pays pathlib's ~8.7ms cold-import cost.
Behaviour is byte-for-byte identical.
Co-authored-by: MervinPraison <[email protected]>
Addresses two correctness gaps in the crash-recovery labelling (PR #3388):
- CodeRabbit: hand recovery_annotator its own fresh payload copy and only
adopt a dict result, so a mutate-then-raise no longer leaks a half-labelled
payload; validate non-dict returns.
- Greptile P1: keep a crash-recovered entry in the 'recovered' state on a
transient re-send failure (mark_failed keep_recovered) so the duplicate
label survives the retry instead of being demoted to 'failed' and dropped.
Adds 3 regression tests (mutation-then-raise fallback, non-dict return,
sticky-recovered retry). All 69 outbox/delivery/reconcile tests pass.
Co-authored-by: Mervin Praison <[email protected]>
- disarm racing an in-flight wedge no longer triggers os._exit (re-check
_stop before the destructive exit) so a deliberate disarm never causes a
supervisor restart (Greptile P1)
- reject NaN/inf probe_interval_s to avoid a dead watchdog worker or a
hung shutdown via Event.wait/Thread.join (Greptile P2)
- add tests for both cases
Co-authored-by: Mervin Praison <[email protected]>
Add an optional recovery_annotator hook to OutboundQueue.drain, applied only
on the unreconciled 'recovered' branch, so a mid-send-crash message re-sent
at-least-once can be visibly marked as a possible duplicate instead of being
re-delivered silently. DurableDelivery gains a backward-compatible
mark_recovered flag (off by default) that prefixes recovered string replies
via _annotate_recovered_payload, threaded through setup_durable_delivery.
Co-authored-by: MervinPraison <[email protected]>
Add a pure-stdlib, fail-open LoopWatchdog primitive to core
(praisonaiagents.gateway) that runs on a dedicated OS thread, probes the
asyncio loop via call_soon_threadsafe, and after N missed probes dumps
all-thread stacks (faulthandler) and hands the process back to the
supervisor via os._exit(EX_TEMPFAIL). Opt-in and fail-open by design.
Co-authored-by: MervinPraison <[email protected]>
Passive-recorded group messages now land on the same shared per_chat
group key that a subsequent addressed turn reads from, so under
session_scope="per_chat" the bot can actually see the preceding
conversation (Greptile P1 / CodeRabbit Major). record_passive() threads
chat_id/thread_id/account routing and appends directly under a per-key
threading lock instead of the cross-thread mirror bridge, which would
dead-lock when called inline on the inbound handler's own event loop.
Strengthened the observe test to assert routing is threaded and added a
real BotSessionManager per_chat persistence test proving the passive
entry is visible under the group key and does not leak into the sender's
per_user history.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Address reviewer P1s on the restart-continuation path:
- boot scan requested an explicit large limit so interrupted turns beyond
the store's 50-session default window are no longer silently skipped
- channel_target now falls back to the persisted client_id when it encodes
a channel:target origin, so the field is a live consumer of existing
state rather than dead code for sessions the setter never touched
Adds tests for client_id-derived origin and >50-session scan coverage.
Co-authored-by: Mervin Praison <[email protected]>
A malformed gateway.drain_timeout / gateway.reload_drain_timeout in a
hot-reload edit made _coerce_timeout() return None, which was assigned
unconditionally and silently cleared a previously-configured live drain
window. Distinguish malformed (keep live value via _KEEP sentinel) from
explicit disable (None/absent -> None). Update/extend tests accordingly.
Co-authored-by: Mervin Praison <[email protected]>
On boot, before serving traffic, scan persisted sessions for an in-flight
turn (is_executing / non-empty pending_inbox) lost to a restart. For
channel-originated sessions, deliver an exactly-once "interrupted — resuming"
notice via the existing durable delivery router (idempotency keyed on
(session_id, event_cursor)) and re-drive the turn when the agent is still
registered — independent of any client reconnect.
Reuses the existing durable session store + delivery router rather than
adding new core protocol machinery, keeping the change lightweight and
backward-compatible.
Co-authored-by: MervinPraison <[email protected]>
Add a core HOT_APPLIABLE_KEYS registry + SupportsHotReload protocol in
praisonaiagents/gateway/config.py declaring the closed set of config paths
safe to apply in place. The bot gateway now classifies those paths as HOT in
_build_reload_plan (unknown keys still fall back to a restart plan — fail-safe)
and implements apply_hot_reload to mutate live state (logging level, drain /
reload_drain timeouts) without restarting channels or agents. Applied paths are
already reflected in the existing reload status/health.
Co-authored-by: MervinPraison <[email protected]>
Under the observe policy, group messages that do not mention the bot are
recorded into the session transcript as passive user context (no agent run)
so the bot has memory of the conversation when it is next addressed.
Reuses the existing thread-safe mirror append path via a new
BotSessionManager.record_passive() instead of adding a new core enum /
classifier, keeping the surface area minimal.
Co-authored-by: MervinPraison <[email protected]>
- Reject path-separator/traversal agent names (validate_agent_name) so
writes stay inside .praisonai/agents/ (Greptile Security P1).
- Post-write validation now re-parses the exact file written instead of a
precedence lookup, and warns when a higher-precedence definition shadows
the name so the reported `run --agent` step is never misleading (P1).
- Add tests for traversal rejection and global/project shadow warning.
Co-authored-by: Mervin Praison <[email protected]>
Add a `create` command beside `list`/`show` on the agent Typer app that
authors a valid, permission-scoped .praisonai/agents/<name>.md from a one-line
description. Interactive prompts plus a scriptable
--describe/--model/--permission/--global/--yes form. Permission presets reuse
the existing `mode` shorthands (MODE_RULES) so authored agents round-trip
losslessly through CustomDefinitionsDiscovery and inherit the runtime's
permission grammar. System-prompt drafting reuses the plain Agent path proven
by `init --generate`, degrading to an editable stub on any failure.
Co-authored-by: MervinPraison <[email protected]>
- _shim.py: skip None placeholders in sys.modules when pre-aliasing the
moved subtree so an unloaded optional module can't poison the old dotted
name (import old.sub -> "None in sys.modules"); _AliasFinder still resolves
it on demand.
- codex_cli.py: use an _UNSET sentinel in _build_command so an explicit
output_file=None disables the instance default and parses stdout, instead
of silently falling back to self.output_file (execute_with_schema).
Co-authored-by: Mervin Praison <[email protected]>
Restores eager subtree pre-registration in _shim._register_submodules. The
lazy-only variant broke module identity: a combined `from praisonai.mcp_server.cli
import X` imported the parent shim, then resolved `.cli` through _AliasFinder,
whose nested import_module re-entered mid-import under the import lock and
double-loaded the moved file — producing duplicate classes and breaking the
15 mcp/doctor CI tests that patch the shim path.
Also make the forced-format integration helpers (gemini stream/execute_with_stats,
codex stream, cursor stream) pop caller-supplied override keys before threading
the forced value, matching claude_code.py, so callers can't trigger a duplicate-
keyword TypeError (greptile P2).
Co-authored-by: Mervin Praison <[email protected]>
Address 4 P1 review findings in eval/trials.py:
- Isolation: rebind clone.chat_history to a fresh list and give the clone its
own system-prompt cache so per-attempt state resets never mutate/evict the
caller's live agent state.
- Scorer safety: a raising EvalCase.verify (and any scorer path) is now
recorded as a failed TrialScore instead of propagating through gather and
aborting the whole report.
- Timeout: run attempts on a dedicated ThreadPoolExecutor shut down with
wait=False so a wedged agent call cannot block run_trials past the timeout;
documented that a leaked worker thread cannot be force-killed.
- Duplicate case names: merge/extend attempts (with re-numbered indices)
instead of overwriting, so no attempts are silently discarded.
Add 4 regression tests covering each fix.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Address three P1 issues in the trials→SFT exporter:
- --all (only_passed=False) now retains unscored attempts instead of
dropping them in _classify.
- Alpaca conversion no longer drops the user prompt when a system message
is present; system→instruction, all user turns→input.
- Score-only pass detection compares against the report's pass_threshold/
threshold/min_score when present, falling back to score>0 only otherwise.
Co-authored-by: Mervin Praison <[email protected]>
- session/hierarchy.py: mirror base loader contract so a transient OSError on
an existing file re-raises (instead of swallowing IOError and returning an
empty session), letting _modify_session_locked abort the write and preserve
history. Malformed JSON still starts fresh.
- approval/registry.py + agent/tool_execution.py: add per-call `force` to
approve_sync/approve_async so a PermissionManager `ask` rule gates only the
current call without mutating shared registry state, fixing the leak where an
unnamed agent's rule registered globally and gated other agents.
Co-authored-by: Mervin Praison <[email protected]>
Bridge verification to data generation: take a serialised trials report
(K scored attempts per case with captured trajectories), keep the passing
attempts via rejection sampling, and write them as a ShareGPT dataset the
existing trainer consumes unchanged. Adds export_trials() + ExportSummary,
provenance sidecar ({path}.meta.json), and a `from-trials` CLI command
beside generate/dedup/validate. Report is duck-typed over JSON (the contract
with the sibling core trials engine) so there's no hard dependency on unlanded
core code. Pure stdlib, additive, confined to the data namespace.
Co-authored-by: MervinPraison <[email protected]>
Gap 1: replace threading.local() handoff-chain storage with contextvars.ContextVar so concurrent asyncio handoffs no longer share one chain and corrupt each other's cycle/depth checks.
Gap 2: distinguish a transient OS read error from no-file/corrupt-JSON in the session store so a momentary read failure aborts the write (returns False) instead of silently overwriting real history with an empty session.
Gap 3: scope ApprovalRegistry tool-requirement state per agent so a per-agent PermissionManager ask rule no longer leaks an approval gate onto unrelated agents in the same process. Global registration (DEFAULT_DANGEROUS_TOOLS) is unaffected.
Co-authored-by: MervinPraison <[email protected]>
- Accept cached `huggingface-cli login` (get_token) in preflight, not just HF_TOKEN env (Greptile P1)
- Gate the hard CUDA check on train enabled so publish/export-only (train: false) validates on CPU (CodeRabbit Major)
- Resolve Ollama readiness endpoint from OLLAMA_HOST instead of hardcoded 127.0.0.1:11434 (Greptile P1)
- Translate `ollama create` failure into a clean RuntimeError and catch CalledProcessError in main()/export (CodeRabbit)
- Close serve_err TemporaryFile deterministically (CodeRabbit)
- Fix "Free up free space" typo (CodeRabbit)
- Guard non-mapping YAML config and require --model-dir to be a directory in export (CodeRabbit)
- Validate quantization_method in for_export too (CodeRabbit)
- Add tests for cached-login, train:false CPU, OLLAMA_HOST, and ollama-create failure
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Mervin Praison <[email protected]>
Make praisonai-train usable by a non-developer: replace raw tracebacks with
clean actionable errors, validate the config up front, and harden the
Ollama/GGUF/HF export paths against the exact failures hit in practice.
trainer.py:
- main() wraps construction + run(); ValueError/RuntimeError print as a single
"ERROR: <msg>" line and exit 1 (no traceback); KeyboardInterrupt exits cleanly.
- validate_config() preflight (fails fast BEFORE model/GPU load): no-CUDA GPU,
missing HF_TOKEN when publishing, <10GB free disk, invalid quantization_method.
- check_gpu() guarded so it no longer crashes raw on a CPU-only box.
- Unknown-key warning now collects all keys and suggests the closest known key
(difflib) in one clear block.
- Silent-behavior NOTES: num_samples head-N-before-shuffle, split_type
validation/test contamination into training, and the max_steps=2800 default.
- HF push (merged/gguf) translates HfHubHTTPError 401/403 into actionable
messages; rmtree guarded so a namespaced repo id ("user/model") is never
treated as a local path to delete.
- Template fixes: warn when no known chat template matches, drop the broken empty
DeepSeek stop tokens, and never emit an empty PARAMETER stop line.
- for_export() classmethod to build an export-only trainer (no dataset needed).
_ollama.py:
- create_and_push_ollama_model() accepts optional quantization (--quantize),
disk pre-check on the OLLAMA_MODELS volume, and translates push 401/403 into a
message pointing at ~/.ollama/id_ed25519.pub + ollama.com/settings/keys.
- ensure_ollama_running() waits 15s, probes GET /api/version, and surfaces serve
stderr in the timeout message.
cli/commands/train.py:
- New `export <ollama|gguf|hf>` command to publish an already-trained model
without re-running training.
tests: tests/unit/train/test_robustness.py (14 tests, all deps stubbed).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Add build_openai_tool_dict to mcp_schema_utils.py and delegate from the
three transport tool classes (HTTPStreamMCPTool, SSEMCPTool,
WebSocketMCPTool). Output dicts, including the __praisonai_deferrable__
marker, are unchanged.
Co-authored-by: MervinPraison <[email protected]>
Replace flat len(text)//4 offline fallback in ContextCompactor.estimate_tokens
with the canonical estimate_tokens_heuristic from context.tokens, mirroring
context/aggregator.py. This removes duplicate logic and fixes under-counting
of non-ASCII (CJK/dense-script) text on the offline path.
Co-authored-by: MervinPraison <[email protected]>
Move wrapper-package discovery out of the Tier 2 ui.py CLI into the lazy
_wrapper_bridge via a new wrapper_package_path() helper, per ARCHITECTURE.md
§2 cross-tier boundary. Preserves preset resolution and the None fallback
(install hint) when the wrapper is absent.
Co-authored-by: Mervin Praison <[email protected]>
The UI loader was extracted to praisonai_code but resolved the bundled
default_app.py path via __file__-relative arithmetic anchored in
praisonai_code, which has no ui_* directories. The five bundled UI presets
ship in the praisonai wrapper package. Resolve the path against the installed
wrapper via importlib.util.find_spec("praisonai"), falling back to a clear
'pip install "praisonai[ui]"' hint when the wrapper is absent.
Co-authored-by: MervinPraison <[email protected]>
The executor context manager called shutdown(wait=True) on __exit__, which
re-blocked on a coroutine still hung past PRAISONAI_RUN_SYNC_TIMEOUT — defeating
the timeout (Greptile P1). Manage the pool explicitly and shutdown(wait=False)
in finally so a timed-out tool call returns immediately.
Co-authored-by: Mervin Praison <[email protected]>
The wrapper copy of agent_tools.py had drifted from the canonical
praisonai_code copy (missing the ACP cwd forwarding fix), and the two
resolved via different import spellings. Collapse the wrapper copy to a
thin re-export of the canonical praisonai_code implementation so both
import styles resolve to a single owner.
Reconcile the canonical module-local _run_sync bridge to keep the union
of both historical guarantees: running-loop safety (offload to a worker
thread) AND a timeout, without adding a wrapper dependency (C7 gate).
Co-authored-by: MervinPraison <[email protected]>
Address Greptile P1 review findings on PR #3358:
- _remove_worktree now returns a reason string when `git worktree remove`
fails (nonzero exit or exception) instead of silently returning None.
The caller keeps the tracking entry and records a preservation comment,
preventing orphaned worktrees/branches with dropped tracking.
- create_task persists the resolved top-level repo_path into task metadata
so the repo linkage that drove the worktree auto-upgrade is retained
between create and dispatch (an explicit metadata.repo_path still wins).
Adds tests for failed/exception removal tracking and repo_path persistence.
Co-authored-by: Mervin Praison <[email protected]>
Refinement 1: repo-linked tasks auto-upgrade workspace_kind to "worktree" at
create_task time (linkage-based, not content-inspecting). Explicit
workspace_kind and auto_worktree=false are respected. Derived branch
kanban/<task_id> is persisted for the dispatcher to consume.
Refinement 2: _remove_worktree is lossless by default — refuse to delete a
worktree that is dirty or has commits not in base; preserve it and record a
worktree_preserved comment. Removal with outstanding work is an explicit
force operation only.
Co-authored-by: Mervin Praison <[email protected]>
Addresses two P1 review issues on the workspace-binding change:
- Resolve the UnifiedSession once (before workspace binding + tool load) and
pass it into _bind_resume_workspace, eliminating a second independent
last-session lookup that could race a concurrent CLI process and bind tools
to a different directory than the loaded conversation.
- When a resumed session's saved workspace no longer exists, pin
PRAISONAI_WORKSPACE to cwd instead of leaving it unset, so a stray legacy
PRAISON_WORKSPACE cannot leak through the tool loader and contradict the
"using current directory" warning.
Adds regressions for cwd-pinning over the legacy var and for
provided-session-without-relookup; updates the existing fallback test.
Co-authored-by: Mervin Praison <[email protected]>
Bot/gateway file tools resolve against a per-session Workspace attached to
the agent after construction, but the FileSnapshot backing Agent.undo was
created rooted at os.getcwd() — so /undo tracked the wrong directory.
Add Agent.set_snapshot_root() (core, parameter-only) and call it from the
bot smart-defaults after attaching the workspace so change tracking follows
the tools. Deliberately minimal: the per-turn workspace swap (bug 3) and
policy worktrees (blocked on #3354's WorktreeService) are out of scope here.
Co-authored-by: MervinPraison <[email protected]>
The `praisonai code --workspace` flag was dead: it wrote PRAISONAI_WORKSPACE
but ToolConfig.from_env read PRAISON_WORKSPACE, and _load_interactive_tools
unconditionally overrode config.workspace with os.getcwd(). Fix the env-name
mismatch (honour both, prefer canonical PRAISONAI_) and resolve the workspace
as PRAISONAI_WORKSPACE -> PRAISON_WORKSPACE -> cwd instead of hardcoding cwd.
Also bind resumed sessions to their directory: --continue/--session now reads
back the persisted UnifiedSession.workspace and roots the tools there, warning
and falling back to cwd when that directory is gone.
Scope kept minimal per AGENTS.md: no new WorktreeService — worktree isolation
already exists via core GitWorktreeAdapter (wired into `praisonai run`), so a
parallel wrapper service would duplicate it.
Co-authored-by: MervinPraison <[email protected]>
The gateway status command now passes the additive deep flag to
handler.status(); update the assertion to match the new backward-
compatible default.
Co-authored-by: Mervin Praison <[email protected]>
- Only attach GATEWAY_AUTH_TOKEN over loopback or HTTPS; never over remote
plaintext HTTP in preflight probes and version-skew check
- Slack @mention: use hook-returned content verbatim so an intentionally
emptied/redacted message is not silently restored to raw event text
- Inbound metrics: track whether a baseline pre-existed so the first check
against pre-existing traffic reports "baseline established" instead of a
false "no inbound"
Co-authored-by: Mervin Praison <[email protected]>
Extend gateway test/status with --check-runtime, --check-inbound, and
--check-duplicates; add sessions list/show, enriched /health, metrics
delta, doctor deep checks, and Slack @mention liveness fixes so operators
can prove live delivery and detect competing gateways.
Addresses the Greptile finding that reconnect() restarts the same bot
instance still holding the rejected token. On wake from the parked
credential state the supervisor now opportunistically calls an optional,
duck-typed refresh_credentials() hook so an out-of-band credential repair
(e.g. a rotated env var) driving a bare reconnect() self-heals without a
full restart. No-op for bots without the hook (env-var tokens are already
re-resolved by start()/hot-reload). Best-effort: a failing hook cannot
wedge supervision.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Address two P1 review findings on the gateway restart-replay change:
- Only persist the start-flags artefact AFTER startup validation passes
(import/config/agent-file), just before the gateway binds, so a failed or
rejected start on the same host:port no longer clobbers the running
gateway's saved posture.
- restart --drain-timeout now defaults to None; an omitted flag replays the
persisted drain window (and falls back to 10s only when none is persisted)
instead of silently forcing a fixed 10s and shortening production drains.
An explicit value still wins for both the OLD-process drain and the relaunch.
Adds 3 regression tests covering omitted/explicit drain replay and the
no-persisted-value fallback.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-authored-by: Mervin Praison <[email protected]>
Direct (non-service) `gateway restart` silently dropped the runtime flags
the process was started with (--openai-api, --reliability,
--max-concurrent-runs, ...), reverting production settings to defaults.
`start` now persists the non-None runtime flags to
~/.praisonai/gateway.start.<host>.<port>.json (keyed by bind host:port,
matching the PID lock), and `restart` loads and replays them so the
relaunch faithfully reproduces the running gateway. Explicit restart flags
(config / agents / drain-timeout) still win over persisted values.
Co-authored-by: MervinPraison <[email protected]>
Classify runtime 401/403 (and platform text equivalents like invalid_auth/
token_revoked) as a distinct CREDENTIAL_UNAVAILABLE outcome instead of a
generic FAILED. On a runtime credential rejection the supervisor now parks in a
named, redacted degraded state (reusing the boot-time "credential unavailable"
surface from #3159 in health()), stops hammering the invalid token, and
auto-recovers on reconnect/hot-reload without a full process restart.
Co-authored-by: MervinPraison <[email protected]>
Address two P1 review findings on praisonai init --generate:
- Streaming stringified: _generate_agents_md used agent.start(), which
auto-enables streaming in a TTY and returns a generator whose repr was
written to AGENTS.md. Switch to agent.run() (always silent, returns str).
- Global mode wrote ~/AGENTS.md: with --global, base.parent is $HOME so the
agent scanned home and wrote home-tailored rules outside the repo. Resolve
the AGENTS.md target from git root / cwd independently of --global.
Add tests: --global still writes AGENTS.md to the repo root (not home), and
_generate_agents_md calls run() (not start()) returning the string body.
Co-authored-by: Mervin Praison <[email protected]>
Addresses the two Greptile P1 findings on #3346 so the non-zero-on-failure
contract holds across every reachable run path:
- _run_from_file: add `except typer.Exit: raise` so the deliberate failure
exit is not recaught by the broad handler (fixes duplicate reporting).
- _try_attach_runtime: classify the warm-runtime result and report failure so
an empty result exits non-zero instead of silently succeeding.
- _run_prompt_profiled / _run_from_file_profiled: apply the same failure
contract after the profiling report so profiled runs honour it too.
Adds a regression test for the warm-runtime attach path.
Co-authored-by: Mervin Praison <[email protected]>
Adds an opt-in `--generate` flag to `praisonai init` that runs a short,
read-only analysis agent (reusing the existing Agent runtime) to produce a
concise, repository-tailored AGENTS.md at the repo root. Feeds the agent a
cheap pre-scan (top-level tree, detected manifests, README head) to bound cost.
Non-destructive: respects --force, never overwrites an existing AGENTS.md, and
degrades gracefully to the static scaffold when no provider credential is
present or generation fails. The output lands where rules_manager already
discovers it on the next run.
Co-authored-by: Mervin Praison <[email protected]>
praisonai run reported success and exited 0 regardless of whether the
agent produced a result. Wire the existing run result (None/empty ==
failure) through the CLI: emit ok=<success> to the event bridge, a
machine-readable {"status": "failed"} object under --output json, an
actionable remediation, and raise typer.Exit(1) on failure — mirroring
the existing arg-error exit convention. Successful runs are unchanged.
Co-authored-by: Mervin Praison <[email protected]>
The running-loop fallback in _structured_completion/_run_coro_sync spawned a
bare threading.Thread, which does not inherit contextvars. run_sync() inside it
therefore resolved via _default_bridge() -> empty _bridge_var -> shared _BG,
bypassing an active scoped_bridge() and leaking the fallback onto the global
loop. Run the worker under contextvars.copy_context() so both paths resolve the
same AsyncBridge (scoped override, else shared default), preserving per-session
loop isolation.
Co-authored-by: Mervin Praison <[email protected]>
- auto.py: runner threads in _structured_completion and _run_coro_sync now
dispatch to the shared AsyncBridge via run_sync() instead of asyncio.run(),
reusing one process-wide loop/thread and per-loop connection pools.
- auto.py: _completion_impl preserves the original LiteLLM exception and
chains it into the OpenAI fallback failure (raise ... from) so users see
both provider tracebacks instead of only the masking OpenAI one.
- registry.py: _validate_adapter treats a **kwargs catch-all as satisfying
the protocol keywords and also validates arun, unblocking third-party
entry-point adapters that forward **kwargs.
Co-authored-by: MervinPraison <[email protected]>
Embed turn prerequisite errors into the JSON payload instead of printing a
trailing plain-text line, so --json consumers get a single valid document.
Also drop dead critical-tool branch in is_already_approved.
Co-authored-by: Mervin Praison <[email protected]>
1. Circuit breakers: register weakref.finalize at breaker creation so the
process-global registry entry is removed when the Agent is GC'd, closing
the CPython id-reuse window where a new agent could inherit a stale OPEN
breaker. No longer relies on close()/aclose() being called. Uses a
staticmethod callback so the finalizer does not keep the agent alive.
2. Global tool-registry fallback: gate the @tool registry lookup in
execute_tool() behind ToolConfig(allow_global_tools=True) (default False).
By default an agent is scoped strictly to its declared tools=[...] and
cannot execute an @tool-decorated callable it was never given. Plugin-host
style dynamic dispatch can opt in via tool_config.
3. Sync run_task(): re-raise on fail_on_callback_error / fail_on_memory_error,
mirroring arun_task() so the documented fail-fast flags behave identically
on the sync and async surfaces.
Co-authored-by: MervinPraison <[email protected]>
Extract shared probe/shell/turn logic to praisonai_bot.gateway.preflight,
add praisonai gateway test orchestrator, doctor bots shell/probe checks,
fix doctor --json single-document output, and per-channel turn probe gate.
Critical tools honour mark_approved via the approval registry, shell enable
syncs AutoApproveBackend to the registry, @mentions run routing handlers,
and gateway doctor gains an offline --turn probe for inbound simulation.
Addresses greptile P1: CapabilityValidator cached the first _get_available_servers()
snapshot, so a skill validated before its MCP server registered via with_tool_prefix()
stayed permanently UNAVAILABLE under STRICT even after the server connected. The MCP
registry read is a cheap set copy, so query it live and drop the dead _server_cache
field (issue #3307).
Co-authored-by: Mervin Praison <[email protected]>
Gap 1: Port sync workflow()'s loop-task pre-expansion into aworkflow() so
async CSV-driven loop start tasks run per-row instead of once.
Gap 2: Guard the per-turn tool buffer (_turn_tools_used) with a DualLock via
_reset_turn_tools/_record_turn_tool/_drain_turn_tools helpers, matching the
existing chat_history protection, so concurrent chat()/achat() turns on one
Agent no longer corrupt hook/self-improvement data.
Gap 3: Track namespaced MCP server names in a process-level registry and let
the skills CapabilityValidator discover them, so MCP-server-gated skills can
pass STRICT validation instead of always failing closed.
Co-authored-by: Mervin Praison <[email protected]>
2026-07-23 10:27:43 +00:00
740 changed files with 71268 additions and 8362 deletions
@@ -7,9 +7,9 @@ You are working on the PraisonAI project.
- Be concise and helpful in responses
- Test implementation thoroughly
- Ensure backward compatibility with existing APIs
- Follow protocol-driven design across the eight packages: core protocols in `praisonaiagents/`, agentic terminal CLI in `praisonai-code/`, bots/gateway in `praisonai-bot/`, LLM fine-tuning + agent training in `praisonai-train/`, browser automation in `praisonai-browser/`, MCP server host in `praisonai-mcp/`, sandbox backends in `praisonai-sandbox/`, integrations/serve/dashboard in the `praisonai/` wrapper
- Follow protocol-driven design across the nine packages: core protocols in `praisonaiagents/`, agentic terminal CLI in `praisonai-code/`, bots/gateway in `praisonai-bot/`, LLM fine-tuning + agent training in `praisonai-train/`, browser automation in `praisonai-browser/`, MCP server host in `praisonai-mcp/`, sandbox backends in `praisonai-sandbox/`, deployment in `praisonai-deploy/`, integrations/serve/dashboard in the `praisonai/` wrapper
- Preserve old `praisonai.*` import paths via shims when moving code between packages (see §2.3 in `src/praisonai-agents/AGENTS.md`; shim helpers in `src/praisonai/praisonai/cli/_shim.py`)
- Package boundaries and dependency rules: `ARCHITECTURE.md` §2 (Tier 2 packages must never PyPI-depend on the wrapper; cross-tier access goes through lazy `_*_bridge` modules)
- When reviewing a PR or an issue, evaluate whether the change addresses a framework concern or a user goal, and design its surface (params, naming, defaults) accordingly
- The aim of this package is to stay **lightweight and powerful**. Do a critical review at each stage — when triaging an issue, when planning a fix, and when reviewing/implementing a PR. Reject scope creep for the sake of adding features: if a capability already exists (e.g. via existing Agent params like `instructions`/`backstory`/`tools`/`hooks`/`memory`), prefer it over a new API surface. A change must genuinely strengthen the SDK (simpler, more robust, more user-friendly) — do not add knobs, params, modules, or exports that have no live consumer or that merely duplicate existing behaviour.
| 2a | `src/praisonai-code/` | `run`/`chat`/`code`, Typer, runtime, LLM, tool resolution | **`praisonai` as a PyPI dependency** (optional lazy imports via `_wrapper_bridge` only) |
| 2b | `src/praisonai-bot/` | Bots, gateway, channel CLI, OS daemon, gateway scheduler tick | **`praisonai` as a PyPI dependency** (optional lazy `_wrapper_bridge` for jobs/UI) |
| 2c | `src/praisonai-train/` | LLM fine-tuning (Unsloth), agent training, `train` CLI, conda env setup | **`praisonai` as a PyPI dependency** (lazy `_code_bridge` for legacy dispatch) |
| 2d | `src/praisonai-browser/` | Extension bridge, CDP/hybrid automation, `browser` CLI | **`praisonai` as a PyPI dependency** (none required; depends on `praisonaiagents` only) |
| 2e | `src/praisonai-mcp/` | MCP server host, auth, transports, `mcp` CLI | **`praisonai` as a PyPI dependency** (lazy `_wrapper_bridge` for full capability registry) |
| 2f | `src/praisonai-sandbox/` | Sandbox backends (Docker, E2B, Modal, Sandlock, SSH), `SandboxRegistry` | **`praisonai` as a PyPI dependency** (lazy `_code_bridge` for `PluginRegistry`) |
| 2g | `src/praisonai-deploy/` | Deploy API/Docker/cloud, `deploy` CLI, scheduler integration | **`praisonai` as a PyPI dependency** (lazy `_plugin_registry` via `_code_bridge`) |
| 3 | `src/praisonai/` | `framework_adapters/`, serve, dashboard, async jobs API | — |
### Repo infra (not PyPI)
Deployment packaging that stays in the git checkout, not in any tier-2 wheel:
**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`).
**Install order (CI and local dev):**`praisonai-agents` → `praisonai-code` →`praisonai-bot` → `praisonai-train` → `praisonai-browser` → `praisonai-mcp` → `praisonai-sandbox` → `praisonai-deploy` →`praisonai` (see `.github/actions/install-monorepo-packages`).
Loaded 100 of 740 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.