xtr-ai 6.6.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 (52) hide show
  1. xtr_ai-6.6.0/LICENSE +21 -0
  2. xtr_ai-6.6.0/PKG-INFO +396 -0
  3. xtr_ai-6.6.0/README.md +351 -0
  4. xtr_ai-6.6.0/pyproject.toml +198 -0
  5. xtr_ai-6.6.0/pyproject.toml.orig +204 -0
  6. xtr_ai-6.6.0/src/xtr_ai/.agents/skills/xtr-ai/SKILL.md +175 -0
  7. xtr_ai-6.6.0/src/xtr_ai/.agents/skills/xtr-ai/references/conversations.md +92 -0
  8. xtr_ai-6.6.0/src/xtr_ai/__init__.py +41 -0
  9. xtr_ai-6.6.0/src/xtr_ai/_platform_model.py +38 -0
  10. xtr_ai-6.6.0/src/xtr_ai/agent_tool.py +53 -0
  11. xtr_ai-6.6.0/src/xtr_ai/bundle/__init__.py +37 -0
  12. xtr_ai-6.6.0/src/xtr_ai/bundle/_agent_builder.py +129 -0
  13. xtr_ai-6.6.0/src/xtr_ai/bundle/_agent_factories.py +73 -0
  14. xtr_ai-6.6.0/src/xtr_ai/bundle/_placeholder.py +17 -0
  15. xtr_ai-6.6.0/src/xtr_ai/bundle/_platform_factories.py +82 -0
  16. xtr_ai-6.6.0/src/xtr_ai/bundle/_references.py +73 -0
  17. xtr_ai-6.6.0/src/xtr_ai/bundle/_store_factories.py +61 -0
  18. xtr_ai-6.6.0/src/xtr_ai/bundle/_tool_wiring.py +57 -0
  19. xtr_ai-6.6.0/src/xtr_ai/bundle/agent_config.py +93 -0
  20. xtr_ai-6.6.0/src/xtr_ai/bundle/ai_bundle.py +250 -0
  21. xtr_ai-6.6.0/src/xtr_ai/bundle/ai_config.py +35 -0
  22. xtr_ai-6.6.0/src/xtr_ai/bundle/conversation_store_configs.py +49 -0
  23. xtr_ai-6.6.0/src/xtr_ai/bundle/model_ref.py +22 -0
  24. xtr_ai-6.6.0/src/xtr_ai/bundle/platform_config.py +48 -0
  25. xtr_ai-6.6.0/src/xtr_ai/bundle/toolset_configs.py +42 -0
  26. xtr_ai-6.6.0/src/xtr_ai/command/__init__.py +9 -0
  27. xtr_ai-6.6.0/src/xtr_ai/command/_registry.py +12 -0
  28. xtr_ai-6.6.0/src/xtr_ai/command/agent_call_command.py +56 -0
  29. xtr_ai-6.6.0/src/xtr_ai/command/debug_ai_command.py +90 -0
  30. xtr_ai-6.6.0/src/xtr_ai/command/platform_invoke_command.py +57 -0
  31. xtr_ai-6.6.0/src/xtr_ai/decorator/__init__.py +16 -0
  32. xtr_ai-6.6.0/src/xtr_ai/decorator/as_tool.py +73 -0
  33. xtr_ai-6.6.0/src/xtr_ai/decorator/service_toolset.py +30 -0
  34. xtr_ai-6.6.0/src/xtr_ai/decorator/tool_declaration.py +26 -0
  35. xtr_ai-6.6.0/src/xtr_ai/decorator/tools_declared_on.py +32 -0
  36. xtr_ai-6.6.0/src/xtr_ai/exception/__init__.py +18 -0
  37. xtr_ai-6.6.0/src/xtr_ai/exception/ai_error.py +14 -0
  38. xtr_ai-6.6.0/src/xtr_ai/exception/invalid_configuration_error.py +18 -0
  39. xtr_ai-6.6.0/src/xtr_ai/exception/platform_unavailable_error.py +33 -0
  40. xtr_ai-6.6.0/src/xtr_ai/platform.py +234 -0
  41. xtr_ai-6.6.0/src/xtr_ai/platform_interface.py +54 -0
  42. xtr_ai-6.6.0/src/xtr_ai/platform_registry.py +125 -0
  43. xtr_ai-6.6.0/src/xtr_ai/platform_registry_interface.py +57 -0
  44. xtr_ai-6.6.0/src/xtr_ai/py.typed +0 -0
  45. xtr_ai-6.6.0/src/xtr_ai/recipe/__init__.py +14 -0
  46. xtr_ai-6.6.0/src/xtr_ai/recipe/files/config/ai.py.tmpl +37 -0
  47. xtr_ai-6.6.0/src/xtr_ai/recipe/manifest.toml +17 -0
  48. xtr_ai-6.6.0/src/xtr_ai/run_clock.py +42 -0
  49. xtr_ai-6.6.0/src/xtr_ai/run_defaults_agent.py +57 -0
  50. xtr_ai-6.6.0/src/xtr_ai/run_logging.py +115 -0
  51. xtr_ai-6.6.0/src/xtr_ai/testing/__init__.py +11 -0
  52. xtr_ai-6.6.0/src/xtr_ai/testing/override_agent_model.py +38 -0
xtr_ai-6.6.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 xterr
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.
xtr_ai-6.6.0/PKG-INFO ADDED
@@ -0,0 +1,396 @@
1
+ Metadata-Version: 2.4
2
+ Name: xtr-ai
3
+ Version: 6.6.0
4
+ Summary: Named language-model platforms and agents built on pydantic-ai, configured once and injected by name, with conversation stores.
5
+ Keywords: ai,llm,agent,pydantic-ai,openai,anthropic,conversation,asyncio
6
+ Author: Xterr
7
+ Author-email: Xterr <me@xterr.dev>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Framework :: AsyncIO
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Typing :: Typed
18
+ Requires-Dist: anyio>=4.0
19
+ Requires-Dist: pydantic-ai-slim>=2.66,<3
20
+ Requires-Dist: typing-extensions>=4.12
21
+ Requires-Dist: xtr-clock>=6.0,<7
22
+ Requires-Dist: xtr-logging-contracts>=6.0,<7
23
+ Requires-Dist: pydantic-ai-slim[ag-ui]>=2.66,<3 ; extra == 'ag-ui'
24
+ Requires-Dist: pydantic-ai-slim[anthropic]>=2.66,<3 ; extra == 'anthropic'
25
+ Requires-Dist: pydantic-ai-slim[bedrock]>=2.66,<3 ; extra == 'bedrock'
26
+ Requires-Dist: pydantic-ai-slim[google]>=2.66,<3 ; extra == 'google'
27
+ Requires-Dist: pydantic-ai-slim[groq]>=2.66,<3 ; extra == 'groq'
28
+ Requires-Dist: pydantic-ai-slim[mcp]>=2.66,<3 ; extra == 'mcp'
29
+ Requires-Dist: pydantic-ai-slim[mistral]>=2.66,<3 ; extra == 'mistral'
30
+ Requires-Dist: pydantic-ai-slim[openai]>=2.66,<3 ; extra == 'openai'
31
+ Requires-Dist: pydantic-ai-slim[openrouter]>=2.66,<3 ; extra == 'openrouter'
32
+ Requires-Dist: xtr-ai-orm-conversation-store>=6.0,<7 ; extra == 'orm'
33
+ Requires-Python: >=3.11
34
+ Provides-Extra: ag-ui
35
+ Provides-Extra: anthropic
36
+ Provides-Extra: bedrock
37
+ Provides-Extra: google
38
+ Provides-Extra: groq
39
+ Provides-Extra: mcp
40
+ Provides-Extra: mistral
41
+ Provides-Extra: openai
42
+ Provides-Extra: openrouter
43
+ Provides-Extra: orm
44
+ Description-Content-Type: text/markdown
45
+
46
+ # xtr-ai
47
+
48
+ Named language-model platforms and agents, built on
49
+ [pydantic-ai](https://ai.pydantic.dev), with conversation stores on the infrastructure an
50
+ application already runs.
51
+
52
+ ## Why?
53
+
54
+ pydantic-ai already does what an agent needs: providers and models, tools and toolsets,
55
+ capabilities, MCP, conversations and their stores. What every application still writes on top of
56
+ it is the same layer: where the keys come from, which model an agent uses in which environment,
57
+ how an agent reaches the application's services, and where conversations are kept. This package
58
+ is that layer, and nothing more.
59
+
60
+ - **Named platforms.** A platform is a provider with its key and endpoint. An agent's model id is
61
+ `<platform>:<model>`, so moving an agent to another provider, region or a local server is a
62
+ configuration change.
63
+ - **Built when used.** No provider SDK is imported and no client is built while the application
64
+ starts, so the console runs without any key.
65
+ - **pydantic-ai's own types.** An application gets an `AbstractAgent` and a `ConversationStore`
66
+ and can still use every part of pydantic-ai directly.
67
+
68
+ ## Install
69
+
70
+ ```sh
71
+ uv add xtr-ai # the core
72
+ uv add "xtr-ai[openai]" # with a provider: openai, anthropic, google, bedrock, mistral, groq, openrouter
73
+ uv add "xtr-ai[orm]" # conversations kept in the database
74
+ ```
75
+
76
+ The provider extras are pydantic-ai's own, under the same names, plus `mcp` and `ag-ui`.
77
+
78
+ ## Quick start
79
+
80
+ Everything works without a container:
81
+
82
+ ```python
83
+ from pydantic_ai import Agent
84
+ from pydantic_ai.capabilities import ResolveModelId
85
+
86
+ from xtr_ai import Platform, PlatformRegistry
87
+
88
+ registry = PlatformRegistry.of(
89
+ Platform("openai", "openai", api_key="sk-..."),
90
+ Platform("local", "openai", base_url="http://localhost:11434/v1", api_key="local"),
91
+ )
92
+ agent = Agent("openai:gpt-5-mini", capabilities=[ResolveModelId(registry.resolve)])
93
+
94
+ result = await agent.run("Hello")
95
+ result = await agent.run("Hello again", model="local:llama-3.3-70b") # another platform, one run
96
+ ```
97
+
98
+ ## Platforms
99
+
100
+ A `Platform` names a provider and how to reach it: `Platform(name, provider, *, api_key=,
101
+ base_url=, options=, model_settings=)`.
102
+
103
+ - **Any pydantic-ai provider.** `provider` is any name pydantic-ai knows (`openai`, `anthropic`,
104
+ `azure`, `bedrock`, `google`, `mistral`, `groq`, `openrouter`, `ollama`, …). A provider added to
105
+ pydantic-ai needs nothing here.
106
+ - **Offline.** `test` builds pydantic-ai's `TestModel` from `options`, and `function` builds a
107
+ `FunctionModel` from `options["function"]` and/or `options["stream_function"]`. Neither needs a
108
+ key.
109
+ - **Options.** `options` are the provider's other constructor keywords (`api_version`, `region`,
110
+ `project`, …), passed unchanged.
111
+ - **OpenAI-compatible servers.** A local server, such as Ollama or vLLM, is `provider="openai"`
112
+ with its `base_url`. The OpenAI SDK refuses an empty key, so give it any non-empty one.
113
+ - **Defaults.** `model_settings` are defaults every model of the platform carries, beneath the
114
+ agent's and the run's settings.
115
+ - **Many of one kind.** Two Azure deployments are two platforms.
116
+ - **Built once.** The provider is built the first time a model is asked for, then shared by
117
+ every model of that platform.
118
+ - **Errors.** A missing extra, an unknown provider or a keyword the provider refuses raises
119
+ `PlatformUnavailableError`, naming the platform but never a key's value.
120
+ - **HTTP.** The provider SDKs own their HTTP clients. `PlatformRegistry.aclose()`, and the
121
+ bundle at shutdown, close those it opened.
122
+
123
+ `PlatformRegistry.resolve` is the resolver for pydantic-ai's `ResolveModelId`: an id whose prefix
124
+ names a platform resolves there, and any other is left to pydantic-ai.
125
+
126
+ Code depending on a platform or the registry takes `PlatformInterface` or
127
+ `PlatformRegistryInterface`. A platform of your own implements `PlatformInterface`: a name, a
128
+ provider, `model(name)`, `built(model)` and `aclose()`. Examples are models from a client you
129
+ already hold, a gateway, or a recorded double. It goes wherever a `Platform` does, including
130
+ `PlatformRegistry.of(...)`.
131
+
132
+ ## Agents
133
+
134
+ With the bundle, an agent is configured, and injected by name (see
135
+ [Kernel / bundle](#kernel--bundle)). Without it, any pydantic-ai agent works with the pieces
136
+ below:
137
+
138
+ | Piece | What it does |
139
+ |---|---|
140
+ | `RunDefaultsAgent(agent, usage_limits=, timeout=)` | Gives every run defaults pydantic-ai only takes per run; a call passing its own wins. An `AbstractAgent`, not an `Agent` |
141
+ | `RunClock(clock)` | Measures a run's `timeout` against an xtr-clock `ClockInterface`. Message timestamps still come from the system clock |
142
+ | `RunLogging(logger, registry)` | Logs each response and each run's end with ids, token counts and finish reasons: never a prompt, an instruction or an output |
143
+ | `agent_toolset(agent, name=)` | Another agent as a tool. Its usage counts toward the caller's |
144
+
145
+ ## Tools
146
+
147
+ Mark a service's methods with `@as_tool`. With the bundle they become tools of the agents named,
148
+ called on the service the container builds:
149
+
150
+ ```python
151
+ from xtr_ai import as_tool
152
+
153
+
154
+ class Weather:
155
+ def __init__(self, client: WeatherClient) -> None:
156
+ self._client = client
157
+
158
+ @as_tool(agents=("chat",))
159
+ async def forecast(self, city: str) -> str:
160
+ """Tomorrow's forecast for a city."""
161
+ return await self._client.forecast(city)
162
+ ```
163
+
164
+ The first parameter may be a pydantic-ai `RunContext`. Without the container, `service_toolset(
165
+ Weather(client), "chat")` returns the same toolset. A tool's service is built once and shared by
166
+ every run, so it must be a singleton. Per-run state belongs in the run's `deps`.
167
+
168
+ ## Conversations
169
+
170
+ pydantic-ai's `ConversationStore` is the port: load a conversation, run, save the result.
171
+
172
+ ```python
173
+ conversation = await store.load(conversation_id) or Conversation(conversation_id=conversation_id)
174
+ result = await agent.run(prompt, conversation=conversation)
175
+ await store.save(result)
176
+ ```
177
+
178
+ | Store | Package | Keeps |
179
+ |---|---|---|
180
+ | pydantic-ai's `InMemoryConversationStore` | here | this process, until it stops |
181
+ | `OrmConversationStore` | [xtr-ai-orm-conversation-store](../xtr-ai-orm-conversation-store) | two tables on an xtr-orm connection |
182
+ | any `ConversationStore` | the application's | wherever it says |
183
+
184
+ Every store but the in-memory one writes in transactions of its own, never as part of the
185
+ caller's. A conversation cannot be saved atomically with the application's own rows.
186
+
187
+ ## Stopping a run
188
+
189
+ Nothing here tracks runs, because stopping one is either free or the application's own concern:
190
+
191
+ - **A run streamed in its own request** stops when the client goes away. The web framework
192
+ cancels the request's task, and pydantic-ai cancels the run and closes the model's stream.
193
+ - **A run in a background job** belongs to the job that runs it: its status, what cancelling it
194
+ means for partial results, and how the request asking for it reaches the worker. Keep a job
195
+ entity with a cancel flag, and check the flag at the run's steps with a capability calling
196
+ `ctx.cancel()`. Give that capability to every run of the job, child agents included: it stops
197
+ only the run it is given to. The guide
198
+ [Stop an agent run](https://github.com/xterr/python-xtr/tree/main/docs/guides/agent-runs.md)
199
+ builds both cases.
200
+ - **Two runs of one conversation** cannot both save: the second save raises pydantic-ai's
201
+ `ConversationConflict`, because a conversation's revision changes on every save.
202
+
203
+ ## Testing
204
+
205
+ - `override_agent_model(container, "chat", FunctionModel(...))` runs a configured agent on a
206
+ scripted model inside an `async with` block, through pydantic-ai's `agent.override`.
207
+ - `test` and `function` platforms run with no key and no network.
208
+ - `InMemoryConversationStore` keeps conversations in the test's process.
209
+
210
+ ## Use in an application
211
+
212
+ Everything adding this package to an application on
213
+ [xtr-dependency-injection](../xtr-dependency-injection) takes, and, read backwards, what removing it
214
+ undoes.
215
+
216
+ - **Install** — `uv add xtr-ai xtr-dependency-injection`: the bundle needs the container
217
+ installed beside it, never as an extra. Add a provider extra, `uv add "xtr-ai[openai]"`, and
218
+ `xtr-ai[orm]` to keep conversations and runs in the database.
219
+ - **Recipe** — `uv run xtr-recipes recipes:sync` does the *Activate* and *Configure* steps below:
220
+ it lists `AiBundle` and writes `<app>/config/packages/ai.py` with an empty `AiConfig`. There
221
+ is no environment to write and nothing to ignore.
222
+ - **Activate** — `AiBundle: {"all": True}` in `BUNDLES` in `<app>/config/bundles.py`, imported
223
+ from `xtr_ai.bundle`. To keep conversations in the database, list `OrmBundle` too: it is never
224
+ activated for you.
225
+ - **Brings along** — the clock, logging and console bundles, each when its package is installed:
226
+ the clock measures run timeouts, logging adds the `ai` channel, and the console adds the
227
+ commands.
228
+ - **Configure** — the recipe writes `<app>/config/packages/ai.py` declaring nothing. Platforms,
229
+ agents and stores go in that `@configure` function returning `AiConfig`. See
230
+ [Kernel / bundle](#kernel--bundle).
231
+ - **Environment** — nothing required: the keys are the application's to name. A platform reads
232
+ its key with `env(...)`, the first time an agent of it runs.
233
+ - **Ignore** — nothing.
234
+ - **Remove** — drop the `BUNDLES` entry. `uv run xtr-recipes recipes:remove xtr-ai` deletes
235
+ `<app>/config/packages/ai.py`, or moves it aside to `ai.py.removed` if you edited it. Then
236
+ `uv remove xtr-ai`. An orm store's tables stay until a migration drops them.
237
+ - **Check** — `debug:bundles` shows `ai` as `listed` and `active`, and `debug:ai` lists the
238
+ platforms and agents.
239
+
240
+ ## Kernel / bundle
241
+
242
+ ```python
243
+ # app/config/bundles.py
244
+ from xtr_ai.bundle import AiBundle
245
+
246
+ BUNDLES = {AiBundle: {"all": True}}
247
+ ```
248
+
249
+ ```python
250
+ # app/config/packages/ai.py
251
+ from pydantic_ai.usage import UsageLimits
252
+ from xtr_dependency_injection import configure, env
253
+
254
+ from xtr_ai.bundle import (
255
+ AgentConfig,
256
+ AiConfig,
257
+ ModelRef,
258
+ OrmConversationStoreConfig,
259
+ PlatformConfig,
260
+ )
261
+
262
+
263
+ @configure
264
+ def ai() -> AiConfig:
265
+ return AiConfig(
266
+ platforms={
267
+ "openai": PlatformConfig(provider="openai", api_key=env("OPENAI_API_KEY")),
268
+ "local": PlatformConfig(
269
+ provider="openai",
270
+ base_url=env("LLM_BASE_URL"),
271
+ api_key=env("LLM_API_KEY", default="local"),
272
+ ),
273
+ "azure_eu": PlatformConfig(
274
+ provider="azure",
275
+ api_key=env("AZURE_EU_KEY"),
276
+ options={
277
+ "azure_endpoint": env("AZURE_EU_URL"),
278
+ "api_version": "2025-01-01-preview",
279
+ },
280
+ ),
281
+ },
282
+ agents={
283
+ "chat": AgentConfig(
284
+ platform=env("AI_CHAT_PLATFORM", default="openai"),
285
+ model="gpt-5-mini",
286
+ fallbacks=(ModelRef("local", "llama-3.3-70b"),),
287
+ instructions_file="%kernel.project_dir%/prompts/chat.md",
288
+ model_settings={"temperature": 0.2},
289
+ deps_type=ChatDeps,
290
+ capabilities=(ToolResultGuard, {"Thinking": {"effort": "low"}}),
291
+ usage_limits=UsageLimits(request_limit=8),
292
+ timeout=180,
293
+ ),
294
+ },
295
+ conversation_stores={"default": OrmConversationStoreConfig(connection="default")},
296
+ )
297
+ ```
298
+
299
+ ### Platforms and agents
300
+
301
+ - **Platform fields.** `PlatformConfig` takes `provider`, `api_key`, `base_url`, `options` and
302
+ `model_settings`, as [Platforms](#platforms) describes. A key never shows in
303
+ `debug:config` or `debug:ai`.
304
+ - **The model.** An agent's `platform` and `model` make its model id. Either may be `env(...)`,
305
+ so the model is chosen per environment. A literal platform is checked at build; one from the
306
+ environment is checked when the agent is first built, and an unknown one raises
307
+ `PlatformUnavailableError`.
308
+ - **Fallbacks.** `fallbacks` become pydantic-ai's `ModelFallback`, tried in order when the model
309
+ fails.
310
+ - **Instructions.** `instructions` holds them inline. `instructions_file` holds a path instead,
311
+ read when the agent is first built.
312
+ - **Passed to pydantic-ai.** `model_settings`, `retries`, `end_strategy`, `tool_timeout`,
313
+ `metadata`, `deps_type` and `output_type` go to pydantic-ai's `Agent.from_spec` as written.
314
+ - **Capabilities.** Each entry is pydantic-ai's declarative form (`"Thinking"`,
315
+ `{"MCP": {"url": ..., "headers": {...}}}`), or a capability class the container builds with
316
+ injection.
317
+ - **Toolsets.** `ToolsetRef(service=)` is a toolset class the container builds. `AgentToolRef(
318
+ agent=, name=, description=)` calls another configured agent as a tool. Agents calling each
319
+ other in a cycle are refused.
320
+ - **Run defaults.** `usage_limits` and `timeout` apply to every run unless the call passes its
321
+ own. Such an agent is a `RunDefaultsAgent`.
322
+ - **Injection.** Inject one agent with `Annotated[AbstractAgent, Target("chat")]`, or all of them
323
+ with `Mapping[Hashable, AbstractAgent]`. Agents are singletons, built on first injection.
324
+ - **Platforms in the container.** Each configured platform is registered under `PlatformInterface`,
325
+ qualified by its name, as in `Annotated[PlatformInterface, Target("openai")]`. The registry is
326
+ `PlatformRegistryInterface`.
327
+ - **Peer bundles.** With the clock bundle, every agent measures its timeout against
328
+ `ClockInterface`. With the logging bundle, every agent logs on the `ai` channel.
329
+
330
+ ### Conversation stores
331
+
332
+ | Config | Registers |
333
+ |---|---|
334
+ | `InMemoryConversationStoreConfig()` | pydantic-ai's in-memory store |
335
+ | `OrmConversationStoreConfig(connection=, table_prefix=)` | `OrmConversationStore` on that connection's primary; needs `xtr-ai[orm]` |
336
+ | `ServiceConversationStoreConfig(service=)` | the application's own store, registered under that class |
337
+
338
+ - **Injection.** A store is injected by name, `Annotated[ConversationStore, Target("archive")]`;
339
+ `"default"` is also injected without one.
340
+ - **Migrations.** The orm store's tables join the connection's definitions, so
341
+ `orm:migrations:diff` writes them.
342
+ - **A missing bridge.** An orm store whose bridge is not installed fails the build, naming
343
+ `uv add "xtr-ai[orm]"`.
344
+ - **The orm bundle.** An orm store also fails the build when `OrmBundle` is not listed, naming
345
+ it. Installing xtr-orm does not activate it.
346
+
347
+ ### Commands
348
+
349
+ With the console bundle:
350
+
351
+ | Command | Does |
352
+ |---|---|
353
+ | `debug:ai` | Lists platforms (provider, whether a key is configured, never its value) and agents (model, fallbacks, capabilities, toolsets) |
354
+ | `ai:platform:invoke <platform> <model> <message>` | Sends one message and prints the answer and the usage |
355
+ | `ai:agent:call <agent>` | Talks to an agent; an empty line ends the conversation, held in memory |
356
+
357
+ ## Errors
358
+
359
+ Every error derives from `AiError`. pydantic-ai's own are raised as pydantic-ai raises them.
360
+
361
+ | Error | Raised when |
362
+ |---|---|
363
+ | `InvalidConfigurationError` | A configuration cannot be used: an unknown platform or agent named literally, a cycle of agents, an orm store without its bridge or without `OrmBundle` listed, a tool for an unknown agent, a capability or tool service that is not a singleton. Also a `ValueError` |
364
+ | `PlatformUnavailableError` | A platform cannot be built: unknown, its extra missing, its key unset, a keyword refused. Names the platform and the variable, never a value |
365
+
366
+ ## Known limitations
367
+
368
+ - **Message timestamps** come from the system clock: pydantic-ai stamps them itself. The clock
369
+ bundle reaches run timeouts only.
370
+ - **Agents are singletons.** Capabilities, toolsets and tool services they are built from must be
371
+ too.
372
+ - **`isinstance(agent, Agent)`** is false for an agent with run defaults: it is a
373
+ `RunDefaultsAgent` wrapping one.
374
+ - **`isinstance(model, OpenAIChatModel)`** is false when the platform sets `model_settings`: the
375
+ model is a `PlatformModel` carrying them, and `model.wrapped` is the provider's own.
376
+ - **A single model request** with `pydantic_ai.direct.model_request` reads no settings from the
377
+ model, unlike an agent run. Pass `model_settings=model.settings` to apply the platform's
378
+ defaults.
379
+ - **Forking while deleting.** With the orm conversation store, a fork saved while its origin is
380
+ being deleted can lose the messages they share, except on SQLite, where writes take turns.
381
+ - **The provider SDKs own HTTP.** No xtr-http-client is used. Timeouts and retries go in a
382
+ platform's or agent's `model_settings`.
383
+
384
+ ## Development
385
+
386
+ Developed in the [python-xtr](https://github.com/xterr/python-xtr) monorepo, under
387
+ `packages/xtr-ai`; run the commands below from there.
388
+
389
+ ```sh
390
+ uv sync
391
+ uv run ruff check && uv run ruff format --check && uv run basedpyright && uv run ty check && uv run pytest
392
+ ```
393
+
394
+ ## License
395
+
396
+ MIT