actant 0.1.0__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 (86) hide show
  1. actant-0.1.0/.gitignore +20 -0
  2. actant-0.1.0/LICENSE +21 -0
  3. actant-0.1.0/PKG-INFO +383 -0
  4. actant-0.1.0/README.md +342 -0
  5. actant-0.1.0/actant/__init__.py +29 -0
  6. actant-0.1.0/actant/agents.py +64 -0
  7. actant-0.1.0/actant/core.py +15 -0
  8. actant-0.1.0/actant/inbox.py +21 -0
  9. actant-0.1.0/actant/llm/__init__.py +14 -0
  10. actant-0.1.0/actant/llm/base.py +23 -0
  11. actant-0.1.0/actant/llm/errors.py +7 -0
  12. actant-0.1.0/actant/llm/messages.py +156 -0
  13. actant-0.1.0/actant/llm/providers/__init__.py +45 -0
  14. actant-0.1.0/actant/llm/providers/_shared.py +112 -0
  15. actant-0.1.0/actant/llm/providers/anthropic.py +380 -0
  16. actant-0.1.0/actant/llm/providers/fake.py +60 -0
  17. actant-0.1.0/actant/llm/providers/gemini.py +330 -0
  18. actant-0.1.0/actant/llm/providers/openai.py +469 -0
  19. actant-0.1.0/actant/llm/providers/qwen.py +188 -0
  20. actant-0.1.0/actant/llm/rate_limit.py +150 -0
  21. actant-0.1.0/actant/llm/routing.py +76 -0
  22. actant-0.1.0/actant/py.typed +1 -0
  23. actant-0.1.0/actant/runtime/__init__.py +52 -0
  24. actant-0.1.0/actant/runtime/completion.py +31 -0
  25. actant-0.1.0/actant/runtime/coordinator.py +245 -0
  26. actant-0.1.0/actant/runtime/events/__init__.py +13 -0
  27. actant-0.1.0/actant/runtime/events/lifecycle.py +197 -0
  28. actant-0.1.0/actant/runtime/events/publisher.py +21 -0
  29. actant-0.1.0/actant/runtime/events/streaming.py +84 -0
  30. actant-0.1.0/actant/runtime/exceptions.py +46 -0
  31. actant-0.1.0/actant/runtime/interfaces/__init__.py +23 -0
  32. actant-0.1.0/actant/runtime/interfaces/session.py +31 -0
  33. actant-0.1.0/actant/runtime/interfaces/stores.py +158 -0
  34. actant-0.1.0/actant/runtime/runtime.py +90 -0
  35. actant-0.1.0/actant/runtime/session.py +278 -0
  36. actant-0.1.0/actant/runtime/stores/__init__.py +48 -0
  37. actant-0.1.0/actant/runtime/stores/in_memory.py +299 -0
  38. actant-0.1.0/actant/runtime/stores/postgres/__init__.py +44 -0
  39. actant-0.1.0/actant/runtime/stores/postgres/conversion.py +158 -0
  40. actant-0.1.0/actant/runtime/stores/postgres/models.py +148 -0
  41. actant-0.1.0/actant/runtime/stores/postgres/stores.py +505 -0
  42. actant-0.1.0/actant/runtime/temporal/__init__.py +22 -0
  43. actant-0.1.0/actant/runtime/temporal/activities.py +718 -0
  44. actant-0.1.0/actant/runtime/temporal/client.py +357 -0
  45. actant-0.1.0/actant/runtime/temporal/types.py +298 -0
  46. actant-0.1.0/actant/runtime/temporal/worker.py +63 -0
  47. actant-0.1.0/actant/runtime/temporal/workflow.py +378 -0
  48. actant-0.1.0/actant/runtime/types/__init__.py +23 -0
  49. actant-0.1.0/actant/runtime/types/context.py +18 -0
  50. actant-0.1.0/actant/runtime/types/session.py +43 -0
  51. actant-0.1.0/actant/runtime/types/threads.py +71 -0
  52. actant-0.1.0/actant/tools/__init__.py +40 -0
  53. actant-0.1.0/actant/tools/admission.py +138 -0
  54. actant-0.1.0/actant/tools/base.py +106 -0
  55. actant-0.1.0/actant/tools/calls.py +41 -0
  56. actant-0.1.0/actant/tools/registry.py +31 -0
  57. actant-0.1.0/actant/tools/task.py +298 -0
  58. actant-0.1.0/docs/README.md +41 -0
  59. actant-0.1.0/docs/actant-runtime-guide.md +177 -0
  60. actant-0.1.0/docs/architecture.md +303 -0
  61. actant-0.1.0/docs/concepts.md +132 -0
  62. actant-0.1.0/docs/coordinator-guide.md +278 -0
  63. actant-0.1.0/docs/pauses-and-resume.md +93 -0
  64. actant-0.1.0/docs/releasing.md +47 -0
  65. actant-0.1.0/docs/subagents.md +100 -0
  66. actant-0.1.0/docs/tools-guide.md +205 -0
  67. actant-0.1.0/pyproject.toml +84 -0
  68. actant-0.1.0/tests/conftest.py +9 -0
  69. actant-0.1.0/tests/test_admit_execute_infallibility.py +342 -0
  70. actant-0.1.0/tests/test_coordinator_primitives.py +218 -0
  71. actant-0.1.0/tests/test_documentation_snippets.py +111 -0
  72. actant-0.1.0/tests/test_llm_adapters.py +276 -0
  73. actant-0.1.0/tests/test_message_store.py +227 -0
  74. actant-0.1.0/tests/test_message_store_parts.py +155 -0
  75. actant-0.1.0/tests/test_resolve_tool_stale.py +194 -0
  76. actant-0.1.0/tests/test_run_store_finish_idempotency.py +52 -0
  77. actant-0.1.0/tests/test_runtime_import_compatibility.py +25 -0
  78. actant-0.1.0/tests/test_session_multimodal.py +157 -0
  79. actant-0.1.0/tests/test_sqlalchemy_models.py +83 -0
  80. actant-0.1.0/tests/test_streaming.py +70 -0
  81. actant-0.1.0/tests/test_task_tool_per_call.py +152 -0
  82. actant-0.1.0/tests/test_temporal_client.py +235 -0
  83. actant-0.1.0/tests/test_temporal_configuration.py +28 -0
  84. actant-0.1.0/tests/test_tool_call_store.py +217 -0
  85. actant-0.1.0/tests/test_workflow_cancel_finalization.py +317 -0
  86. actant-0.1.0/tests/test_workflow_thread.py +713 -0
@@ -0,0 +1,20 @@
1
+ .venv/
2
+ .env
3
+ .env.*
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ .mypy_cache/
7
+ .pyright/
8
+ .coverage
9
+ htmlcov/
10
+ __pycache__/
11
+ *.py[cod]
12
+ *.egg-info/
13
+ .eggs/
14
+ build/
15
+ dist/
16
+ site/
17
+ .DS_Store
18
+ .idea/
19
+ .vscode/
20
+ *.log
actant-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Johnathan Chiu
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.
actant-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,383 @@
1
+ Metadata-Version: 2.4
2
+ Name: actant
3
+ Version: 0.1.0
4
+ Summary: Temporal-native runtime for persistent agents with durable inboxes, governed tools, and replay
5
+ Project-URL: Homepage, https://github.com/johnathanchiu/actant
6
+ Project-URL: Repository, https://github.com/johnathanchiu/actant
7
+ Project-URL: Issues, https://github.com/johnathanchiu/actant/issues
8
+ Project-URL: Documentation, https://github.com/johnathanchiu/actant/tree/main/docs
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: agents,llm,orchestration,temporal,workflow
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.11
19
+ Requires-Dist: pydantic>=2.0
20
+ Requires-Dist: sqlalchemy>=2.0
21
+ Requires-Dist: temporalio>=1.8
22
+ Provides-Extra: anthropic
23
+ Requires-Dist: anthropic>=0.40; extra == 'anthropic'
24
+ Provides-Extra: dev
25
+ Requires-Dist: pre-commit>=4.0; extra == 'dev'
26
+ Requires-Dist: pyright>=1.1; extra == 'dev'
27
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
28
+ Requires-Dist: pytest>=8.0; extra == 'dev'
29
+ Requires-Dist: ruff>=0.8; extra == 'dev'
30
+ Provides-Extra: gemini
31
+ Requires-Dist: google-genai>=1.0; extra == 'gemini'
32
+ Provides-Extra: openai
33
+ Requires-Dist: openai>=1.60; extra == 'openai'
34
+ Provides-Extra: providers
35
+ Requires-Dist: anthropic>=0.40; extra == 'providers'
36
+ Requires-Dist: google-genai>=1.0; extra == 'providers'
37
+ Requires-Dist: openai>=1.60; extra == 'providers'
38
+ Provides-Extra: qwen
39
+ Requires-Dist: openai>=1.60; extra == 'qwen'
40
+ Description-Content-Type: text/markdown
41
+
42
+ # Actant
43
+
44
+ Actant is a small Python runtime kernel for long-lived agents with
45
+ durable inboxes, governed tools, pause/resume, and replay — built on top of
46
+ [Temporal](https://temporal.io/).
47
+
48
+ The package is intentionally domain-neutral. Applications provide
49
+ agents, tools, domain context, and UI.
50
+
51
+ > Actant is under active development. Public APIs may change before 1.0.
52
+
53
+ **[Read the Actant documentation →](docs/README.md)**
54
+
55
+ ## Why Actant
56
+
57
+ Most agent libraries model one invocation. Actant models a persistent agent
58
+ thread as a durable service:
59
+
60
+ - one Temporal workflow owns each `(agent_id, thread_id)`;
61
+ - messages arrive through a durable inbox, including while work is active;
62
+ - tool admission is explicit: `ALLOW`, `BLOCK`, or `WAIT`;
63
+ - waiting calls can pause for human or external resolution without holding a
64
+ Python process open;
65
+ - threads, runs, messages, and tool calls remain readable through
66
+ application-owned projections;
67
+ - subagents use the same governed tool-call and deferred-resolution model.
68
+
69
+ Temporal owns coordination, stores own readable projections, and applications
70
+ own domain behavior.
71
+
72
+ ## See the Runtime
73
+
74
+ The included viewer demonstrates streaming turns, approvals, multiple-choice
75
+ questions, and two-level subagent delegation. It works without an API key by
76
+ using a deterministic local model:
77
+
78
+ ```bash
79
+ just demo-sync
80
+ just demo
81
+ ```
82
+
83
+ Then open `http://localhost:5173`. See [`examples/`](examples/) for the demo
84
+ architecture and prompts.
85
+
86
+ ## Installation
87
+
88
+ Install Actant with the provider SDKs your application uses:
89
+
90
+ ```bash
91
+ pip install "actant[openai]"
92
+ pip install "actant[anthropic]"
93
+ pip install "actant[gemini]"
94
+ pip install "actant[qwen]"
95
+ ```
96
+
97
+ The provider-neutral runtime can be installed with `pip install actant`.
98
+
99
+ ## Provider Adapters
100
+
101
+ With uv, install only the SDKs you need:
102
+
103
+ ```bash
104
+ uv add --extra openai actant
105
+ uv add --extra anthropic actant
106
+ uv add --extra gemini actant
107
+ uv add --extra qwen actant
108
+ uv add actant
109
+ ```
110
+
111
+ Supported adapters:
112
+
113
+ - `OpenAIProvider` for OpenAI Responses API completions
114
+ - `AnthropicProvider` for Anthropic Messages API completions
115
+ - `GeminiProvider` for Gemini `generate_content`
116
+ - `QwenProvider` for DashScope's OpenAI-compatible endpoint
117
+
118
+ You can route by model prefix:
119
+
120
+ ```python
121
+ import os
122
+
123
+ from actant.llm import llm_for_model
124
+
125
+ llm = llm_for_model(os.environ["ACTANT_MODEL"])
126
+ ```
127
+
128
+ Actant treats model IDs as provider configuration and does not choose a
129
+ "latest" model on your behalf.
130
+
131
+ ## Run an Agent
132
+
133
+ The two runtime objects have different jobs:
134
+
135
+ - `AgentRuntime` is the client facade. APIs and application code use it to
136
+ send messages, cancel threads, resolve waiting tools, and query live state.
137
+ - `TemporalRuntimeWorker` is the execution host. It is a long-running process
138
+ that polls Temporal and performs model calls, tool calls, persistence, and
139
+ hooks.
140
+
141
+ This minimal example runs both roles in one process and prints the agent's
142
+ persisted response. Start Temporal first with `just temporal-up-detached`, then
143
+ run the script:
144
+
145
+ ```python
146
+ import asyncio
147
+ import os
148
+ from contextlib import suppress
149
+ from uuid import uuid4
150
+
151
+ from actant import AgentDefinition
152
+ from actant.llm import llm_for_model
153
+ from actant.llm.messages import Message
154
+ from actant.llm.providers.fake import FakeLLM, FakeResponse
155
+ from actant.runtime import (
156
+ AgentRuntime,
157
+ TemporalRuntimeConfig,
158
+ TemporalRuntimeWorker,
159
+ )
160
+ from actant.runtime.events import AgentThreadHooks
161
+ from actant.runtime.stores import InMemoryRuntimeStores
162
+ from actant.tools import ToolRegistry
163
+
164
+ stores = InMemoryRuntimeStores()
165
+ llm = (
166
+ llm_for_model(os.environ["ACTANT_MODEL"])
167
+ if os.getenv("ACTANT_MODEL")
168
+ else FakeLLM([FakeResponse(text="Hello from Actant.")])
169
+ )
170
+ finished = asyncio.Event()
171
+
172
+
173
+ class ConsoleHooks(AgentThreadHooks):
174
+ async def on_assistant_message(self, message: Message) -> None:
175
+ print(f"Agent: {message.content}")
176
+
177
+ async def on_complete(self, success: bool, reason: str, message: str) -> None:
178
+ finished.set()
179
+
180
+ async def on_error(self, error: Exception) -> None:
181
+ print(f"Agent error: {error}")
182
+ finished.set()
183
+
184
+
185
+ agent = AgentDefinition(
186
+ id="assistant",
187
+ name="Assistant",
188
+ persona="You are a useful assistant.",
189
+ llm=llm,
190
+ tools=ToolRegistry([]),
191
+ )
192
+
193
+ runtime = AgentRuntime(
194
+ stores=stores,
195
+ agents={agent.id: agent},
196
+ temporal=TemporalRuntimeConfig(address="localhost:7233"),
197
+ )
198
+ worker = TemporalRuntimeWorker(
199
+ stores=stores,
200
+ agents={agent.id: agent},
201
+ config=TemporalRuntimeConfig(address="localhost:7233"),
202
+ hooks_factory=lambda _thread: ConsoleHooks(),
203
+ )
204
+
205
+
206
+ async def main() -> None:
207
+ worker_task = asyncio.create_task(worker.run())
208
+ try:
209
+ thread_id = uuid4().hex
210
+ await runtime.send_message(agent.id, thread_id, "hello")
211
+ await asyncio.wait_for(finished.wait(), timeout=60)
212
+ finally:
213
+ worker_task.cancel()
214
+ with suppress(asyncio.CancelledError):
215
+ await worker_task
216
+
217
+
218
+ asyncio.run(main())
219
+ ```
220
+
221
+ Actant represents IDs as strings at the Temporal and storage boundaries. Use
222
+ UUIDs (or another globally unique scheme) for thread and application-generated
223
+ IDs; human-readable strings such as `agent.id` are appropriate for stable
224
+ registered agent names.
225
+
226
+ `send_message()` returns after signaling Temporal; it does not wait for the
227
+ model response. Hooks provide the live completion path, while the message store
228
+ provides the durable reload path. In production, run workers independently from
229
+ API/client processes and use shared durable stores. Temporal load-balances
230
+ workflow and activity tasks across every worker polling the same task queue.
231
+
232
+ ## Execution Anatomy
233
+
234
+ `AgentThreadWorkflow`:
235
+
236
+ - **Agent thread** = the durable, addressable lifetime of the conversation. It remains
237
+ addressable and parks on `wait_condition(inbox)` while idle.
238
+ - **Agent run** = one end-to-end activation caused by draining pending inbound
239
+ messages. It continues until a stop condition is reached.
240
+ - **Agent turn** = one `run_turn` activity invocation: a single model call and
241
+ the assistant output it produces.
242
+ - **Agent loop** = the orchestration algorithm that advances an agent run
243
+ through agent turns and their tool groups. It is behavior, not another
244
+ persisted level in the hierarchy.
245
+ - **Tool fan-out** = parallel `admit_tool` activities, followed by
246
+ `execute_tool` for allowed calls or `await_external_resolution` for
247
+ deferred calls. Deferred calls park as Temporal async activities, not
248
+ workflow signals.
249
+
250
+ ## Cancel + Resolve
251
+
252
+ ```python
253
+ # Cancel an in-flight thread
254
+ await runtime.cancel_thread(agent.id, thread_id)
255
+
256
+ # Resolve a deferred (WAIT) tool call
257
+ await runtime.resolve_deferred_tool_call(
258
+ agent.id,
259
+ thread_id,
260
+ tool_call_id,
261
+ approved=True,
262
+ answer="ok",
263
+ )
264
+
265
+ # Read live state without disturbing the workflow
266
+ state = await runtime.get_state(agent.id, thread_id)
267
+ ```
268
+
269
+ ## Tool Admission
270
+
271
+ Tools can decide whether a requested call can execute immediately. Most
272
+ tools don't define admission logic and run by default. A tool that
273
+ needs approval, consensus, a timer, or another external condition can
274
+ implement `can_execute` and return `allow`, `block`, or `wait`:
275
+
276
+ ```python
277
+ from actant.tools import ToolDecision, ToolWaitRequest
278
+
279
+
280
+ async def can_execute(self, call, invocation, context):
281
+ if await approval_store.approved(call.id):
282
+ return ToolDecision.allow()
283
+ return ToolDecision.wait(
284
+ ToolWaitRequest(
285
+ kind="human_review",
286
+ prompt="waiting for human review",
287
+ payload={"tool_call_id": call.id},
288
+ )
289
+ )
290
+ ```
291
+
292
+ The `admit_tool` activity records WAITING calls and emits an
293
+ `on_tool_waiting` hook. Resolve via `runtime.resolve_deferred_tool_call`, which
294
+ completes the parked Temporal async activity after persisting the
295
+ resolved tool result.
296
+
297
+ ## Runtime Stores
298
+
299
+ `actant.runtime.stores` ships projection-only stores. Coordination
300
+ (durable inbox, single-writer per thread, work scheduling) lives in
301
+ Temporal — these stores hold the readable side of runtime state.
302
+
303
+ In-memory variants for tests/local dev:
304
+ - `InMemoryRuntimeStores` — drop-in for the projection contracts.
305
+
306
+ Postgres backend:
307
+ - `actant.runtime.stores.postgres` — DeclarativeBase models +
308
+ `ACTANT_RUNTIME_METADATA` you can plug into your own Alembic setup.
309
+
310
+ Tables: `actant_threads`, `actant_runs`, `actant_messages`,
311
+ `actant_message_parts`, and `actant_tool_calls`.
312
+
313
+ Applications can implement custom stores against the contracts in
314
+ `actant.runtime.interfaces.stores`.
315
+
316
+ ## Subagents
317
+
318
+ Subagent invocation is represented as a normal tool. Register `TaskTool`
319
+ when an agent is allowed to delegate work:
320
+
321
+ ```python
322
+ from actant.tools import InMemorySubagentRegistry, TaskTool, ToolRegistry
323
+
324
+ registry = InMemorySubagentRegistry({"researcher": researcher_invoker})
325
+ tools = ToolRegistry([TaskTool(invoker=registry)])
326
+ ```
327
+
328
+ The invoker is app-owned, so a subagent can be another Actant
329
+ coordinator, a remote worker, a durable workflow, or a test double.
330
+
331
+ ## Hooks
332
+
333
+ `AgentThreadHooks` exposes async callbacks fired from inside activities
334
+ for live delivery and observability:
335
+
336
+ - user/assistant messages
337
+ - turn start
338
+ - text/thinking deltas (via `StreamListener`)
339
+ - tool calls, tool results, waiting calls, resolved calls
340
+ - completion and errors
341
+
342
+ Hooks announce; they don't write. The canonical state lives in the
343
+ stores. Apps wire hooks to their pubsub/SSE/websocket layer of choice
344
+ via `PublishingThreadHooks` / `PublishingStreamListener` or custom
345
+ implementations.
346
+
347
+ ## Examples
348
+
349
+ See `examples/` for runnable compositions of agents, tools, admission, and
350
+ delegation. `examples/demo/` is the worked FastAPI + React demo.
351
+
352
+ For apps that need multiple agents, `task()`-style delegation, or
353
+ robust state recovery when Temporal and the store diverge, read
354
+ [`docs/coordinator-guide.md`](docs/coordinator-guide.md). The
355
+ `examples/demo/` directory ships a worked example (`DemoCoordinator`)
356
+ that uses the framework's coordinator primitives.
357
+
358
+ ## Documentation
359
+
360
+ Start with the [documentation map](docs/README.md):
361
+
362
+ - [core concepts](docs/concepts.md)
363
+ - [runtime architecture and implementation map](docs/architecture.md)
364
+ - [runtime and deployment](docs/actant-runtime-guide.md)
365
+ - [tools and admission](docs/tools-guide.md)
366
+ - [pauses and deferred work](docs/pauses-and-resume.md)
367
+ - [subagents](docs/subagents.md)
368
+ - [application coordinators](docs/coordinator-guide.md)
369
+
370
+ ## Local Development
371
+
372
+ ```bash
373
+ just sync # install development + provider dependencies
374
+ just temporal-up-detached # start local Temporal stack
375
+ just test # run tests (uses in-memory WorkflowEnvironment)
376
+ just temporal-smoke # full docker round-trip
377
+ ```
378
+
379
+ Release maintainers can follow the [release checklist](docs/releasing.md).
380
+
381
+ ## License
382
+
383
+ Actant is released under the [MIT License](LICENSE).