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.
- actant-0.1.0/.gitignore +20 -0
- actant-0.1.0/LICENSE +21 -0
- actant-0.1.0/PKG-INFO +383 -0
- actant-0.1.0/README.md +342 -0
- actant-0.1.0/actant/__init__.py +29 -0
- actant-0.1.0/actant/agents.py +64 -0
- actant-0.1.0/actant/core.py +15 -0
- actant-0.1.0/actant/inbox.py +21 -0
- actant-0.1.0/actant/llm/__init__.py +14 -0
- actant-0.1.0/actant/llm/base.py +23 -0
- actant-0.1.0/actant/llm/errors.py +7 -0
- actant-0.1.0/actant/llm/messages.py +156 -0
- actant-0.1.0/actant/llm/providers/__init__.py +45 -0
- actant-0.1.0/actant/llm/providers/_shared.py +112 -0
- actant-0.1.0/actant/llm/providers/anthropic.py +380 -0
- actant-0.1.0/actant/llm/providers/fake.py +60 -0
- actant-0.1.0/actant/llm/providers/gemini.py +330 -0
- actant-0.1.0/actant/llm/providers/openai.py +469 -0
- actant-0.1.0/actant/llm/providers/qwen.py +188 -0
- actant-0.1.0/actant/llm/rate_limit.py +150 -0
- actant-0.1.0/actant/llm/routing.py +76 -0
- actant-0.1.0/actant/py.typed +1 -0
- actant-0.1.0/actant/runtime/__init__.py +52 -0
- actant-0.1.0/actant/runtime/completion.py +31 -0
- actant-0.1.0/actant/runtime/coordinator.py +245 -0
- actant-0.1.0/actant/runtime/events/__init__.py +13 -0
- actant-0.1.0/actant/runtime/events/lifecycle.py +197 -0
- actant-0.1.0/actant/runtime/events/publisher.py +21 -0
- actant-0.1.0/actant/runtime/events/streaming.py +84 -0
- actant-0.1.0/actant/runtime/exceptions.py +46 -0
- actant-0.1.0/actant/runtime/interfaces/__init__.py +23 -0
- actant-0.1.0/actant/runtime/interfaces/session.py +31 -0
- actant-0.1.0/actant/runtime/interfaces/stores.py +158 -0
- actant-0.1.0/actant/runtime/runtime.py +90 -0
- actant-0.1.0/actant/runtime/session.py +278 -0
- actant-0.1.0/actant/runtime/stores/__init__.py +48 -0
- actant-0.1.0/actant/runtime/stores/in_memory.py +299 -0
- actant-0.1.0/actant/runtime/stores/postgres/__init__.py +44 -0
- actant-0.1.0/actant/runtime/stores/postgres/conversion.py +158 -0
- actant-0.1.0/actant/runtime/stores/postgres/models.py +148 -0
- actant-0.1.0/actant/runtime/stores/postgres/stores.py +505 -0
- actant-0.1.0/actant/runtime/temporal/__init__.py +22 -0
- actant-0.1.0/actant/runtime/temporal/activities.py +718 -0
- actant-0.1.0/actant/runtime/temporal/client.py +357 -0
- actant-0.1.0/actant/runtime/temporal/types.py +298 -0
- actant-0.1.0/actant/runtime/temporal/worker.py +63 -0
- actant-0.1.0/actant/runtime/temporal/workflow.py +378 -0
- actant-0.1.0/actant/runtime/types/__init__.py +23 -0
- actant-0.1.0/actant/runtime/types/context.py +18 -0
- actant-0.1.0/actant/runtime/types/session.py +43 -0
- actant-0.1.0/actant/runtime/types/threads.py +71 -0
- actant-0.1.0/actant/tools/__init__.py +40 -0
- actant-0.1.0/actant/tools/admission.py +138 -0
- actant-0.1.0/actant/tools/base.py +106 -0
- actant-0.1.0/actant/tools/calls.py +41 -0
- actant-0.1.0/actant/tools/registry.py +31 -0
- actant-0.1.0/actant/tools/task.py +298 -0
- actant-0.1.0/docs/README.md +41 -0
- actant-0.1.0/docs/actant-runtime-guide.md +177 -0
- actant-0.1.0/docs/architecture.md +303 -0
- actant-0.1.0/docs/concepts.md +132 -0
- actant-0.1.0/docs/coordinator-guide.md +278 -0
- actant-0.1.0/docs/pauses-and-resume.md +93 -0
- actant-0.1.0/docs/releasing.md +47 -0
- actant-0.1.0/docs/subagents.md +100 -0
- actant-0.1.0/docs/tools-guide.md +205 -0
- actant-0.1.0/pyproject.toml +84 -0
- actant-0.1.0/tests/conftest.py +9 -0
- actant-0.1.0/tests/test_admit_execute_infallibility.py +342 -0
- actant-0.1.0/tests/test_coordinator_primitives.py +218 -0
- actant-0.1.0/tests/test_documentation_snippets.py +111 -0
- actant-0.1.0/tests/test_llm_adapters.py +276 -0
- actant-0.1.0/tests/test_message_store.py +227 -0
- actant-0.1.0/tests/test_message_store_parts.py +155 -0
- actant-0.1.0/tests/test_resolve_tool_stale.py +194 -0
- actant-0.1.0/tests/test_run_store_finish_idempotency.py +52 -0
- actant-0.1.0/tests/test_runtime_import_compatibility.py +25 -0
- actant-0.1.0/tests/test_session_multimodal.py +157 -0
- actant-0.1.0/tests/test_sqlalchemy_models.py +83 -0
- actant-0.1.0/tests/test_streaming.py +70 -0
- actant-0.1.0/tests/test_task_tool_per_call.py +152 -0
- actant-0.1.0/tests/test_temporal_client.py +235 -0
- actant-0.1.0/tests/test_temporal_configuration.py +28 -0
- actant-0.1.0/tests/test_tool_call_store.py +217 -0
- actant-0.1.0/tests/test_workflow_cancel_finalization.py +317 -0
- actant-0.1.0/tests/test_workflow_thread.py +713 -0
actant-0.1.0/.gitignore
ADDED
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).
|