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.
- lohra-0.0.1/LICENSE +21 -0
- lohra-0.0.1/PKG-INFO +86 -0
- lohra-0.0.1/README.md +47 -0
- lohra-0.0.1/lohra/__init__.py +7 -0
- lohra-0.0.1/lohra/agent/__init__.py +18 -0
- lohra-0.0.1/lohra/agent/agent.py +123 -0
- lohra-0.0.1/lohra/agent/aux.py +55 -0
- lohra-0.0.1/lohra/agent/client.py +341 -0
- lohra-0.0.1/lohra/agent/client_pool.py +114 -0
- lohra-0.0.1/lohra/agent/context.py +95 -0
- lohra-0.0.1/lohra/agent/delegate.py +321 -0
- lohra-0.0.1/lohra/agent/equip.py +144 -0
- lohra-0.0.1/lohra/agent/loop.py +330 -0
- lohra-0.0.1/lohra/agent/overrides.py +44 -0
- lohra-0.0.1/lohra/agent/result_json.py +117 -0
- lohra-0.0.1/lohra/agent/system_prompt.py +99 -0
- lohra-0.0.1/lohra/agent/taint.py +52 -0
- lohra-0.0.1/lohra/agent/types.py +69 -0
- lohra-0.0.1/lohra/cli.py +995 -0
- lohra-0.0.1/lohra/config/__init__.py +1 -0
- lohra-0.0.1/lohra/config/env_file.py +62 -0
- lohra-0.0.1/lohra/cron/__init__.py +6 -0
- lohra-0.0.1/lohra/cron/runner.py +48 -0
- lohra-0.0.1/lohra/cron/schedule.py +82 -0
- lohra-0.0.1/lohra/cron/scheduler.py +70 -0
- lohra-0.0.1/lohra/cron/store.py +126 -0
- lohra-0.0.1/lohra/cron/tool.py +101 -0
- lohra-0.0.1/lohra/gateway/__init__.py +4 -0
- lohra-0.0.1/lohra/gateway/app.py +170 -0
- lohra-0.0.1/lohra/gateway/events.py +46 -0
- lohra-0.0.1/lohra/gateway/manager.py +135 -0
- lohra-0.0.1/lohra/gateway/rpc.py +67 -0
- lohra-0.0.1/lohra/gateway/session.py +178 -0
- lohra-0.0.1/lohra/imagegen/__init__.py +7 -0
- lohra-0.0.1/lohra/imagegen/storage.py +35 -0
- lohra-0.0.1/lohra/imagegen/tool.py +96 -0
- lohra-0.0.1/lohra/mcp/__init__.py +66 -0
- lohra-0.0.1/lohra/mcp/config.py +71 -0
- lohra-0.0.1/lohra/mcp/manager.py +67 -0
- lohra-0.0.1/lohra/mcp/session.py +163 -0
- lohra-0.0.1/lohra/mcp/tools.py +122 -0
- lohra-0.0.1/lohra/memory/__init__.py +4 -0
- lohra-0.0.1/lohra/memory/paths.py +101 -0
- lohra-0.0.1/lohra/memory/soul.py +23 -0
- lohra-0.0.1/lohra/memory/store.py +141 -0
- lohra-0.0.1/lohra/memory/tool.py +90 -0
- lohra-0.0.1/lohra/orchestration/__init__.py +8 -0
- lohra-0.0.1/lohra/orchestration/core.py +335 -0
- lohra-0.0.1/lohra/orchestration/tools.py +142 -0
- lohra-0.0.1/lohra/project/__init__.py +7 -0
- lohra-0.0.1/lohra/project/discover.py +112 -0
- lohra-0.0.1/lohra/providers/__init__.py +25 -0
- lohra-0.0.1/lohra/providers/base.py +90 -0
- lohra-0.0.1/lohra/providers/builtin.py +131 -0
- lohra-0.0.1/lohra/providers/errors.py +115 -0
- lohra-0.0.1/lohra/providers/resolve.py +59 -0
- lohra-0.0.1/lohra/providers/transports/__init__.py +22 -0
- lohra-0.0.1/lohra/providers/transports/anthropic_messages.py +237 -0
- lohra-0.0.1/lohra/providers/transports/base.py +78 -0
- lohra-0.0.1/lohra/providers/transports/chat_completions.py +158 -0
- lohra-0.0.1/lohra/providers/transports/responses.py +226 -0
- lohra-0.0.1/lohra/safeio.py +25 -0
- lohra-0.0.1/lohra/selfupdate/__init__.py +8 -0
- lohra-0.0.1/lohra/selfupdate/repo.py +104 -0
- lohra-0.0.1/lohra/selfupdate/service.py +137 -0
- lohra-0.0.1/lohra/server/__init__.py +5 -0
- lohra-0.0.1/lohra/server/agentic.py +46 -0
- lohra-0.0.1/lohra/server/app.py +306 -0
- lohra-0.0.1/lohra/server/format.py +93 -0
- lohra-0.0.1/lohra/server/responses.py +196 -0
- lohra-0.0.1/lohra/server/service.py +78 -0
- lohra-0.0.1/lohra/skills/__init__.py +8 -0
- lohra-0.0.1/lohra/skills/builtin/workflow-authoring/SKILL.md +579 -0
- lohra-0.0.1/lohra/skills/store.py +290 -0
- lohra-0.0.1/lohra/skills/tool.py +106 -0
- lohra-0.0.1/lohra/state/__init__.py +8 -0
- lohra-0.0.1/lohra/state/compression_lock.py +45 -0
- lohra-0.0.1/lohra/state/db.py +582 -0
- lohra-0.0.1/lohra/state/search.py +66 -0
- lohra-0.0.1/lohra/subscription/__init__.py +9 -0
- lohra-0.0.1/lohra/subscription/codex_creds.py +81 -0
- lohra-0.0.1/lohra/subscription/constants.py +25 -0
- lohra-0.0.1/lohra/subscription/credentials.py +117 -0
- lohra-0.0.1/lohra/subscription/manage.py +61 -0
- lohra-0.0.1/lohra/subscription/oauth.py +174 -0
- lohra-0.0.1/lohra/subscription/provider.py +51 -0
- lohra-0.0.1/lohra/subscription/refresh.py +99 -0
- lohra-0.0.1/lohra/subscription/store.py +77 -0
- lohra-0.0.1/lohra/subscription/token_store.py +90 -0
- lohra-0.0.1/lohra/tools/__init__.py +19 -0
- lohra-0.0.1/lohra/tools/approval.py +120 -0
- lohra-0.0.1/lohra/tools/fs.py +86 -0
- lohra-0.0.1/lohra/tools/intercept.py +28 -0
- lohra-0.0.1/lohra/tools/registry.py +159 -0
- lohra-0.0.1/lohra/tools/terminal.py +67 -0
- lohra-0.0.1/lohra/vision/__init__.py +7 -0
- lohra-0.0.1/lohra/vision/content.py +45 -0
- lohra-0.0.1/lohra/vision/tool.py +84 -0
- lohra-0.0.1/lohra/web/__init__.py +12 -0
- lohra-0.0.1/lohra/web/extract.py +50 -0
- lohra-0.0.1/lohra/web/fetch.py +83 -0
- lohra-0.0.1/lohra/web/safety.py +64 -0
- lohra-0.0.1/lohra/web/search.py +122 -0
- lohra-0.0.1/lohra/web/tool.py +105 -0
- lohra-0.0.1/lohra/workflow/__init__.py +11 -0
- lohra-0.0.1/lohra/workflow/autoresume.py +132 -0
- lohra-0.0.1/lohra/workflow/budget.py +176 -0
- lohra-0.0.1/lohra/workflow/cache.py +75 -0
- lohra-0.0.1/lohra/workflow/engine.py +747 -0
- lohra-0.0.1/lohra/workflow/gates.py +204 -0
- lohra-0.0.1/lohra/workflow/jsonio.py +55 -0
- lohra-0.0.1/lohra/workflow/lease_heartbeat.py +122 -0
- lohra-0.0.1/lohra/workflow/library.py +194 -0
- lohra-0.0.1/lohra/workflow/nodes.py +193 -0
- lohra-0.0.1/lohra/workflow/notify.py +48 -0
- lohra-0.0.1/lohra/workflow/progress.py +103 -0
- lohra-0.0.1/lohra/workflow/prompts.py +51 -0
- lohra-0.0.1/lohra/workflow/refs.py +120 -0
- lohra-0.0.1/lohra/workflow/rollup.py +87 -0
- lohra-0.0.1/lohra/workflow/runstate_store.py +481 -0
- lohra-0.0.1/lohra/workflow/sandbox.py +186 -0
- lohra-0.0.1/lohra/workflow/schema.py +407 -0
- lohra-0.0.1/lohra/workflow/service.py +796 -0
- lohra-0.0.1/lohra/workflow/spend.py +89 -0
- lohra-0.0.1/lohra/workflow/strategies.py +762 -0
- lohra-0.0.1/lohra/workflow/tiers.py +103 -0
- lohra-0.0.1/lohra/workflow/tools.py +323 -0
- lohra-0.0.1/lohra/workflow/validation.py +84 -0
- lohra-0.0.1/lohra.egg-info/PKG-INFO +86 -0
- lohra-0.0.1/lohra.egg-info/SOURCES.txt +227 -0
- lohra-0.0.1/lohra.egg-info/dependency_links.txt +1 -0
- lohra-0.0.1/lohra.egg-info/entry_points.txt +2 -0
- lohra-0.0.1/lohra.egg-info/requires.txt +22 -0
- lohra-0.0.1/lohra.egg-info/top_level.txt +1 -0
- lohra-0.0.1/pyproject.toml +68 -0
- lohra-0.0.1/setup.cfg +4 -0
- lohra-0.0.1/tests/test_agent_memory.py +53 -0
- lohra-0.0.1/tests/test_approval.py +103 -0
- lohra-0.0.1/tests/test_aux_client.py +47 -0
- lohra-0.0.1/tests/test_cli.py +282 -0
- lohra-0.0.1/tests/test_client.py +264 -0
- lohra-0.0.1/tests/test_client_pool.py +112 -0
- lohra-0.0.1/tests/test_compression_lock.py +129 -0
- lohra-0.0.1/tests/test_context_compressor.py +91 -0
- lohra-0.0.1/tests/test_cron_runner.py +93 -0
- lohra-0.0.1/tests/test_cron_schedule.py +153 -0
- lohra-0.0.1/tests/test_cron_scheduler.py +76 -0
- lohra-0.0.1/tests/test_cron_store.py +92 -0
- lohra-0.0.1/tests/test_cron_tool.py +84 -0
- lohra-0.0.1/tests/test_delegate_task.py +318 -0
- lohra-0.0.1/tests/test_env_file.py +67 -0
- lohra-0.0.1/tests/test_gateway_app.py +151 -0
- lohra-0.0.1/tests/test_gateway_manager.py +184 -0
- lohra-0.0.1/tests/test_gateway_rpc.py +52 -0
- lohra-0.0.1/tests/test_gateway_session.py +224 -0
- lohra-0.0.1/tests/test_imagegen_storage.py +47 -0
- lohra-0.0.1/tests/test_imagegen_tool.py +98 -0
- lohra-0.0.1/tests/test_integration_tools.py +52 -0
- lohra-0.0.1/tests/test_loop.py +480 -0
- lohra-0.0.1/tests/test_loop_inbox.py +86 -0
- lohra-0.0.1/tests/test_mcp_config.py +78 -0
- lohra-0.0.1/tests/test_mcp_manager.py +253 -0
- lohra-0.0.1/tests/test_mcp_tools.py +196 -0
- lohra-0.0.1/tests/test_memory_store.py +112 -0
- lohra-0.0.1/tests/test_memory_tool.py +79 -0
- lohra-0.0.1/tests/test_multi_model.py +86 -0
- lohra-0.0.1/tests/test_orchestration_core.py +254 -0
- lohra-0.0.1/tests/test_orchestration_limits.py +49 -0
- lohra-0.0.1/tests/test_orchestration_tools.py +103 -0
- lohra-0.0.1/tests/test_paths_profiles.py +84 -0
- lohra-0.0.1/tests/test_project_discover.py +161 -0
- lohra-0.0.1/tests/test_project_integration.py +45 -0
- lohra-0.0.1/tests/test_project_skills.py +251 -0
- lohra-0.0.1/tests/test_providers.py +105 -0
- lohra-0.0.1/tests/test_responses_transport.py +252 -0
- lohra-0.0.1/tests/test_result_json.py +110 -0
- lohra-0.0.1/tests/test_self_improving_e2e.py +83 -0
- lohra-0.0.1/tests/test_selfupdate.py +294 -0
- lohra-0.0.1/tests/test_server_agentic.py +84 -0
- lohra-0.0.1/tests/test_server_app.py +251 -0
- lohra-0.0.1/tests/test_server_format.py +111 -0
- lohra-0.0.1/tests/test_server_responses.py +181 -0
- lohra-0.0.1/tests/test_server_service.py +174 -0
- lohra-0.0.1/tests/test_session_db.py +130 -0
- lohra-0.0.1/tests/test_session_search.py +97 -0
- lohra-0.0.1/tests/test_skills_store.py +115 -0
- lohra-0.0.1/tests/test_skills_tool.py +89 -0
- lohra-0.0.1/tests/test_smoke.py +94 -0
- lohra-0.0.1/tests/test_soul.py +54 -0
- lohra-0.0.1/tests/test_subscription.py +191 -0
- lohra-0.0.1/tests/test_subscription_oauth.py +201 -0
- lohra-0.0.1/tests/test_subscription_wiring.py +170 -0
- lohra-0.0.1/tests/test_system_prompt.py +112 -0
- lohra-0.0.1/tests/test_tools_fs.py +62 -0
- lohra-0.0.1/tests/test_tools_terminal.py +63 -0
- lohra-0.0.1/tests/test_transport_anthropic.py +422 -0
- lohra-0.0.1/tests/test_transport_chat_completions.py +252 -0
- lohra-0.0.1/tests/test_vision_content.py +56 -0
- lohra-0.0.1/tests/test_vision_tool.py +107 -0
- lohra-0.0.1/tests/test_web_extract.py +35 -0
- lohra-0.0.1/tests/test_web_fetch.py +100 -0
- lohra-0.0.1/tests/test_web_safety.py +68 -0
- lohra-0.0.1/tests/test_web_search.py +79 -0
- lohra-0.0.1/tests/test_web_tool.py +110 -0
- lohra-0.0.1/tests/test_workflow_authoring_skill.py +177 -0
- lohra-0.0.1/tests/test_workflow_authoring_surface.py +52 -0
- lohra-0.0.1/tests/test_workflow_cache.py +244 -0
- lohra-0.0.1/tests/test_workflow_dogfood_fixes.py +552 -0
- lohra-0.0.1/tests/test_workflow_durable_state.py +731 -0
- lohra-0.0.1/tests/test_workflow_engine.py +150 -0
- lohra-0.0.1/tests/test_workflow_failclosed.py +437 -0
- lohra-0.0.1/tests/test_workflow_forced_output.py +162 -0
- lohra-0.0.1/tests/test_workflow_library.py +102 -0
- lohra-0.0.1/tests/test_workflow_lifecycle.py +540 -0
- lohra-0.0.1/tests/test_workflow_m7_features.py +776 -0
- lohra-0.0.1/tests/test_workflow_m7_fixes.py +527 -0
- lohra-0.0.1/tests/test_workflow_nesting.py +145 -0
- lohra-0.0.1/tests/test_workflow_operability.py +650 -0
- lohra-0.0.1/tests/test_workflow_pipeline.py +173 -0
- lohra-0.0.1/tests/test_workflow_pipeline_hardening.py +228 -0
- lohra-0.0.1/tests/test_workflow_quota.py +559 -0
- lohra-0.0.1/tests/test_workflow_refs.py +63 -0
- lohra-0.0.1/tests/test_workflow_rigor.py +156 -0
- lohra-0.0.1/tests/test_workflow_sandbox.py +97 -0
- lohra-0.0.1/tests/test_workflow_schema.py +139 -0
- lohra-0.0.1/tests/test_workflow_taint.py +188 -0
- lohra-0.0.1/tests/test_workflow_token_budget.py +763 -0
- lohra-0.0.1/tests/test_workflow_tools.py +218 -0
- 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,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
|