limbo-code 0.1.1__tar.gz → 0.1.9__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 (181) hide show
  1. {limbo_code-0.1.1 → limbo_code-0.1.9}/.github/workflows/test.yml +9 -3
  2. {limbo_code-0.1.1 → limbo_code-0.1.9}/.gitignore +1 -0
  3. limbo_code-0.1.9/AGENTS.md +94 -0
  4. limbo_code-0.1.9/CONTEXT.md +43 -0
  5. {limbo_code-0.1.1 → limbo_code-0.1.9}/PKG-INFO +99 -4
  6. {limbo_code-0.1.1 → limbo_code-0.1.9}/README.md +93 -2
  7. limbo_code-0.1.9/docs/assets/walkthrough/limbo-dark/1_idle.svg +165 -0
  8. limbo_code-0.1.9/docs/assets/walkthrough/limbo-dark/2_thinking_tool_running.svg +166 -0
  9. limbo_code-0.1.9/docs/assets/walkthrough/limbo-dark/3_tool_success.svg +167 -0
  10. limbo_code-0.1.9/docs/assets/walkthrough/limbo-dark/4_tool_error.svg +168 -0
  11. limbo_code-0.1.9/docs/assets/walkthrough/limbo-dark/5_llm_error.svg +166 -0
  12. limbo_code-0.1.9/docs/assets/walkthrough/limbo-dark/6_edit_diff.svg +168 -0
  13. limbo_code-0.1.9/docs/assets/walkthrough/limbo-light/1_idle.svg +166 -0
  14. limbo_code-0.1.9/docs/assets/walkthrough/limbo-light/2_thinking_tool_running.svg +167 -0
  15. limbo_code-0.1.9/docs/assets/walkthrough/limbo-light/3_tool_success.svg +168 -0
  16. limbo_code-0.1.9/docs/assets/walkthrough/limbo-light/4_tool_error.svg +169 -0
  17. limbo_code-0.1.9/docs/assets/walkthrough/limbo-light/5_llm_error.svg +167 -0
  18. limbo_code-0.1.9/docs/assets/walkthrough/limbo-light/6_edit_diff.svg +169 -0
  19. {limbo_code-0.1.1 → limbo_code-0.1.9}/docs/skills.md +14 -2
  20. {limbo_code-0.1.1 → limbo_code-0.1.9}/pyproject.toml +15 -3
  21. limbo_code-0.1.9/scripts/check_contrast.py +28 -0
  22. limbo_code-0.1.9/scripts/gen_banner.py +103 -0
  23. limbo_code-0.1.9/scripts/ui_walkthrough.py +130 -0
  24. limbo_code-0.1.9/src/limbo/__init__.py +8 -0
  25. limbo_code-0.1.9/src/limbo/agent.py +947 -0
  26. {limbo_code-0.1.1 → limbo_code-0.1.9}/src/limbo/app.py +12 -8
  27. limbo_code-0.1.9/src/limbo/attachments.py +65 -0
  28. limbo_code-0.1.9/src/limbo/compaction.py +173 -0
  29. {limbo_code-0.1.1 → limbo_code-0.1.9}/src/limbo/config.py +92 -2
  30. {limbo_code-0.1.1 → limbo_code-0.1.9}/src/limbo/history.py +0 -19
  31. {limbo_code-0.1.1 → limbo_code-0.1.9}/src/limbo/llm/anthropic_client.py +88 -53
  32. limbo_code-0.1.9/src/limbo/llm/catalog.py +396 -0
  33. {limbo_code-0.1.1 → limbo_code-0.1.9}/src/limbo/llm/client.py +6 -0
  34. {limbo_code-0.1.1 → limbo_code-0.1.9}/src/limbo/llm/factory.py +3 -0
  35. {limbo_code-0.1.1 → limbo_code-0.1.9}/src/limbo/llm/openai_client.py +87 -36
  36. limbo_code-0.1.9/src/limbo/llm/responses_client.py +389 -0
  37. {limbo_code-0.1.1 → limbo_code-0.1.9}/src/limbo/llm/retry.py +8 -3
  38. limbo_code-0.1.9/src/limbo/llm/scaffold.py +92 -0
  39. limbo_code-0.1.9/src/limbo/llm/sse.py +31 -0
  40. limbo_code-0.1.9/src/limbo/llm/usage.py +160 -0
  41. limbo_code-0.1.9/src/limbo/model_switch.py +103 -0
  42. {limbo_code-0.1.1 → limbo_code-0.1.9}/src/limbo/models.py +36 -2
  43. limbo_code-0.1.9/src/limbo/prompt.py +103 -0
  44. {limbo_code-0.1.1 → limbo_code-0.1.9}/src/limbo/sessions.py +96 -6
  45. limbo_code-0.1.9/src/limbo/skills.py +190 -0
  46. limbo_code-0.1.9/src/limbo/steer.py +76 -0
  47. limbo_code-0.1.9/src/limbo/tools/base.py +294 -0
  48. {limbo_code-0.1.1 → limbo_code-0.1.9}/src/limbo/tools/bash.py +39 -7
  49. {limbo_code-0.1.1 → limbo_code-0.1.9}/src/limbo/tools/edit.py +23 -19
  50. {limbo_code-0.1.1 → limbo_code-0.1.9}/src/limbo/tools/find.py +12 -6
  51. {limbo_code-0.1.1 → limbo_code-0.1.9}/src/limbo/tools/grep.py +18 -10
  52. limbo_code-0.1.9/src/limbo/tools/mutation_queue.py +36 -0
  53. {limbo_code-0.1.1 → limbo_code-0.1.9}/src/limbo/tools/read.py +56 -18
  54. {limbo_code-0.1.1 → limbo_code-0.1.9}/src/limbo/tools/registry.py +35 -2
  55. {limbo_code-0.1.1 → limbo_code-0.1.9}/src/limbo/tools/write.py +7 -5
  56. {limbo_code-0.1.1 → limbo_code-0.1.9}/src/limbo/ui/app.py +17 -6
  57. limbo_code-0.1.9/src/limbo/ui/app.tcss +316 -0
  58. limbo_code-0.1.9/src/limbo/ui/banner.py +117 -0
  59. limbo_code-0.1.9/src/limbo/ui/clipboard.py +314 -0
  60. {limbo_code-0.1.1 → limbo_code-0.1.9}/src/limbo/ui/commands.py +4 -0
  61. limbo_code-0.1.9/src/limbo/ui/contrast.py +139 -0
  62. {limbo_code-0.1.1 → limbo_code-0.1.9}/src/limbo/ui/screens/main.py +267 -17
  63. limbo_code-0.1.9/src/limbo/ui/screens/model_picker.py +122 -0
  64. limbo_code-0.1.9/src/limbo/ui/syntax.py +84 -0
  65. limbo_code-0.1.9/src/limbo/ui/theme.py +101 -0
  66. limbo_code-0.1.9/src/limbo/ui/widgets/chat.py +320 -0
  67. limbo_code-0.1.9/src/limbo/ui/widgets/input.py +409 -0
  68. limbo_code-0.1.9/src/limbo/ui/widgets/status_bar.py +116 -0
  69. {limbo_code-0.1.1 → limbo_code-0.1.9}/src/limbo/ui/widgets/tool_card.py +18 -8
  70. limbo_code-0.1.9/src/limbo/user_paths.py +82 -0
  71. limbo_code-0.1.9/tests/conftest.py +21 -0
  72. limbo_code-0.1.9/tests/test_agent.py +2038 -0
  73. {limbo_code-0.1.1 → limbo_code-0.1.9}/tests/test_anthropic_client.py +277 -2
  74. limbo_code-0.1.9/tests/test_attachments.py +78 -0
  75. limbo_code-0.1.9/tests/test_catalog.py +293 -0
  76. {limbo_code-0.1.1 → limbo_code-0.1.9}/tests/test_cli.py +12 -0
  77. limbo_code-0.1.9/tests/test_clipboard.py +205 -0
  78. limbo_code-0.1.9/tests/test_compaction.py +163 -0
  79. {limbo_code-0.1.1 → limbo_code-0.1.9}/tests/test_config.py +74 -3
  80. {limbo_code-0.1.1 → limbo_code-0.1.9}/tests/test_history.py +0 -13
  81. {limbo_code-0.1.1 → limbo_code-0.1.9}/tests/test_llm_client.py +276 -2
  82. limbo_code-0.1.9/tests/test_llm_scaffold.py +89 -0
  83. limbo_code-0.1.9/tests/test_model_switch.py +140 -0
  84. limbo_code-0.1.9/tests/test_prompt.py +85 -0
  85. limbo_code-0.1.9/tests/test_responses_client.py +582 -0
  86. {limbo_code-0.1.1 → limbo_code-0.1.9}/tests/test_retry.py +4 -0
  87. {limbo_code-0.1.1 → limbo_code-0.1.9}/tests/test_sessions.py +181 -4
  88. limbo_code-0.1.9/tests/test_skills.py +187 -0
  89. limbo_code-0.1.9/tests/test_steer.py +66 -0
  90. limbo_code-0.1.9/tests/test_usage.py +159 -0
  91. limbo_code-0.1.9/tests/test_user_paths.py +87 -0
  92. limbo_code-0.1.9/tests/tools/test_base.py +244 -0
  93. {limbo_code-0.1.1 → limbo_code-0.1.9}/tests/tools/test_bash.py +66 -6
  94. {limbo_code-0.1.1 → limbo_code-0.1.9}/tests/tools/test_find.py +20 -0
  95. limbo_code-0.1.9/tests/tools/test_mutation_queue.py +47 -0
  96. {limbo_code-0.1.1 → limbo_code-0.1.9}/tests/tools/test_read.py +89 -2
  97. {limbo_code-0.1.1 → limbo_code-0.1.9}/tests/tools/test_registry.py +32 -0
  98. limbo_code-0.1.9/tests/ui/__snapshots__/test_snapshots/test_snapshot_idle.raw +172 -0
  99. limbo_code-0.1.9/tests/ui/__snapshots__/test_snapshots/test_snapshot_session_picker.raw +168 -0
  100. limbo_code-0.1.9/tests/ui/__snapshots__/test_snapshots/test_snapshot_thinking_and_running_tool.raw +174 -0
  101. limbo_code-0.1.9/tests/ui/__snapshots__/test_snapshots/test_snapshot_tool_error_and_error_message.raw +175 -0
  102. {limbo_code-0.1.1 → limbo_code-0.1.9}/tests/ui/test_app_smoke.py +9 -4
  103. limbo_code-0.1.9/tests/ui/test_compact_ui.py +188 -0
  104. limbo_code-0.1.9/tests/ui/test_contrast.py +41 -0
  105. limbo_code-0.1.9/tests/ui/test_input_attachments.py +168 -0
  106. limbo_code-0.1.9/tests/ui/test_input_paste.py +314 -0
  107. limbo_code-0.1.9/tests/ui/test_main_screen.py +284 -0
  108. limbo_code-0.1.9/tests/ui/test_model_picker.py +393 -0
  109. limbo_code-0.1.9/tests/ui/test_scroll_follow.py +85 -0
  110. limbo_code-0.1.9/tests/ui/test_snapshots.py +136 -0
  111. limbo_code-0.1.9/tests/ui/test_startup_art.py +137 -0
  112. limbo_code-0.1.9/tests/ui/test_steer_ui.py +532 -0
  113. limbo_code-0.1.9/tests/ui/test_theme.py +104 -0
  114. limbo_code-0.1.9/uv.lock +936 -0
  115. limbo_code-0.1.1/AGENTS.md +0 -183
  116. limbo_code-0.1.1/src/limbo/__init__.py +0 -3
  117. limbo_code-0.1.1/src/limbo/agent.py +0 -456
  118. limbo_code-0.1.1/src/limbo/llm/catalog.py +0 -234
  119. limbo_code-0.1.1/src/limbo/skills.py +0 -89
  120. limbo_code-0.1.1/src/limbo/tools/base.py +0 -102
  121. limbo_code-0.1.1/src/limbo/ui/app.tcss +0 -174
  122. limbo_code-0.1.1/src/limbo/ui/banner.py +0 -83
  123. limbo_code-0.1.1/src/limbo/ui/widgets/chat.py +0 -149
  124. limbo_code-0.1.1/src/limbo/ui/widgets/input.py +0 -199
  125. limbo_code-0.1.1/src/limbo/ui/widgets/status_bar.py +0 -32
  126. limbo_code-0.1.1/tests/test_agent.py +0 -729
  127. limbo_code-0.1.1/tests/test_catalog.py +0 -160
  128. limbo_code-0.1.1/tests/test_skills.py +0 -89
  129. limbo_code-0.1.1/tests/tools/test_base.py +0 -85
  130. limbo_code-0.1.1/tests/ui/test_main_screen.py +0 -150
  131. limbo_code-0.1.1/tests/ui/test_startup_art.py +0 -94
  132. {limbo_code-0.1.1 → limbo_code-0.1.9}/.agents/skills/grill-with-docs/SKILL.md +0 -0
  133. {limbo_code-0.1.1 → limbo_code-0.1.9}/.agents/skills/grill-with-docs/agents/openai.yaml +0 -0
  134. {limbo_code-0.1.1 → limbo_code-0.1.9}/.agents/skills/improve-codebase-architecture/HTML-REPORT.md +0 -0
  135. {limbo_code-0.1.1 → limbo_code-0.1.9}/.agents/skills/improve-codebase-architecture/SKILL.md +0 -0
  136. {limbo_code-0.1.1 → limbo_code-0.1.9}/.agents/skills/improve-codebase-architecture/agents/openai.yaml +0 -0
  137. {limbo_code-0.1.1 → limbo_code-0.1.9}/.agents/skills/tdd/SKILL.md +0 -0
  138. {limbo_code-0.1.1 → limbo_code-0.1.9}/.agents/skills/tdd/agents/openai.yaml +0 -0
  139. {limbo_code-0.1.1 → limbo_code-0.1.9}/.agents/skills/tdd/mocking.md +0 -0
  140. {limbo_code-0.1.1 → limbo_code-0.1.9}/.agents/skills/tdd/tests.md +0 -0
  141. {limbo_code-0.1.1 → limbo_code-0.1.9}/.github/workflows/publish.yml +0 -0
  142. {limbo_code-0.1.1 → limbo_code-0.1.9}/design/confirm_view.html +0 -0
  143. {limbo_code-0.1.1 → limbo_code-0.1.9}/design/limbo-ui-minimal.md +0 -0
  144. {limbo_code-0.1.1 → limbo_code-0.1.9}/design/limbo-ui-redesign.md +0 -0
  145. {limbo_code-0.1.1 → limbo_code-0.1.9}/design/prototype-minimal-confirm.png +0 -0
  146. {limbo_code-0.1.1 → limbo_code-0.1.9}/design/prototype-minimal.html +0 -0
  147. {limbo_code-0.1.1 → limbo_code-0.1.9}/design/prototype-minimal.png +0 -0
  148. {limbo_code-0.1.1 → limbo_code-0.1.9}/design/prototype.html +0 -0
  149. {limbo_code-0.1.1 → limbo_code-0.1.9}/design/prototype.png +0 -0
  150. {limbo_code-0.1.1 → limbo_code-0.1.9}/docs/assets/limbo-current-ui.png +0 -0
  151. {limbo_code-0.1.1 → limbo_code-0.1.9}/docs/assets/limbo-new-ui.png +0 -0
  152. {limbo_code-0.1.1 → limbo_code-0.1.9}/docs/session-management.md +0 -0
  153. {limbo_code-0.1.1 → limbo_code-0.1.9}/docs/ui-redesign-proposal.md +0 -0
  154. {limbo_code-0.1.1 → limbo_code-0.1.9}/skills-lock.json +0 -0
  155. {limbo_code-0.1.1 → limbo_code-0.1.9}/src/limbo/__main__.py +0 -0
  156. {limbo_code-0.1.1 → limbo_code-0.1.9}/src/limbo/llm/__init__.py +0 -0
  157. {limbo_code-0.1.1 → limbo_code-0.1.9}/src/limbo/tools/__init__.py +0 -0
  158. {limbo_code-0.1.1 → limbo_code-0.1.9}/src/limbo/tools/ignore.py +0 -0
  159. {limbo_code-0.1.1 → limbo_code-0.1.9}/src/limbo/tools/ls.py +0 -0
  160. {limbo_code-0.1.1 → limbo_code-0.1.9}/src/limbo/trace.py +0 -0
  161. {limbo_code-0.1.1 → limbo_code-0.1.9}/src/limbo/ui/__init__.py +0 -0
  162. {limbo_code-0.1.1 → limbo_code-0.1.9}/src/limbo/ui/screens/__init__.py +0 -0
  163. {limbo_code-0.1.1 → limbo_code-0.1.9}/src/limbo/ui/screens/game2048.py +0 -0
  164. {limbo_code-0.1.1 → limbo_code-0.1.9}/src/limbo/ui/screens/session_picker.py +0 -0
  165. {limbo_code-0.1.1 → limbo_code-0.1.9}/src/limbo/ui/widgets/__init__.py +0 -0
  166. {limbo_code-0.1.1 → limbo_code-0.1.9}/src/limbo/ui/widgets/command_menu.py +0 -0
  167. {limbo_code-0.1.1 → limbo_code-0.1.9}/tests/test_integration.py +0 -0
  168. {limbo_code-0.1.1 → limbo_code-0.1.9}/tests/test_models.py +0 -0
  169. {limbo_code-0.1.1 → limbo_code-0.1.9}/tests/test_trace.py +0 -0
  170. {limbo_code-0.1.1 → limbo_code-0.1.9}/tests/tools/test_edit.py +0 -0
  171. {limbo_code-0.1.1 → limbo_code-0.1.9}/tests/tools/test_grep.py +0 -0
  172. {limbo_code-0.1.1 → limbo_code-0.1.9}/tests/tools/test_ignore.py +0 -0
  173. {limbo_code-0.1.1 → limbo_code-0.1.9}/tests/tools/test_ls.py +0 -0
  174. {limbo_code-0.1.1 → limbo_code-0.1.9}/tests/tools/test_write.py +0 -0
  175. {limbo_code-0.1.1 → limbo_code-0.1.9}/tests/ui/test_command_menu.py +0 -0
  176. {limbo_code-0.1.1 → limbo_code-0.1.9}/tests/ui/test_commands.py +0 -0
  177. {limbo_code-0.1.1 → limbo_code-0.1.9}/tests/ui/test_game2048.py +0 -0
  178. {limbo_code-0.1.1 → limbo_code-0.1.9}/tests/ui/test_input_history.py +0 -0
  179. {limbo_code-0.1.1 → limbo_code-0.1.9}/tests/ui/test_sessions_ui.py +0 -0
  180. {limbo_code-0.1.1 → limbo_code-0.1.9}/tests/ui/test_skills_ui.py +0 -0
  181. {limbo_code-0.1.1 → limbo_code-0.1.9}/tests/ui/test_widgets.py +0 -0
@@ -17,14 +17,20 @@ jobs:
17
17
  steps:
18
18
  - uses: actions/checkout@v4
19
19
 
20
+ - name: Install uv
21
+ uses: astral-sh/setup-uv@v5
22
+
20
23
  - name: Set up Python
21
24
  uses: actions/setup-python@v5
22
25
  with:
23
26
  python-version: ${{ matrix.python-version }}
24
- cache: pip
25
27
 
28
+ # Install from uv.lock so CI renders snapshots with the exact same
29
+ # textual/rich/syrupy versions the baselines were generated with.
26
30
  - name: Install dependencies
27
- run: pip install -e ".[dev]"
31
+ run: uv sync --frozen --extra dev
32
+ env:
33
+ UV_PYTHON: ${{ matrix.python-version }}
28
34
 
29
35
  - name: Run tests
30
- run: python -m pytest -v
36
+ run: uv run python -m pytest -v
@@ -11,3 +11,4 @@ venv/
11
11
  dist/
12
12
  build/
13
13
  .pi-subagents/
14
+ snapshot_report.html
@@ -0,0 +1,94 @@
1
+ # Limbo — Agent Guide
2
+
3
+ **Limbo** is a minimal terminal AI coding agent (Python 3.11+, Textual). Users converse with an LLM in a TUI to explore, read, edit, and write code via 7 tools.
4
+
5
+ **Tools execute immediately — no confirmation flow.** Guardrails: a workdir fence on file tools (+ session-scoped grants from user-mentioned paths), a sensitive-file blocklist on `read`, and a heuristic dangerous-command filter on `bash` (rejects matches outright; bypassable via subshells/variables — documented in the tool description).
6
+
7
+ ## Quick Start
8
+
9
+ ```bash
10
+ pip install -e .
11
+ limbo --workdir /path/to/project # config: ~/.limbo/config.toml
12
+ ```
13
+
14
+ ## Architecture
15
+
16
+ ```
17
+ src/limbo/
18
+ ├── app.py # CLI entry: args, config, launch
19
+ ├── config.py # Pydantic config: llm/ui/safety/tools/compaction/providers
20
+ ├── models.py # Message, Attachment, ToolResult, LLMEvent
21
+ ├── agent.py # Conversation loop (see below)
22
+ ├── attachments.py # Attachment policy: vision gate, inline vs path-reference degrade
23
+ ├── compaction.py # Context compaction decision/prompt logic (LIM-14)
24
+ ├── history.py # tool_call↔result pairing + resume repair
25
+ ├── model_switch.py # /model domain logic: validate, swap client, persist (UI is an adapter)
26
+ ├── prompt.py # System-prompt assembly (tools section derived from the registry)
27
+ ├── steer.py # Mid-turn steer queue (LIM-20): queueing semantics, cancel boundary
28
+ ├── sessions.py # Session JSONL save/load/list/export
29
+ ├── trace.py # Append-only JSONL run log (sessions/traces/)
30
+ ├── skills.py # SKILL.md discovery (user + project dirs)
31
+ ├── user_paths.py # Fence grants from paths in user messages
32
+ ├── llm/ # catalog.py (provider/model specs), factory.py (dialect→client),
33
+ │ # openai_client.py, anthropic_client.py, responses_client.py, retry.py, sse.py,
34
+ │ # usage.py (token accounting: usage normalization + prompt-size estimation),
35
+ │ # scaffold.py (plumbing shared by dialect clients: credentials, retry, images)
36
+ ├── tools/ # base.py (BaseTool + fence + truncation), registry.py (dispatch, grants),
37
+ │ # mutation_queue.py (per-file locks), ignore.py (.gitignore),
38
+ │ # read/bash/edit/write/grep/find/ls.py
39
+ └── ui/ # app.py + app.tcss (ALL styles here; theme vars only, no bare hex),
40
+ # theme.py (limbo-dark/-light, RFC LIM-16), commands.py (slash registry),
41
+ # screens/ (main, session_picker, model_picker, game2048),
42
+ # widgets/ (chat, input, status_bar, tool_card, command_menu)
43
+ ```
44
+
45
+ ## Agent Loop
46
+
47
+ - `Agent.run()` yields `AgentEvent`: `TextDelta`, `ThinkingDelta`, `ToolCallRequest`, `ToolResultEvent`, `ErrorEvent`, `CompactionEvent`, `UsageUpdate`, `SteerEvent`
48
+ - Loop top: auto-compaction check → steer drain → LLM call. Turn ends on a tool-call-free response; `max_iterations` (default 50) cancels pending calls with placeholder results
49
+ - Tool calls in one turn run **concurrently** (`[tools] parallel`); results stream in completion order, recorded in source order; same-file mutations serialized by `mutation_queue`
50
+ - `finish_reason` `length`/`max_tokens` → whole batch failed without execution, model re-issues
51
+ - Mid-turn user input queues as *steer* (LIM-20): injected at loop top, or as turn-end follow-up
52
+ - Reasoning is stored on assistant messages and replayed per dialect (Anthropic thinking signatures, Kimi `reasoning_content`, Responses encrypted items)
53
+ - Usage counters normalize in `llm/usage.py` (`input_tokens`+`cache_read`, `prompt_tokens`, DeepSeek cache hits) and feed the compaction trigger via `PromptSizeEstimator`
54
+
55
+ ## LLM & Config
56
+
57
+ Providers/models live in `llm/catalog.py`; unknown models get generic OpenAI-compatible defaults. Resolution: `[providers.<id>]` overrides → explicit `[llm]` values → catalog defaults → env-var API keys.
58
+
59
+ ```toml
60
+ [llm] # model, api_key, base_url, temperature, max_iterations,
61
+ # max_tokens, thinking_effort, max_retries, timeout, ...
62
+ [tools] # bash_enabled = true, parallel = true
63
+ [compaction] # enabled, reserve_tokens = 16384, keep_recent_tokens = 20000
64
+ [safety] # dangerous_commands, sensitive_files, auto_grant_user_paths
65
+ [ui] # theme, show_banner
66
+ [providers.<id>] # base_url / api_key / api_key_env / headers
67
+ ```
68
+
69
+ `/model` rebuilds the client mid-session (refused while busy) and persists via tomlkit. Sessions resume via `--continue` / `--resume` / `/sessions`; resume repairs dangling tool calls and restores grants. Trace events: `session_start`, `user_message`, `llm_request` (full body), `llm_response`, `tool_call`, `tool_result`, `compaction*`, `error`, `turn_end`. `/export [path]` merges meta + trace + message snapshot (Markdown if `.md`).
70
+
71
+ Skills: `~/.limbo/skills/<name>/SKILL.md` (user) and `<workdir>/.agents/skills/<name>/SKILL.md` (project, wins collisions) — injected as a `<available_skills>` catalog in the system prompt and invocable via `/<name> [args]`.
72
+
73
+ ## UI
74
+
75
+ Single column: status bar (state/elapsed/tokens/model/workdir/queued) → scrolling chat flow (user `❯`, streaming Markdown, tool cards, errors) → input box (Enter submits, Shift+Enter newline, paste markers, `ctrl+v` image attach) → hint line. Slash commands: `/sessions /new /export /compact /model /help /2048` + skill commands. After palette changes run `python scripts/check_contrast.py` (≥ 4.5:1).
76
+
77
+ ## Key Decisions
78
+
79
+ - Async agent, sync tools via `asyncio.to_thread()`
80
+ - System prompt = hardcoded tool guidelines + `<workdir>/AGENTS.md` & `~/.limbo/AGENTS.md` (XML-wrapped) + skills catalog. **This file is injected into every request — keep it accurate and lean.**
81
+ - Session files fully rewritten per save (atomic, 0600); trace holds full-fidelity record
82
+
83
+ ## Testing
84
+
85
+ ```bash
86
+ pytest tests/ -v # pytest-asyncio, respx HTTP mocks, pytest-textual-snapshot
87
+ ruff check src tests && mypy src # lint + type check
88
+ pytest --snapshot-update # after intentional visual changes; review the SVG diff
89
+ ```
90
+
91
+ ## Adding Things
92
+
93
+ - **Tool**: subclass `BaseTool` in `tools/<name>.py` (`name`/`description`/`parameters` + `run()`; use `resolve_existing()`/`resolve_creatable()`, raise `ToolError`), register in `ToolRegistry.__init__()`, wrap mutations in `mutation_lock_for(path)`, add `tests/tools/test_<name>.py`
94
+ - **Provider/model**: add specs in `llm/catalog.py`; new wire format → implement `LLMClient` + `register_client()` in `llm/factory.py`. User-specific endpoint/key tweaks belong in `[providers.<id>]` config, not the catalog
@@ -0,0 +1,43 @@
1
+ # Context — Limbo domain glossary
2
+
3
+ Domain language for Limbo's concepts. Architecture reviews and refactor
4
+ proposals should name modules with these terms.
5
+
6
+ ## Token accounting
7
+
8
+ - **Usage** — the provider-reported token counters on one completion. Each
9
+ dialect reports them differently (OpenAI `prompt_tokens`, Anthropic
10
+ `input_tokens` + cache counters, DeepSeek `prompt_cache_hit_tokens`,
11
+ Responses `input_tokens_details`). Normalized by `llm/usage.py` into
12
+ `UsageTotals` (prompt / cached / total); the agent loop and compaction
13
+ never read provider field names.
14
+ - **Prompt-size estimation** — predicting the *next* request's prompt size:
15
+ the last real usage figure plus the chars/4 estimate of everything
16
+ appended since (the **watermark**). Owned by `PromptSizeEstimator` in
17
+ `llm/usage.py`; invalidated (reset) when compaction rewrites history.
18
+
19
+ ## Conversation
20
+
21
+ - **Turn** — one user input through the agent loop until a tool-call-free
22
+ response (or max_iterations). Turns are the save/trace unit.
23
+ - **Steer** — a user message queued mid-turn, injected at consistency
24
+ points (loop top, turn end, run head). The `SteerQueue` (`steer.py`) owns
25
+ queueing semantics — id generation, FIFO, and the cancel boundary (an
26
+ item can be cancelled by id only until drained); the Agent owns drain
27
+ timing.
28
+ - **Compaction** — summarizing the old region of history into a synthetic
29
+ summary message, keeping the recent tail. Triggered automatically
30
+ (loop-top, before context overflow) or manually (`/compact`).
31
+ - **Attachment policy** — how a submitted attachment reaches the model
32
+ (`attachments.py`): images become multimodal blocks for vision models
33
+ (encoded per dialect, `llm/scaffold.py`), everything else degrades to
34
+ text — small files inline (≤50KB), the rest as path references.
35
+ - **Model switch** — `/model` mid-session (`model_switch.py`): validate the
36
+ target (API key, thinking-effort compatibility), swap the client
37
+ (converging on the latest config model), persist to config.toml. The UI
38
+ screen is an adapter translating verdicts into chat messages.
39
+
40
+ ## Safety
41
+
42
+ - **Fence** — the workdir boundary on file tools, widened by session-scoped
43
+ **grants** (paths a real user mentions in a submitted message).
@@ -1,19 +1,23 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: limbo-code
3
- Version: 0.1.1
3
+ Version: 0.1.9
4
4
  Summary: A minimal terminal AI coding agent
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: httpx>=0.27
7
7
  Requires-Dist: openai>=1.30
8
8
  Requires-Dist: pydantic>=2.0
9
- Requires-Dist: textual>=0.58
9
+ Requires-Dist: pygments>=2.17
10
+ Requires-Dist: textual>=8.0
10
11
  Requires-Dist: toml>=0.10
12
+ Requires-Dist: tomlkit>=0.13
11
13
  Provides-Extra: dev
12
14
  Requires-Dist: mypy>=1.10; extra == 'dev'
13
15
  Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
16
+ Requires-Dist: pytest-textual-snapshot>=1.0; extra == 'dev'
14
17
  Requires-Dist: pytest>=8.0; extra == 'dev'
15
18
  Requires-Dist: respx>=0.21; extra == 'dev'
16
19
  Requires-Dist: ruff>=0.4; extra == 'dev'
20
+ Requires-Dist: types-pygments>=2.17; extra == 'dev'
17
21
  Description-Content-Type: text/markdown
18
22
 
19
23
  # Limbo
@@ -55,6 +59,40 @@ Built-in providers:
55
59
  | `deepseek` | openai-completions | `https://api.deepseek.com/v1` | `DEEPSEEK_API_KEY` |
56
60
  | `moonshotai` (Kimi) | openai-completions | `https://api.moonshot.ai/v1` | `MOONSHOT_API_KEY` |
57
61
  | `kimi-coding` (Kimi For Coding) | anthropic-messages | `https://api.kimi.com/coding` | `KIMI_API_KEY` |
62
+ | `glm` (GLM Coding Plan) | openai-completions | `https://open.bigmodel.cn/api/coding/paas/v4` | `ZHIPUAI_API_KEY` |
63
+ | `codex` (OpenAI Codex) | openai-responses | `https://api.openai.com/v1` (override with your relay) | `CODEX_API_KEY` |
64
+
65
+ Built-in Codex models include `gpt-5.5`, `gpt-5.6-sol`/`terra`/`luna`,
66
+ `gpt-5.4`, `gpt-5.4-mini`, and `gpt-5.3-codex-spark`. **`gpt-5.5` is
67
+ verified by live test against the target relay** (its 1M context is
68
+ relay-reported); the other entries mirror pi's built-in catalog and are
69
+ unverified — availability depends on the relay in use, and unknown or
70
+ unsupported models fall back to the generic OpenAI-compatible defaults.
71
+ Codex speaks the
72
+ OpenAI **Responses API** (`POST {base_url}/responses`), served by a
73
+ dedicated client (`src/limbo/llm/responses_client.py`, plain httpx SSE):
74
+ system messages become `instructions`, tool definitions are flattened, and
75
+ reasoning models stream `reasoning` summaries and replay encrypted
76
+ reasoning items across turns. Limbo targets **API-key relays**, not the
77
+ ChatGPT subscription backend (OAuth) — point the provider at your relay
78
+ and make sure its model IDs match the catalog:
79
+
80
+ ```toml
81
+ [llm]
82
+ model = "gpt-5.5"
83
+
84
+ [providers.codex]
85
+ base_url = "https://my-relay.example.com/v1"
86
+ api_key_env = "CODEX_API_KEY"
87
+ ```
88
+
89
+ Built-in GLM Coding Plan models: `glm-4.7`, `glm-5.1`, `glm-5.2` (1M
90
+ context), `glm-5-turbo`, `glm-5v-turbo` (vision), and `glm-4.5-air`. The
91
+ Coding Plan is a subscription with its own endpoint and keys — a coding-plan
92
+ key only works on the `/api/coding/paas/v4` endpoints (not the pay-per-token
93
+ `/api/paas/v4` ones) and vice versa. For the international endpoint set
94
+ `[providers.glm] base_url = "https://api.z.ai/api/coding/paas/v4"` (see
95
+ below).
58
96
 
59
97
  Built-in Kimi models include `kimi-k3` (1M context, pay-per-token), and for
60
98
  Kimi For Coding subscriptions: `k3` (1M context), `kimi-for-coding`, and
@@ -78,14 +116,47 @@ assistant thinking blocks with their signature.
78
116
 
79
117
  For the mainland-China Moonshot endpoint, set `base_url =
80
118
  "https://api.moonshot.cn/v1"` explicitly (a configured `base_url` always
81
- wins over the catalog).
119
+ wins over the catalog — unless a `[providers.<id>]` override says otherwise,
120
+ see below).
121
+
122
+ ### Per-provider overrides
123
+
124
+ An optional `[providers.<id>]` section overrides a single catalog provider
125
+ without touching the global `[llm]` settings — e.g. pointing a provider at a
126
+ relay, renaming its credential env var, or adding extra headers:
127
+
128
+ ```toml
129
+ [providers.glm]
130
+ base_url = "https://api.z.ai/api/coding/paas/v4" # international endpoint
131
+
132
+ [providers.codex]
133
+ base_url = "https://my-relay.example.com/v1"
134
+ api_key_env = "CODEX_API_KEY" # rename the env var read for the key
135
+ headers = { x-relay = "on" } # extra headers on every request
136
+ ```
137
+
138
+ All fields are optional; unset fields fall back to the catalog. Resolution
139
+ order for `base_url` (first hit wins):
140
+
141
+ 1. `[providers.<id>] base_url`
142
+ 2. `[llm] base_url` (when changed from the DeepSeek default)
143
+ 3. the catalog provider's built-in endpoint
144
+
145
+ and for the API key: `[providers.<id>] api_key` → `[llm] api_key` → the
146
+ environment variable (`[providers.<id>] api_key_env` rename, else the
147
+ catalog's default env var).
148
+
149
+ > **Note:** rule 1 is the single exception to the long-standing "an explicit
150
+ > `[llm] base_url` always wins" behavior — a per-provider override is more
151
+ > specific than the global setting. If you configure both, the
152
+ > `[providers.<id>]` value is used for that provider's models.
82
153
 
83
154
  ### Optional LLM settings
84
155
 
85
156
  ```toml
86
157
  [llm]
87
158
  temperature = 0.2 # 0.0 - 2.0, default 0.2
88
- max_iterations = 10 # safety limit on tool-turn loops, default 10
159
+ max_iterations = 50 # safety limit on tool-turn loops, default 50
89
160
  max_tokens = 8192 # output cap; default = the model's catalog value
90
161
  thinking_effort = "high" # reasoning control; default = provider behavior
91
162
  ```
@@ -100,6 +171,13 @@ thinking_effort = "high" # reasoning control; default = provider behavior
100
171
  - `kimi-k2-thinking`, `kimi-k2.5`+ (DeepSeek-style): any non-`off` value →
101
172
  `thinking: {type: enabled}`; `"off"` → `thinking: {type: disabled}`
102
173
  (except `kimi-k2.7-code*`, where thinking is always on).
174
+ - `glm-*` (z.ai-style): like DeepSeek-style but with `clear_thinking: false`
175
+ so thinking is preserved across turns; `glm-5.2` additionally maps
176
+ `low`/`high`/`max` to `reasoning_effort` (`low` clamps to `high`).
177
+ - `gpt-5.*` codex models (Responses API): `low` | `medium` | `high` |
178
+ `xhigh` → `reasoning: {effort, summary: auto}`; 5.6-generation models
179
+ (`gpt-5.6-*`) also accept `max`. Temperature is omitted for reasoning
180
+ models.
103
181
  - Non-reasoning models: ignored.
104
182
 
105
183
  Reasoning output streams into the chat as muted thinking blocks and is
@@ -126,8 +204,25 @@ you no longer need them.
126
204
 
127
205
  ```bash
128
206
  limbo --workdir /path/to/project
207
+ limbo --model glm-4.7 # override the configured model for this run
129
208
  ```
130
209
 
210
+ ### Switching models at runtime
211
+
212
+ Use `/model` in the TUI: without an argument it opens a picker that lists
213
+ catalog models grouped by provider (context window, reasoning capability,
214
+ and the current model are annotated; providers without a resolvable API key
215
+ are dimmed with a hint). `/model <name>` switches directly — unknown names
216
+ fall back to generic OpenAI-compatible defaults. The picker re-reads
217
+ `config.toml` every time it opens, so edits (e.g. a newly added
218
+ `[providers.<id>]`) apply without a restart.
219
+
220
+ A switch takes effect immediately (no restart): the LLM client is rebuilt,
221
+ and the new model is written back to `config.toml` (comments preserved via
222
+ tomlkit) so the next launch keeps it. If the write fails, the switch still
223
+ applies for the current session. Switching is refused while a turn is in
224
+ flight.
225
+
131
226
  ## Safety
132
227
 
133
228
  Limbo executes every tool call immediately, without asking for confirmation.
@@ -37,6 +37,40 @@ Built-in providers:
37
37
  | `deepseek` | openai-completions | `https://api.deepseek.com/v1` | `DEEPSEEK_API_KEY` |
38
38
  | `moonshotai` (Kimi) | openai-completions | `https://api.moonshot.ai/v1` | `MOONSHOT_API_KEY` |
39
39
  | `kimi-coding` (Kimi For Coding) | anthropic-messages | `https://api.kimi.com/coding` | `KIMI_API_KEY` |
40
+ | `glm` (GLM Coding Plan) | openai-completions | `https://open.bigmodel.cn/api/coding/paas/v4` | `ZHIPUAI_API_KEY` |
41
+ | `codex` (OpenAI Codex) | openai-responses | `https://api.openai.com/v1` (override with your relay) | `CODEX_API_KEY` |
42
+
43
+ Built-in Codex models include `gpt-5.5`, `gpt-5.6-sol`/`terra`/`luna`,
44
+ `gpt-5.4`, `gpt-5.4-mini`, and `gpt-5.3-codex-spark`. **`gpt-5.5` is
45
+ verified by live test against the target relay** (its 1M context is
46
+ relay-reported); the other entries mirror pi's built-in catalog and are
47
+ unverified — availability depends on the relay in use, and unknown or
48
+ unsupported models fall back to the generic OpenAI-compatible defaults.
49
+ Codex speaks the
50
+ OpenAI **Responses API** (`POST {base_url}/responses`), served by a
51
+ dedicated client (`src/limbo/llm/responses_client.py`, plain httpx SSE):
52
+ system messages become `instructions`, tool definitions are flattened, and
53
+ reasoning models stream `reasoning` summaries and replay encrypted
54
+ reasoning items across turns. Limbo targets **API-key relays**, not the
55
+ ChatGPT subscription backend (OAuth) — point the provider at your relay
56
+ and make sure its model IDs match the catalog:
57
+
58
+ ```toml
59
+ [llm]
60
+ model = "gpt-5.5"
61
+
62
+ [providers.codex]
63
+ base_url = "https://my-relay.example.com/v1"
64
+ api_key_env = "CODEX_API_KEY"
65
+ ```
66
+
67
+ Built-in GLM Coding Plan models: `glm-4.7`, `glm-5.1`, `glm-5.2` (1M
68
+ context), `glm-5-turbo`, `glm-5v-turbo` (vision), and `glm-4.5-air`. The
69
+ Coding Plan is a subscription with its own endpoint and keys — a coding-plan
70
+ key only works on the `/api/coding/paas/v4` endpoints (not the pay-per-token
71
+ `/api/paas/v4` ones) and vice versa. For the international endpoint set
72
+ `[providers.glm] base_url = "https://api.z.ai/api/coding/paas/v4"` (see
73
+ below).
40
74
 
41
75
  Built-in Kimi models include `kimi-k3` (1M context, pay-per-token), and for
42
76
  Kimi For Coding subscriptions: `k3` (1M context), `kimi-for-coding`, and
@@ -60,14 +94,47 @@ assistant thinking blocks with their signature.
60
94
 
61
95
  For the mainland-China Moonshot endpoint, set `base_url =
62
96
  "https://api.moonshot.cn/v1"` explicitly (a configured `base_url` always
63
- wins over the catalog).
97
+ wins over the catalog — unless a `[providers.<id>]` override says otherwise,
98
+ see below).
99
+
100
+ ### Per-provider overrides
101
+
102
+ An optional `[providers.<id>]` section overrides a single catalog provider
103
+ without touching the global `[llm]` settings — e.g. pointing a provider at a
104
+ relay, renaming its credential env var, or adding extra headers:
105
+
106
+ ```toml
107
+ [providers.glm]
108
+ base_url = "https://api.z.ai/api/coding/paas/v4" # international endpoint
109
+
110
+ [providers.codex]
111
+ base_url = "https://my-relay.example.com/v1"
112
+ api_key_env = "CODEX_API_KEY" # rename the env var read for the key
113
+ headers = { x-relay = "on" } # extra headers on every request
114
+ ```
115
+
116
+ All fields are optional; unset fields fall back to the catalog. Resolution
117
+ order for `base_url` (first hit wins):
118
+
119
+ 1. `[providers.<id>] base_url`
120
+ 2. `[llm] base_url` (when changed from the DeepSeek default)
121
+ 3. the catalog provider's built-in endpoint
122
+
123
+ and for the API key: `[providers.<id>] api_key` → `[llm] api_key` → the
124
+ environment variable (`[providers.<id>] api_key_env` rename, else the
125
+ catalog's default env var).
126
+
127
+ > **Note:** rule 1 is the single exception to the long-standing "an explicit
128
+ > `[llm] base_url` always wins" behavior — a per-provider override is more
129
+ > specific than the global setting. If you configure both, the
130
+ > `[providers.<id>]` value is used for that provider's models.
64
131
 
65
132
  ### Optional LLM settings
66
133
 
67
134
  ```toml
68
135
  [llm]
69
136
  temperature = 0.2 # 0.0 - 2.0, default 0.2
70
- max_iterations = 10 # safety limit on tool-turn loops, default 10
137
+ max_iterations = 50 # safety limit on tool-turn loops, default 50
71
138
  max_tokens = 8192 # output cap; default = the model's catalog value
72
139
  thinking_effort = "high" # reasoning control; default = provider behavior
73
140
  ```
@@ -82,6 +149,13 @@ thinking_effort = "high" # reasoning control; default = provider behavior
82
149
  - `kimi-k2-thinking`, `kimi-k2.5`+ (DeepSeek-style): any non-`off` value →
83
150
  `thinking: {type: enabled}`; `"off"` → `thinking: {type: disabled}`
84
151
  (except `kimi-k2.7-code*`, where thinking is always on).
152
+ - `glm-*` (z.ai-style): like DeepSeek-style but with `clear_thinking: false`
153
+ so thinking is preserved across turns; `glm-5.2` additionally maps
154
+ `low`/`high`/`max` to `reasoning_effort` (`low` clamps to `high`).
155
+ - `gpt-5.*` codex models (Responses API): `low` | `medium` | `high` |
156
+ `xhigh` → `reasoning: {effort, summary: auto}`; 5.6-generation models
157
+ (`gpt-5.6-*`) also accept `max`. Temperature is omitted for reasoning
158
+ models.
85
159
  - Non-reasoning models: ignored.
86
160
 
87
161
  Reasoning output streams into the chat as muted thinking blocks and is
@@ -108,8 +182,25 @@ you no longer need them.
108
182
 
109
183
  ```bash
110
184
  limbo --workdir /path/to/project
185
+ limbo --model glm-4.7 # override the configured model for this run
111
186
  ```
112
187
 
188
+ ### Switching models at runtime
189
+
190
+ Use `/model` in the TUI: without an argument it opens a picker that lists
191
+ catalog models grouped by provider (context window, reasoning capability,
192
+ and the current model are annotated; providers without a resolvable API key
193
+ are dimmed with a hint). `/model <name>` switches directly — unknown names
194
+ fall back to generic OpenAI-compatible defaults. The picker re-reads
195
+ `config.toml` every time it opens, so edits (e.g. a newly added
196
+ `[providers.<id>]`) apply without a restart.
197
+
198
+ A switch takes effect immediately (no restart): the LLM client is rebuilt,
199
+ and the new model is written back to `config.toml` (comments preserved via
200
+ tomlkit) so the next launch keeps it. If the write fails, the switch still
201
+ applies for the current session. Switching is refused while a turn is in
202
+ flight.
203
+
113
204
  ## Safety
114
205
 
115
206
  Limbo executes every tool call immediately, without asking for confirmation.