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.
- {tokenloop-0.1.0 → tokenloop-0.1.3}/.gitignore +7 -0
- tokenloop-0.1.3/.mcp.json +8 -0
- tokenloop-0.1.3/PKG-INFO +188 -0
- tokenloop-0.1.3/README.md +173 -0
- tokenloop-0.1.3/docs/HITL_PARITY.md +303 -0
- tokenloop-0.1.3/examples/basic_chat.py +36 -0
- tokenloop-0.1.3/examples/session_state.py +68 -0
- tokenloop-0.1.3/examples/skills/explain-topic/SKILL.md +108 -0
- tokenloop-0.1.3/examples/skills/explain-topic/references/rust,md +274 -0
- tokenloop-0.1.3/examples/skills/weather/SKILL.md +139 -0
- tokenloop-0.1.3/examples/skills/weather/output_schema.json +61 -0
- tokenloop-0.1.3/examples/skills/weather/scripts/weather.sh +245 -0
- tokenloop-0.1.3/examples/smoke_test.py +97 -0
- tokenloop-0.1.3/examples/streaming.py +49 -0
- tokenloop-0.1.3/examples/tool.py +68 -0
- tokenloop-0.1.3/examples/weather_agent.py +51 -0
- tokenloop-0.1.3/pyproject.toml +91 -0
- tokenloop-0.1.3/tests/test_hitl_resume.py +103 -0
- tokenloop-0.1.3/tests/test_hitl_skipped.py +62 -0
- tokenloop-0.1.3/tests/test_images.py +90 -0
- tokenloop-0.1.3/tests/test_run_control_service.py +72 -0
- tokenloop-0.1.3/tests/test_state.py +58 -0
- tokenloop-0.1.3/tokenloop/__init__.py +181 -0
- tokenloop-0.1.3/tokenloop/compression/base.py +17 -0
- tokenloop-0.1.3/tokenloop/compression/simple.py +49 -0
- tokenloop-0.1.3/tokenloop/compression/window.py +65 -0
- tokenloop-0.1.3/tokenloop/core/cli.py +335 -0
- tokenloop-0.1.3/tokenloop/core/compact.py +90 -0
- tokenloop-0.1.3/tokenloop/core/context.py +94 -0
- tokenloop-0.1.3/tokenloop/core/dispatch.py +305 -0
- tokenloop-0.1.3/tokenloop/core/events.py +211 -0
- tokenloop-0.1.3/tokenloop/core/loop.py +163 -0
- tokenloop-0.1.3/tokenloop/core/prompt.py +76 -0
- tokenloop-0.1.3/tokenloop/core/registry.py +104 -0
- tokenloop-0.1.3/tokenloop/core/retry.py +75 -0
- tokenloop-0.1.3/tokenloop/core/run.py +47 -0
- tokenloop-0.1.3/tokenloop/core/session.py +27 -0
- tokenloop-0.1.3/tokenloop/core/shared.py +133 -0
- tokenloop-0.1.3/tokenloop/core/stream.py +349 -0
- tokenloop-0.1.3/tokenloop/core/types.py +134 -0
- tokenloop-0.1.3/tokenloop/guardrails/__init__.py +0 -0
- tokenloop-0.1.3/tokenloop/guardrails/base.py +24 -0
- tokenloop-0.1.3/tokenloop/hitl/__init__.py +0 -0
- tokenloop-0.1.3/tokenloop/hitl/base.py +122 -0
- tokenloop-0.1.3/tokenloop/mcp/__init__.py +0 -0
- tokenloop-0.1.3/tokenloop/mcp/client.py +41 -0
- tokenloop-0.1.3/tokenloop/mcp/manager.py +36 -0
- tokenloop-0.1.3/tokenloop/provider/__init__.py +0 -0
- tokenloop-0.1.3/tokenloop/provider/anthropic.py +207 -0
- tokenloop-0.1.3/tokenloop/provider/base.py +26 -0
- tokenloop-0.1.3/tokenloop/provider/openai.py +203 -0
- tokenloop-0.1.3/tokenloop/session_service/__init__.py +0 -0
- tokenloop-0.1.3/tokenloop/session_service/base.py +56 -0
- tokenloop-0.1.3/tokenloop/session_service/memory.py +24 -0
- tokenloop-0.1.3/tokenloop/skills/__init__.py +0 -0
- tokenloop-0.1.3/tokenloop/skills/base.py +36 -0
- tokenloop-0.1.3/tokenloop/skills/manager.py +72 -0
- tokenloop-0.1.3/tokenloop/state.py +127 -0
- tokenloop-0.1.3/tokenloop/stream/__init__.py +0 -0
- tokenloop-0.1.3/tokenloop/stream/agui.py +203 -0
- tokenloop-0.1.3/tokenloop/stream/sink.py +8 -0
- tokenloop-0.1.3/tokenloop/stream/tracing.py +83 -0
- tokenloop-0.1.3/tokenloop/tool/__init__.py +0 -0
- tokenloop-0.1.3/tokenloop/tool/base.py +57 -0
- tokenloop-0.1.3/tokenloop/tool/builtins/__init__.py +0 -0
- tokenloop-0.1.3/tokenloop/tool/builtins/bash_tool.py +84 -0
- tokenloop-0.1.3/tokenloop/tool/builtins/executor.py +164 -0
- tokenloop-0.1.3/tokenloop/tool/builtins/file_store.py +57 -0
- tokenloop-0.1.3/tokenloop/tool/builtins/read_tool.py +46 -0
- tokenloop-0.1.3/tokenloop/tool/builtins/update_state.py +28 -0
- tokenloop-0.1.3/tokenloop/tool/hooks.py +27 -0
- tokenloop-0.1.3/tokenloop/tool/registry.py +68 -0
- tokenloop-0.1.3/tokenloop/tool/schema.py +79 -0
- tokenloop-0.1.3/uv.lock +1169 -0
- tokenloop-0.1.0/.python-version +0 -1
- tokenloop-0.1.0/PKG-INFO +0 -6
- tokenloop-0.1.0/pyproject.toml +0 -14
- tokenloop-0.1.0/src/tokenloop/__init__.py +0 -2
- /tokenloop-0.1.0/README.md → /tokenloop-0.1.3/tokenloop/compression/__init__.py +0 -0
- /tokenloop-0.1.0/src/tokenloop/py.typed → /tokenloop-0.1.3/tokenloop/core/__init__.py +0 -0
tokenloop-0.1.3/PKG-INFO
ADDED
|
@@ -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
|
+
[](https://pypi.org/project/tokenloop/)
|
|
19
|
+
[](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
|
+
[](https://pypi.org/project/tokenloop/)
|
|
4
|
+
[](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.
|