agent-core-channel 0.9.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.
- agent_core_channel-0.9.0/.gitignore +248 -0
- agent_core_channel-0.9.0/CHANGELOG.md +23 -0
- agent_core_channel-0.9.0/PKG-INFO +11 -0
- agent_core_channel-0.9.0/README.md +60 -0
- agent_core_channel-0.9.0/VERSION +1 -0
- agent_core_channel-0.9.0/pyproject.toml +32 -0
- agent_core_channel-0.9.0/src/agent_core_channel/__init__.py +1 -0
- agent_core_channel-0.9.0/src/agent_core_channel/__main__.py +95 -0
- agent_core_channel-0.9.0/src/agent_core_channel/bus_client.py +80 -0
- agent_core_channel-0.9.0/src/agent_core_channel/config.py +118 -0
- agent_core_channel-0.9.0/src/agent_core_channel/rendering.py +508 -0
- agent_core_channel-0.9.0/src/agent_core_channel/sse_client.py +83 -0
- agent_core_channel-0.9.0/src/agent_core_channel/stdio_server.py +362 -0
- agent_core_channel-0.9.0/src/agent_core_channel/wake_audit.py +89 -0
- agent_core_channel-0.9.0/tests/test_bus_client.py +94 -0
- agent_core_channel-0.9.0/tests/test_cli.py +35 -0
- agent_core_channel-0.9.0/tests/test_config.py +128 -0
- agent_core_channel-0.9.0/tests/test_end_to_end_relay.py +138 -0
- agent_core_channel-0.9.0/tests/test_rendering.py +727 -0
- agent_core_channel-0.9.0/tests/test_rendering_plugin_dispatch.py +153 -0
- agent_core_channel-0.9.0/tests/test_sse_client.py +214 -0
- agent_core_channel-0.9.0/tests/test_stdio_server.py +216 -0
- agent_core_channel-0.9.0/tests/test_stdio_server_inline.py +226 -0
- agent_core_channel-0.9.0/tests/test_wake_audit.py +83 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# Memory compiler runtime state
|
|
2
|
+
memory-compiler/daily/
|
|
3
|
+
memory-compiler/reports/
|
|
4
|
+
memory-compiler/scripts/state.json
|
|
5
|
+
memory-compiler/scripts/last-flush.json
|
|
6
|
+
memory-compiler/scripts/flush.log
|
|
7
|
+
memory-compiler/scripts/compile.log
|
|
8
|
+
memory-compiler/scripts/session-flush-*.md
|
|
9
|
+
memory-compiler/scripts/flush-context-*.md
|
|
10
|
+
|
|
11
|
+
# Handoff writer state (placed alongside handoff files)
|
|
12
|
+
**/handoff-state.json
|
|
13
|
+
|
|
14
|
+
# Git worktrees (superpowers / parallel feature work)
|
|
15
|
+
.worktrees/
|
|
16
|
+
# Native (harness) worktrees
|
|
17
|
+
.claude/worktrees/
|
|
18
|
+
|
|
19
|
+
# Superpowers brainstorm mockups / scratch
|
|
20
|
+
.superpowers/
|
|
21
|
+
|
|
22
|
+
# Agent-core runtime state (handoffs, bus.sqlite, transcripts, etc.)
|
|
23
|
+
.agent-core/
|
|
24
|
+
|
|
25
|
+
# Local scratch / smoke-test artifacts
|
|
26
|
+
.tmp/
|
|
27
|
+
|
|
28
|
+
# Claude Code per-machine local overrides
|
|
29
|
+
.claude/settings.local.json
|
|
30
|
+
.claude/*.lock
|
|
31
|
+
|
|
32
|
+
# Byte-compiled / optimized / DLL files
|
|
33
|
+
__pycache__/
|
|
34
|
+
*.py[codz]
|
|
35
|
+
*$py.class
|
|
36
|
+
|
|
37
|
+
# C extensions
|
|
38
|
+
*.so
|
|
39
|
+
|
|
40
|
+
# Distribution / packaging
|
|
41
|
+
.Python
|
|
42
|
+
build/
|
|
43
|
+
develop-eggs/
|
|
44
|
+
dist/
|
|
45
|
+
downloads/
|
|
46
|
+
eggs/
|
|
47
|
+
.eggs/
|
|
48
|
+
lib/
|
|
49
|
+
lib64/
|
|
50
|
+
parts/
|
|
51
|
+
sdist/
|
|
52
|
+
var/
|
|
53
|
+
wheels/
|
|
54
|
+
share/python-wheels/
|
|
55
|
+
*.egg-info/
|
|
56
|
+
.installed.cfg
|
|
57
|
+
*.egg
|
|
58
|
+
MANIFEST
|
|
59
|
+
|
|
60
|
+
# PyInstaller
|
|
61
|
+
# Usually these files are written by a python script from a template
|
|
62
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
63
|
+
*.manifest
|
|
64
|
+
*.spec
|
|
65
|
+
|
|
66
|
+
# Installer logs
|
|
67
|
+
pip-log.txt
|
|
68
|
+
pip-delete-this-directory.txt
|
|
69
|
+
|
|
70
|
+
# Unit test / coverage reports
|
|
71
|
+
htmlcov/
|
|
72
|
+
.tox/
|
|
73
|
+
.nox/
|
|
74
|
+
.coverage
|
|
75
|
+
.coverage.*
|
|
76
|
+
.cache
|
|
77
|
+
nosetests.xml
|
|
78
|
+
coverage.xml
|
|
79
|
+
*.cover
|
|
80
|
+
*.py.cover
|
|
81
|
+
.hypothesis/
|
|
82
|
+
.pytest_cache/
|
|
83
|
+
cover/
|
|
84
|
+
|
|
85
|
+
# Translations
|
|
86
|
+
*.mo
|
|
87
|
+
*.pot
|
|
88
|
+
|
|
89
|
+
# Django stuff:
|
|
90
|
+
*.log
|
|
91
|
+
local_settings.py
|
|
92
|
+
db.sqlite3
|
|
93
|
+
db.sqlite3-journal
|
|
94
|
+
|
|
95
|
+
# Flask stuff:
|
|
96
|
+
instance/
|
|
97
|
+
.webassets-cache
|
|
98
|
+
|
|
99
|
+
# Scrapy stuff:
|
|
100
|
+
.scrapy
|
|
101
|
+
|
|
102
|
+
# Sphinx documentation
|
|
103
|
+
docs/_build/
|
|
104
|
+
|
|
105
|
+
# PyBuilder
|
|
106
|
+
.pybuilder/
|
|
107
|
+
target/
|
|
108
|
+
|
|
109
|
+
# Jupyter Notebook
|
|
110
|
+
.ipynb_checkpoints
|
|
111
|
+
|
|
112
|
+
# IPython
|
|
113
|
+
profile_default/
|
|
114
|
+
ipython_config.py
|
|
115
|
+
|
|
116
|
+
# pyenv
|
|
117
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
118
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
119
|
+
# .python-version
|
|
120
|
+
|
|
121
|
+
# pipenv
|
|
122
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
123
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
124
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
125
|
+
# install all needed dependencies.
|
|
126
|
+
#Pipfile.lock
|
|
127
|
+
|
|
128
|
+
# UV
|
|
129
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
130
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
131
|
+
# commonly ignored for libraries.
|
|
132
|
+
#uv.lock
|
|
133
|
+
|
|
134
|
+
# poetry
|
|
135
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
136
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
137
|
+
# commonly ignored for libraries.
|
|
138
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
139
|
+
#poetry.lock
|
|
140
|
+
#poetry.toml
|
|
141
|
+
|
|
142
|
+
# pdm
|
|
143
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
144
|
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
145
|
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
146
|
+
#pdm.lock
|
|
147
|
+
#pdm.toml
|
|
148
|
+
.pdm-python
|
|
149
|
+
.pdm-build/
|
|
150
|
+
|
|
151
|
+
# pixi
|
|
152
|
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
153
|
+
#pixi.lock
|
|
154
|
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
155
|
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
156
|
+
.pixi
|
|
157
|
+
|
|
158
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
159
|
+
__pypackages__/
|
|
160
|
+
|
|
161
|
+
# Celery stuff
|
|
162
|
+
celerybeat-schedule
|
|
163
|
+
celerybeat.pid
|
|
164
|
+
|
|
165
|
+
# SageMath parsed files
|
|
166
|
+
*.sage.py
|
|
167
|
+
|
|
168
|
+
# Environments
|
|
169
|
+
.env
|
|
170
|
+
.envrc
|
|
171
|
+
.venv
|
|
172
|
+
env/
|
|
173
|
+
venv/
|
|
174
|
+
ENV/
|
|
175
|
+
env.bak/
|
|
176
|
+
venv.bak/
|
|
177
|
+
# ...but agent_core.venv is a SOURCE package, not a virtualenv — don't ignore it.
|
|
178
|
+
!packages/core/src/agent_core/venv/
|
|
179
|
+
!packages/core/src/agent_core/venv/**
|
|
180
|
+
|
|
181
|
+
# Spyder project settings
|
|
182
|
+
.spyderproject
|
|
183
|
+
.spyproject
|
|
184
|
+
|
|
185
|
+
# Rope project settings
|
|
186
|
+
.ropeproject
|
|
187
|
+
|
|
188
|
+
# mkdocs documentation
|
|
189
|
+
/site
|
|
190
|
+
|
|
191
|
+
# mypy
|
|
192
|
+
.mypy_cache/
|
|
193
|
+
.dmypy.json
|
|
194
|
+
dmypy.json
|
|
195
|
+
|
|
196
|
+
# Pyre type checker
|
|
197
|
+
.pyre/
|
|
198
|
+
|
|
199
|
+
# pytype static type analyzer
|
|
200
|
+
.pytype/
|
|
201
|
+
|
|
202
|
+
# Cython debug symbols
|
|
203
|
+
cython_debug/
|
|
204
|
+
|
|
205
|
+
# PyCharm
|
|
206
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
207
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
208
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
209
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
210
|
+
#.idea/
|
|
211
|
+
|
|
212
|
+
# Abstra
|
|
213
|
+
# Abstra is an AI-powered process automation framework.
|
|
214
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
215
|
+
# Learn more at https://abstra.io/docs
|
|
216
|
+
.abstra/
|
|
217
|
+
|
|
218
|
+
# Visual Studio Code
|
|
219
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
220
|
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
221
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
222
|
+
# you could uncomment the following to ignore the entire vscode folder
|
|
223
|
+
# .vscode/
|
|
224
|
+
|
|
225
|
+
# Ruff stuff:
|
|
226
|
+
.ruff_cache/
|
|
227
|
+
|
|
228
|
+
# PyPI configuration file
|
|
229
|
+
.pypirc
|
|
230
|
+
|
|
231
|
+
# Cursor
|
|
232
|
+
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
|
|
233
|
+
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
|
234
|
+
# refer to https://docs.cursor.com/context/ignore-files
|
|
235
|
+
.cursorignore
|
|
236
|
+
.cursorindexingignore
|
|
237
|
+
|
|
238
|
+
# Marimo
|
|
239
|
+
marimo/_static/
|
|
240
|
+
marimo/_lsp/
|
|
241
|
+
__marimo__/
|
|
242
|
+
.gstack/
|
|
243
|
+
|
|
244
|
+
# macOS Finder metadata (occasionally sneaks in via vendored sources)
|
|
245
|
+
.DS_Store
|
|
246
|
+
|
|
247
|
+
# Operational docs that must not live in source control
|
|
248
|
+
pepper-vault-plan.txt
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.8.2](https://github.com/jeffrichley/agent_core/compare/v0.8.1...agent-core-channel-v0.8.2) (2026-08-03)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* republish package family to complete rate-limited first publish ([#477](https://github.com/jeffrichley/agent_core/issues/477)) ([32977ac](https://github.com/jeffrichley/agent_core/commit/32977ac78da4a15553329bb4e1fdaa9e894834ad))
|
|
9
|
+
|
|
10
|
+
## [0.8.1](https://github.com/jeffrichley/agent_core/compare/v0.8.0...agent-core-channel-v0.8.1) (2026-07-22)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* republish package family to complete rate-limited first publish ([#477](https://github.com/jeffrichley/agent_core/issues/477)) ([32977ac](https://github.com/jeffrichley/agent_core/commit/32977ac78da4a15553329bb4e1fdaa9e894834ad))
|
|
16
|
+
|
|
17
|
+
## [0.8.0](https://github.com/jeffrichley/agent_core/compare/v0.7.0...agent-core-channel-v0.8.0) (2026-07-21)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Features
|
|
21
|
+
|
|
22
|
+
* **hatchery:** ungendered templates + .mcp.json generation ([#81](https://github.com/jeffrichley/agent_core/issues/81)/[#82](https://github.com/jeffrichley/agent_core/issues/82)) ([#357](https://github.com/jeffrichley/agent_core/issues/357)) ([f48762d](https://github.com/jeffrichley/agent_core/commit/f48762df78eb12cfa9524e9b5a277741534ea8a7)), closes [#311](https://github.com/jeffrichley/agent_core/issues/311)
|
|
23
|
+
* **release:** configure release-please for 12-package synchronized version train ([#428](https://github.com/jeffrichley/agent_core/issues/428)) ([c1b4097](https://github.com/jeffrichley/agent_core/commit/c1b409765c61d09a6657b0e8b5e43a27cb235b3e))
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agent-core-channel
|
|
3
|
+
Version: 0.9.0
|
|
4
|
+
Summary: Stdio MCP channel relay — bridges agent-core daemon notifications to Claude Code's wake mechanism.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.12
|
|
7
|
+
Requires-Dist: anyio>=4.0
|
|
8
|
+
Requires-Dist: httpx>=0.27
|
|
9
|
+
Requires-Dist: mcp>=1.0
|
|
10
|
+
Requires-Dist: pyyaml>=6.0
|
|
11
|
+
Requires-Dist: typer>=0.12
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# agent-core-channel
|
|
2
|
+
|
|
3
|
+
`agent-core-channel` is a stdio MCP channel relay for Claude Code. It connects
|
|
4
|
+
to the agent_core daemon's `/notify/<agent>` SSE stream and re-emits each event
|
|
5
|
+
as `notifications/claude/channel` so Claude Code can wake autonomously when
|
|
6
|
+
mail arrives on the bus.
|
|
7
|
+
|
|
8
|
+
Claude Code still uses the daemon's HTTP MCP endpoint for tools such as
|
|
9
|
+
`send`, `list_pending`, and `handle`. This package only relays notifications.
|
|
10
|
+
|
|
11
|
+
## Claude Code Configuration
|
|
12
|
+
|
|
13
|
+
On Windows in this workspace, use the workspace venv executable for now:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"mcpServers": {
|
|
18
|
+
"agent-core": {
|
|
19
|
+
"type": "http",
|
|
20
|
+
"url": "http://localhost:8788/mcp/agent-testbot"
|
|
21
|
+
},
|
|
22
|
+
"agent-core-channel": {
|
|
23
|
+
"command": "E:\\workspaces\\ai\\agents\\agent_core\\.venv\\Scripts\\agent-core-channel.exe",
|
|
24
|
+
"args": ["--agent", "agent-testbot"]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Launch Claude Code with the development channel enabled:
|
|
31
|
+
|
|
32
|
+
```powershell
|
|
33
|
+
claude --dangerously-load-development-channels server:agent-core-channel
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Windows Development Caveat
|
|
37
|
+
|
|
38
|
+
As of the 2026-04-30 live validation, `uv tool install --from
|
|
39
|
+
packages/agent-core-channel agent-core-channel` is not reliable on Windows in
|
|
40
|
+
this workspace. The installed tool environment includes `pywin32` wheels, but
|
|
41
|
+
the post-install step that stages `_win32sysloader.pyd` does not run, and the
|
|
42
|
+
MCP SDK's Windows utility import can fail at startup.
|
|
43
|
+
|
|
44
|
+
For non-Windows installs, or once the Windows packaging issue is fixed, a
|
|
45
|
+
PATH-resolved binary is the intended shape:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"mcpServers": {
|
|
50
|
+
"agent-core-channel": {
|
|
51
|
+
"command": "agent-core-channel",
|
|
52
|
+
"args": ["--agent", "agent-testbot"]
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Do not switch the testbot config back to a PATH-resolved `uv tool install`
|
|
59
|
+
binary until the `pywin32` packaging issue is fixed or the install path moves
|
|
60
|
+
to a wrapper that runs the required post-install setup.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.8.2
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "agent-core-channel"
|
|
3
|
+
dynamic = ["version"]
|
|
4
|
+
description = "Stdio MCP channel relay — bridges agent-core daemon notifications to Claude Code's wake mechanism."
|
|
5
|
+
license = "MIT"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"mcp>=1.0",
|
|
9
|
+
"anyio>=4.0",
|
|
10
|
+
"httpx>=0.27",
|
|
11
|
+
"typer>=0.12",
|
|
12
|
+
"pyyaml>=6.0",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
agent-core-channel = "agent_core_channel.__main__:app"
|
|
17
|
+
|
|
18
|
+
[build-system]
|
|
19
|
+
requires = ["hatchling", "uv-dynamic-versioning"]
|
|
20
|
+
build-backend = "hatchling.build"
|
|
21
|
+
|
|
22
|
+
[tool.hatch.build.targets.wheel]
|
|
23
|
+
packages = ["src/agent_core_channel"]
|
|
24
|
+
|
|
25
|
+
[tool.hatch.version]
|
|
26
|
+
source = "uv-dynamic-versioning"
|
|
27
|
+
|
|
28
|
+
[tool.uv-dynamic-versioning]
|
|
29
|
+
fallback-version = "0.0.0"
|
|
30
|
+
|
|
31
|
+
[tool.uv]
|
|
32
|
+
cache-keys = [{ file = "pyproject.toml" }, { git = { commit = true, tags = true } }]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""agent-core-channel — stdio MCP relay for the agent-core daemon."""
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Typer CLI entry point for agent-core-channel.
|
|
2
|
+
|
|
3
|
+
Parses --agent, --daemon-url, and the inline-content config flags
|
|
4
|
+
(issue #70), resolves layered RelayConfig (CLI > env > YAML > defaults),
|
|
5
|
+
then hands off to run_relay().
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from types import SimpleNamespace
|
|
13
|
+
|
|
14
|
+
import anyio
|
|
15
|
+
import typer
|
|
16
|
+
|
|
17
|
+
app = typer.Typer(add_completion=False)
|
|
18
|
+
|
|
19
|
+
_DEFAULT_CONFIG_PATH = Path.home() / ".agent-core" / "agent_core.yaml"
|
|
20
|
+
_CONFIG_PATH_OPTION = typer.Option(
|
|
21
|
+
_DEFAULT_CONFIG_PATH,
|
|
22
|
+
"--config-path",
|
|
23
|
+
help="Path to agent_core.yaml (for the channel_relay block).",
|
|
24
|
+
)
|
|
25
|
+
_INLINE_MODE_OPTION = typer.Option(
|
|
26
|
+
None,
|
|
27
|
+
"--inline-mode",
|
|
28
|
+
help="Override channel_relay.inline_mode (full|preview|disabled).",
|
|
29
|
+
)
|
|
30
|
+
_INLINE_MAX_ENVELOPES_OPTION = typer.Option(
|
|
31
|
+
None,
|
|
32
|
+
"--inline-max-envelopes",
|
|
33
|
+
help="Override channel_relay.max_envelopes.",
|
|
34
|
+
)
|
|
35
|
+
_INLINE_MAX_BYTES_OPTION = typer.Option(
|
|
36
|
+
None,
|
|
37
|
+
"--inline-max-bytes",
|
|
38
|
+
help="Override channel_relay.max_bytes.",
|
|
39
|
+
)
|
|
40
|
+
_INLINE_PER_ENVELOPE_BYTES_OPTION = typer.Option(
|
|
41
|
+
None,
|
|
42
|
+
"--inline-per-envelope-bytes",
|
|
43
|
+
help="Override channel_relay.per_envelope_bytes.",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@app.command()
|
|
48
|
+
def main(
|
|
49
|
+
agent: str = typer.Option(..., "--agent", help="Agent name on the bus."),
|
|
50
|
+
daemon_url: str = typer.Option(
|
|
51
|
+
"http://127.0.0.1:8788",
|
|
52
|
+
"--daemon-url",
|
|
53
|
+
help="agent-core daemon URL (default: http://127.0.0.1:8788).",
|
|
54
|
+
),
|
|
55
|
+
config_path: Path = _CONFIG_PATH_OPTION,
|
|
56
|
+
inline_mode: str | None = _INLINE_MODE_OPTION,
|
|
57
|
+
inline_max_envelopes: int | None = _INLINE_MAX_ENVELOPES_OPTION,
|
|
58
|
+
inline_max_bytes: int | None = _INLINE_MAX_BYTES_OPTION,
|
|
59
|
+
inline_per_envelope_bytes: int | None = _INLINE_PER_ENVELOPE_BYTES_OPTION,
|
|
60
|
+
) -> None:
|
|
61
|
+
"""Run the agent-core stdio channel relay."""
|
|
62
|
+
from agent_core.plugins.manager import create_plugin_manager, get_envelope_renderers
|
|
63
|
+
from agent_core_channel.config import load_config
|
|
64
|
+
from agent_core_channel.rendering import set_plugin_renderers
|
|
65
|
+
from agent_core_channel.stdio_server import run_relay
|
|
66
|
+
|
|
67
|
+
# Wire plugin-registered envelope renderers into the rendering dispatch
|
|
68
|
+
# table. agent_core PR #124 added the capability + dispatch but deferred
|
|
69
|
+
# the bootstrap wiring; without this call, plugin renderers like
|
|
70
|
+
# pepper-roots' `Desire` renderer never load and dispatch falls back to
|
|
71
|
+
# JSON. Caught by Pepper's behavioral round-trip 2026-05-25.
|
|
72
|
+
pm = create_plugin_manager()
|
|
73
|
+
set_plugin_renderers(get_envelope_renderers(pm))
|
|
74
|
+
|
|
75
|
+
cli_args = SimpleNamespace(
|
|
76
|
+
inline_mode=inline_mode,
|
|
77
|
+
inline_max_envelopes=inline_max_envelopes,
|
|
78
|
+
inline_max_bytes=inline_max_bytes,
|
|
79
|
+
inline_per_envelope_bytes=inline_per_envelope_bytes,
|
|
80
|
+
)
|
|
81
|
+
config = load_config(
|
|
82
|
+
agent=agent,
|
|
83
|
+
config_path=config_path,
|
|
84
|
+
cli_args=cli_args,
|
|
85
|
+
env=os.environ,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
async def _run() -> None:
|
|
89
|
+
await run_relay(agent, daemon_url, config=config)
|
|
90
|
+
|
|
91
|
+
anyio.run(_run)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
if __name__ == "__main__":
|
|
95
|
+
app()
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Issue #70: persistent MCP client connection from the relay to the bus.
|
|
2
|
+
|
|
3
|
+
The relay calls consume(auto_ack=False, batch_window_seconds=N) on every
|
|
4
|
+
wake to fetch the queue snapshot. This client wraps fastmcp.Client with
|
|
5
|
+
the relay-specific contract (no ack, parameter shape).
|
|
6
|
+
|
|
7
|
+
Connection lifecycle: opened on relay startup, kept alive for the relay's
|
|
8
|
+
lifetime. Reconnects on connection loss are handled by fastmcp.Client's
|
|
9
|
+
underlying transport — when call_tool raises a transport error, the
|
|
10
|
+
caller (stdio_server) catches and falls back to the bare-wake shape for
|
|
11
|
+
that wake. Subsequent wakes get a fresh attempt.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import TYPE_CHECKING, Any, Self
|
|
17
|
+
|
|
18
|
+
from fastmcp import Client
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from fastmcp import FastMCP
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class BusClient:
|
|
25
|
+
"""Persistent MCP client to the bus daemon's per-agent endpoint.
|
|
26
|
+
|
|
27
|
+
Two construction modes:
|
|
28
|
+
|
|
29
|
+
- ``BusClient(daemon_url, agent)`` — production: connects to
|
|
30
|
+
``{daemon_url}/mcp/{agent}`` over HTTP.
|
|
31
|
+
- ``BusClient.from_in_process(fastmcp_server)`` — tests: connects
|
|
32
|
+
directly to a FastMCP server instance, no HTTP transport.
|
|
33
|
+
|
|
34
|
+
Use as an async context manager. ``consume_no_ack`` is the single
|
|
35
|
+
operation the relay needs; further tools (peek, etc.) can be added
|
|
36
|
+
here as the relay grows.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
# Typed as Any to accommodate the two Client transport variants:
|
|
40
|
+
# Client[StreamableHttpTransport] (HTTP mode) and
|
|
41
|
+
# Client[FastMCPTransport] (in-process mode). Both share the same
|
|
42
|
+
# call_tool / __aenter__ / __aexit__ interface.
|
|
43
|
+
_client: Any
|
|
44
|
+
|
|
45
|
+
def __init__(self, daemon_url: str, agent: str) -> None:
|
|
46
|
+
url = f"{daemon_url.rstrip('/')}/mcp/{agent}"
|
|
47
|
+
self._client = Client(url)
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def from_in_process(cls, server: FastMCP) -> Self:
|
|
51
|
+
instance = cls.__new__(cls)
|
|
52
|
+
instance._client = Client(server)
|
|
53
|
+
return instance
|
|
54
|
+
|
|
55
|
+
async def __aenter__(self) -> Self:
|
|
56
|
+
await self._client.__aenter__()
|
|
57
|
+
return self
|
|
58
|
+
|
|
59
|
+
async def __aexit__(
|
|
60
|
+
self,
|
|
61
|
+
exc_type: type[BaseException] | None,
|
|
62
|
+
exc: BaseException | None,
|
|
63
|
+
tb: Any,
|
|
64
|
+
) -> None:
|
|
65
|
+
await self._client.__aexit__(exc_type, exc, tb)
|
|
66
|
+
|
|
67
|
+
async def consume_no_ack(self, *, batch_window_seconds: int = 30) -> dict:
|
|
68
|
+
"""Fetch the queue snapshot without acking any envelopes.
|
|
69
|
+
|
|
70
|
+
Returns the consume() response shape:
|
|
71
|
+
``{"meta": {...}, "items": [...]}``.
|
|
72
|
+
"""
|
|
73
|
+
result = await self._client.call_tool(
|
|
74
|
+
"consume",
|
|
75
|
+
{
|
|
76
|
+
"auto_ack": False,
|
|
77
|
+
"batch_window_seconds": batch_window_seconds,
|
|
78
|
+
},
|
|
79
|
+
)
|
|
80
|
+
return result.data
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Issue #70: relay configuration with layered precedence.
|
|
2
|
+
|
|
3
|
+
Order of precedence (highest first):
|
|
4
|
+
1. CLI flag (--inline-mode, etc.)
|
|
5
|
+
2. Env var (AGENT_CORE_CHANNEL_INLINE_MODE, etc.)
|
|
6
|
+
3. YAML config (agent_core.yaml endpoints[name=<agent>].params.channel_relay)
|
|
7
|
+
4. Hardcoded defaults
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections.abc import Mapping
|
|
13
|
+
from dataclasses import dataclass, replace
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Literal
|
|
16
|
+
|
|
17
|
+
import yaml
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class RelayConfig:
|
|
22
|
+
inline_mode: Literal["full", "preview", "disabled"] = "full"
|
|
23
|
+
max_envelopes: int = 5
|
|
24
|
+
max_bytes: int = 8192
|
|
25
|
+
per_envelope_bytes: int = 4096
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
_ENV_PREFIX = "AGENT_CORE_CHANNEL_"
|
|
29
|
+
_VALID_MODES = ("full", "preview", "disabled")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _coerce_mode(value: Any) -> Literal["full", "preview", "disabled"] | None:
|
|
33
|
+
if value is None:
|
|
34
|
+
return None
|
|
35
|
+
s = str(value).strip().lower()
|
|
36
|
+
if s in _VALID_MODES:
|
|
37
|
+
return s # type: ignore[return-value]
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _coerce_int(value: Any) -> int | None:
|
|
42
|
+
if value is None:
|
|
43
|
+
return None
|
|
44
|
+
try:
|
|
45
|
+
return int(value)
|
|
46
|
+
except (TypeError, ValueError):
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _yaml_layer(path: Path, agent: str) -> dict[str, Any]:
|
|
51
|
+
if not path.exists():
|
|
52
|
+
return {}
|
|
53
|
+
try:
|
|
54
|
+
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
55
|
+
except yaml.YAMLError:
|
|
56
|
+
return {}
|
|
57
|
+
endpoints = data.get("endpoints", []) if isinstance(data, dict) else []
|
|
58
|
+
for ep in endpoints:
|
|
59
|
+
if not isinstance(ep, dict):
|
|
60
|
+
continue
|
|
61
|
+
if ep.get("name") != agent:
|
|
62
|
+
continue
|
|
63
|
+
params = ep.get("params", {}) or {}
|
|
64
|
+
block = params.get("channel_relay") if isinstance(params, dict) else None
|
|
65
|
+
if isinstance(block, dict):
|
|
66
|
+
return block
|
|
67
|
+
return {}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _env_layer(env: Mapping[str, str]) -> dict[str, Any]:
|
|
71
|
+
out: dict[str, Any] = {}
|
|
72
|
+
if (v := env.get(f"{_ENV_PREFIX}INLINE_MODE")) is not None:
|
|
73
|
+
out["inline_mode"] = v
|
|
74
|
+
if (v := env.get(f"{_ENV_PREFIX}INLINE_MAX_ENVELOPES")) is not None:
|
|
75
|
+
out["max_envelopes"] = v
|
|
76
|
+
if (v := env.get(f"{_ENV_PREFIX}INLINE_MAX_BYTES")) is not None:
|
|
77
|
+
out["max_bytes"] = v
|
|
78
|
+
if (v := env.get(f"{_ENV_PREFIX}INLINE_PER_ENVELOPE_BYTES")) is not None:
|
|
79
|
+
out["per_envelope_bytes"] = v
|
|
80
|
+
return out
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _cli_layer(args: Any) -> dict[str, Any]:
|
|
84
|
+
out: dict[str, Any] = {}
|
|
85
|
+
if getattr(args, "inline_mode", None) is not None:
|
|
86
|
+
out["inline_mode"] = args.inline_mode
|
|
87
|
+
if getattr(args, "inline_max_envelopes", None) is not None:
|
|
88
|
+
out["max_envelopes"] = args.inline_max_envelopes
|
|
89
|
+
if getattr(args, "inline_max_bytes", None) is not None:
|
|
90
|
+
out["max_bytes"] = args.inline_max_bytes
|
|
91
|
+
if getattr(args, "inline_per_envelope_bytes", None) is not None:
|
|
92
|
+
out["per_envelope_bytes"] = args.inline_per_envelope_bytes
|
|
93
|
+
return out
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def load_config(
|
|
97
|
+
*,
|
|
98
|
+
agent: str,
|
|
99
|
+
config_path: Path,
|
|
100
|
+
cli_args: Any,
|
|
101
|
+
env: Mapping[str, str],
|
|
102
|
+
) -> RelayConfig:
|
|
103
|
+
"""Resolve RelayConfig with precedence: CLI > env > YAML > defaults."""
|
|
104
|
+
config = RelayConfig()
|
|
105
|
+
|
|
106
|
+
layers = [_yaml_layer(config_path, agent), _env_layer(env), _cli_layer(cli_args)]
|
|
107
|
+
|
|
108
|
+
for layer in layers:
|
|
109
|
+
if (mode := _coerce_mode(layer.get("inline_mode"))) is not None:
|
|
110
|
+
config = replace(config, inline_mode=mode)
|
|
111
|
+
if (n := _coerce_int(layer.get("max_envelopes"))) is not None:
|
|
112
|
+
config = replace(config, max_envelopes=n)
|
|
113
|
+
if (n := _coerce_int(layer.get("max_bytes"))) is not None:
|
|
114
|
+
config = replace(config, max_bytes=n)
|
|
115
|
+
if (n := _coerce_int(layer.get("per_envelope_bytes"))) is not None:
|
|
116
|
+
config = replace(config, per_envelope_bytes=n)
|
|
117
|
+
|
|
118
|
+
return config
|