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.
Files changed (309) hide show
  1. praisonai_code/__init__.py +17 -0
  2. praisonai_code/cli/__init__.py +12 -0
  3. praisonai_code/cli/_forward_shim.py +10 -0
  4. praisonai_code/cli/_paths.py +88 -0
  5. praisonai_code/cli/_warnings.py +58 -0
  6. praisonai_code/cli/app.py +757 -0
  7. praisonai_code/cli/approval_backend.py +272 -0
  8. praisonai_code/cli/branding.py +94 -0
  9. praisonai_code/cli/commands/__init__.py +114 -0
  10. praisonai_code/cli/commands/acp.py +80 -0
  11. praisonai_code/cli/commands/agent.py +116 -0
  12. praisonai_code/cli/commands/agents.py +80 -0
  13. praisonai_code/cli/commands/app.py +139 -0
  14. praisonai_code/cli/commands/attach.py +95 -0
  15. praisonai_code/cli/commands/audit.py +102 -0
  16. praisonai_code/cli/commands/auth.py +508 -0
  17. praisonai_code/cli/commands/batch.py +848 -0
  18. praisonai_code/cli/commands/benchmark.py +286 -0
  19. praisonai_code/cli/commands/browser.py +299 -0
  20. praisonai_code/cli/commands/call.py +45 -0
  21. praisonai_code/cli/commands/chat.py +332 -0
  22. praisonai_code/cli/commands/checkpoint.py +170 -0
  23. praisonai_code/cli/commands/code.py +276 -0
  24. praisonai_code/cli/commands/command.py +114 -0
  25. praisonai_code/cli/commands/commit.py +47 -0
  26. praisonai_code/cli/commands/completion.py +333 -0
  27. praisonai_code/cli/commands/config.py +681 -0
  28. praisonai_code/cli/commands/context.py +414 -0
  29. praisonai_code/cli/commands/daemon.py +203 -0
  30. praisonai_code/cli/commands/debug.py +142 -0
  31. praisonai_code/cli/commands/deploy.py +71 -0
  32. praisonai_code/cli/commands/diag.py +55 -0
  33. praisonai_code/cli/commands/docs.py +1575 -0
  34. praisonai_code/cli/commands/doctor.py +332 -0
  35. praisonai_code/cli/commands/endpoints.py +51 -0
  36. praisonai_code/cli/commands/environment.py +179 -0
  37. praisonai_code/cli/commands/eval.py +131 -0
  38. praisonai_code/cli/commands/examples.py +953 -0
  39. praisonai_code/cli/commands/flow.py +436 -0
  40. praisonai_code/cli/commands/github.py +752 -0
  41. praisonai_code/cli/commands/hooks.py +74 -0
  42. praisonai_code/cli/commands/init.py +174 -0
  43. praisonai_code/cli/commands/knowledge.py +440 -0
  44. praisonai_code/cli/commands/langextract.py +120 -0
  45. praisonai_code/cli/commands/langfuse.py +984 -0
  46. praisonai_code/cli/commands/loop.py +211 -0
  47. praisonai_code/cli/commands/lsp.py +112 -0
  48. praisonai_code/cli/commands/managed.py +659 -0
  49. praisonai_code/cli/commands/mcp.py +763 -0
  50. praisonai_code/cli/commands/memory.py +298 -0
  51. praisonai_code/cli/commands/models.py +264 -0
  52. praisonai_code/cli/commands/n8n.py +326 -0
  53. praisonai_code/cli/commands/obs.py +19 -0
  54. praisonai_code/cli/commands/package.py +76 -0
  55. praisonai_code/cli/commands/paths.py +106 -0
  56. praisonai_code/cli/commands/permissions.py +272 -0
  57. praisonai_code/cli/commands/plugins.py +609 -0
  58. praisonai_code/cli/commands/port.py +530 -0
  59. praisonai_code/cli/commands/profile.py +466 -0
  60. praisonai_code/cli/commands/publish.py +193 -0
  61. praisonai_code/cli/commands/rag.py +913 -0
  62. praisonai_code/cli/commands/realtime.py +52 -0
  63. praisonai_code/cli/commands/recipe.py +684 -0
  64. praisonai_code/cli/commands/registry.py +59 -0
  65. praisonai_code/cli/commands/replay.py +830 -0
  66. praisonai_code/cli/commands/research.py +49 -0
  67. praisonai_code/cli/commands/retrieval.py +377 -0
  68. praisonai_code/cli/commands/rules.py +71 -0
  69. praisonai_code/cli/commands/run.py +1573 -0
  70. praisonai_code/cli/commands/sandbox.py +371 -0
  71. praisonai_code/cli/commands/schedule.py +529 -0
  72. praisonai_code/cli/commands/serve.py +690 -0
  73. praisonai_code/cli/commands/session.py +450 -0
  74. praisonai_code/cli/commands/setup.py +174 -0
  75. praisonai_code/cli/commands/skills.py +545 -0
  76. praisonai_code/cli/commands/standardise.py +711 -0
  77. praisonai_code/cli/commands/templates.py +54 -0
  78. praisonai_code/cli/commands/test.py +558 -0
  79. praisonai_code/cli/commands/todo.py +74 -0
  80. praisonai_code/cli/commands/tools.py +205 -0
  81. praisonai_code/cli/commands/traces.py +145 -0
  82. praisonai_code/cli/commands/tracker.py +852 -0
  83. praisonai_code/cli/commands/train.py +613 -0
  84. praisonai_code/cli/commands/ui.py +172 -0
  85. praisonai_code/cli/commands/up.py +354 -0
  86. praisonai_code/cli/commands/validate.py +291 -0
  87. praisonai_code/cli/commands/version.py +101 -0
  88. praisonai_code/cli/commands/workflow.py +97 -0
  89. praisonai_code/cli/config_loader.py +437 -0
  90. praisonai_code/cli/configuration/__init__.py +27 -0
  91. praisonai_code/cli/configuration/config.schema.json +57 -0
  92. praisonai_code/cli/configuration/credentials.py +446 -0
  93. praisonai_code/cli/configuration/loader.py +364 -0
  94. praisonai_code/cli/configuration/model_resolver.py +161 -0
  95. praisonai_code/cli/configuration/oauth.py +389 -0
  96. praisonai_code/cli/configuration/paths.py +224 -0
  97. praisonai_code/cli/configuration/resolver.py +687 -0
  98. praisonai_code/cli/configuration/schema.py +317 -0
  99. praisonai_code/cli/execution/__init__.py +99 -0
  100. praisonai_code/cli/execution/core.py +208 -0
  101. praisonai_code/cli/execution/profiler.py +898 -0
  102. praisonai_code/cli/execution/request.py +85 -0
  103. praisonai_code/cli/execution/result.py +74 -0
  104. praisonai_code/cli/fallback_schema.py +416 -0
  105. praisonai_code/cli/features/__init__.py +278 -0
  106. praisonai_code/cli/features/_endpoint_registry.py +64 -0
  107. praisonai_code/cli/features/_search_registry.py +43 -0
  108. praisonai_code/cli/features/acp.py +236 -0
  109. praisonai_code/cli/features/action_orchestrator.py +576 -0
  110. praisonai_code/cli/features/agent_scheduler.py +773 -0
  111. praisonai_code/cli/features/agent_tools.py +603 -0
  112. praisonai_code/cli/features/agents.py +397 -0
  113. praisonai_code/cli/features/at_mentions.py +471 -0
  114. praisonai_code/cli/features/audit_cli.py +270 -0
  115. praisonai_code/cli/features/auto_memory.py +182 -0
  116. praisonai_code/cli/features/auto_mode.py +552 -0
  117. praisonai_code/cli/features/autonomy_mode.py +546 -0
  118. praisonai_code/cli/features/background.py +356 -0
  119. praisonai_code/cli/features/base.py +168 -0
  120. praisonai_code/cli/features/benchmark.py +1462 -0
  121. praisonai_code/cli/features/capabilities.py +1326 -0
  122. praisonai_code/cli/features/checkpoints.py +345 -0
  123. praisonai_code/cli/features/cli_profiler.py +335 -0
  124. praisonai_code/cli/features/code_intelligence.py +666 -0
  125. praisonai_code/cli/features/compaction.py +294 -0
  126. praisonai_code/cli/features/compare.py +534 -0
  127. praisonai_code/cli/features/config_hierarchy.py +366 -0
  128. praisonai_code/cli/features/context_manager.py +597 -0
  129. praisonai_code/cli/features/cost_tracker.py +514 -0
  130. praisonai_code/cli/features/csv_test_runner.py +736 -0
  131. praisonai_code/cli/features/custom_definitions.py +790 -0
  132. praisonai_code/cli/features/debug.py +810 -0
  133. praisonai_code/cli/features/deploy.py +605 -0
  134. praisonai_code/cli/features/diag.py +289 -0
  135. praisonai_code/cli/features/display_jsonl.py +173 -0
  136. praisonai_code/cli/features/doctor/__init__.py +63 -0
  137. praisonai_code/cli/features/doctor/checks/__init__.py +29 -0
  138. praisonai_code/cli/features/doctor/checks/acp_checks.py +220 -0
  139. praisonai_code/cli/features/doctor/checks/bot_checks.py +340 -0
  140. praisonai_code/cli/features/doctor/checks/config_checks.py +373 -0
  141. praisonai_code/cli/features/doctor/checks/db_checks.py +366 -0
  142. praisonai_code/cli/features/doctor/checks/env_checks.py +637 -0
  143. praisonai_code/cli/features/doctor/checks/gateway_checks.py +387 -0
  144. praisonai_code/cli/features/doctor/checks/lsp_checks.py +231 -0
  145. praisonai_code/cli/features/doctor/checks/mcp_checks.py +367 -0
  146. praisonai_code/cli/features/doctor/checks/memory_checks.py +268 -0
  147. praisonai_code/cli/features/doctor/checks/network_checks.py +251 -0
  148. praisonai_code/cli/features/doctor/checks/obs_checks.py +328 -0
  149. praisonai_code/cli/features/doctor/checks/packaging_checks.py +422 -0
  150. praisonai_code/cli/features/doctor/checks/performance_checks.py +235 -0
  151. praisonai_code/cli/features/doctor/checks/permissions_checks.py +259 -0
  152. praisonai_code/cli/features/doctor/checks/runtime_checks.py +650 -0
  153. praisonai_code/cli/features/doctor/checks/runtime_migration_checks.py +220 -0
  154. praisonai_code/cli/features/doctor/checks/selftest_checks.py +322 -0
  155. praisonai_code/cli/features/doctor/checks/serve_checks.py +426 -0
  156. praisonai_code/cli/features/doctor/checks/skills_checks.py +327 -0
  157. praisonai_code/cli/features/doctor/checks/tools_checks.py +371 -0
  158. praisonai_code/cli/features/doctor/engine.py +266 -0
  159. praisonai_code/cli/features/doctor/formatters.py +377 -0
  160. praisonai_code/cli/features/doctor/handler.py +564 -0
  161. praisonai_code/cli/features/doctor/models.py +276 -0
  162. praisonai_code/cli/features/doctor/registry.py +239 -0
  163. praisonai_code/cli/features/endpoints.py +1016 -0
  164. praisonai_code/cli/features/eval.py +559 -0
  165. praisonai_code/cli/features/examples.py +707 -0
  166. praisonai_code/cli/features/external_agents.py +231 -0
  167. praisonai_code/cli/features/fast_context.py +410 -0
  168. praisonai_code/cli/features/file_history.py +320 -0
  169. praisonai_code/cli/features/flow_display.py +566 -0
  170. praisonai_code/cli/features/git_attribution.py +159 -0
  171. praisonai_code/cli/features/git_integration.py +651 -0
  172. praisonai_code/cli/features/guardrail.py +171 -0
  173. praisonai_code/cli/features/handoff.py +252 -0
  174. praisonai_code/cli/features/hooks.py +583 -0
  175. praisonai_code/cli/features/hybrid_workflow.py +391 -0
  176. praisonai_code/cli/features/image.py +384 -0
  177. praisonai_code/cli/features/interactive_core_headless.py +450 -0
  178. praisonai_code/cli/features/interactive_runtime.py +600 -0
  179. praisonai_code/cli/features/interactive_test_harness.py +537 -0
  180. praisonai_code/cli/features/interactive_tools.py +428 -0
  181. praisonai_code/cli/features/interactive_tui.py +603 -0
  182. praisonai_code/cli/features/job_workflow.py +906 -0
  183. praisonai_code/cli/features/jobs.py +632 -0
  184. praisonai_code/cli/features/knowledge.py +531 -0
  185. praisonai_code/cli/features/knowledge_cli.py +438 -0
  186. praisonai_code/cli/features/lite.py +244 -0
  187. praisonai_code/cli/features/logs.py +200 -0
  188. praisonai_code/cli/features/lsp_cli.py +225 -0
  189. praisonai_code/cli/features/lsp_diagnostics.py +185 -0
  190. praisonai_code/cli/features/mcp.py +344 -0
  191. praisonai_code/cli/features/message_queue.py +587 -0
  192. praisonai_code/cli/features/metrics.py +210 -0
  193. praisonai_code/cli/features/migrate.py +1329 -0
  194. praisonai_code/cli/features/migration_flow.py +463 -0
  195. praisonai_code/cli/features/migration_spec.py +276 -0
  196. praisonai_code/cli/features/n8n.py +703 -0
  197. praisonai_code/cli/features/observability.py +293 -0
  198. praisonai_code/cli/features/ollama.py +361 -0
  199. praisonai_code/cli/features/output_modes.py +155 -0
  200. praisonai_code/cli/features/output_style.py +273 -0
  201. praisonai_code/cli/features/package.py +631 -0
  202. praisonai_code/cli/features/performance.py +308 -0
  203. praisonai_code/cli/features/persistence.py +636 -0
  204. praisonai_code/cli/features/profiler/__init__.py +81 -0
  205. praisonai_code/cli/features/profiler/core.py +558 -0
  206. praisonai_code/cli/features/profiler/optimizations.py +652 -0
  207. praisonai_code/cli/features/profiler/suite.py +386 -0
  208. praisonai_code/cli/features/queue/__init__.py +73 -0
  209. praisonai_code/cli/features/queue/manager.py +435 -0
  210. praisonai_code/cli/features/queue/models.py +289 -0
  211. praisonai_code/cli/features/queue/persistence.py +564 -0
  212. praisonai_code/cli/features/queue/scheduler.py +529 -0
  213. praisonai_code/cli/features/queue/worker.py +400 -0
  214. praisonai_code/cli/features/recipe.py +2187 -0
  215. praisonai_code/cli/features/recipe_creator.py +996 -0
  216. praisonai_code/cli/features/recipe_optimizer.py +1364 -0
  217. praisonai_code/cli/features/recipe_prompts.py +226 -0
  218. praisonai_code/cli/features/registry.py +229 -0
  219. praisonai_code/cli/features/repo_map.py +860 -0
  220. praisonai_code/cli/features/router.py +466 -0
  221. praisonai_code/cli/features/safe_shell.py +427 -0
  222. praisonai_code/cli/features/sandbox_cli.py +283 -0
  223. praisonai_code/cli/features/sandbox_executor.py +536 -0
  224. praisonai_code/cli/features/sdk_knowledge.py +500 -0
  225. praisonai_code/cli/features/session.py +222 -0
  226. praisonai_code/cli/features/session_checkpoints.py +208 -0
  227. praisonai_code/cli/features/setup/__init__.py +9 -0
  228. praisonai_code/cli/features/setup/handler.py +355 -0
  229. praisonai_code/cli/features/setup/templates.py +62 -0
  230. praisonai_code/cli/features/skills.py +940 -0
  231. praisonai_code/cli/features/slash_commands.py +692 -0
  232. praisonai_code/cli/features/telemetry.py +179 -0
  233. praisonai_code/cli/features/templates.py +1390 -0
  234. praisonai_code/cli/features/thinking.py +343 -0
  235. praisonai_code/cli/features/todo.py +334 -0
  236. praisonai_code/cli/features/tools.py +680 -0
  237. praisonai_code/cli/features/tui/__init__.py +83 -0
  238. praisonai_code/cli/features/tui/app.py +871 -0
  239. praisonai_code/cli/features/tui/cli.py +580 -0
  240. praisonai_code/cli/features/tui/config.py +150 -0
  241. praisonai_code/cli/features/tui/debug.py +526 -0
  242. praisonai_code/cli/features/tui/events.py +99 -0
  243. praisonai_code/cli/features/tui/mock_provider.py +328 -0
  244. praisonai_code/cli/features/tui/orchestrator.py +652 -0
  245. praisonai_code/cli/features/tui/screens/__init__.py +50 -0
  246. praisonai_code/cli/features/tui/screens/help.py +157 -0
  247. praisonai_code/cli/features/tui/screens/main.py +568 -0
  248. praisonai_code/cli/features/tui/screens/queue.py +174 -0
  249. praisonai_code/cli/features/tui/screens/session.py +124 -0
  250. praisonai_code/cli/features/tui/screens/settings.py +148 -0
  251. praisonai_code/cli/features/tui/session_store.py +198 -0
  252. praisonai_code/cli/features/tui/widgets/__init__.py +56 -0
  253. praisonai_code/cli/features/tui/widgets/chat.py +263 -0
  254. praisonai_code/cli/features/tui/widgets/command_popup.py +258 -0
  255. praisonai_code/cli/features/tui/widgets/composer.py +292 -0
  256. praisonai_code/cli/features/tui/widgets/file_popup.py +207 -0
  257. praisonai_code/cli/features/tui/widgets/queue_panel.py +223 -0
  258. praisonai_code/cli/features/tui/widgets/status.py +181 -0
  259. praisonai_code/cli/features/tui/widgets/tool_panel.py +307 -0
  260. praisonai_code/cli/features/wizard.py +289 -0
  261. praisonai_code/cli/features/workflow.py +802 -0
  262. praisonai_code/cli/features/yaml_utils.py +321 -0
  263. praisonai_code/cli/interactive/__init__.py +48 -0
  264. praisonai_code/cli/interactive/async_tui.py +1218 -0
  265. praisonai_code/cli/interactive/config.py +139 -0
  266. praisonai_code/cli/interactive/core.py +618 -0
  267. praisonai_code/cli/interactive/events.py +131 -0
  268. praisonai_code/cli/interactive/frontends/__init__.py +31 -0
  269. praisonai_code/cli/interactive/frontends/rich_frontend.py +462 -0
  270. praisonai_code/cli/interactive/frontends/textual_frontend.py +157 -0
  271. praisonai_code/cli/interactive/praison_io.py +502 -0
  272. praisonai_code/cli/interactive/repl.py +297 -0
  273. praisonai_code/cli/interactive/split_tui.py +456 -0
  274. praisonai_code/cli/interactive/tui_app.py +457 -0
  275. praisonai_code/cli/langfuse_client.py +360 -0
  276. praisonai_code/cli/main.py +7421 -0
  277. praisonai_code/cli/output/__init__.py +25 -0
  278. praisonai_code/cli/output/console.py +456 -0
  279. praisonai_code/cli/output/event_bridge.py +191 -0
  280. praisonai_code/cli/schedule_cli.py +54 -0
  281. praisonai_code/cli/schema_provider.py +23 -0
  282. praisonai_code/cli/session/__init__.py +16 -0
  283. praisonai_code/cli/session/resume.py +148 -0
  284. praisonai_code/cli/session/unified.py +548 -0
  285. praisonai_code/cli/state/__init__.py +31 -0
  286. praisonai_code/cli/state/identifiers.py +161 -0
  287. praisonai_code/cli/state/project_sessions.py +383 -0
  288. praisonai_code/cli/state/sessions.py +390 -0
  289. praisonai_code/cli/ui/__init__.py +160 -0
  290. praisonai_code/cli/ui/config.py +46 -0
  291. praisonai_code/cli/ui/events.py +61 -0
  292. praisonai_code/cli/ui/mg_backend.py +342 -0
  293. praisonai_code/cli/ui/plain.py +133 -0
  294. praisonai_code/cli/ui/rich_backend.py +162 -0
  295. praisonai_code/cli/unified_schema.py +655 -0
  296. praisonai_code/cli/utils/env_utils.py +126 -0
  297. praisonai_code/cli/utils/project.py +131 -0
  298. praisonai_code/cli_backends/__init__.py +73 -0
  299. praisonai_code/cli_backends/claude.py +373 -0
  300. praisonai_code/cli_backends/registry.py +113 -0
  301. praisonai_code/runtime/__init__.py +36 -0
  302. praisonai_code/runtime/__main__.py +81 -0
  303. praisonai_code/runtime/client.py +131 -0
  304. praisonai_code/runtime/descriptor.py +209 -0
  305. praisonai_code/runtime/server.py +356 -0
  306. praisonai_code-0.0.1.dist-info/METADATA +80 -0
  307. praisonai_code-0.0.1.dist-info/RECORD +309 -0
  308. praisonai_code-0.0.1.dist-info/WHEEL +5 -0
  309. 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.