lohra 0.0.1__tar.gz

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 (229) hide show
  1. lohra-0.0.1/LICENSE +21 -0
  2. lohra-0.0.1/PKG-INFO +86 -0
  3. lohra-0.0.1/README.md +47 -0
  4. lohra-0.0.1/lohra/__init__.py +7 -0
  5. lohra-0.0.1/lohra/agent/__init__.py +18 -0
  6. lohra-0.0.1/lohra/agent/agent.py +123 -0
  7. lohra-0.0.1/lohra/agent/aux.py +55 -0
  8. lohra-0.0.1/lohra/agent/client.py +341 -0
  9. lohra-0.0.1/lohra/agent/client_pool.py +114 -0
  10. lohra-0.0.1/lohra/agent/context.py +95 -0
  11. lohra-0.0.1/lohra/agent/delegate.py +321 -0
  12. lohra-0.0.1/lohra/agent/equip.py +144 -0
  13. lohra-0.0.1/lohra/agent/loop.py +330 -0
  14. lohra-0.0.1/lohra/agent/overrides.py +44 -0
  15. lohra-0.0.1/lohra/agent/result_json.py +117 -0
  16. lohra-0.0.1/lohra/agent/system_prompt.py +99 -0
  17. lohra-0.0.1/lohra/agent/taint.py +52 -0
  18. lohra-0.0.1/lohra/agent/types.py +69 -0
  19. lohra-0.0.1/lohra/cli.py +995 -0
  20. lohra-0.0.1/lohra/config/__init__.py +1 -0
  21. lohra-0.0.1/lohra/config/env_file.py +62 -0
  22. lohra-0.0.1/lohra/cron/__init__.py +6 -0
  23. lohra-0.0.1/lohra/cron/runner.py +48 -0
  24. lohra-0.0.1/lohra/cron/schedule.py +82 -0
  25. lohra-0.0.1/lohra/cron/scheduler.py +70 -0
  26. lohra-0.0.1/lohra/cron/store.py +126 -0
  27. lohra-0.0.1/lohra/cron/tool.py +101 -0
  28. lohra-0.0.1/lohra/gateway/__init__.py +4 -0
  29. lohra-0.0.1/lohra/gateway/app.py +170 -0
  30. lohra-0.0.1/lohra/gateway/events.py +46 -0
  31. lohra-0.0.1/lohra/gateway/manager.py +135 -0
  32. lohra-0.0.1/lohra/gateway/rpc.py +67 -0
  33. lohra-0.0.1/lohra/gateway/session.py +178 -0
  34. lohra-0.0.1/lohra/imagegen/__init__.py +7 -0
  35. lohra-0.0.1/lohra/imagegen/storage.py +35 -0
  36. lohra-0.0.1/lohra/imagegen/tool.py +96 -0
  37. lohra-0.0.1/lohra/mcp/__init__.py +66 -0
  38. lohra-0.0.1/lohra/mcp/config.py +71 -0
  39. lohra-0.0.1/lohra/mcp/manager.py +67 -0
  40. lohra-0.0.1/lohra/mcp/session.py +163 -0
  41. lohra-0.0.1/lohra/mcp/tools.py +122 -0
  42. lohra-0.0.1/lohra/memory/__init__.py +4 -0
  43. lohra-0.0.1/lohra/memory/paths.py +101 -0
  44. lohra-0.0.1/lohra/memory/soul.py +23 -0
  45. lohra-0.0.1/lohra/memory/store.py +141 -0
  46. lohra-0.0.1/lohra/memory/tool.py +90 -0
  47. lohra-0.0.1/lohra/orchestration/__init__.py +8 -0
  48. lohra-0.0.1/lohra/orchestration/core.py +335 -0
  49. lohra-0.0.1/lohra/orchestration/tools.py +142 -0
  50. lohra-0.0.1/lohra/project/__init__.py +7 -0
  51. lohra-0.0.1/lohra/project/discover.py +112 -0
  52. lohra-0.0.1/lohra/providers/__init__.py +25 -0
  53. lohra-0.0.1/lohra/providers/base.py +90 -0
  54. lohra-0.0.1/lohra/providers/builtin.py +131 -0
  55. lohra-0.0.1/lohra/providers/errors.py +115 -0
  56. lohra-0.0.1/lohra/providers/resolve.py +59 -0
  57. lohra-0.0.1/lohra/providers/transports/__init__.py +22 -0
  58. lohra-0.0.1/lohra/providers/transports/anthropic_messages.py +237 -0
  59. lohra-0.0.1/lohra/providers/transports/base.py +78 -0
  60. lohra-0.0.1/lohra/providers/transports/chat_completions.py +158 -0
  61. lohra-0.0.1/lohra/providers/transports/responses.py +226 -0
  62. lohra-0.0.1/lohra/safeio.py +25 -0
  63. lohra-0.0.1/lohra/selfupdate/__init__.py +8 -0
  64. lohra-0.0.1/lohra/selfupdate/repo.py +104 -0
  65. lohra-0.0.1/lohra/selfupdate/service.py +137 -0
  66. lohra-0.0.1/lohra/server/__init__.py +5 -0
  67. lohra-0.0.1/lohra/server/agentic.py +46 -0
  68. lohra-0.0.1/lohra/server/app.py +306 -0
  69. lohra-0.0.1/lohra/server/format.py +93 -0
  70. lohra-0.0.1/lohra/server/responses.py +196 -0
  71. lohra-0.0.1/lohra/server/service.py +78 -0
  72. lohra-0.0.1/lohra/skills/__init__.py +8 -0
  73. lohra-0.0.1/lohra/skills/builtin/workflow-authoring/SKILL.md +579 -0
  74. lohra-0.0.1/lohra/skills/store.py +290 -0
  75. lohra-0.0.1/lohra/skills/tool.py +106 -0
  76. lohra-0.0.1/lohra/state/__init__.py +8 -0
  77. lohra-0.0.1/lohra/state/compression_lock.py +45 -0
  78. lohra-0.0.1/lohra/state/db.py +582 -0
  79. lohra-0.0.1/lohra/state/search.py +66 -0
  80. lohra-0.0.1/lohra/subscription/__init__.py +9 -0
  81. lohra-0.0.1/lohra/subscription/codex_creds.py +81 -0
  82. lohra-0.0.1/lohra/subscription/constants.py +25 -0
  83. lohra-0.0.1/lohra/subscription/credentials.py +117 -0
  84. lohra-0.0.1/lohra/subscription/manage.py +61 -0
  85. lohra-0.0.1/lohra/subscription/oauth.py +174 -0
  86. lohra-0.0.1/lohra/subscription/provider.py +51 -0
  87. lohra-0.0.1/lohra/subscription/refresh.py +99 -0
  88. lohra-0.0.1/lohra/subscription/store.py +77 -0
  89. lohra-0.0.1/lohra/subscription/token_store.py +90 -0
  90. lohra-0.0.1/lohra/tools/__init__.py +19 -0
  91. lohra-0.0.1/lohra/tools/approval.py +120 -0
  92. lohra-0.0.1/lohra/tools/fs.py +86 -0
  93. lohra-0.0.1/lohra/tools/intercept.py +28 -0
  94. lohra-0.0.1/lohra/tools/registry.py +159 -0
  95. lohra-0.0.1/lohra/tools/terminal.py +67 -0
  96. lohra-0.0.1/lohra/vision/__init__.py +7 -0
  97. lohra-0.0.1/lohra/vision/content.py +45 -0
  98. lohra-0.0.1/lohra/vision/tool.py +84 -0
  99. lohra-0.0.1/lohra/web/__init__.py +12 -0
  100. lohra-0.0.1/lohra/web/extract.py +50 -0
  101. lohra-0.0.1/lohra/web/fetch.py +83 -0
  102. lohra-0.0.1/lohra/web/safety.py +64 -0
  103. lohra-0.0.1/lohra/web/search.py +122 -0
  104. lohra-0.0.1/lohra/web/tool.py +105 -0
  105. lohra-0.0.1/lohra/workflow/__init__.py +11 -0
  106. lohra-0.0.1/lohra/workflow/autoresume.py +132 -0
  107. lohra-0.0.1/lohra/workflow/budget.py +176 -0
  108. lohra-0.0.1/lohra/workflow/cache.py +75 -0
  109. lohra-0.0.1/lohra/workflow/engine.py +747 -0
  110. lohra-0.0.1/lohra/workflow/gates.py +204 -0
  111. lohra-0.0.1/lohra/workflow/jsonio.py +55 -0
  112. lohra-0.0.1/lohra/workflow/lease_heartbeat.py +122 -0
  113. lohra-0.0.1/lohra/workflow/library.py +194 -0
  114. lohra-0.0.1/lohra/workflow/nodes.py +193 -0
  115. lohra-0.0.1/lohra/workflow/notify.py +48 -0
  116. lohra-0.0.1/lohra/workflow/progress.py +103 -0
  117. lohra-0.0.1/lohra/workflow/prompts.py +51 -0
  118. lohra-0.0.1/lohra/workflow/refs.py +120 -0
  119. lohra-0.0.1/lohra/workflow/rollup.py +87 -0
  120. lohra-0.0.1/lohra/workflow/runstate_store.py +481 -0
  121. lohra-0.0.1/lohra/workflow/sandbox.py +186 -0
  122. lohra-0.0.1/lohra/workflow/schema.py +407 -0
  123. lohra-0.0.1/lohra/workflow/service.py +796 -0
  124. lohra-0.0.1/lohra/workflow/spend.py +89 -0
  125. lohra-0.0.1/lohra/workflow/strategies.py +762 -0
  126. lohra-0.0.1/lohra/workflow/tiers.py +103 -0
  127. lohra-0.0.1/lohra/workflow/tools.py +323 -0
  128. lohra-0.0.1/lohra/workflow/validation.py +84 -0
  129. lohra-0.0.1/lohra.egg-info/PKG-INFO +86 -0
  130. lohra-0.0.1/lohra.egg-info/SOURCES.txt +227 -0
  131. lohra-0.0.1/lohra.egg-info/dependency_links.txt +1 -0
  132. lohra-0.0.1/lohra.egg-info/entry_points.txt +2 -0
  133. lohra-0.0.1/lohra.egg-info/requires.txt +22 -0
  134. lohra-0.0.1/lohra.egg-info/top_level.txt +1 -0
  135. lohra-0.0.1/pyproject.toml +68 -0
  136. lohra-0.0.1/setup.cfg +4 -0
  137. lohra-0.0.1/tests/test_agent_memory.py +53 -0
  138. lohra-0.0.1/tests/test_approval.py +103 -0
  139. lohra-0.0.1/tests/test_aux_client.py +47 -0
  140. lohra-0.0.1/tests/test_cli.py +282 -0
  141. lohra-0.0.1/tests/test_client.py +264 -0
  142. lohra-0.0.1/tests/test_client_pool.py +112 -0
  143. lohra-0.0.1/tests/test_compression_lock.py +129 -0
  144. lohra-0.0.1/tests/test_context_compressor.py +91 -0
  145. lohra-0.0.1/tests/test_cron_runner.py +93 -0
  146. lohra-0.0.1/tests/test_cron_schedule.py +153 -0
  147. lohra-0.0.1/tests/test_cron_scheduler.py +76 -0
  148. lohra-0.0.1/tests/test_cron_store.py +92 -0
  149. lohra-0.0.1/tests/test_cron_tool.py +84 -0
  150. lohra-0.0.1/tests/test_delegate_task.py +318 -0
  151. lohra-0.0.1/tests/test_env_file.py +67 -0
  152. lohra-0.0.1/tests/test_gateway_app.py +151 -0
  153. lohra-0.0.1/tests/test_gateway_manager.py +184 -0
  154. lohra-0.0.1/tests/test_gateway_rpc.py +52 -0
  155. lohra-0.0.1/tests/test_gateway_session.py +224 -0
  156. lohra-0.0.1/tests/test_imagegen_storage.py +47 -0
  157. lohra-0.0.1/tests/test_imagegen_tool.py +98 -0
  158. lohra-0.0.1/tests/test_integration_tools.py +52 -0
  159. lohra-0.0.1/tests/test_loop.py +480 -0
  160. lohra-0.0.1/tests/test_loop_inbox.py +86 -0
  161. lohra-0.0.1/tests/test_mcp_config.py +78 -0
  162. lohra-0.0.1/tests/test_mcp_manager.py +253 -0
  163. lohra-0.0.1/tests/test_mcp_tools.py +196 -0
  164. lohra-0.0.1/tests/test_memory_store.py +112 -0
  165. lohra-0.0.1/tests/test_memory_tool.py +79 -0
  166. lohra-0.0.1/tests/test_multi_model.py +86 -0
  167. lohra-0.0.1/tests/test_orchestration_core.py +254 -0
  168. lohra-0.0.1/tests/test_orchestration_limits.py +49 -0
  169. lohra-0.0.1/tests/test_orchestration_tools.py +103 -0
  170. lohra-0.0.1/tests/test_paths_profiles.py +84 -0
  171. lohra-0.0.1/tests/test_project_discover.py +161 -0
  172. lohra-0.0.1/tests/test_project_integration.py +45 -0
  173. lohra-0.0.1/tests/test_project_skills.py +251 -0
  174. lohra-0.0.1/tests/test_providers.py +105 -0
  175. lohra-0.0.1/tests/test_responses_transport.py +252 -0
  176. lohra-0.0.1/tests/test_result_json.py +110 -0
  177. lohra-0.0.1/tests/test_self_improving_e2e.py +83 -0
  178. lohra-0.0.1/tests/test_selfupdate.py +294 -0
  179. lohra-0.0.1/tests/test_server_agentic.py +84 -0
  180. lohra-0.0.1/tests/test_server_app.py +251 -0
  181. lohra-0.0.1/tests/test_server_format.py +111 -0
  182. lohra-0.0.1/tests/test_server_responses.py +181 -0
  183. lohra-0.0.1/tests/test_server_service.py +174 -0
  184. lohra-0.0.1/tests/test_session_db.py +130 -0
  185. lohra-0.0.1/tests/test_session_search.py +97 -0
  186. lohra-0.0.1/tests/test_skills_store.py +115 -0
  187. lohra-0.0.1/tests/test_skills_tool.py +89 -0
  188. lohra-0.0.1/tests/test_smoke.py +94 -0
  189. lohra-0.0.1/tests/test_soul.py +54 -0
  190. lohra-0.0.1/tests/test_subscription.py +191 -0
  191. lohra-0.0.1/tests/test_subscription_oauth.py +201 -0
  192. lohra-0.0.1/tests/test_subscription_wiring.py +170 -0
  193. lohra-0.0.1/tests/test_system_prompt.py +112 -0
  194. lohra-0.0.1/tests/test_tools_fs.py +62 -0
  195. lohra-0.0.1/tests/test_tools_terminal.py +63 -0
  196. lohra-0.0.1/tests/test_transport_anthropic.py +422 -0
  197. lohra-0.0.1/tests/test_transport_chat_completions.py +252 -0
  198. lohra-0.0.1/tests/test_vision_content.py +56 -0
  199. lohra-0.0.1/tests/test_vision_tool.py +107 -0
  200. lohra-0.0.1/tests/test_web_extract.py +35 -0
  201. lohra-0.0.1/tests/test_web_fetch.py +100 -0
  202. lohra-0.0.1/tests/test_web_safety.py +68 -0
  203. lohra-0.0.1/tests/test_web_search.py +79 -0
  204. lohra-0.0.1/tests/test_web_tool.py +110 -0
  205. lohra-0.0.1/tests/test_workflow_authoring_skill.py +177 -0
  206. lohra-0.0.1/tests/test_workflow_authoring_surface.py +52 -0
  207. lohra-0.0.1/tests/test_workflow_cache.py +244 -0
  208. lohra-0.0.1/tests/test_workflow_dogfood_fixes.py +552 -0
  209. lohra-0.0.1/tests/test_workflow_durable_state.py +731 -0
  210. lohra-0.0.1/tests/test_workflow_engine.py +150 -0
  211. lohra-0.0.1/tests/test_workflow_failclosed.py +437 -0
  212. lohra-0.0.1/tests/test_workflow_forced_output.py +162 -0
  213. lohra-0.0.1/tests/test_workflow_library.py +102 -0
  214. lohra-0.0.1/tests/test_workflow_lifecycle.py +540 -0
  215. lohra-0.0.1/tests/test_workflow_m7_features.py +776 -0
  216. lohra-0.0.1/tests/test_workflow_m7_fixes.py +527 -0
  217. lohra-0.0.1/tests/test_workflow_nesting.py +145 -0
  218. lohra-0.0.1/tests/test_workflow_operability.py +650 -0
  219. lohra-0.0.1/tests/test_workflow_pipeline.py +173 -0
  220. lohra-0.0.1/tests/test_workflow_pipeline_hardening.py +228 -0
  221. lohra-0.0.1/tests/test_workflow_quota.py +559 -0
  222. lohra-0.0.1/tests/test_workflow_refs.py +63 -0
  223. lohra-0.0.1/tests/test_workflow_rigor.py +156 -0
  224. lohra-0.0.1/tests/test_workflow_sandbox.py +97 -0
  225. lohra-0.0.1/tests/test_workflow_schema.py +139 -0
  226. lohra-0.0.1/tests/test_workflow_taint.py +188 -0
  227. lohra-0.0.1/tests/test_workflow_token_budget.py +763 -0
  228. lohra-0.0.1/tests/test_workflow_tools.py +218 -0
  229. lohra-0.0.1/tests/test_workflow_validation.py +155 -0
lohra-0.0.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Marcelus Fernandes
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
lohra-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,86 @@
1
+ Metadata-Version: 2.4
2
+ Name: lohra
3
+ Version: 0.0.1
4
+ Summary: Lohra — self-improving AI agent runtime: CLI, orchestration envelope (--json), OpenAI-compatible server; optional desktop app
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/marcelusfernandes/lohra
7
+ Keywords: ai,agent,workflows,orchestration,self-improving,llm
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Software Development :: Libraries
17
+ Requires-Python: <3.14,>=3.11
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: openai>=1.0.0
21
+ Requires-Dist: anthropic>=0.40.0
22
+ Requires-Dist: fastapi>=0.110.0
23
+ Requires-Dist: uvicorn[standard]>=0.29.0
24
+ Requires-Dist: pyyaml>=6.0
25
+ Requires-Dist: httpx>=0.27.0
26
+ Requires-Dist: jsonschema>=4.0
27
+ Provides-Extra: anthropic
28
+ Requires-Dist: anthropic>=0.40.0; extra == "anthropic"
29
+ Provides-Extra: openai
30
+ Requires-Dist: openai>=1.40.0; extra == "openai"
31
+ Provides-Extra: mcp
32
+ Requires-Dist: mcp>=1.0.0; extra == "mcp"
33
+ Provides-Extra: dev
34
+ Requires-Dist: pytest>=8.0; extra == "dev"
35
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
36
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
37
+ Requires-Dist: ruff>=0.6; extra == "dev"
38
+ Dynamic: license-file
39
+
40
+ # Lohra
41
+
42
+ **A self-improving AI agent runtime** — persistent memory, self-authored skills, and a
43
+ declarative multi-agent workflow harness with Claude-Code-grade rigor. Runs headless:
44
+ CLI, structured orchestration envelope, and an OpenAI-compatible server. No UI required.
45
+
46
+ ```bash
47
+ pip install lohra # Python 3.11–3.13
48
+ lohra chat "hello"
49
+ ```
50
+
51
+ ## Four entry points, none of them a UI
52
+
53
+ | Port | Command | For |
54
+ |---|---|---|
55
+ | Human CLI | `lohra chat` | you, in a terminal |
56
+ | Orchestration envelope | `lohra chat --json` | other agents/scripts — one parseable JSON per turn (input/output/reasoning/tool_calls/usage) |
57
+ | OpenAI-compatible API | `lohra serve` | any OpenAI client becomes a Lohra client |
58
+ | WS/REST gateway | `lohra dashboard` | optional, only if a UI attaches |
59
+
60
+ ## What makes it interesting
61
+
62
+ - **Dynamic workflows as inert data**: the agent authors a typed DAG (10 node types —
63
+ `agent`, `parallel`, `pipeline`, `loop_until_dry`, `verify`, `judge_panel`, `gate`,
64
+ `completeness_check`, `checkpoint`, nested `workflow`) that an interpreter runs.
65
+ No agent-authored code is ever executed; escape is inexpressible, not forbidden.
66
+ - **Failure is never silent**: every failure path produces a fault with its cause;
67
+ run status is honest (`complete | degraded | failed | cancelled | paused`).
68
+ - **Never pay twice**: content-addressed per-cell cache — a resumed run replays
69
+ completed work at zero token cost, across process restarts.
70
+ - **Human in the loop**: `checkpoint` nodes pause a run until a person answers —
71
+ in another terminal, another process, another day. Durable state + single-winner leases.
72
+ - **Cost control**: token budgets with soft pre-spawn gates, quota pauses with
73
+ auto-resume, per-node model/effort/provider routing, operator-owned model tiers.
74
+ - **Self-improving**: persistent memory, self-authored skills, and a workflow library
75
+ that turns clean runs into reusable templates and bad runs into recorded priors.
76
+ - **Leaf sandbox**: filesystem allowlist (ro/rw), egress allowlist, and taint tracking —
77
+ operator policy, never the spec.
78
+
79
+ ## Configuration
80
+
81
+ State lives in `~/.lohra` (or per-workspace via `--profile`):
82
+ `.env` (API keys) · `workflow_policy.json` (leaf fs/egress) · `workflow_tiers.json`
83
+ (model tiers). Providers out of the box: Anthropic, OpenAI, OpenRouter, DeepSeek, Groq, Together,
84
+ Gemini, Ollama — plus an opt-in subscription mode (see the ToS warning in `lohra auth`).
85
+
86
+ MIT license. Alpha software — built and validated live, but young.
lohra-0.0.1/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # Lohra
2
+
3
+ **A self-improving AI agent runtime** — persistent memory, self-authored skills, and a
4
+ declarative multi-agent workflow harness with Claude-Code-grade rigor. Runs headless:
5
+ CLI, structured orchestration envelope, and an OpenAI-compatible server. No UI required.
6
+
7
+ ```bash
8
+ pip install lohra # Python 3.11–3.13
9
+ lohra chat "hello"
10
+ ```
11
+
12
+ ## Four entry points, none of them a UI
13
+
14
+ | Port | Command | For |
15
+ |---|---|---|
16
+ | Human CLI | `lohra chat` | you, in a terminal |
17
+ | Orchestration envelope | `lohra chat --json` | other agents/scripts — one parseable JSON per turn (input/output/reasoning/tool_calls/usage) |
18
+ | OpenAI-compatible API | `lohra serve` | any OpenAI client becomes a Lohra client |
19
+ | WS/REST gateway | `lohra dashboard` | optional, only if a UI attaches |
20
+
21
+ ## What makes it interesting
22
+
23
+ - **Dynamic workflows as inert data**: the agent authors a typed DAG (10 node types —
24
+ `agent`, `parallel`, `pipeline`, `loop_until_dry`, `verify`, `judge_panel`, `gate`,
25
+ `completeness_check`, `checkpoint`, nested `workflow`) that an interpreter runs.
26
+ No agent-authored code is ever executed; escape is inexpressible, not forbidden.
27
+ - **Failure is never silent**: every failure path produces a fault with its cause;
28
+ run status is honest (`complete | degraded | failed | cancelled | paused`).
29
+ - **Never pay twice**: content-addressed per-cell cache — a resumed run replays
30
+ completed work at zero token cost, across process restarts.
31
+ - **Human in the loop**: `checkpoint` nodes pause a run until a person answers —
32
+ in another terminal, another process, another day. Durable state + single-winner leases.
33
+ - **Cost control**: token budgets with soft pre-spawn gates, quota pauses with
34
+ auto-resume, per-node model/effort/provider routing, operator-owned model tiers.
35
+ - **Self-improving**: persistent memory, self-authored skills, and a workflow library
36
+ that turns clean runs into reusable templates and bad runs into recorded priors.
37
+ - **Leaf sandbox**: filesystem allowlist (ro/rw), egress allowlist, and taint tracking —
38
+ operator policy, never the spec.
39
+
40
+ ## Configuration
41
+
42
+ State lives in `~/.lohra` (or per-workspace via `--profile`):
43
+ `.env` (API keys) · `workflow_policy.json` (leaf fs/egress) · `workflow_tiers.json`
44
+ (model tiers). Providers out of the box: Anthropic, OpenAI, OpenRouter, DeepSeek, Groq, Together,
45
+ Gemini, Ollama — plus an opt-in subscription mode (see the ToS warning in `lohra auth`).
46
+
47
+ MIT license. Alpha software — built and validated live, but young.
@@ -0,0 +1,7 @@
1
+ """Lohra — self-improving AI agent with a desktop app.
2
+
3
+ Backend package: agent core, provider abstraction, tool system, memory/skills,
4
+ and the FastAPI gateway. See docs/ARCHITECTURE.md for the full design.
5
+ """
6
+
7
+ __version__ = "0.0.1"
@@ -0,0 +1,18 @@
1
+ """Agent core: conversation loop, transports, prompt assembly.
2
+
3
+ See docs/specs/01-agent-core.md.
4
+ """
5
+
6
+ from lohra.agent.agent import Agent
7
+ from lohra.agent.client import AnthropicClient, ModelClient
8
+ from lohra.agent.loop import run_conversation
9
+ from lohra.agent.system_prompt import SystemPromptSnapshot, build_system_prompt
10
+
11
+ __all__ = [
12
+ "Agent",
13
+ "AnthropicClient",
14
+ "ModelClient",
15
+ "run_conversation",
16
+ "SystemPromptSnapshot",
17
+ "build_system_prompt",
18
+ ]
@@ -0,0 +1,123 @@
1
+ """Agent — the live session controller.
2
+
3
+ Unlike the canonical response types (frozen, immutable), the Agent is a
4
+ stateful session object: it caches the frozen system prompt (Invariante #1),
5
+ holds the interrupt flag, and resolves per-call configuration. The spec models
6
+ this same mutable state (``agent._cached_system_prompt``,
7
+ ``agent._interrupt_requested``).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass, field
13
+ from typing import Any, Callable, Mapping
14
+
15
+ from lohra.agent.aux import AuxClient
16
+ from lohra.agent.client import ModelClient
17
+ from lohra.agent.context import ContextEngine
18
+ from lohra.agent.system_prompt import SystemPromptSnapshot, build_system_prompt
19
+ from lohra.memory.store import MemoryStore
20
+ from lohra.providers.base import ProviderProfile
21
+ from lohra.skills.store import SkillStore
22
+ from lohra.providers.transports.base import Transport, get_transport
23
+
24
+ DEFAULT_MAX_ITERATIONS = 8
25
+
26
+ # A tool executor: (name, parsed args) -> JSON-string result envelope.
27
+ ToolDispatch = Callable[[str, dict[str, Any]], str]
28
+
29
+
30
+ @dataclass
31
+ class Agent:
32
+ """A conversation session bound to one provider/model/client."""
33
+
34
+ model: str
35
+ provider: ProviderProfile
36
+ client: ModelClient
37
+
38
+ # system-prompt inputs (frozen into the snapshot on first use)
39
+ identity: str | None = None # SOUL.md persona; None -> built-in identity
40
+ system_message: str | None = None
41
+ context_files: tuple[tuple[str, str], ...] = ()
42
+ environment_hints: Mapping[str, str] = field(default_factory=dict)
43
+ # memory + skills snapshots are captured once and frozen into the prompt
44
+ # (Invariante #1)
45
+ memory_store: MemoryStore | None = None
46
+ skill_store: SkillStore | None = None
47
+
48
+ # tools: definitions sent to the model + an executor for tool_use turns.
49
+ # Both unset = chat-only (Phase 1 behavior).
50
+ tool_definitions: tuple[dict, ...] = ()
51
+ tool_dispatch: ToolDispatch | None = None
52
+
53
+ # forced structured output (workflow §5.2): when set to a tool def, the turn
54
+ # sends ONLY that tool and forces tool_choice; the tool's arguments ARE the
55
+ # answer. None (default) -> byte-identical normal behavior. Set per-leaf via
56
+ # the core.spawn `configure` hook; never on a frozen prompt (rides in tools=).
57
+ forced_tool: dict | None = None
58
+
59
+ # loop configuration
60
+ max_iterations: int = DEFAULT_MAX_ITERATIONS
61
+ max_tokens: int | None = None
62
+ temperature: float | None = None
63
+ # reasoning effort (provider-specific; emitted only where supported — OpenAI/
64
+ # Responses). None → unchanged. Mutable per spawn like ``model``.
65
+ effort: str | None = None
66
+
67
+ # context compression (both set = preflight compaction enabled)
68
+ context_window: int = 200_000
69
+ context_engine: ContextEngine | None = None
70
+ aux_client: AuxClient | None = None
71
+
72
+ # mutable session state (never part of construction)
73
+ _cached_system_prompt: SystemPromptSnapshot | None = field(
74
+ default=None, init=False, repr=False
75
+ )
76
+ _interrupt_requested: bool = field(default=False, init=False, repr=False)
77
+
78
+ @property
79
+ def transport(self) -> Transport:
80
+ """The transport for this provider's api_mode; fail fast if unregistered."""
81
+ transport = get_transport(self.provider.api_mode)
82
+ if transport is None:
83
+ raise LookupError(
84
+ f"no transport registered for api_mode {self.provider.api_mode!r} "
85
+ f"(provider {self.provider.name!r})"
86
+ )
87
+ return transport
88
+
89
+ def system_prompt(self) -> SystemPromptSnapshot:
90
+ """Restore-or-build the frozen snapshot (built once, then reused).
91
+
92
+ The memory snapshot is captured here on first use and frozen into the
93
+ prompt; mid-session memory writes hit disk but never this prompt.
94
+ """
95
+ if self._cached_system_prompt is None:
96
+ memory_snapshot, user_profile = "", ""
97
+ if self.memory_store is not None:
98
+ snapshot = self.memory_store.snapshot()
99
+ memory_snapshot, user_profile = snapshot["memory"], snapshot["user"]
100
+ skills_index = self.skill_store.snapshot() if self.skill_store is not None else ""
101
+ self._cached_system_prompt = build_system_prompt(
102
+ identity=self.identity,
103
+ system_message=self.system_message,
104
+ context_files=self.context_files,
105
+ environment_hints=self.environment_hints,
106
+ memory_snapshot=memory_snapshot,
107
+ user_profile=user_profile,
108
+ skills_index=skills_index,
109
+ )
110
+ return self._cached_system_prompt
111
+
112
+ def resolve_max_tokens(self) -> int | None:
113
+ """Explicit override wins; otherwise the provider profile owns the default."""
114
+ if self.max_tokens is not None:
115
+ return self.max_tokens
116
+ return self.provider.get_max_tokens(self.model)
117
+
118
+ def request_interrupt(self) -> None:
119
+ """Signal the loop to stop at the next safe boundary."""
120
+ self._interrupt_requested = True
121
+
122
+ def clear_interrupt(self) -> None:
123
+ self._interrupt_requested = False
@@ -0,0 +1,55 @@
1
+ """Auxiliary client — routes side tasks to a cheap, fast model (spec §8).
2
+
3
+ Compaction summaries and session titles don't need the main model. AuxClient
4
+ wraps a ModelClient + transport at the provider's aux model (e.g. Haiku) and
5
+ exposes ``summarize`` / ``title``, plus ``summarizer()`` to hand the compressor
6
+ a plain Callable[[str], str].
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any, Callable
12
+
13
+ from lohra.agent.client import ModelClient
14
+ from lohra.providers.transports.base import Transport
15
+
16
+ SUMMARY_SYSTEM = (
17
+ "You are compacting a long conversation to fit the context window. Summarize "
18
+ "the transcript below into a concise but complete reference. Under these "
19
+ "headings, keep only what is still relevant: Active Task; Goal; Completed "
20
+ "Actions (results, not narration); Active State (files/vars/decisions in "
21
+ "play); Blocked/Open; Key Decisions (and why); Pending User Asks; Remaining "
22
+ "Work. Be factual and terse. Never invent details."
23
+ )
24
+
25
+ TITLE_SYSTEM = (
26
+ "Write a short (≤6 words) title for this conversation. Reply with the title "
27
+ "only — no quotes, no punctuation at the end."
28
+ )
29
+
30
+
31
+ class AuxClient:
32
+ def __init__(self, *, client: ModelClient, transport: Transport, model: str) -> None:
33
+ self._client = client
34
+ self._transport = transport
35
+ self._model = model
36
+
37
+ def complete(self, system: str, user: str, *, max_tokens: int = 1024) -> str:
38
+ kwargs: dict[str, Any] = self._transport.build_kwargs(
39
+ model=self._model,
40
+ messages=[{"role": "user", "content": user}],
41
+ system=system,
42
+ max_tokens=max_tokens,
43
+ )
44
+ response = self._transport.normalize_response(self._client.create(**kwargs))
45
+ return (response.content or "").strip()
46
+
47
+ def summarize(self, transcript: str) -> str:
48
+ return self.complete(SUMMARY_SYSTEM, transcript)
49
+
50
+ def title(self, transcript: str) -> str:
51
+ return self.complete(TITLE_SYSTEM, transcript, max_tokens=32)
52
+
53
+ def summarizer(self) -> Callable[[str], str]:
54
+ """A plain callable for ContextCompressor.compress(summarize=...)."""
55
+ return self.summarize