tokenloop 0.1.0__tar.gz → 0.1.3__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 (80) hide show
  1. {tokenloop-0.1.0 → tokenloop-0.1.3}/.gitignore +7 -0
  2. tokenloop-0.1.3/.mcp.json +8 -0
  3. tokenloop-0.1.3/PKG-INFO +188 -0
  4. tokenloop-0.1.3/README.md +173 -0
  5. tokenloop-0.1.3/docs/HITL_PARITY.md +303 -0
  6. tokenloop-0.1.3/examples/basic_chat.py +36 -0
  7. tokenloop-0.1.3/examples/session_state.py +68 -0
  8. tokenloop-0.1.3/examples/skills/explain-topic/SKILL.md +108 -0
  9. tokenloop-0.1.3/examples/skills/explain-topic/references/rust,md +274 -0
  10. tokenloop-0.1.3/examples/skills/weather/SKILL.md +139 -0
  11. tokenloop-0.1.3/examples/skills/weather/output_schema.json +61 -0
  12. tokenloop-0.1.3/examples/skills/weather/scripts/weather.sh +245 -0
  13. tokenloop-0.1.3/examples/smoke_test.py +97 -0
  14. tokenloop-0.1.3/examples/streaming.py +49 -0
  15. tokenloop-0.1.3/examples/tool.py +68 -0
  16. tokenloop-0.1.3/examples/weather_agent.py +51 -0
  17. tokenloop-0.1.3/pyproject.toml +91 -0
  18. tokenloop-0.1.3/tests/test_hitl_resume.py +103 -0
  19. tokenloop-0.1.3/tests/test_hitl_skipped.py +62 -0
  20. tokenloop-0.1.3/tests/test_images.py +90 -0
  21. tokenloop-0.1.3/tests/test_run_control_service.py +72 -0
  22. tokenloop-0.1.3/tests/test_state.py +58 -0
  23. tokenloop-0.1.3/tokenloop/__init__.py +181 -0
  24. tokenloop-0.1.3/tokenloop/compression/base.py +17 -0
  25. tokenloop-0.1.3/tokenloop/compression/simple.py +49 -0
  26. tokenloop-0.1.3/tokenloop/compression/window.py +65 -0
  27. tokenloop-0.1.3/tokenloop/core/cli.py +335 -0
  28. tokenloop-0.1.3/tokenloop/core/compact.py +90 -0
  29. tokenloop-0.1.3/tokenloop/core/context.py +94 -0
  30. tokenloop-0.1.3/tokenloop/core/dispatch.py +305 -0
  31. tokenloop-0.1.3/tokenloop/core/events.py +211 -0
  32. tokenloop-0.1.3/tokenloop/core/loop.py +163 -0
  33. tokenloop-0.1.3/tokenloop/core/prompt.py +76 -0
  34. tokenloop-0.1.3/tokenloop/core/registry.py +104 -0
  35. tokenloop-0.1.3/tokenloop/core/retry.py +75 -0
  36. tokenloop-0.1.3/tokenloop/core/run.py +47 -0
  37. tokenloop-0.1.3/tokenloop/core/session.py +27 -0
  38. tokenloop-0.1.3/tokenloop/core/shared.py +133 -0
  39. tokenloop-0.1.3/tokenloop/core/stream.py +349 -0
  40. tokenloop-0.1.3/tokenloop/core/types.py +134 -0
  41. tokenloop-0.1.3/tokenloop/guardrails/__init__.py +0 -0
  42. tokenloop-0.1.3/tokenloop/guardrails/base.py +24 -0
  43. tokenloop-0.1.3/tokenloop/hitl/__init__.py +0 -0
  44. tokenloop-0.1.3/tokenloop/hitl/base.py +122 -0
  45. tokenloop-0.1.3/tokenloop/mcp/__init__.py +0 -0
  46. tokenloop-0.1.3/tokenloop/mcp/client.py +41 -0
  47. tokenloop-0.1.3/tokenloop/mcp/manager.py +36 -0
  48. tokenloop-0.1.3/tokenloop/provider/__init__.py +0 -0
  49. tokenloop-0.1.3/tokenloop/provider/anthropic.py +207 -0
  50. tokenloop-0.1.3/tokenloop/provider/base.py +26 -0
  51. tokenloop-0.1.3/tokenloop/provider/openai.py +203 -0
  52. tokenloop-0.1.3/tokenloop/session_service/__init__.py +0 -0
  53. tokenloop-0.1.3/tokenloop/session_service/base.py +56 -0
  54. tokenloop-0.1.3/tokenloop/session_service/memory.py +24 -0
  55. tokenloop-0.1.3/tokenloop/skills/__init__.py +0 -0
  56. tokenloop-0.1.3/tokenloop/skills/base.py +36 -0
  57. tokenloop-0.1.3/tokenloop/skills/manager.py +72 -0
  58. tokenloop-0.1.3/tokenloop/state.py +127 -0
  59. tokenloop-0.1.3/tokenloop/stream/__init__.py +0 -0
  60. tokenloop-0.1.3/tokenloop/stream/agui.py +203 -0
  61. tokenloop-0.1.3/tokenloop/stream/sink.py +8 -0
  62. tokenloop-0.1.3/tokenloop/stream/tracing.py +83 -0
  63. tokenloop-0.1.3/tokenloop/tool/__init__.py +0 -0
  64. tokenloop-0.1.3/tokenloop/tool/base.py +57 -0
  65. tokenloop-0.1.3/tokenloop/tool/builtins/__init__.py +0 -0
  66. tokenloop-0.1.3/tokenloop/tool/builtins/bash_tool.py +84 -0
  67. tokenloop-0.1.3/tokenloop/tool/builtins/executor.py +164 -0
  68. tokenloop-0.1.3/tokenloop/tool/builtins/file_store.py +57 -0
  69. tokenloop-0.1.3/tokenloop/tool/builtins/read_tool.py +46 -0
  70. tokenloop-0.1.3/tokenloop/tool/builtins/update_state.py +28 -0
  71. tokenloop-0.1.3/tokenloop/tool/hooks.py +27 -0
  72. tokenloop-0.1.3/tokenloop/tool/registry.py +68 -0
  73. tokenloop-0.1.3/tokenloop/tool/schema.py +79 -0
  74. tokenloop-0.1.3/uv.lock +1169 -0
  75. tokenloop-0.1.0/.python-version +0 -1
  76. tokenloop-0.1.0/PKG-INFO +0 -6
  77. tokenloop-0.1.0/pyproject.toml +0 -14
  78. tokenloop-0.1.0/src/tokenloop/__init__.py +0 -2
  79. /tokenloop-0.1.0/README.md → /tokenloop-0.1.3/tokenloop/compression/__init__.py +0 -0
  80. /tokenloop-0.1.0/src/tokenloop/py.typed → /tokenloop-0.1.3/tokenloop/core/__init__.py +0 -0
@@ -8,3 +8,10 @@ wheels/
8
8
 
9
9
  # Virtual environments
10
10
  .venv
11
+
12
+ # Local secrets / editor tooling
13
+ .env
14
+ .claude/
15
+ .ruff_cache/
16
+ .pytest_cache/
17
+ .pypi.md
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "agno-docs": {
4
+ "type": "http",
5
+ "url": "https://docs.agno.com/mcp"
6
+ }
7
+ }
8
+ }
@@ -0,0 +1,188 @@
1
+ Metadata-Version: 2.5
2
+ Name: tokenloop
3
+ Version: 0.1.3
4
+ Summary: Add your description here
5
+ Project-URL: Homepage, https://github.com/RahulDas-dev/loop
6
+ Project-URL: Repository, https://github.com/RahulDas-dev/loop
7
+ Project-URL: Releases, https://github.com/RahulDas-dev/loop/releases
8
+ Author-email: RahulDas-dev <r.das699@gmail.com>
9
+ Requires-Python: >=3.10
10
+ Requires-Dist: ag-ui-protocol>=0.1.20
11
+ Requires-Dist: anthropic>=0.125.0
12
+ Requires-Dist: mcp>=2.0.0
13
+ Requires-Dist: openai>=2.48.0
14
+ Description-Content-Type: text/markdown
15
+
16
+ # ⚡ tokenloop
17
+
18
+ [![PyPI](https://img.shields.io/pypi/v/tokenloop)](https://pypi.org/project/tokenloop/)
19
+ [![Python](https://img.shields.io/badge/python-3.10%2B-blue)](https://pypi.org/project/tokenloop/)
20
+
21
+ A protocol-driven, model-agnostic agent loop for Python. `AgentLoop` runs
22
+ the prompt → model → tool-call → model loop for you — streaming, retries,
23
+ human-in-the-loop pauses, history compaction, and session persistence
24
+ included — while every swappable piece (provider, tool, guardrail, approver,
25
+ compressor, session store) is a plain `typing.Protocol`, not a base class
26
+ you're forced to inherit from.
27
+
28
+ `tokenloop` is also an active, ongoing **parity port of a companion Rust
29
+ crate** — much of the module layout is deliberately file-for-file with its
30
+ Rust counterpart (`core/loop.py` ↔ `agent/agent.rs`, `core/stream.py` ↔
31
+ `agent/stream.rs`, `core/registry.py` ↔ `run/registry.rs`, and so on), which
32
+ is why you'll find "Python counterpart of ..." in a lot of the source
33
+ docstrings. A few Rust features are deliberately **not** ported yet — see
34
+ [Deliberately not ported](#deliberately-not-ported-yet) below.
35
+
36
+ ## Philosophy
37
+
38
+ - **Protocol-driven, not inheritance-driven.** `LLMProvider`, `Tool`,
39
+ `Guardrail`, `Approver`, `Compressor`, `SessionService`, `ToolHook` — every
40
+ one of these is a structural `Protocol`. Anything with the right shape
41
+ works; you never subclass a framework base class to plug something in.
42
+ - **`AgentLoop` is stateless; `Session` isn't.** Build one `AgentLoop`, reuse
43
+ it for every conversation and every concurrent run. All per-conversation
44
+ state — history, app/user/tmp state, past runs — lives in a `Session`,
45
+ loaded and saved through a `SessionService` keyed by `session_id`. Nothing
46
+ conversational is ever held on the `AgentLoop` object itself.
47
+ - **Two different lifetimes, two different objects.** `RunContext` is the
48
+ *live* object threaded through hooks and tools during one `run()`/
49
+ `astream()` call — it's never persisted. `RunRecord` is what actually gets
50
+ written to `Session.runs` once a run finishes: an immutable summary, not a
51
+ working object.
52
+ - **Every event tells you who and when.** All 20+ event types share one
53
+ `BaseEvent` (`run_id`, `session_id`, `agent_id`, `created_at`) — full run
54
+ lifecycle (`RunStarted`/`RunPaused`/`RunError`/`RunCompleted`), per-turn
55
+ model events, and per-tool-call events, all correlatable.
56
+
57
+ ## Features
58
+
59
+ - **Core loop** — `AgentLoop` runs a real, working turn loop
60
+ (`astream()`; `run()` just drains it and returns the final message):
61
+ automatic retry with exponential backoff on 408/409/429/5xx, best-effort
62
+ history compaction, a `RunControlService` for pausing/resuming/cancelling
63
+ a run by `run_id`, and a working interactive CLI (`agent.as_cli()`) for
64
+ local dev/testing.
65
+ - **Providers** — `LLMProvider` is a `Protocol`; ships with working
66
+ `AnthropicProvider` and `OpenAIProvider` implementations, both lazily
67
+ imported so you only pay for the SDK you actually use.
68
+ - **Tools** — `FunctionTool` builds a JSON Schema straight from a plain
69
+ Python function's signature (stdlib `inspect`/`typing` reflection, no
70
+ Pydantic). Tool calls in one turn dispatch concurrently. Ships with four
71
+ builtin tools: `update_state`, `BashTool`, `Read`, and `FileStore`.
72
+ - **Human-in-the-Loop** — a tool can pause a run for `Confirmation`,
73
+ `InputRequired`, or `ExternalExecution`; an `Approver` can `ALLOW`/`DENY`/
74
+ `AUDIT` any tool call; `RunMode.INTERACTIVE` vs `NON_INTERACTIVE` controls
75
+ whether pausing is even allowed; full resume support built in.
76
+ - **Image input** — attach `ImageBase64`/`ImageUrl` sources to a user turn
77
+ (`image_from_file()` reads and encodes a local file); both providers
78
+ serialize them into their own content-block format, and context
79
+ compaction accounts for their token cost.
80
+ - **Guardrails** — `pre_hooks` validate the user's message *before* the
81
+ model is ever called; `post_hooks` validate the assistant's response.
82
+ Both also get a fire-and-forget `on_completion` for logging/eval that
83
+ never adds latency to the run.
84
+ - **Compression** — `SimpleCompressor` trims oldest messages under a token
85
+ budget (tool-call/result pairing preserved); `HistoryWindow` separately
86
+ caps the *persisted* session history by message or run count.
87
+ - **Skills** — disk-based, compatible with Anthropic's Agent Skills layout
88
+ (a `SKILL.md` per skill); every discovered skill resolves into a single
89
+ `invoke_skill` meta-tool rather than one tool per skill.
90
+ - **MCP** — a client-side bridge (`MCPToolProvider`/`MCPManager`) adapts any
91
+ MCP server's tools onto the same `Tool` protocol as everything else.
92
+ - **Sessions** — `SessionService` protocol, with an `InMemorySessionService`
93
+ included (single-process — bring your own for multi-replica deployments).
94
+ - **State** — a standalone `State` utility with delta tracking and an RFC
95
+ 6902 JSON-Patch `diff()`, for building your own state-sync pipeline
96
+ (not wired into `RunContext`/the AG-UI bridge, which stay plain dicts and
97
+ full-snapshot-only today).
98
+ - **Streaming & observability** — every run emits a typed `LoopEvent`
99
+ stream; an `AGUIBridge` translates it into AG-UI protocol events for
100
+ frontend consumption, and a zero-dependency `SpanCollector` turns it into
101
+ spans for whatever tracing backend you want to export to.
102
+
103
+ ## Installation
104
+
105
+ ```bash
106
+ pip install tokenloop
107
+ ```
108
+
109
+ `ag-ui-protocol`, `anthropic`, `mcp`, and `openai` are currently all
110
+ unconditional dependencies — one install gets you both providers and the
111
+ AG-UI/MCP protocol surface. (No optional extras exist yet.)
112
+
113
+ ## Quickstart
114
+
115
+ ```python
116
+ import asyncio
117
+ import os
118
+
119
+ from tokenloop import AgentLoop
120
+ from tokenloop.provider.anthropic import AnthropicProvider
121
+
122
+
123
+ async def main() -> None:
124
+ agent = AgentLoop(
125
+ AnthropicProvider(model=os.environ["ANTHROPIC_MODEL"]),
126
+ instruction="You are a concise, helpful assistant.",
127
+ )
128
+
129
+ # Interactive REPL, good for trying things out:
130
+ await agent.as_cli(None)
131
+
132
+ # Or drive it programmatically:
133
+ # message = await agent.run("session-1", "run-1", "What's 2 + 2?")
134
+ # print(message.content)
135
+
136
+
137
+ if __name__ == "__main__":
138
+ asyncio.run(main())
139
+ ```
140
+
141
+ Adding a tool is just a plain Python function:
142
+
143
+ ```python
144
+ def get_weather(city: str) -> str:
145
+ """Get the current weather for a city."""
146
+ ...
147
+
148
+ agent.add_tool(get_weather)
149
+ ```
150
+
151
+ See `examples/` for more — tool calling, session state, streaming events,
152
+ and a skills-based agent.
153
+
154
+ ## Testing
155
+
156
+ ```bash
157
+ uv run pytest -v # 35 passed, 0 failed, 0 skipped
158
+ uv run ruff check # all checks pass
159
+ uv run python examples/smoke_test.py # offline, no API key needed
160
+ ```
161
+
162
+ The test suite covers HITL resume (all three pause kinds), a HITL-skipped
163
+ tool not aborting the rest of its batch, `RunControlService`
164
+ (pause/resume/cancel), image serialization for both providers, and
165
+ `State`'s delta-tracking/diff behavior. `examples/smoke_test.py` exercises
166
+ the tool-call round trip and a blocking guardrail end-to-end against a fake
167
+ provider — no network or API key required.
168
+
169
+ ## Documentation
170
+
171
+ There's no separate docs site yet — this README is it, plus:
172
+
173
+ - [`docs/HITL_PARITY.md`](docs/HITL_PARITY.md) — a deep dive into the
174
+ HITL/`RunControlService` design and its parity with the Rust crate.
175
+ - [Repository](https://github.com/RahulDas-dev/loop)
176
+ - [Releases](https://github.com/RahulDas-dev/loop/releases)
177
+
178
+ ## Deliberately not ported (yet)
179
+
180
+ A few things the companion Rust crate has that this port doesn't, by
181
+ explicit scope decision (see the relevant source docstrings):
182
+
183
+ - LLM-summarizing compaction — today's `SimpleCompressor` only trims old
184
+ messages, it doesn't ask the model to summarize them.
185
+ - Hook-triggered HITL pauses — only a tool's own execution body can raise a
186
+ pause today; a `ToolHook.before_call` cannot.
187
+ - Inline skill-script execution sharing the Bash engine.
188
+ - Rust's `{key}`-style prompt templating.
@@ -0,0 +1,173 @@
1
+ # ⚡ tokenloop
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/tokenloop)](https://pypi.org/project/tokenloop/)
4
+ [![Python](https://img.shields.io/badge/python-3.10%2B-blue)](https://pypi.org/project/tokenloop/)
5
+
6
+ A protocol-driven, model-agnostic agent loop for Python. `AgentLoop` runs
7
+ the prompt → model → tool-call → model loop for you — streaming, retries,
8
+ human-in-the-loop pauses, history compaction, and session persistence
9
+ included — while every swappable piece (provider, tool, guardrail, approver,
10
+ compressor, session store) is a plain `typing.Protocol`, not a base class
11
+ you're forced to inherit from.
12
+
13
+ `tokenloop` is also an active, ongoing **parity port of a companion Rust
14
+ crate** — much of the module layout is deliberately file-for-file with its
15
+ Rust counterpart (`core/loop.py` ↔ `agent/agent.rs`, `core/stream.py` ↔
16
+ `agent/stream.rs`, `core/registry.py` ↔ `run/registry.rs`, and so on), which
17
+ is why you'll find "Python counterpart of ..." in a lot of the source
18
+ docstrings. A few Rust features are deliberately **not** ported yet — see
19
+ [Deliberately not ported](#deliberately-not-ported-yet) below.
20
+
21
+ ## Philosophy
22
+
23
+ - **Protocol-driven, not inheritance-driven.** `LLMProvider`, `Tool`,
24
+ `Guardrail`, `Approver`, `Compressor`, `SessionService`, `ToolHook` — every
25
+ one of these is a structural `Protocol`. Anything with the right shape
26
+ works; you never subclass a framework base class to plug something in.
27
+ - **`AgentLoop` is stateless; `Session` isn't.** Build one `AgentLoop`, reuse
28
+ it for every conversation and every concurrent run. All per-conversation
29
+ state — history, app/user/tmp state, past runs — lives in a `Session`,
30
+ loaded and saved through a `SessionService` keyed by `session_id`. Nothing
31
+ conversational is ever held on the `AgentLoop` object itself.
32
+ - **Two different lifetimes, two different objects.** `RunContext` is the
33
+ *live* object threaded through hooks and tools during one `run()`/
34
+ `astream()` call — it's never persisted. `RunRecord` is what actually gets
35
+ written to `Session.runs` once a run finishes: an immutable summary, not a
36
+ working object.
37
+ - **Every event tells you who and when.** All 20+ event types share one
38
+ `BaseEvent` (`run_id`, `session_id`, `agent_id`, `created_at`) — full run
39
+ lifecycle (`RunStarted`/`RunPaused`/`RunError`/`RunCompleted`), per-turn
40
+ model events, and per-tool-call events, all correlatable.
41
+
42
+ ## Features
43
+
44
+ - **Core loop** — `AgentLoop` runs a real, working turn loop
45
+ (`astream()`; `run()` just drains it and returns the final message):
46
+ automatic retry with exponential backoff on 408/409/429/5xx, best-effort
47
+ history compaction, a `RunControlService` for pausing/resuming/cancelling
48
+ a run by `run_id`, and a working interactive CLI (`agent.as_cli()`) for
49
+ local dev/testing.
50
+ - **Providers** — `LLMProvider` is a `Protocol`; ships with working
51
+ `AnthropicProvider` and `OpenAIProvider` implementations, both lazily
52
+ imported so you only pay for the SDK you actually use.
53
+ - **Tools** — `FunctionTool` builds a JSON Schema straight from a plain
54
+ Python function's signature (stdlib `inspect`/`typing` reflection, no
55
+ Pydantic). Tool calls in one turn dispatch concurrently. Ships with four
56
+ builtin tools: `update_state`, `BashTool`, `Read`, and `FileStore`.
57
+ - **Human-in-the-Loop** — a tool can pause a run for `Confirmation`,
58
+ `InputRequired`, or `ExternalExecution`; an `Approver` can `ALLOW`/`DENY`/
59
+ `AUDIT` any tool call; `RunMode.INTERACTIVE` vs `NON_INTERACTIVE` controls
60
+ whether pausing is even allowed; full resume support built in.
61
+ - **Image input** — attach `ImageBase64`/`ImageUrl` sources to a user turn
62
+ (`image_from_file()` reads and encodes a local file); both providers
63
+ serialize them into their own content-block format, and context
64
+ compaction accounts for their token cost.
65
+ - **Guardrails** — `pre_hooks` validate the user's message *before* the
66
+ model is ever called; `post_hooks` validate the assistant's response.
67
+ Both also get a fire-and-forget `on_completion` for logging/eval that
68
+ never adds latency to the run.
69
+ - **Compression** — `SimpleCompressor` trims oldest messages under a token
70
+ budget (tool-call/result pairing preserved); `HistoryWindow` separately
71
+ caps the *persisted* session history by message or run count.
72
+ - **Skills** — disk-based, compatible with Anthropic's Agent Skills layout
73
+ (a `SKILL.md` per skill); every discovered skill resolves into a single
74
+ `invoke_skill` meta-tool rather than one tool per skill.
75
+ - **MCP** — a client-side bridge (`MCPToolProvider`/`MCPManager`) adapts any
76
+ MCP server's tools onto the same `Tool` protocol as everything else.
77
+ - **Sessions** — `SessionService` protocol, with an `InMemorySessionService`
78
+ included (single-process — bring your own for multi-replica deployments).
79
+ - **State** — a standalone `State` utility with delta tracking and an RFC
80
+ 6902 JSON-Patch `diff()`, for building your own state-sync pipeline
81
+ (not wired into `RunContext`/the AG-UI bridge, which stay plain dicts and
82
+ full-snapshot-only today).
83
+ - **Streaming & observability** — every run emits a typed `LoopEvent`
84
+ stream; an `AGUIBridge` translates it into AG-UI protocol events for
85
+ frontend consumption, and a zero-dependency `SpanCollector` turns it into
86
+ spans for whatever tracing backend you want to export to.
87
+
88
+ ## Installation
89
+
90
+ ```bash
91
+ pip install tokenloop
92
+ ```
93
+
94
+ `ag-ui-protocol`, `anthropic`, `mcp`, and `openai` are currently all
95
+ unconditional dependencies — one install gets you both providers and the
96
+ AG-UI/MCP protocol surface. (No optional extras exist yet.)
97
+
98
+ ## Quickstart
99
+
100
+ ```python
101
+ import asyncio
102
+ import os
103
+
104
+ from tokenloop import AgentLoop
105
+ from tokenloop.provider.anthropic import AnthropicProvider
106
+
107
+
108
+ async def main() -> None:
109
+ agent = AgentLoop(
110
+ AnthropicProvider(model=os.environ["ANTHROPIC_MODEL"]),
111
+ instruction="You are a concise, helpful assistant.",
112
+ )
113
+
114
+ # Interactive REPL, good for trying things out:
115
+ await agent.as_cli(None)
116
+
117
+ # Or drive it programmatically:
118
+ # message = await agent.run("session-1", "run-1", "What's 2 + 2?")
119
+ # print(message.content)
120
+
121
+
122
+ if __name__ == "__main__":
123
+ asyncio.run(main())
124
+ ```
125
+
126
+ Adding a tool is just a plain Python function:
127
+
128
+ ```python
129
+ def get_weather(city: str) -> str:
130
+ """Get the current weather for a city."""
131
+ ...
132
+
133
+ agent.add_tool(get_weather)
134
+ ```
135
+
136
+ See `examples/` for more — tool calling, session state, streaming events,
137
+ and a skills-based agent.
138
+
139
+ ## Testing
140
+
141
+ ```bash
142
+ uv run pytest -v # 35 passed, 0 failed, 0 skipped
143
+ uv run ruff check # all checks pass
144
+ uv run python examples/smoke_test.py # offline, no API key needed
145
+ ```
146
+
147
+ The test suite covers HITL resume (all three pause kinds), a HITL-skipped
148
+ tool not aborting the rest of its batch, `RunControlService`
149
+ (pause/resume/cancel), image serialization for both providers, and
150
+ `State`'s delta-tracking/diff behavior. `examples/smoke_test.py` exercises
151
+ the tool-call round trip and a blocking guardrail end-to-end against a fake
152
+ provider — no network or API key required.
153
+
154
+ ## Documentation
155
+
156
+ There's no separate docs site yet — this README is it, plus:
157
+
158
+ - [`docs/HITL_PARITY.md`](docs/HITL_PARITY.md) — a deep dive into the
159
+ HITL/`RunControlService` design and its parity with the Rust crate.
160
+ - [Repository](https://github.com/RahulDas-dev/loop)
161
+ - [Releases](https://github.com/RahulDas-dev/loop/releases)
162
+
163
+ ## Deliberately not ported (yet)
164
+
165
+ A few things the companion Rust crate has that this port doesn't, by
166
+ explicit scope decision (see the relevant source docstrings):
167
+
168
+ - LLM-summarizing compaction — today's `SimpleCompressor` only trims old
169
+ messages, it doesn't ask the model to summarize them.
170
+ - Hook-triggered HITL pauses — only a tool's own execution body can raise a
171
+ pause today; a `ToolHook.before_call` cannot.
172
+ - Inline skill-script execution sharing the Bash engine.
173
+ - Rust's `{key}`-style prompt templating.