fred-runtime 2.0.4__tar.gz → 2.0.7__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.
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/PKG-INFO +51 -36
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/README.md +50 -35
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/app/_catalogs.py +31 -1
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/app/agent_app.py +63 -29
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/app/config.py +9 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/cli/completion.py +1 -1
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/common/context_aware_tool.py +35 -2
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/common/kf_workspace_client.py +18 -6
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/deep/deep_runtime.py +0 -3
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/graph/graph_runtime.py +135 -11
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/integrations/v2_runtime/adapters.py +73 -47
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/react/react_prompting.py +25 -38
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/react/react_runtime.py +53 -2
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime.egg-info/PKG-INFO +51 -36
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/pyproject.toml +1 -1
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/tests/test_agent_app.py +186 -2
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/tests/test_config_loader.py +81 -3
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/tests/test_conversational_memory.py +3 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/tests/test_graph_runtime_observability.py +11 -1
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/tests/test_mcp_config.py +33 -1
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/tests/test_model_routing.py +13 -6
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/__init__.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/app/__init__.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/app/config_loader.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/app/container.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/app/context.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/app/dependencies.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/app/mcp_config.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/app/observability_factory.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/app/openai_compat_router.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/cli/__init__.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/cli/entrypoint.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/cli/history_display.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/cli/kpi_display.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/cli/pod_client.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/cli/repl.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/cli/repl_helpers.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/cli/url_helpers.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/client.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/common/__init__.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/common/kf_base_client.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/common/kf_fast_text_client.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/common/kf_http_client.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/common/kf_logs_client.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/common/kf_markdown_media_client.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/common/kf_vectorsearch_client.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/common/mcp_interceptors.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/common/mcp_runtime.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/common/mcp_toolkit.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/common/mcp_utils.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/common/structures.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/common/token_expiry.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/common/tool_node_utils.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/deep/__init__.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/eval/__init__.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/eval/collector.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/graph/__init__.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/integrations/__init__.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/integrations/v2_runtime/__init__.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/model_routing/__init__.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/model_routing/catalog.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/model_routing/contracts.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/model_routing/provider.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/model_routing/resolver.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/react/__init__.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/react/react_langchain_adapter.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/react/react_message_codec.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/react/react_model_adapter.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/react/react_stream_adapter.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/react/react_tool_binding.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/react/react_tool_loop.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/react/react_tool_rendering.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/react/react_tool_resolution.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/react/react_tool_utils.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/react/react_tracing.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/runtime_context.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/runtime_support/__init__.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/runtime_support/checkpoints.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/runtime_support/model_metadata.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/runtime_support/request_context_helpers.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/runtime_support/sql_checkpointer.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/runtime_support/user_token_refresher.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/support/__init__.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/support/filesystem_context.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/support/tool_approval.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime/support/tool_loop.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime.egg-info/SOURCES.txt +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime.egg-info/dependency_links.txt +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime.egg-info/entry_points.txt +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime.egg-info/requires.txt +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/fred_runtime.egg-info/top_level.txt +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/setup.cfg +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/tests/test_client.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/tests/test_context.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/tests/test_eval_collector.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/tests/test_eval_trace.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/tests/test_history.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/tests/test_kf_workspace_client.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/tests/test_kpi_display.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/tests/test_openai_compat_router.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/tests/test_pod_client.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/tests/test_repl_helpers.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/tests/test_smoke.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/tests/test_token_expiry.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/tests/test_url_helpers.py +0 -0
- {fred_runtime-2.0.4 → fred_runtime-2.0.7}/tests/test_user_token_refresher.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fred-runtime
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.7
|
|
4
4
|
Summary: Runtime adapters and infrastructure wiring for Fred v2 agents.
|
|
5
5
|
Author-email: Thales <noreply@thalesgroup.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -58,8 +58,9 @@ fred-runtime Platform adapters + pod factory (this package)
|
|
|
58
58
|
```
|
|
59
59
|
|
|
60
60
|
**Rule of thumb:**
|
|
61
|
-
|
|
62
|
-
- Write
|
|
61
|
+
|
|
62
|
+
- Write agent logic in `fred-sdk`.
|
|
63
|
+
- Write infrastructure adapters (DB, MCP server, Keycloak, object store) in `fred-runtime`.
|
|
63
64
|
- `fred-sdk` must stay importable on a bare laptop with no services running.
|
|
64
65
|
|
|
65
66
|
---
|
|
@@ -79,15 +80,15 @@ app = create_agent_app(registry=REGISTRY, config=config)
|
|
|
79
80
|
|
|
80
81
|
`create_agent_app` returns a FastAPI application that exposes:
|
|
81
82
|
|
|
82
|
-
| Method | Path
|
|
83
|
-
|
|
84
|
-
| `POST` | `{base_url}/agents/execute`
|
|
85
|
-
| `POST` | `{base_url}/agents/execute/stream`
|
|
86
|
-
| `GET` | `{base_url}/agents`
|
|
87
|
-
| `GET` | `{base_url}/agents/sessions`
|
|
88
|
-
| `GET` | `{base_url}/agents/sessions/{id}/messages` | Full conversation history for a session
|
|
89
|
-
| `GET` | `/v1/models`
|
|
90
|
-
| `POST` | `/v1/chat/completions`
|
|
83
|
+
| Method | Path | Description |
|
|
84
|
+
| ------ | ------------------------------------------ | ------------------------------------------------------------------------ |
|
|
85
|
+
| `POST` | `{base_url}/agents/execute` | Single-turn execution — returns final JSON |
|
|
86
|
+
| `POST` | `{base_url}/agents/execute/stream` | Streaming SSE execution — yields `RuntimeEvent` objects |
|
|
87
|
+
| `GET` | `{base_url}/agents` | List registered agent IDs |
|
|
88
|
+
| `GET` | `{base_url}/agents/sessions` | List session IDs for a user |
|
|
89
|
+
| `GET` | `{base_url}/agents/sessions/{id}/messages` | Full conversation history for a session |
|
|
90
|
+
| `GET` | `/v1/models` | OpenAI model list (agent IDs as model names) |
|
|
91
|
+
| `POST` | `/v1/chat/completions` | OpenAI chat completions — works with Open WebUI, openai-python SDK, etc. |
|
|
91
92
|
|
|
92
93
|
The OpenAI-compatible `/v1` surface is **enabled by default**.
|
|
93
94
|
Set `app.openai_compat: false` in `configuration.yaml` to disable it for internal pods.
|
|
@@ -99,11 +100,11 @@ the SQL checkpointer. The session ID is the LangGraph `thread_id`.
|
|
|
99
100
|
|
|
100
101
|
### `fred_runtime.runtime_support` — Infrastructure adapters
|
|
101
102
|
|
|
102
|
-
| Module
|
|
103
|
-
|
|
104
|
-
| `sql_checkpointer`
|
|
105
|
-
| `user_token_refresher`
|
|
106
|
-
| `request_context_helpers` | FastAPI dependency helpers for extracting user/session context
|
|
103
|
+
| Module | What it provides |
|
|
104
|
+
| ------------------------- | -------------------------------------------------------------------------- |
|
|
105
|
+
| `sql_checkpointer` | Durable LangGraph checkpointer backed by SQLite (dev) or PostgreSQL (prod) |
|
|
106
|
+
| `user_token_refresher` | Transparent Keycloak token refresh for long-lived agent sessions |
|
|
107
|
+
| `request_context_helpers` | FastAPI dependency helpers for extracting user/session context |
|
|
107
108
|
|
|
108
109
|
---
|
|
109
110
|
|
|
@@ -119,16 +120,16 @@ Providers: OpenAI, Azure OpenAI, Mistral, Ollama, and any LangChain-compatible b
|
|
|
119
120
|
|
|
120
121
|
HTTP clients that connect agent tools to the Fred platform services:
|
|
121
122
|
|
|
122
|
-
| Client
|
|
123
|
-
|
|
124
|
-
| `kf_http_client`
|
|
125
|
-
| `kf_vectorsearch_client`
|
|
126
|
-
| `kf_markdown_media_client`
|
|
127
|
-
| `kf_workspace_client`
|
|
128
|
-
| `kf_logs_client`
|
|
129
|
-
| `kf_fast_text_client`
|
|
130
|
-
| `mcp_runtime` / `mcp_toolkit` | MCP server lifecycle and tool injection
|
|
131
|
-
| `context_aware_tool`
|
|
123
|
+
| Client | Connects to |
|
|
124
|
+
| ----------------------------- | ----------------------------------------------------------------------- |
|
|
125
|
+
| `kf_http_client` | Knowledge Flow REST API (generic) |
|
|
126
|
+
| `kf_vectorsearch_client` | Vector search / retrieval |
|
|
127
|
+
| `kf_markdown_media_client` | Document content (Markdown + media) |
|
|
128
|
+
| `kf_workspace_client` | Workspace and library management |
|
|
129
|
+
| `kf_logs_client` | Audit log retrieval |
|
|
130
|
+
| `kf_fast_text_client` | FastText classification |
|
|
131
|
+
| `mcp_runtime` / `mcp_toolkit` | MCP server lifecycle and tool injection |
|
|
132
|
+
| `context_aware_tool` | Tool base class that propagates the runtime context (user, team, token) |
|
|
132
133
|
|
|
133
134
|
---
|
|
134
135
|
|
|
@@ -176,9 +177,9 @@ or overridden with `--base-url` / `FRED_AGENT_POD_URL`.
|
|
|
176
177
|
|
|
177
178
|
Every Fred pod uses the same two-file convention:
|
|
178
179
|
|
|
179
|
-
| File
|
|
180
|
-
|
|
181
|
-
| `.env` (path from `ENV_FILE`)
|
|
180
|
+
| File | Purpose |
|
|
181
|
+
| ---------------------------------------------- | ------------------------------------------------------------------ |
|
|
182
|
+
| `.env` (path from `ENV_FILE`) | Secrets: API keys, DB URLs, Keycloak credentials |
|
|
182
183
|
| `configuration.yaml` (path from `CONFIG_FILE`) | App settings: port, base URL, LLM routing, observability, security |
|
|
183
184
|
|
|
184
185
|
Minimal `configuration.yaml` for a local pod:
|
|
@@ -190,6 +191,7 @@ app:
|
|
|
190
191
|
host: "0.0.0.0"
|
|
191
192
|
port: 8010
|
|
192
193
|
log_level: "info"
|
|
194
|
+
limit_concurrency: 200
|
|
193
195
|
metrics_address: "127.0.0.1"
|
|
194
196
|
metrics_port: 9115
|
|
195
197
|
kpi_process_metrics_interval_sec: 10
|
|
@@ -208,6 +210,10 @@ When `observability.metrics: prometheus` is enabled, `create_agent_app(...)`
|
|
|
208
210
|
starts a dedicated Prometheus exporter on `app.metrics_address:app.metrics_port`
|
|
209
211
|
and restores the shared Fred KPI pipeline, including process and SQL pool KPIs.
|
|
210
212
|
|
|
213
|
+
Set `app.limit_concurrency: null` to disable Uvicorn connection limiting, or a
|
|
214
|
+
positive integer to reject excess concurrent HTTP and WebSocket connections
|
|
215
|
+
with `503` before application code runs.
|
|
216
|
+
|
|
211
217
|
---
|
|
212
218
|
|
|
213
219
|
## Installation
|
|
@@ -229,6 +235,7 @@ Requires Python 3.12.
|
|
|
229
235
|
A minimal pod is three files:
|
|
230
236
|
|
|
231
237
|
**`main.py`**
|
|
238
|
+
|
|
232
239
|
```python
|
|
233
240
|
from fred_runtime.app import create_agent_app, load_agent_pod_config
|
|
234
241
|
from myapp.registry import REGISTRY
|
|
@@ -238,19 +245,27 @@ app = create_agent_app(registry=REGISTRY, config=config)
|
|
|
238
245
|
```
|
|
239
246
|
|
|
240
247
|
**`__main__.py`**
|
|
248
|
+
|
|
241
249
|
```python
|
|
242
250
|
import uvicorn
|
|
243
251
|
from fred_runtime.app import load_agent_pod_config
|
|
244
252
|
|
|
245
253
|
def main():
|
|
246
254
|
config = load_agent_pod_config()
|
|
247
|
-
uvicorn.run(
|
|
255
|
+
uvicorn.run(
|
|
256
|
+
"myapp.main:app",
|
|
257
|
+
host=config.app.host,
|
|
258
|
+
port=config.app.port,
|
|
259
|
+
limit_concurrency=config.app.limit_concurrency,
|
|
260
|
+
reload=True,
|
|
261
|
+
)
|
|
248
262
|
|
|
249
263
|
if __name__ == "__main__":
|
|
250
264
|
main()
|
|
251
265
|
```
|
|
252
266
|
|
|
253
267
|
**`registry.py`**
|
|
268
|
+
|
|
254
269
|
```python
|
|
255
270
|
from fred_sdk.contracts.models import ReActAgentDefinition
|
|
256
271
|
|
|
@@ -267,11 +282,11 @@ See [fred-samples](https://github.com/ThalesGroup/fred) for a working reference
|
|
|
267
282
|
|
|
268
283
|
## Related packages
|
|
269
284
|
|
|
270
|
-
| Package
|
|
271
|
-
|
|
272
|
-
| `fred-core`
|
|
273
|
-
| `fred-sdk`
|
|
274
|
-
| `fred-runtime` | [pypi](https://pypi.org/project/fred-runtime/) | This package
|
|
285
|
+
| Package | PyPI | Role |
|
|
286
|
+
| -------------- | ---------------------------------------------- | ----------------------------------------------------------------------------- |
|
|
287
|
+
| `fred-core` | [pypi](https://pypi.org/project/fred-core/) | Pure utilities — logging, model factories, embeddings, portable observability |
|
|
288
|
+
| `fred-sdk` | [pypi](https://pypi.org/project/fred-sdk/) | Agent authoring — ReAct, Graph, tool contracts |
|
|
289
|
+
| `fred-runtime` | [pypi](https://pypi.org/project/fred-runtime/) | This package |
|
|
275
290
|
|
|
276
291
|
---
|
|
277
292
|
|
|
@@ -23,8 +23,9 @@ fred-runtime Platform adapters + pod factory (this package)
|
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
**Rule of thumb:**
|
|
26
|
-
|
|
27
|
-
- Write
|
|
26
|
+
|
|
27
|
+
- Write agent logic in `fred-sdk`.
|
|
28
|
+
- Write infrastructure adapters (DB, MCP server, Keycloak, object store) in `fred-runtime`.
|
|
28
29
|
- `fred-sdk` must stay importable on a bare laptop with no services running.
|
|
29
30
|
|
|
30
31
|
---
|
|
@@ -44,15 +45,15 @@ app = create_agent_app(registry=REGISTRY, config=config)
|
|
|
44
45
|
|
|
45
46
|
`create_agent_app` returns a FastAPI application that exposes:
|
|
46
47
|
|
|
47
|
-
| Method | Path
|
|
48
|
-
|
|
49
|
-
| `POST` | `{base_url}/agents/execute`
|
|
50
|
-
| `POST` | `{base_url}/agents/execute/stream`
|
|
51
|
-
| `GET` | `{base_url}/agents`
|
|
52
|
-
| `GET` | `{base_url}/agents/sessions`
|
|
53
|
-
| `GET` | `{base_url}/agents/sessions/{id}/messages` | Full conversation history for a session
|
|
54
|
-
| `GET` | `/v1/models`
|
|
55
|
-
| `POST` | `/v1/chat/completions`
|
|
48
|
+
| Method | Path | Description |
|
|
49
|
+
| ------ | ------------------------------------------ | ------------------------------------------------------------------------ |
|
|
50
|
+
| `POST` | `{base_url}/agents/execute` | Single-turn execution — returns final JSON |
|
|
51
|
+
| `POST` | `{base_url}/agents/execute/stream` | Streaming SSE execution — yields `RuntimeEvent` objects |
|
|
52
|
+
| `GET` | `{base_url}/agents` | List registered agent IDs |
|
|
53
|
+
| `GET` | `{base_url}/agents/sessions` | List session IDs for a user |
|
|
54
|
+
| `GET` | `{base_url}/agents/sessions/{id}/messages` | Full conversation history for a session |
|
|
55
|
+
| `GET` | `/v1/models` | OpenAI model list (agent IDs as model names) |
|
|
56
|
+
| `POST` | `/v1/chat/completions` | OpenAI chat completions — works with Open WebUI, openai-python SDK, etc. |
|
|
56
57
|
|
|
57
58
|
The OpenAI-compatible `/v1` surface is **enabled by default**.
|
|
58
59
|
Set `app.openai_compat: false` in `configuration.yaml` to disable it for internal pods.
|
|
@@ -64,11 +65,11 @@ the SQL checkpointer. The session ID is the LangGraph `thread_id`.
|
|
|
64
65
|
|
|
65
66
|
### `fred_runtime.runtime_support` — Infrastructure adapters
|
|
66
67
|
|
|
67
|
-
| Module
|
|
68
|
-
|
|
69
|
-
| `sql_checkpointer`
|
|
70
|
-
| `user_token_refresher`
|
|
71
|
-
| `request_context_helpers` | FastAPI dependency helpers for extracting user/session context
|
|
68
|
+
| Module | What it provides |
|
|
69
|
+
| ------------------------- | -------------------------------------------------------------------------- |
|
|
70
|
+
| `sql_checkpointer` | Durable LangGraph checkpointer backed by SQLite (dev) or PostgreSQL (prod) |
|
|
71
|
+
| `user_token_refresher` | Transparent Keycloak token refresh for long-lived agent sessions |
|
|
72
|
+
| `request_context_helpers` | FastAPI dependency helpers for extracting user/session context |
|
|
72
73
|
|
|
73
74
|
---
|
|
74
75
|
|
|
@@ -84,16 +85,16 @@ Providers: OpenAI, Azure OpenAI, Mistral, Ollama, and any LangChain-compatible b
|
|
|
84
85
|
|
|
85
86
|
HTTP clients that connect agent tools to the Fred platform services:
|
|
86
87
|
|
|
87
|
-
| Client
|
|
88
|
-
|
|
89
|
-
| `kf_http_client`
|
|
90
|
-
| `kf_vectorsearch_client`
|
|
91
|
-
| `kf_markdown_media_client`
|
|
92
|
-
| `kf_workspace_client`
|
|
93
|
-
| `kf_logs_client`
|
|
94
|
-
| `kf_fast_text_client`
|
|
95
|
-
| `mcp_runtime` / `mcp_toolkit` | MCP server lifecycle and tool injection
|
|
96
|
-
| `context_aware_tool`
|
|
88
|
+
| Client | Connects to |
|
|
89
|
+
| ----------------------------- | ----------------------------------------------------------------------- |
|
|
90
|
+
| `kf_http_client` | Knowledge Flow REST API (generic) |
|
|
91
|
+
| `kf_vectorsearch_client` | Vector search / retrieval |
|
|
92
|
+
| `kf_markdown_media_client` | Document content (Markdown + media) |
|
|
93
|
+
| `kf_workspace_client` | Workspace and library management |
|
|
94
|
+
| `kf_logs_client` | Audit log retrieval |
|
|
95
|
+
| `kf_fast_text_client` | FastText classification |
|
|
96
|
+
| `mcp_runtime` / `mcp_toolkit` | MCP server lifecycle and tool injection |
|
|
97
|
+
| `context_aware_tool` | Tool base class that propagates the runtime context (user, team, token) |
|
|
97
98
|
|
|
98
99
|
---
|
|
99
100
|
|
|
@@ -141,9 +142,9 @@ or overridden with `--base-url` / `FRED_AGENT_POD_URL`.
|
|
|
141
142
|
|
|
142
143
|
Every Fred pod uses the same two-file convention:
|
|
143
144
|
|
|
144
|
-
| File
|
|
145
|
-
|
|
146
|
-
| `.env` (path from `ENV_FILE`)
|
|
145
|
+
| File | Purpose |
|
|
146
|
+
| ---------------------------------------------- | ------------------------------------------------------------------ |
|
|
147
|
+
| `.env` (path from `ENV_FILE`) | Secrets: API keys, DB URLs, Keycloak credentials |
|
|
147
148
|
| `configuration.yaml` (path from `CONFIG_FILE`) | App settings: port, base URL, LLM routing, observability, security |
|
|
148
149
|
|
|
149
150
|
Minimal `configuration.yaml` for a local pod:
|
|
@@ -155,6 +156,7 @@ app:
|
|
|
155
156
|
host: "0.0.0.0"
|
|
156
157
|
port: 8010
|
|
157
158
|
log_level: "info"
|
|
159
|
+
limit_concurrency: 200
|
|
158
160
|
metrics_address: "127.0.0.1"
|
|
159
161
|
metrics_port: 9115
|
|
160
162
|
kpi_process_metrics_interval_sec: 10
|
|
@@ -173,6 +175,10 @@ When `observability.metrics: prometheus` is enabled, `create_agent_app(...)`
|
|
|
173
175
|
starts a dedicated Prometheus exporter on `app.metrics_address:app.metrics_port`
|
|
174
176
|
and restores the shared Fred KPI pipeline, including process and SQL pool KPIs.
|
|
175
177
|
|
|
178
|
+
Set `app.limit_concurrency: null` to disable Uvicorn connection limiting, or a
|
|
179
|
+
positive integer to reject excess concurrent HTTP and WebSocket connections
|
|
180
|
+
with `503` before application code runs.
|
|
181
|
+
|
|
176
182
|
---
|
|
177
183
|
|
|
178
184
|
## Installation
|
|
@@ -194,6 +200,7 @@ Requires Python 3.12.
|
|
|
194
200
|
A minimal pod is three files:
|
|
195
201
|
|
|
196
202
|
**`main.py`**
|
|
203
|
+
|
|
197
204
|
```python
|
|
198
205
|
from fred_runtime.app import create_agent_app, load_agent_pod_config
|
|
199
206
|
from myapp.registry import REGISTRY
|
|
@@ -203,19 +210,27 @@ app = create_agent_app(registry=REGISTRY, config=config)
|
|
|
203
210
|
```
|
|
204
211
|
|
|
205
212
|
**`__main__.py`**
|
|
213
|
+
|
|
206
214
|
```python
|
|
207
215
|
import uvicorn
|
|
208
216
|
from fred_runtime.app import load_agent_pod_config
|
|
209
217
|
|
|
210
218
|
def main():
|
|
211
219
|
config = load_agent_pod_config()
|
|
212
|
-
uvicorn.run(
|
|
220
|
+
uvicorn.run(
|
|
221
|
+
"myapp.main:app",
|
|
222
|
+
host=config.app.host,
|
|
223
|
+
port=config.app.port,
|
|
224
|
+
limit_concurrency=config.app.limit_concurrency,
|
|
225
|
+
reload=True,
|
|
226
|
+
)
|
|
213
227
|
|
|
214
228
|
if __name__ == "__main__":
|
|
215
229
|
main()
|
|
216
230
|
```
|
|
217
231
|
|
|
218
232
|
**`registry.py`**
|
|
233
|
+
|
|
219
234
|
```python
|
|
220
235
|
from fred_sdk.contracts.models import ReActAgentDefinition
|
|
221
236
|
|
|
@@ -232,11 +247,11 @@ See [fred-samples](https://github.com/ThalesGroup/fred) for a working reference
|
|
|
232
247
|
|
|
233
248
|
## Related packages
|
|
234
249
|
|
|
235
|
-
| Package
|
|
236
|
-
|
|
237
|
-
| `fred-core`
|
|
238
|
-
| `fred-sdk`
|
|
239
|
-
| `fred-runtime` | [pypi](https://pypi.org/project/fred-runtime/) | This package
|
|
250
|
+
| Package | PyPI | Role |
|
|
251
|
+
| -------------- | ---------------------------------------------- | ----------------------------------------------------------------------------- |
|
|
252
|
+
| `fred-core` | [pypi](https://pypi.org/project/fred-core/) | Pure utilities — logging, model factories, embeddings, portable observability |
|
|
253
|
+
| `fred-sdk` | [pypi](https://pypi.org/project/fred-sdk/) | Agent authoring — ReAct, Graph, tool contracts |
|
|
254
|
+
| `fred-runtime` | [pypi](https://pypi.org/project/fred-runtime/) | This package |
|
|
240
255
|
|
|
241
256
|
---
|
|
242
257
|
|
|
@@ -39,7 +39,7 @@ from typing import Any, Literal
|
|
|
39
39
|
|
|
40
40
|
import yaml
|
|
41
41
|
from fred_sdk.contracts.models import MCPServerConfiguration
|
|
42
|
-
from pydantic import BaseModel, ConfigDict, Field
|
|
42
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
43
43
|
|
|
44
44
|
from .config import AgentPodConfig
|
|
45
45
|
|
|
@@ -114,6 +114,36 @@ class _McpCatalog(_CatalogFile):
|
|
|
114
114
|
version: Literal["v1"] = "v1"
|
|
115
115
|
servers: list[MCPServerConfiguration] = Field(default_factory=list)
|
|
116
116
|
|
|
117
|
+
@model_validator(mode="after")
|
|
118
|
+
def _reject_duplicate_server_ids(self) -> "_McpCatalog":
|
|
119
|
+
"""
|
|
120
|
+
Reject duplicate MCP server ids in one catalog.
|
|
121
|
+
|
|
122
|
+
Why this exists:
|
|
123
|
+
- the managed-agent contract now stores per-server config keyed by MCP
|
|
124
|
+
server id, so duplicates would make selection and config resolution
|
|
125
|
+
ambiguous and unsafe
|
|
126
|
+
|
|
127
|
+
How to use it:
|
|
128
|
+
- triggered automatically during `_McpCatalog.model_validate(...)`
|
|
129
|
+
|
|
130
|
+
Example:
|
|
131
|
+
- `load_mcp_catalog("./config/mcp_catalog.yaml")`
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
seen: set[str] = set()
|
|
135
|
+
duplicates: list[str] = []
|
|
136
|
+
for server in self.servers:
|
|
137
|
+
if server.id in seen and server.id not in duplicates:
|
|
138
|
+
duplicates.append(server.id)
|
|
139
|
+
seen.add(server.id)
|
|
140
|
+
if duplicates:
|
|
141
|
+
duplicates_text = ", ".join(repr(server_id) for server_id in duplicates)
|
|
142
|
+
raise ValueError(
|
|
143
|
+
f"Duplicate MCP server id(s) in catalog: {duplicates_text}"
|
|
144
|
+
)
|
|
145
|
+
return self
|
|
146
|
+
|
|
117
147
|
|
|
118
148
|
def _load_yaml_mapping(path: Path) -> dict[str, Any]:
|
|
119
149
|
"""
|
|
@@ -849,7 +849,9 @@ class _ResolvedExecutionTarget:
|
|
|
849
849
|
|
|
850
850
|
|
|
851
851
|
def _apply_runtime_tuning(
|
|
852
|
-
definition: ReActAgentDefinition | GraphAgentDefinition,
|
|
852
|
+
definition: ReActAgentDefinition | GraphAgentDefinition,
|
|
853
|
+
tuning: AgentTuning,
|
|
854
|
+
available_mcp_servers: list[MCPServerConfiguration],
|
|
853
855
|
) -> ReActAgentDefinition | GraphAgentDefinition:
|
|
854
856
|
"""
|
|
855
857
|
Overlay persisted business tuning onto one registered agent template.
|
|
@@ -863,11 +865,11 @@ def _apply_runtime_tuning(
|
|
|
863
865
|
- call after resolving an `agent_instance_id` from control-plane
|
|
864
866
|
|
|
865
867
|
Example:
|
|
866
|
-
- `definition = _apply_runtime_tuning(template_definition, resolution.tuning)`
|
|
868
|
+
- `definition = _apply_runtime_tuning(template_definition, resolution.tuning, catalog)`
|
|
867
869
|
"""
|
|
868
870
|
|
|
869
871
|
mcp_servers = tuning.mcp_servers
|
|
870
|
-
if tuning.selected_mcp_server_ids:
|
|
872
|
+
if tuning.selected_mcp_server_ids is not None:
|
|
871
873
|
selected = frozenset(tuning.selected_mcp_server_ids)
|
|
872
874
|
mcp_servers = [s for s in mcp_servers if s.id in selected]
|
|
873
875
|
|
|
@@ -886,9 +888,25 @@ def _apply_runtime_tuning(
|
|
|
886
888
|
}
|
|
887
889
|
if isinstance(definition, ReActAgentDefinition):
|
|
888
890
|
# Also overlay system_prompt_template directly for ReAct runtime compatibility.
|
|
891
|
+
base_system_prompt = str(getattr(definition, "system_prompt_template", ""))
|
|
892
|
+
effective_system_prompt = base_system_prompt
|
|
889
893
|
system_prompt = tuning.values.get("prompts.system")
|
|
890
894
|
if isinstance(system_prompt, str) and system_prompt.strip():
|
|
891
|
-
|
|
895
|
+
effective_system_prompt = system_prompt
|
|
896
|
+
available_by_id = {server.id: server for server in available_mcp_servers}
|
|
897
|
+
fragments = [
|
|
898
|
+
catalog_entry.agent_instructions.strip()
|
|
899
|
+
for server_ref in mcp_servers
|
|
900
|
+
if (catalog_entry := available_by_id.get(server_ref.id)) is not None
|
|
901
|
+
and isinstance(catalog_entry.agent_instructions, str)
|
|
902
|
+
and catalog_entry.agent_instructions.strip()
|
|
903
|
+
]
|
|
904
|
+
if fragments:
|
|
905
|
+
effective_system_prompt = f"{effective_system_prompt}\n\n" + "\n\n".join(
|
|
906
|
+
fragments
|
|
907
|
+
)
|
|
908
|
+
if effective_system_prompt != base_system_prompt:
|
|
909
|
+
update["system_prompt_template"] = effective_system_prompt
|
|
892
910
|
return definition.model_copy(update=update)
|
|
893
911
|
|
|
894
912
|
|
|
@@ -952,6 +970,7 @@ async def _resolve_agent_instance(
|
|
|
952
970
|
f"Known agents: {list(registry.keys())}",
|
|
953
971
|
)
|
|
954
972
|
if request.inline_tuning:
|
|
973
|
+
available_mcp_servers = _available_mcp_servers_for_definition(definition)
|
|
955
974
|
definition = _apply_runtime_tuning(
|
|
956
975
|
definition,
|
|
957
976
|
AgentTuning(
|
|
@@ -962,6 +981,7 @@ async def _resolve_agent_instance(
|
|
|
962
981
|
mcp_servers=list(definition.default_mcp_servers),
|
|
963
982
|
values=request.inline_tuning,
|
|
964
983
|
),
|
|
984
|
+
available_mcp_servers,
|
|
965
985
|
)
|
|
966
986
|
return _ResolvedExecutionTarget(
|
|
967
987
|
definition=definition,
|
|
@@ -1007,8 +1027,11 @@ async def _resolve_agent_instance(
|
|
|
1007
1027
|
f"Resolved template_agent_id '{resolution.template_agent_id}' is not registered in this pod."
|
|
1008
1028
|
),
|
|
1009
1029
|
)
|
|
1030
|
+
available_mcp_servers = _available_mcp_servers_for_definition(definition)
|
|
1010
1031
|
return _ResolvedExecutionTarget(
|
|
1011
|
-
definition=_apply_runtime_tuning(
|
|
1032
|
+
definition=_apply_runtime_tuning(
|
|
1033
|
+
definition, resolution.tuning, available_mcp_servers
|
|
1034
|
+
),
|
|
1012
1035
|
effective_agent_id=resolution.agent_instance_id,
|
|
1013
1036
|
team_id=resolution.owner_team_id,
|
|
1014
1037
|
)
|
|
@@ -1473,6 +1496,7 @@ def _build_eval_trace(
|
|
|
1473
1496
|
agent_id: str,
|
|
1474
1497
|
session_id: str,
|
|
1475
1498
|
turn_start: float,
|
|
1499
|
+
agent_tags: tuple[str, ...] = (),
|
|
1476
1500
|
) -> EvalTrace:
|
|
1477
1501
|
outcome = _parse_turn_outcome(payloads, turn_start)
|
|
1478
1502
|
steps: list[EvalStep] = []
|
|
@@ -1531,6 +1555,7 @@ def _build_eval_trace(
|
|
|
1531
1555
|
return EvalTrace(
|
|
1532
1556
|
session_id=session_id,
|
|
1533
1557
|
agent_id=agent_id,
|
|
1558
|
+
agent_tags=agent_tags,
|
|
1534
1559
|
input=input_text,
|
|
1535
1560
|
output=outcome.final_content,
|
|
1536
1561
|
error=error,
|
|
@@ -1825,18 +1850,6 @@ async def _iterate_runtime_event_payloads(
|
|
|
1825
1850
|
registry=registry,
|
|
1826
1851
|
access_token=access_token,
|
|
1827
1852
|
)
|
|
1828
|
-
if isinstance(definition, GraphAgentDefinition):
|
|
1829
|
-
runtime: ReActRuntime | GraphRuntime = GraphRuntime(
|
|
1830
|
-
definition=definition,
|
|
1831
|
-
services=services,
|
|
1832
|
-
)
|
|
1833
|
-
else:
|
|
1834
|
-
runtime = ReActRuntime(
|
|
1835
|
-
definition=definition,
|
|
1836
|
-
services=services,
|
|
1837
|
-
)
|
|
1838
|
-
runtime.bind(binding)
|
|
1839
|
-
|
|
1840
1853
|
# session_id drives LangGraph checkpointing: the agent resumes its graph
|
|
1841
1854
|
# state on every turn. Falls back to request_id for one-shot calls so
|
|
1842
1855
|
# LangGraph's checkpointer invariant (thread_id required internally) is met.
|
|
@@ -1847,10 +1860,16 @@ async def _iterate_runtime_event_payloads(
|
|
|
1847
1860
|
invocation_turns=getattr(request, "invocation_turns", ()),
|
|
1848
1861
|
)
|
|
1849
1862
|
|
|
1863
|
+
runtime: ReActRuntime | GraphRuntime | None = None
|
|
1850
1864
|
try:
|
|
1851
|
-
await runtime.activate()
|
|
1852
|
-
executor = await runtime.get_executor()
|
|
1853
1865
|
if isinstance(definition, GraphAgentDefinition):
|
|
1866
|
+
runtime = GraphRuntime(
|
|
1867
|
+
definition=definition,
|
|
1868
|
+
services=services,
|
|
1869
|
+
)
|
|
1870
|
+
runtime.bind(binding)
|
|
1871
|
+
await runtime.activate()
|
|
1872
|
+
executor = await runtime.get_executor()
|
|
1854
1873
|
# Graph agents receive their typed input schema; the agent's
|
|
1855
1874
|
# build_turn_state() maps it to graph state before the first node runs.
|
|
1856
1875
|
# The standard contract is a single "message" field in the input schema.
|
|
@@ -1863,12 +1882,25 @@ async def _iterate_runtime_event_payloads(
|
|
|
1863
1882
|
graph_input = input_cls.model_validate(
|
|
1864
1883
|
{"message": request.message or ""}
|
|
1865
1884
|
)
|
|
1866
|
-
|
|
1885
|
+
async for event in executor.stream(graph_input, execution_config):
|
|
1886
|
+
payload = event.model_dump(mode="json")
|
|
1887
|
+
if not isinstance(payload, dict):
|
|
1888
|
+
raise RuntimeError(
|
|
1889
|
+
"RuntimeEvent payload must serialize to a JSON object."
|
|
1890
|
+
)
|
|
1891
|
+
yield payload
|
|
1867
1892
|
else:
|
|
1893
|
+
runtime = ReActRuntime(
|
|
1894
|
+
definition=definition,
|
|
1895
|
+
services=services,
|
|
1896
|
+
)
|
|
1897
|
+
runtime.bind(binding)
|
|
1898
|
+
await runtime.activate()
|
|
1899
|
+
executor = await runtime.get_executor()
|
|
1868
1900
|
# On HITL resume, messages are ignored by the codec — the graph
|
|
1869
1901
|
# resumes from its checkpointed interrupt via Command(resume=...).
|
|
1870
1902
|
# On a normal turn, the user message is the only input.
|
|
1871
|
-
|
|
1903
|
+
react_input = ReActInput(
|
|
1872
1904
|
messages=(
|
|
1873
1905
|
()
|
|
1874
1906
|
if request.resume_payload is not None
|
|
@@ -1879,20 +1911,21 @@ async def _iterate_runtime_event_payloads(
|
|
|
1879
1911
|
)
|
|
1880
1912
|
),
|
|
1881
1913
|
)
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1914
|
+
async for event in executor.stream(react_input, execution_config):
|
|
1915
|
+
payload = event.model_dump(mode="json")
|
|
1916
|
+
if not isinstance(payload, dict):
|
|
1917
|
+
raise RuntimeError(
|
|
1918
|
+
"RuntimeEvent payload must serialize to a JSON object."
|
|
1919
|
+
)
|
|
1920
|
+
yield payload
|
|
1889
1921
|
except Exception as exc:
|
|
1890
1922
|
logger.exception(
|
|
1891
1923
|
"[fred-runtime] agent execution error agent_id=%s", definition.agent_id
|
|
1892
1924
|
)
|
|
1893
1925
|
yield RuntimeErrorEvent(message=str(exc)).model_dump(mode="json")
|
|
1894
1926
|
finally:
|
|
1895
|
-
|
|
1927
|
+
if runtime is not None:
|
|
1928
|
+
await runtime.dispose()
|
|
1896
1929
|
|
|
1897
1930
|
|
|
1898
1931
|
def _terminal_execute_payload(
|
|
@@ -2632,6 +2665,7 @@ def _build_agent_router(
|
|
|
2632
2665
|
payloads=payloads,
|
|
2633
2666
|
input_text=request.input or "",
|
|
2634
2667
|
agent_id=target.definition.agent_id,
|
|
2668
|
+
agent_tags=target.definition.tags,
|
|
2635
2669
|
session_id=eval_session_id,
|
|
2636
2670
|
turn_start=turn_start,
|
|
2637
2671
|
)
|
|
@@ -110,6 +110,15 @@ class PodAppConfig(BaseModel):
|
|
|
110
110
|
host: str = "127.0.0.1"
|
|
111
111
|
port: int = 8000
|
|
112
112
|
log_level: str = "info"
|
|
113
|
+
limit_concurrency: int | None = Field(
|
|
114
|
+
default=None,
|
|
115
|
+
ge=1,
|
|
116
|
+
description=(
|
|
117
|
+
"Optional maximum number of concurrent HTTP or WebSocket "
|
|
118
|
+
"connections accepted by Uvicorn. Leave unset to disable the "
|
|
119
|
+
"limit."
|
|
120
|
+
),
|
|
121
|
+
)
|
|
113
122
|
gcu_version: str | None = None
|
|
114
123
|
metrics_address: str = "127.0.0.1"
|
|
115
124
|
metrics_port: int = 9000
|
|
@@ -36,7 +36,7 @@ _COMMANDS: tuple[str, ...] = (
|
|
|
36
36
|
"/whoami",
|
|
37
37
|
)
|
|
38
38
|
|
|
39
|
-
# Scenario keywords for fred.
|
|
39
|
+
# Scenario keywords for fred.github.test_assistant — used for /run tab-completion.
|
|
40
40
|
_TEST_ASSISTANT_SCENARIOS: tuple[str, ...] = (
|
|
41
41
|
"echo",
|
|
42
42
|
"error",
|