praisonai-code 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- praisonai_code/__init__.py +17 -0
- praisonai_code/cli/__init__.py +12 -0
- praisonai_code/cli/_forward_shim.py +10 -0
- praisonai_code/cli/_paths.py +88 -0
- praisonai_code/cli/_warnings.py +58 -0
- praisonai_code/cli/app.py +757 -0
- praisonai_code/cli/approval_backend.py +272 -0
- praisonai_code/cli/branding.py +94 -0
- praisonai_code/cli/commands/__init__.py +114 -0
- praisonai_code/cli/commands/acp.py +80 -0
- praisonai_code/cli/commands/agent.py +116 -0
- praisonai_code/cli/commands/agents.py +80 -0
- praisonai_code/cli/commands/app.py +139 -0
- praisonai_code/cli/commands/attach.py +95 -0
- praisonai_code/cli/commands/audit.py +102 -0
- praisonai_code/cli/commands/auth.py +508 -0
- praisonai_code/cli/commands/batch.py +848 -0
- praisonai_code/cli/commands/benchmark.py +286 -0
- praisonai_code/cli/commands/browser.py +299 -0
- praisonai_code/cli/commands/call.py +45 -0
- praisonai_code/cli/commands/chat.py +332 -0
- praisonai_code/cli/commands/checkpoint.py +170 -0
- praisonai_code/cli/commands/code.py +276 -0
- praisonai_code/cli/commands/command.py +114 -0
- praisonai_code/cli/commands/commit.py +47 -0
- praisonai_code/cli/commands/completion.py +333 -0
- praisonai_code/cli/commands/config.py +681 -0
- praisonai_code/cli/commands/context.py +414 -0
- praisonai_code/cli/commands/daemon.py +203 -0
- praisonai_code/cli/commands/debug.py +142 -0
- praisonai_code/cli/commands/deploy.py +71 -0
- praisonai_code/cli/commands/diag.py +55 -0
- praisonai_code/cli/commands/docs.py +1575 -0
- praisonai_code/cli/commands/doctor.py +332 -0
- praisonai_code/cli/commands/endpoints.py +51 -0
- praisonai_code/cli/commands/environment.py +179 -0
- praisonai_code/cli/commands/eval.py +131 -0
- praisonai_code/cli/commands/examples.py +953 -0
- praisonai_code/cli/commands/flow.py +436 -0
- praisonai_code/cli/commands/github.py +752 -0
- praisonai_code/cli/commands/hooks.py +74 -0
- praisonai_code/cli/commands/init.py +174 -0
- praisonai_code/cli/commands/knowledge.py +440 -0
- praisonai_code/cli/commands/langextract.py +120 -0
- praisonai_code/cli/commands/langfuse.py +984 -0
- praisonai_code/cli/commands/loop.py +211 -0
- praisonai_code/cli/commands/lsp.py +112 -0
- praisonai_code/cli/commands/managed.py +659 -0
- praisonai_code/cli/commands/mcp.py +763 -0
- praisonai_code/cli/commands/memory.py +298 -0
- praisonai_code/cli/commands/models.py +264 -0
- praisonai_code/cli/commands/n8n.py +326 -0
- praisonai_code/cli/commands/obs.py +19 -0
- praisonai_code/cli/commands/package.py +76 -0
- praisonai_code/cli/commands/paths.py +106 -0
- praisonai_code/cli/commands/permissions.py +272 -0
- praisonai_code/cli/commands/plugins.py +609 -0
- praisonai_code/cli/commands/port.py +530 -0
- praisonai_code/cli/commands/profile.py +466 -0
- praisonai_code/cli/commands/publish.py +193 -0
- praisonai_code/cli/commands/rag.py +913 -0
- praisonai_code/cli/commands/realtime.py +52 -0
- praisonai_code/cli/commands/recipe.py +684 -0
- praisonai_code/cli/commands/registry.py +59 -0
- praisonai_code/cli/commands/replay.py +830 -0
- praisonai_code/cli/commands/research.py +49 -0
- praisonai_code/cli/commands/retrieval.py +377 -0
- praisonai_code/cli/commands/rules.py +71 -0
- praisonai_code/cli/commands/run.py +1573 -0
- praisonai_code/cli/commands/sandbox.py +371 -0
- praisonai_code/cli/commands/schedule.py +529 -0
- praisonai_code/cli/commands/serve.py +690 -0
- praisonai_code/cli/commands/session.py +450 -0
- praisonai_code/cli/commands/setup.py +174 -0
- praisonai_code/cli/commands/skills.py +545 -0
- praisonai_code/cli/commands/standardise.py +711 -0
- praisonai_code/cli/commands/templates.py +54 -0
- praisonai_code/cli/commands/test.py +558 -0
- praisonai_code/cli/commands/todo.py +74 -0
- praisonai_code/cli/commands/tools.py +205 -0
- praisonai_code/cli/commands/traces.py +145 -0
- praisonai_code/cli/commands/tracker.py +852 -0
- praisonai_code/cli/commands/train.py +613 -0
- praisonai_code/cli/commands/ui.py +172 -0
- praisonai_code/cli/commands/up.py +354 -0
- praisonai_code/cli/commands/validate.py +291 -0
- praisonai_code/cli/commands/version.py +101 -0
- praisonai_code/cli/commands/workflow.py +97 -0
- praisonai_code/cli/config_loader.py +437 -0
- praisonai_code/cli/configuration/__init__.py +27 -0
- praisonai_code/cli/configuration/config.schema.json +57 -0
- praisonai_code/cli/configuration/credentials.py +446 -0
- praisonai_code/cli/configuration/loader.py +364 -0
- praisonai_code/cli/configuration/model_resolver.py +161 -0
- praisonai_code/cli/configuration/oauth.py +389 -0
- praisonai_code/cli/configuration/paths.py +224 -0
- praisonai_code/cli/configuration/resolver.py +687 -0
- praisonai_code/cli/configuration/schema.py +317 -0
- praisonai_code/cli/execution/__init__.py +99 -0
- praisonai_code/cli/execution/core.py +208 -0
- praisonai_code/cli/execution/profiler.py +898 -0
- praisonai_code/cli/execution/request.py +85 -0
- praisonai_code/cli/execution/result.py +74 -0
- praisonai_code/cli/fallback_schema.py +416 -0
- praisonai_code/cli/features/__init__.py +278 -0
- praisonai_code/cli/features/_endpoint_registry.py +64 -0
- praisonai_code/cli/features/_search_registry.py +43 -0
- praisonai_code/cli/features/acp.py +236 -0
- praisonai_code/cli/features/action_orchestrator.py +576 -0
- praisonai_code/cli/features/agent_scheduler.py +773 -0
- praisonai_code/cli/features/agent_tools.py +603 -0
- praisonai_code/cli/features/agents.py +397 -0
- praisonai_code/cli/features/at_mentions.py +471 -0
- praisonai_code/cli/features/audit_cli.py +270 -0
- praisonai_code/cli/features/auto_memory.py +182 -0
- praisonai_code/cli/features/auto_mode.py +552 -0
- praisonai_code/cli/features/autonomy_mode.py +546 -0
- praisonai_code/cli/features/background.py +356 -0
- praisonai_code/cli/features/base.py +168 -0
- praisonai_code/cli/features/benchmark.py +1462 -0
- praisonai_code/cli/features/capabilities.py +1326 -0
- praisonai_code/cli/features/checkpoints.py +345 -0
- praisonai_code/cli/features/cli_profiler.py +335 -0
- praisonai_code/cli/features/code_intelligence.py +666 -0
- praisonai_code/cli/features/compaction.py +294 -0
- praisonai_code/cli/features/compare.py +534 -0
- praisonai_code/cli/features/config_hierarchy.py +366 -0
- praisonai_code/cli/features/context_manager.py +597 -0
- praisonai_code/cli/features/cost_tracker.py +514 -0
- praisonai_code/cli/features/csv_test_runner.py +736 -0
- praisonai_code/cli/features/custom_definitions.py +790 -0
- praisonai_code/cli/features/debug.py +810 -0
- praisonai_code/cli/features/deploy.py +605 -0
- praisonai_code/cli/features/diag.py +289 -0
- praisonai_code/cli/features/display_jsonl.py +173 -0
- praisonai_code/cli/features/doctor/__init__.py +63 -0
- praisonai_code/cli/features/doctor/checks/__init__.py +29 -0
- praisonai_code/cli/features/doctor/checks/acp_checks.py +220 -0
- praisonai_code/cli/features/doctor/checks/bot_checks.py +340 -0
- praisonai_code/cli/features/doctor/checks/config_checks.py +373 -0
- praisonai_code/cli/features/doctor/checks/db_checks.py +366 -0
- praisonai_code/cli/features/doctor/checks/env_checks.py +637 -0
- praisonai_code/cli/features/doctor/checks/gateway_checks.py +387 -0
- praisonai_code/cli/features/doctor/checks/lsp_checks.py +231 -0
- praisonai_code/cli/features/doctor/checks/mcp_checks.py +367 -0
- praisonai_code/cli/features/doctor/checks/memory_checks.py +268 -0
- praisonai_code/cli/features/doctor/checks/network_checks.py +251 -0
- praisonai_code/cli/features/doctor/checks/obs_checks.py +328 -0
- praisonai_code/cli/features/doctor/checks/packaging_checks.py +422 -0
- praisonai_code/cli/features/doctor/checks/performance_checks.py +235 -0
- praisonai_code/cli/features/doctor/checks/permissions_checks.py +259 -0
- praisonai_code/cli/features/doctor/checks/runtime_checks.py +650 -0
- praisonai_code/cli/features/doctor/checks/runtime_migration_checks.py +220 -0
- praisonai_code/cli/features/doctor/checks/selftest_checks.py +322 -0
- praisonai_code/cli/features/doctor/checks/serve_checks.py +426 -0
- praisonai_code/cli/features/doctor/checks/skills_checks.py +327 -0
- praisonai_code/cli/features/doctor/checks/tools_checks.py +371 -0
- praisonai_code/cli/features/doctor/engine.py +266 -0
- praisonai_code/cli/features/doctor/formatters.py +377 -0
- praisonai_code/cli/features/doctor/handler.py +564 -0
- praisonai_code/cli/features/doctor/models.py +276 -0
- praisonai_code/cli/features/doctor/registry.py +239 -0
- praisonai_code/cli/features/endpoints.py +1016 -0
- praisonai_code/cli/features/eval.py +559 -0
- praisonai_code/cli/features/examples.py +707 -0
- praisonai_code/cli/features/external_agents.py +231 -0
- praisonai_code/cli/features/fast_context.py +410 -0
- praisonai_code/cli/features/file_history.py +320 -0
- praisonai_code/cli/features/flow_display.py +566 -0
- praisonai_code/cli/features/git_attribution.py +159 -0
- praisonai_code/cli/features/git_integration.py +651 -0
- praisonai_code/cli/features/guardrail.py +171 -0
- praisonai_code/cli/features/handoff.py +252 -0
- praisonai_code/cli/features/hooks.py +583 -0
- praisonai_code/cli/features/hybrid_workflow.py +391 -0
- praisonai_code/cli/features/image.py +384 -0
- praisonai_code/cli/features/interactive_core_headless.py +450 -0
- praisonai_code/cli/features/interactive_runtime.py +600 -0
- praisonai_code/cli/features/interactive_test_harness.py +537 -0
- praisonai_code/cli/features/interactive_tools.py +428 -0
- praisonai_code/cli/features/interactive_tui.py +603 -0
- praisonai_code/cli/features/job_workflow.py +906 -0
- praisonai_code/cli/features/jobs.py +632 -0
- praisonai_code/cli/features/knowledge.py +531 -0
- praisonai_code/cli/features/knowledge_cli.py +438 -0
- praisonai_code/cli/features/lite.py +244 -0
- praisonai_code/cli/features/logs.py +200 -0
- praisonai_code/cli/features/lsp_cli.py +225 -0
- praisonai_code/cli/features/lsp_diagnostics.py +185 -0
- praisonai_code/cli/features/mcp.py +344 -0
- praisonai_code/cli/features/message_queue.py +587 -0
- praisonai_code/cli/features/metrics.py +210 -0
- praisonai_code/cli/features/migrate.py +1329 -0
- praisonai_code/cli/features/migration_flow.py +463 -0
- praisonai_code/cli/features/migration_spec.py +276 -0
- praisonai_code/cli/features/n8n.py +703 -0
- praisonai_code/cli/features/observability.py +293 -0
- praisonai_code/cli/features/ollama.py +361 -0
- praisonai_code/cli/features/output_modes.py +155 -0
- praisonai_code/cli/features/output_style.py +273 -0
- praisonai_code/cli/features/package.py +631 -0
- praisonai_code/cli/features/performance.py +308 -0
- praisonai_code/cli/features/persistence.py +636 -0
- praisonai_code/cli/features/profiler/__init__.py +81 -0
- praisonai_code/cli/features/profiler/core.py +558 -0
- praisonai_code/cli/features/profiler/optimizations.py +652 -0
- praisonai_code/cli/features/profiler/suite.py +386 -0
- praisonai_code/cli/features/queue/__init__.py +73 -0
- praisonai_code/cli/features/queue/manager.py +435 -0
- praisonai_code/cli/features/queue/models.py +289 -0
- praisonai_code/cli/features/queue/persistence.py +564 -0
- praisonai_code/cli/features/queue/scheduler.py +529 -0
- praisonai_code/cli/features/queue/worker.py +400 -0
- praisonai_code/cli/features/recipe.py +2187 -0
- praisonai_code/cli/features/recipe_creator.py +996 -0
- praisonai_code/cli/features/recipe_optimizer.py +1364 -0
- praisonai_code/cli/features/recipe_prompts.py +226 -0
- praisonai_code/cli/features/registry.py +229 -0
- praisonai_code/cli/features/repo_map.py +860 -0
- praisonai_code/cli/features/router.py +466 -0
- praisonai_code/cli/features/safe_shell.py +427 -0
- praisonai_code/cli/features/sandbox_cli.py +283 -0
- praisonai_code/cli/features/sandbox_executor.py +536 -0
- praisonai_code/cli/features/sdk_knowledge.py +500 -0
- praisonai_code/cli/features/session.py +222 -0
- praisonai_code/cli/features/session_checkpoints.py +208 -0
- praisonai_code/cli/features/setup/__init__.py +9 -0
- praisonai_code/cli/features/setup/handler.py +355 -0
- praisonai_code/cli/features/setup/templates.py +62 -0
- praisonai_code/cli/features/skills.py +940 -0
- praisonai_code/cli/features/slash_commands.py +692 -0
- praisonai_code/cli/features/telemetry.py +179 -0
- praisonai_code/cli/features/templates.py +1390 -0
- praisonai_code/cli/features/thinking.py +343 -0
- praisonai_code/cli/features/todo.py +334 -0
- praisonai_code/cli/features/tools.py +680 -0
- praisonai_code/cli/features/tui/__init__.py +83 -0
- praisonai_code/cli/features/tui/app.py +871 -0
- praisonai_code/cli/features/tui/cli.py +580 -0
- praisonai_code/cli/features/tui/config.py +150 -0
- praisonai_code/cli/features/tui/debug.py +526 -0
- praisonai_code/cli/features/tui/events.py +99 -0
- praisonai_code/cli/features/tui/mock_provider.py +328 -0
- praisonai_code/cli/features/tui/orchestrator.py +652 -0
- praisonai_code/cli/features/tui/screens/__init__.py +50 -0
- praisonai_code/cli/features/tui/screens/help.py +157 -0
- praisonai_code/cli/features/tui/screens/main.py +568 -0
- praisonai_code/cli/features/tui/screens/queue.py +174 -0
- praisonai_code/cli/features/tui/screens/session.py +124 -0
- praisonai_code/cli/features/tui/screens/settings.py +148 -0
- praisonai_code/cli/features/tui/session_store.py +198 -0
- praisonai_code/cli/features/tui/widgets/__init__.py +56 -0
- praisonai_code/cli/features/tui/widgets/chat.py +263 -0
- praisonai_code/cli/features/tui/widgets/command_popup.py +258 -0
- praisonai_code/cli/features/tui/widgets/composer.py +292 -0
- praisonai_code/cli/features/tui/widgets/file_popup.py +207 -0
- praisonai_code/cli/features/tui/widgets/queue_panel.py +223 -0
- praisonai_code/cli/features/tui/widgets/status.py +181 -0
- praisonai_code/cli/features/tui/widgets/tool_panel.py +307 -0
- praisonai_code/cli/features/wizard.py +289 -0
- praisonai_code/cli/features/workflow.py +802 -0
- praisonai_code/cli/features/yaml_utils.py +321 -0
- praisonai_code/cli/interactive/__init__.py +48 -0
- praisonai_code/cli/interactive/async_tui.py +1218 -0
- praisonai_code/cli/interactive/config.py +139 -0
- praisonai_code/cli/interactive/core.py +618 -0
- praisonai_code/cli/interactive/events.py +131 -0
- praisonai_code/cli/interactive/frontends/__init__.py +31 -0
- praisonai_code/cli/interactive/frontends/rich_frontend.py +462 -0
- praisonai_code/cli/interactive/frontends/textual_frontend.py +157 -0
- praisonai_code/cli/interactive/praison_io.py +502 -0
- praisonai_code/cli/interactive/repl.py +297 -0
- praisonai_code/cli/interactive/split_tui.py +456 -0
- praisonai_code/cli/interactive/tui_app.py +457 -0
- praisonai_code/cli/langfuse_client.py +360 -0
- praisonai_code/cli/main.py +7421 -0
- praisonai_code/cli/output/__init__.py +25 -0
- praisonai_code/cli/output/console.py +456 -0
- praisonai_code/cli/output/event_bridge.py +191 -0
- praisonai_code/cli/schedule_cli.py +54 -0
- praisonai_code/cli/schema_provider.py +23 -0
- praisonai_code/cli/session/__init__.py +16 -0
- praisonai_code/cli/session/resume.py +148 -0
- praisonai_code/cli/session/unified.py +548 -0
- praisonai_code/cli/state/__init__.py +31 -0
- praisonai_code/cli/state/identifiers.py +161 -0
- praisonai_code/cli/state/project_sessions.py +383 -0
- praisonai_code/cli/state/sessions.py +390 -0
- praisonai_code/cli/ui/__init__.py +160 -0
- praisonai_code/cli/ui/config.py +46 -0
- praisonai_code/cli/ui/events.py +61 -0
- praisonai_code/cli/ui/mg_backend.py +342 -0
- praisonai_code/cli/ui/plain.py +133 -0
- praisonai_code/cli/ui/rich_backend.py +162 -0
- praisonai_code/cli/unified_schema.py +655 -0
- praisonai_code/cli/utils/env_utils.py +126 -0
- praisonai_code/cli/utils/project.py +131 -0
- praisonai_code/cli_backends/__init__.py +73 -0
- praisonai_code/cli_backends/claude.py +373 -0
- praisonai_code/cli_backends/registry.py +113 -0
- praisonai_code/runtime/__init__.py +36 -0
- praisonai_code/runtime/__main__.py +81 -0
- praisonai_code/runtime/client.py +131 -0
- praisonai_code/runtime/descriptor.py +209 -0
- praisonai_code/runtime/server.py +356 -0
- praisonai_code-0.0.1.dist-info/METADATA +80 -0
- praisonai_code-0.0.1.dist-info/RECORD +309 -0
- praisonai_code-0.0.1.dist-info/WHEEL +5 -0
- praisonai_code-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Warm local runtime server.
|
|
3
|
+
|
|
4
|
+
A long-lived loopback HTTP process that keeps provider clients (and, through the
|
|
5
|
+
agent, any configured MCP connections) warm across ``praisonai run`` invocations.
|
|
6
|
+
It holds one warm :class:`~praisonaiagents.Agent` per model and reuses it, so the
|
|
7
|
+
second and subsequent runs skip per-invocation cold-start.
|
|
8
|
+
|
|
9
|
+
Built only on the standard library (``http.server``) to keep the dependency
|
|
10
|
+
surface and import cost minimal. Local-first security: binds loopback, requires a
|
|
11
|
+
per-process bearer token, and auto-shuts down after an idle timeout.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import secrets
|
|
19
|
+
import threading
|
|
20
|
+
import time
|
|
21
|
+
import queue
|
|
22
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
23
|
+
from typing import Any, Dict, List, Optional
|
|
24
|
+
from urllib.parse import unquote, urlsplit
|
|
25
|
+
|
|
26
|
+
from .descriptor import RuntimeDescriptor, get_runtime_version
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SessionEventHub:
|
|
30
|
+
"""Fan out per-session events to all attached clients.
|
|
31
|
+
|
|
32
|
+
Each ``/run`` carries an optional ``session_id``. While a prompt executes,
|
|
33
|
+
the runtime publishes structured events (start, result, error) tagged with
|
|
34
|
+
that session id; any number of ``attach`` clients subscribed to the same id
|
|
35
|
+
receive them in real time via Server-Sent Events. This is the building block
|
|
36
|
+
that lets a second terminal observe a live session.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self) -> None:
|
|
40
|
+
self._lock = threading.Lock()
|
|
41
|
+
# session_id -> list of subscriber queues
|
|
42
|
+
self._subscribers: Dict[str, List["queue.Queue[Optional[Dict[str, Any]]]"]] = {}
|
|
43
|
+
|
|
44
|
+
def subscribe(self, session_id: str) -> "queue.Queue[Optional[Dict[str, Any]]]":
|
|
45
|
+
"""Register a subscriber for ``session_id`` and return its queue."""
|
|
46
|
+
q: "queue.Queue[Optional[Dict[str, Any]]]" = queue.Queue()
|
|
47
|
+
with self._lock:
|
|
48
|
+
self._subscribers.setdefault(session_id, []).append(q)
|
|
49
|
+
return q
|
|
50
|
+
|
|
51
|
+
def unsubscribe(self, session_id: str, q: "queue.Queue[Optional[Dict[str, Any]]]") -> None:
|
|
52
|
+
"""Remove a subscriber queue for ``session_id`` (idempotent)."""
|
|
53
|
+
with self._lock:
|
|
54
|
+
subs = self._subscribers.get(session_id)
|
|
55
|
+
if not subs:
|
|
56
|
+
return
|
|
57
|
+
try:
|
|
58
|
+
subs.remove(q)
|
|
59
|
+
except ValueError:
|
|
60
|
+
pass
|
|
61
|
+
if not subs:
|
|
62
|
+
self._subscribers.pop(session_id, None)
|
|
63
|
+
|
|
64
|
+
def publish(self, session_id: str, event: Dict[str, Any]) -> None:
|
|
65
|
+
"""Publish ``event`` to every subscriber of ``session_id``."""
|
|
66
|
+
if not session_id:
|
|
67
|
+
return
|
|
68
|
+
with self._lock:
|
|
69
|
+
subs = list(self._subscribers.get(session_id, ()))
|
|
70
|
+
for q in subs:
|
|
71
|
+
q.put(event)
|
|
72
|
+
|
|
73
|
+
def has_subscribers(self, session_id: str) -> bool:
|
|
74
|
+
with self._lock:
|
|
75
|
+
return bool(self._subscribers.get(session_id))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class WarmRuntime:
|
|
79
|
+
"""Holds warm agent state and executes prompts for the runtime server."""
|
|
80
|
+
|
|
81
|
+
def __init__(self, model: Optional[str] = None, hub: Optional[SessionEventHub] = None):
|
|
82
|
+
self._default_model = model
|
|
83
|
+
self._agents: Dict[str, Any] = {}
|
|
84
|
+
self._lock = threading.Lock()
|
|
85
|
+
self._agent_locks: Dict[str, threading.Lock] = {}
|
|
86
|
+
self.last_activity = time.time()
|
|
87
|
+
# Event hub used to fan out live session events to attached clients.
|
|
88
|
+
self.hub = hub or SessionEventHub()
|
|
89
|
+
|
|
90
|
+
def _agent_key(self, model: Optional[str]) -> str:
|
|
91
|
+
return model or self._default_model or "__default__"
|
|
92
|
+
|
|
93
|
+
def _get_agent(self, key: str):
|
|
94
|
+
"""Return a warm Agent for ``key``, creating it once and reusing it."""
|
|
95
|
+
with self._lock:
|
|
96
|
+
agent = self._agents.get(key)
|
|
97
|
+
if agent is None:
|
|
98
|
+
from praisonaiagents import Agent
|
|
99
|
+
|
|
100
|
+
config: Dict[str, Any] = {
|
|
101
|
+
"name": "RuntimeAgent",
|
|
102
|
+
"role": "Assistant",
|
|
103
|
+
"goal": "Complete the task",
|
|
104
|
+
}
|
|
105
|
+
resolved = None if key == "__default__" else key
|
|
106
|
+
resolved = resolved or self._default_model
|
|
107
|
+
if resolved:
|
|
108
|
+
config["llm"] = resolved
|
|
109
|
+
agent = Agent(**config)
|
|
110
|
+
self._agents[key] = agent
|
|
111
|
+
return agent
|
|
112
|
+
|
|
113
|
+
def _lock_for(self, key: str) -> threading.Lock:
|
|
114
|
+
"""Return a per-agent lock so concurrent /run calls serialize per model."""
|
|
115
|
+
with self._lock:
|
|
116
|
+
lock = self._agent_locks.get(key)
|
|
117
|
+
if lock is None:
|
|
118
|
+
lock = threading.Lock()
|
|
119
|
+
self._agent_locks[key] = lock
|
|
120
|
+
return lock
|
|
121
|
+
|
|
122
|
+
def _evict_agent(self, key: str) -> None:
|
|
123
|
+
"""Drop the cached agent for ``key`` so the next call gets a fresh one."""
|
|
124
|
+
with self._lock:
|
|
125
|
+
self._agents.pop(key, None)
|
|
126
|
+
|
|
127
|
+
def run(
|
|
128
|
+
self,
|
|
129
|
+
prompt: str,
|
|
130
|
+
model: Optional[str] = None,
|
|
131
|
+
session_id: Optional[str] = None,
|
|
132
|
+
) -> str:
|
|
133
|
+
"""Execute a prompt against the warm agent and return the result text.
|
|
134
|
+
|
|
135
|
+
Access to each cached Agent is serialized via a per-model lock because
|
|
136
|
+
the ThreadingHTTPServer dispatches requests on parallel threads and a
|
|
137
|
+
single ``Agent`` instance is not safe for concurrent ``start`` calls.
|
|
138
|
+
|
|
139
|
+
If ``agent.start`` raises (LLM error, timeout, etc.) the agent may be
|
|
140
|
+
left with partial conversation state (e.g. an unmatched user turn), so
|
|
141
|
+
it is evicted from the cache and the next call rebuilds a clean agent.
|
|
142
|
+
|
|
143
|
+
When ``session_id`` is given, live events (start/result/error) are
|
|
144
|
+
published to the hub so attached clients can observe the session in real
|
|
145
|
+
time.
|
|
146
|
+
"""
|
|
147
|
+
self.last_activity = time.time()
|
|
148
|
+
key = self._agent_key(model)
|
|
149
|
+
if session_id:
|
|
150
|
+
self.hub.publish(session_id, {
|
|
151
|
+
"type": "run.start",
|
|
152
|
+
"session_id": session_id,
|
|
153
|
+
"prompt": prompt,
|
|
154
|
+
})
|
|
155
|
+
with self._lock_for(key):
|
|
156
|
+
agent = self._get_agent(key)
|
|
157
|
+
try:
|
|
158
|
+
result = agent.start(prompt)
|
|
159
|
+
except Exception as e:
|
|
160
|
+
self._evict_agent(key)
|
|
161
|
+
if session_id:
|
|
162
|
+
self.hub.publish(session_id, {
|
|
163
|
+
"type": "run.error",
|
|
164
|
+
"session_id": session_id,
|
|
165
|
+
"error": str(e),
|
|
166
|
+
})
|
|
167
|
+
raise
|
|
168
|
+
# Each /run call must be isolated: clear in-memory history so prior
|
|
169
|
+
# prompts cannot leak into the next invocation (the warm agent is
|
|
170
|
+
# reused only for client/MCP cold-start savings, not conversation state).
|
|
171
|
+
if hasattr(agent, "_replace_chat_history"):
|
|
172
|
+
agent._replace_chat_history([])
|
|
173
|
+
elif hasattr(agent, "chat_history"):
|
|
174
|
+
agent.chat_history = []
|
|
175
|
+
self.last_activity = time.time()
|
|
176
|
+
text = str(result) if result is not None else ""
|
|
177
|
+
if session_id:
|
|
178
|
+
self.hub.publish(session_id, {
|
|
179
|
+
"type": "run.result",
|
|
180
|
+
"session_id": session_id,
|
|
181
|
+
"ok": True,
|
|
182
|
+
"result": text,
|
|
183
|
+
})
|
|
184
|
+
return text
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _make_handler(token: str, runtime: WarmRuntime):
|
|
188
|
+
"""Build a request handler bound to the runtime's token and state."""
|
|
189
|
+
|
|
190
|
+
class _Handler(BaseHTTPRequestHandler):
|
|
191
|
+
# Silence default stderr request logging to keep the daemon quiet.
|
|
192
|
+
def log_message(self, *args: Any) -> None: # noqa: D401
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
def _authorized(self) -> bool:
|
|
196
|
+
header = self.headers.get("Authorization", "")
|
|
197
|
+
expected = f"Bearer {token}"
|
|
198
|
+
return secrets.compare_digest(header, expected)
|
|
199
|
+
|
|
200
|
+
def _send_json(self, status: int, payload: Dict[str, Any]) -> None:
|
|
201
|
+
body = json.dumps(payload).encode("utf-8")
|
|
202
|
+
self.send_response(status)
|
|
203
|
+
self.send_header("Content-Type", "application/json")
|
|
204
|
+
self.send_header("Content-Length", str(len(body)))
|
|
205
|
+
self.end_headers()
|
|
206
|
+
self.wfile.write(body)
|
|
207
|
+
|
|
208
|
+
def _read_json(self) -> Dict[str, Any]:
|
|
209
|
+
length = int(self.headers.get("Content-Length", 0) or 0)
|
|
210
|
+
if length <= 0:
|
|
211
|
+
return {}
|
|
212
|
+
raw = self.rfile.read(length)
|
|
213
|
+
try:
|
|
214
|
+
return json.loads(raw.decode("utf-8")) if raw else {}
|
|
215
|
+
except ValueError:
|
|
216
|
+
return {}
|
|
217
|
+
|
|
218
|
+
def do_POST(self) -> None: # noqa: N802
|
|
219
|
+
if not self._authorized():
|
|
220
|
+
self._send_json(401, {"ok": False, "error": "unauthorized"})
|
|
221
|
+
return
|
|
222
|
+
|
|
223
|
+
if self.path == "/healthz":
|
|
224
|
+
# Advertise the runtime version so clients can run the
|
|
225
|
+
# version-compat handshake over the wire as well as via the
|
|
226
|
+
# lockfile descriptor.
|
|
227
|
+
self._send_json(200, {"ok": True, "version": get_runtime_version()})
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
if self.path == "/run":
|
|
231
|
+
payload = self._read_json()
|
|
232
|
+
prompt = payload.get("prompt")
|
|
233
|
+
if not prompt:
|
|
234
|
+
self._send_json(400, {"ok": False, "error": "missing prompt"})
|
|
235
|
+
return
|
|
236
|
+
try:
|
|
237
|
+
result = runtime.run(
|
|
238
|
+
prompt,
|
|
239
|
+
model=payload.get("model"),
|
|
240
|
+
session_id=payload.get("session_id"),
|
|
241
|
+
)
|
|
242
|
+
self._send_json(200, {"ok": True, "result": result})
|
|
243
|
+
except Exception as e: # noqa: BLE001 - surface to client as error
|
|
244
|
+
self._send_json(200, {"ok": False, "error": str(e)})
|
|
245
|
+
return
|
|
246
|
+
|
|
247
|
+
self._send_json(404, {"ok": False, "error": "not found"})
|
|
248
|
+
|
|
249
|
+
def do_GET(self) -> None: # noqa: N802
|
|
250
|
+
if not self._authorized():
|
|
251
|
+
self._send_json(401, {"ok": False, "error": "unauthorized"})
|
|
252
|
+
return
|
|
253
|
+
|
|
254
|
+
# Live session event stream: /sessions/{id}/events (Server-Sent
|
|
255
|
+
# Events). An attached client opens this and receives each event the
|
|
256
|
+
# runtime publishes for that session until it disconnects.
|
|
257
|
+
# Strip any query string before matching the path, then percent-
|
|
258
|
+
# decode the session id so it round-trips with the client's
|
|
259
|
+
# ``quote(session_id, safe="")`` encoding.
|
|
260
|
+
path = urlsplit(self.path).path
|
|
261
|
+
prefix, suffix = "/sessions/", "/events"
|
|
262
|
+
if path.startswith(prefix) and path.endswith(suffix):
|
|
263
|
+
session_id = unquote(path[len(prefix):-len(suffix)])
|
|
264
|
+
if not session_id:
|
|
265
|
+
self._send_json(400, {"ok": False, "error": "missing session id"})
|
|
266
|
+
return
|
|
267
|
+
self._stream_session_events(session_id)
|
|
268
|
+
return
|
|
269
|
+
|
|
270
|
+
self._send_json(404, {"ok": False, "error": "not found"})
|
|
271
|
+
|
|
272
|
+
def _stream_session_events(self, session_id: str) -> None:
|
|
273
|
+
"""Stream session events as SSE until the client disconnects."""
|
|
274
|
+
q = runtime.hub.subscribe(session_id)
|
|
275
|
+
try:
|
|
276
|
+
self.send_response(200)
|
|
277
|
+
self.send_header("Content-Type", "text/event-stream")
|
|
278
|
+
self.send_header("Cache-Control", "no-cache")
|
|
279
|
+
self.send_header("Connection", "keep-alive")
|
|
280
|
+
self.end_headers()
|
|
281
|
+
# Initial comment so the client knows the stream is live.
|
|
282
|
+
self.wfile.write(b": connected\n\n")
|
|
283
|
+
self.wfile.flush()
|
|
284
|
+
while True:
|
|
285
|
+
try:
|
|
286
|
+
event = q.get(timeout=15.0)
|
|
287
|
+
except queue.Empty:
|
|
288
|
+
# Heartbeat keeps idle connections (and proxies) alive.
|
|
289
|
+
self.wfile.write(b": keep-alive\n\n")
|
|
290
|
+
self.wfile.flush()
|
|
291
|
+
continue
|
|
292
|
+
if event is None:
|
|
293
|
+
break
|
|
294
|
+
line = "data: " + json.dumps(event) + "\n\n"
|
|
295
|
+
self.wfile.write(line.encode("utf-8"))
|
|
296
|
+
self.wfile.flush()
|
|
297
|
+
except (BrokenPipeError, ConnectionResetError, OSError):
|
|
298
|
+
# Client disconnected; fall through to cleanup.
|
|
299
|
+
pass
|
|
300
|
+
finally:
|
|
301
|
+
runtime.hub.unsubscribe(session_id, q)
|
|
302
|
+
|
|
303
|
+
return _Handler
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def serve_runtime(
|
|
307
|
+
host: str = "127.0.0.1",
|
|
308
|
+
port: int = 0,
|
|
309
|
+
model: Optional[str] = None,
|
|
310
|
+
idle_timeout: float = 1800.0,
|
|
311
|
+
project_path: Optional[str] = None,
|
|
312
|
+
) -> None:
|
|
313
|
+
"""Start the warm runtime, write the lockfile, and serve until stopped.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
host: Loopback host to bind (defaults to ``127.0.0.1``).
|
|
317
|
+
port: Port to bind; ``0`` auto-selects a free port.
|
|
318
|
+
model: Default model for warm agents.
|
|
319
|
+
idle_timeout: Seconds of inactivity before auto-shutdown (``0`` disables).
|
|
320
|
+
project_path: Project root for the lockfile location.
|
|
321
|
+
"""
|
|
322
|
+
runtime = WarmRuntime(model=model)
|
|
323
|
+
token = secrets.token_urlsafe(32)
|
|
324
|
+
handler = _make_handler(token, runtime)
|
|
325
|
+
|
|
326
|
+
httpd = ThreadingHTTPServer((host, port), handler)
|
|
327
|
+
bound_host, bound_port = httpd.server_address[0], httpd.server_address[1]
|
|
328
|
+
|
|
329
|
+
descriptor = RuntimeDescriptor(
|
|
330
|
+
host=str(bound_host),
|
|
331
|
+
port=int(bound_port),
|
|
332
|
+
token=token,
|
|
333
|
+
pid=os.getpid(),
|
|
334
|
+
version=get_runtime_version(),
|
|
335
|
+
)
|
|
336
|
+
descriptor.write(project_path)
|
|
337
|
+
|
|
338
|
+
# Background idle-shutdown watcher: stop the server when inactive too long.
|
|
339
|
+
stop_event = threading.Event()
|
|
340
|
+
|
|
341
|
+
def _idle_watch() -> None:
|
|
342
|
+
if idle_timeout and idle_timeout > 0:
|
|
343
|
+
while not stop_event.wait(timeout=min(idle_timeout, 30.0)):
|
|
344
|
+
if time.time() - runtime.last_activity > idle_timeout:
|
|
345
|
+
httpd.shutdown()
|
|
346
|
+
return
|
|
347
|
+
|
|
348
|
+
watcher = threading.Thread(target=_idle_watch, daemon=True)
|
|
349
|
+
watcher.start()
|
|
350
|
+
|
|
351
|
+
try:
|
|
352
|
+
httpd.serve_forever()
|
|
353
|
+
finally:
|
|
354
|
+
stop_event.set()
|
|
355
|
+
httpd.server_close()
|
|
356
|
+
RuntimeDescriptor.remove(project_path)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: praisonai-code
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Agentic terminal CLI for PraisonAI — the terminal-native agent (run, chat, code, runtime, cli backends) extracted from the praisonai wrapper.
|
|
5
|
+
Author: Mervin Praison
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://docs.praison.ai
|
|
8
|
+
Project-URL: Repository, https://github.com/mervinpraison/PraisonAI
|
|
9
|
+
Requires-Python: <3.15,>=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: praisonaiagents>=1.6.101
|
|
12
|
+
Requires-Dist: rich>=13.7
|
|
13
|
+
Requires-Dist: typer>=0.12.0
|
|
14
|
+
Requires-Dist: click>=8.4.2
|
|
15
|
+
Requires-Dist: textual>=0.47.0
|
|
16
|
+
Requires-Dist: PyYAML>=6.0
|
|
17
|
+
Requires-Dist: python-dotenv>=0.19.0
|
|
18
|
+
Requires-Dist: litellm<2,>=1.83.14
|
|
19
|
+
Requires-Dist: mcp>=1.20.0
|
|
20
|
+
Requires-Dist: pydantic>=2.0.0
|
|
21
|
+
|
|
22
|
+
# praisonai-code
|
|
23
|
+
|
|
24
|
+
Agentic terminal CLI for PraisonAI — the terminal-native agent product
|
|
25
|
+
(`run`, `chat`, `code`, warm runtime, CLI backends) extracted from the
|
|
26
|
+
`praisonai` wrapper.
|
|
27
|
+
|
|
28
|
+
**Analogues:** opencode, codex, gemini-cli.
|
|
29
|
+
|
|
30
|
+
## Status
|
|
31
|
+
|
|
32
|
+
Migration in progress. `runtime/` and `cli_backends/` have moved into
|
|
33
|
+
`praisonai_code` (step C1); the remaining terminal-agent modules follow
|
|
34
|
+
incrementally in steps C2–C6 (see issue #2512):
|
|
35
|
+
|
|
36
|
+
| Step | Scope |
|
|
37
|
+
|------|-------|
|
|
38
|
+
| C0 | Scaffold |
|
|
39
|
+
| C1 | `runtime/` + `cli_backends/` |
|
|
40
|
+
| C2 | `interactive/`, `execution/`, `ui/`, `output/`, `state/` |
|
|
41
|
+
| C3 | Agentic commands |
|
|
42
|
+
| C4 | Agentic features |
|
|
43
|
+
| C5 | `main.py`, `app.py`, config/session/utils + shims |
|
|
44
|
+
| C6 | Integration gate |
|
|
45
|
+
|
|
46
|
+
- `praisonai_code.runtime` — warm local runtime (daemon + thin client).
|
|
47
|
+
- `praisonai_code.cli_backends` — CLI backend implementations (e.g. Claude Code).
|
|
48
|
+
|
|
49
|
+
## Dependency rules
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
praisonai (main) → depends on praisonai-code
|
|
53
|
+
praisonai-code → depends on praisonaiagents (core SDK)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`praisonai-code` also pulls in its own third-party runtime deps (rich, typer,
|
|
57
|
+
click, textual, PyYAML, python-dotenv, litellm, mcp, pydantic — see
|
|
58
|
+
`pyproject.toml`). The rules above govern the **inter-package** direction.
|
|
59
|
+
|
|
60
|
+
> **Migration note (C1):** `cli_backends/registry.py` still imports
|
|
61
|
+
> `PluginRegistry` from the `praisonai` main package (same "keep main-package
|
|
62
|
+
> import" pattern as `runtime/descriptor.py`'s `from praisonai.version`).
|
|
63
|
+
> `praisonai-code` is wired via an editable path dependency and `praisonai` is
|
|
64
|
+
> always present at runtime during migration, so this residual back-import is a
|
|
65
|
+
> deliberate C1 tradeoff to be removed in a later step — not a standalone-publish
|
|
66
|
+
> guarantee yet.
|
|
67
|
+
|
|
68
|
+
Backward compatibility is preserved via PEP 562 shims at the old
|
|
69
|
+
`praisonai.*` import paths, so `pip install praisonai` and
|
|
70
|
+
`from praisonai.cli.main import PraisonAI` keep working unchanged.
|
|
71
|
+
|
|
72
|
+
## Install (development)
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip install -e src/praisonai-code
|
|
76
|
+
python -c "import praisonai_code; print(praisonai_code.__version__)"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`praisonai-code` is **not** published standalone to PyPI during migration;
|
|
80
|
+
`praisonai` remains the user-facing install.
|