Skip to content

Commit 991ec8c

Browse files
authored
Ty update (#789)
* interim typesafety updates * typesafety * typesafe * mcp plugin * stale test
1 parent 6c035cc commit 991ec8c

70 files changed

Lines changed: 1705 additions & 650 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

plan/plugin-spec.md

Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
# Plugin Runtime Capability Spec
2+
3+
## Goal
4+
5+
Expose a small, stable runtime facade to plugin command actions so plugins can
6+
mutate live agent capabilities without depending on concrete agent classes,
7+
slash-command handlers, or internal rebuild details.
8+
9+
Initial capabilities:
10+
11+
- Attach/detach MCP servers at runtime.
12+
- Refresh skills after a plugin writes or installs skill files.
13+
14+
The API should make common plugin code straightforward while keeping the
15+
implementation free to reuse the existing `AgentApp` runtime callbacks and
16+
instruction refresh machinery.
17+
18+
## Current state
19+
20+
Plugin command actions receive a `PluginCommandActionContext`:
21+
22+
```python
23+
@dataclass(frozen=True, slots=True)
24+
class PluginCommandActionContext:
25+
command_name: str
26+
arguments: str
27+
agent: PluginCommandAgentProtocol
28+
settings: Settings | None = None
29+
session_cwd: Path | None = None
30+
```
31+
32+
Today a plugin can technically reach live MCP and skill refresh behavior by
33+
using concrete/internal APIs, for example:
34+
35+
- `McpAgentProtocol.attach_mcp_server(...)`
36+
- `rebuild_agent_instruction(...)`
37+
- skill registry reload helpers
38+
39+
That works, but it is not a supported plugin contract.
40+
41+
## Proposal
42+
43+
Add a plugin runtime facade to `PluginCommandActionContext`:
44+
45+
```python
46+
@dataclass(frozen=True, slots=True)
47+
class PluginCommandActionContext:
48+
command_name: str
49+
arguments: str
50+
agent: PluginCommandAgentProtocol
51+
settings: Settings | None = None
52+
session_cwd: Path | None = None
53+
runtime: PluginRuntime | None = None
54+
```
55+
56+
`runtime` is optional for backward compatibility and for execution contexts
57+
that cannot provide live runtime mutation.
58+
59+
Plugin authors should feature-test the facade:
60+
61+
```python
62+
if ctx.runtime is None:
63+
return PluginCommandActionResult(message="Runtime capabilities are not available.")
64+
```
65+
66+
## Runtime protocol
67+
68+
Minimal v1:
69+
70+
```python
71+
from __future__ import annotations
72+
73+
from typing import Protocol
74+
75+
from fast_agent.config import MCPServerSettings
76+
from fast_agent.mcp.mcp_aggregator import MCPAttachOptions, MCPAttachResult, MCPDetachResult
77+
78+
79+
class PluginRuntime(Protocol):
80+
"""Stable live-runtime capabilities exposed to plugin command actions."""
81+
82+
async def attach_mcp_server(
83+
self,
84+
*,
85+
server_name: str,
86+
agent_name: str | None = None,
87+
server_config: MCPServerSettings | None = None,
88+
options: MCPAttachOptions | None = None,
89+
) -> MCPAttachResult:
90+
"""Attach an MCP server to a running MCP-capable agent and refresh instructions."""
91+
92+
async def detach_mcp_server(
93+
self,
94+
*,
95+
server_name: str,
96+
agent_name: str | None = None,
97+
) -> MCPDetachResult:
98+
"""Detach an MCP server from a running MCP-capable agent and refresh instructions."""
99+
100+
async def refresh_skills(
101+
self,
102+
*,
103+
agent_name: str | None = None,
104+
) -> PluginSkillRefreshResult:
105+
"""Reload skill manifests and rebuild the target agent's instructions."""
106+
```
107+
108+
### Agent targeting
109+
110+
If `agent_name` is omitted, methods operate on the plugin context's current
111+
agent.
112+
113+
This keeps common plugin code terse:
114+
115+
```python
116+
await ctx.runtime.attach_mcp_server(
117+
server_name="github",
118+
server_config=server_config,
119+
)
120+
```
121+
122+
Plugins can still target another registered agent explicitly:
123+
124+
```python
125+
await ctx.runtime.refresh_skills(agent_name="planner")
126+
```
127+
128+
## Result types
129+
130+
Skill refresh should return simple structured data rather than a UI-oriented
131+
`CommandOutcome`.
132+
133+
```python
134+
from dataclasses import dataclass
135+
136+
from fast_agent.skills import SkillManifest
137+
from fast_agent.skills.registry import SkillRegistry
138+
139+
140+
@dataclass(frozen=True, slots=True)
141+
class PluginSkillRefreshResult:
142+
agent_name: str
143+
manifests: tuple[SkillManifest, ...]
144+
registry: SkillRegistry
145+
skill_count: int
146+
```
147+
148+
For MCP attach/detach, v1 can reuse existing runtime result types:
149+
150+
- `MCPAttachResult`
151+
- `MCPDetachResult`
152+
153+
If those are too internal or unstable, add plugin-specific wrappers later.
154+
155+
## Optional future methods
156+
157+
These are intentionally not part of minimal v1, but the protocol should leave
158+
room for them.
159+
160+
```python
161+
from pathlib import Path
162+
163+
164+
@dataclass(frozen=True, slots=True)
165+
class PluginSkillInstallResult:
166+
agent_name: str
167+
name: str
168+
path: Path
169+
refreshed: PluginSkillRefreshResult
170+
171+
172+
@dataclass(frozen=True, slots=True)
173+
class PluginSkillRemoveResult:
174+
agent_name: str
175+
name: str
176+
removed_paths: tuple[Path, ...]
177+
refreshed: PluginSkillRefreshResult
178+
179+
180+
class PluginRuntime(Protocol):
181+
async def install_skill(
182+
self,
183+
skill: str,
184+
*,
185+
agent_name: str | None = None,
186+
registry_url: str | None = None,
187+
) -> PluginSkillInstallResult:
188+
"""Install a marketplace skill, refresh manifests, and rebuild instructions."""
189+
190+
async def remove_skill(
191+
self,
192+
skill: str,
193+
*,
194+
agent_name: str | None = None,
195+
) -> PluginSkillRemoveResult:
196+
"""Remove a managed skill, refresh manifests, and rebuild instructions."""
197+
```
198+
199+
A lower-level instruction refresh hook may also be useful, but should be added
200+
carefully because it allows plugins to alter instruction context directly:
201+
202+
```python
203+
async def refresh_agent_instruction(
204+
self,
205+
*,
206+
agent_name: str | None = None,
207+
) -> None:
208+
"""Rebuild the target agent's instruction from current runtime state."""
209+
```
210+
211+
## Example plugin: connect an MCP server
212+
213+
```python
214+
from fast_agent.command_actions.models import (
215+
PluginCommandActionContext,
216+
PluginCommandActionResult,
217+
)
218+
from fast_agent.config import MCPServerSettings
219+
220+
221+
async def connect_github(ctx: PluginCommandActionContext) -> PluginCommandActionResult:
222+
if ctx.runtime is None:
223+
return PluginCommandActionResult(message="Runtime capabilities are not available.")
224+
225+
server_config = MCPServerSettings(
226+
command="uvx",
227+
args=["mcp-server-github"],
228+
)
229+
230+
await ctx.runtime.attach_mcp_server(
231+
server_name="github",
232+
server_config=server_config,
233+
)
234+
235+
return PluginCommandActionResult(message="Connected MCP server: github")
236+
```
237+
238+
## Example plugin: write a skill then refresh
239+
240+
```python
241+
from pathlib import Path
242+
243+
from fast_agent.command_actions.models import (
244+
PluginCommandActionContext,
245+
PluginCommandActionResult,
246+
)
247+
248+
249+
async def add_local_skill(ctx: PluginCommandActionContext) -> PluginCommandActionResult:
250+
if ctx.runtime is None:
251+
return PluginCommandActionResult(message="Runtime capabilities are not available.")
252+
253+
skill_dir = Path(".fast-agent/skills/example")
254+
skill_dir.mkdir(parents=True, exist_ok=True)
255+
skill_dir.joinpath("SKILL.md").write_text(
256+
"# Example\n\nUse this skill for example tasks.\n",
257+
encoding="utf-8",
258+
)
259+
260+
refreshed = await ctx.runtime.refresh_skills()
261+
262+
return PluginCommandActionResult(
263+
message=f"Skills refreshed. Loaded {refreshed.skill_count} skills."
264+
)
265+
```
266+
267+
## Implementation notes
268+
269+
### Runtime facade construction
270+
271+
The plugin dispatch sites should construct a runtime facade next to the existing
272+
`PluginCommandActionContext`.
273+
274+
Interactive dispatch and ACP slash dispatch should pass equivalent capability
275+
objects so plugin behavior is consistent across transports.
276+
277+
### MCP implementation
278+
279+
The facade should reuse existing `AgentApp` callbacks where available:
280+
281+
- `attach_mcp_server(...)`
282+
- `detach_mcp_server(...)`
283+
- `list_attached_mcp_servers(...)`, if added later
284+
- `list_configured_detached_mcp_servers(...)`, if added later
285+
286+
The existing callback path already performs instruction rebuild after attach or
287+
detach.
288+
289+
### Skill refresh implementation
290+
291+
`refresh_skills(...)` should mirror the current `/skills` refresh behavior:
292+
293+
1. Resolve skill directories from settings.
294+
2. Reload skill manifests.
295+
3. Format skill instructions.
296+
4. Rebuild the target agent instruction with the refreshed manifests, registry,
297+
and instruction context.
298+
299+
It should not require a full `CommandContext` or UI `CommandIO`.
300+
301+
## Error behavior
302+
303+
Runtime methods should raise normal Python exceptions for programmer-visible
304+
failures, matching existing plugin command behavior where dispatcher catches
305+
exceptions and reports command failure.
306+
307+
Expected examples:
308+
309+
- target agent not found
310+
- target agent does not support MCP server management
311+
- invalid MCP server configuration
312+
- skill manifest reload failure
313+
314+
Plugins that want friendlier UX can catch and translate exceptions into
315+
`PluginCommandActionResult`.
316+
317+
## Backward compatibility
318+
319+
- Existing plugin command handlers continue to work.
320+
- `runtime` is optional on the context.
321+
- New code should check `ctx.runtime is not None` before using live runtime
322+
capabilities.
323+
324+
## Open questions
325+
326+
1. Should v1 expose MCP list methods as well as attach/detach?
327+
2. Should `runtime` be guaranteed in interactive and ACP modes, or optional in
328+
all modes?
329+
3. Should `MCPAttachResult`/`MCPDetachResult` be considered stable enough for
330+
plugin authors, or should plugin-specific wrappers be introduced immediately?
331+
4. Should marketplace `install_skill(...)` be part of v1, or should plugins
332+
initially write/copy skill files themselves and call `refresh_skills(...)`?
333+
5. Should plugin runtime methods enforce any allowlist/safety policy before
334+
attaching external MCP servers or writing skills?

publish/hf-inference-acp/src/hf_inference_acp/agents.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import uuid
88
from importlib.metadata import version as package_version
99
from pathlib import Path
10-
from typing import TYPE_CHECKING
10+
from typing import TYPE_CHECKING, Literal
1111

1212
from acp.helpers import text_block, tool_content
1313
from acp.schema import (
@@ -42,6 +42,8 @@
4242
)
4343
from hf_inference_acp.wizard.model_catalog import format_model_list_help
4444

45+
ToolCallStatus = Literal["pending", "in_progress", "completed", "failed"]
46+
4547
logger = get_logger(__name__)
4648

4749

@@ -639,7 +641,7 @@ async def _handle_connect(self, arguments: str) -> str:
639641
async def _send_connect_update(
640642
*,
641643
title: str | None = None,
642-
status: str | None = None,
644+
status: ToolCallStatus | None = None,
643645
message: str | None = None,
644646
) -> None:
645647
if not self.acp:
@@ -657,7 +659,7 @@ async def _send_connect_update(
657659
ToolCallProgress(
658660
tool_call_id=tool_call_id,
659661
title=title,
660-
status=status, # type: ignore[arg-type]
662+
status=status,
661663
content=content,
662664
session_update="tool_call_update",
663665
)

pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ dependencies = [
2020
"pydantic-settings==2.13.0",
2121
"pydantic==2.13.3",
2222
"pyyaml==6.0.3",
23-
"rich==14.3.3",
24-
"typer==0.24.1",
23+
"rich==15.0.0",
24+
"typer==0.25.1",
2525
"anthropic[vertex]==0.100.0",
2626
"openai[aiohttp]==2.36.0",
2727
"prompt-toolkit==3.0.52",
@@ -134,7 +134,7 @@ dev = [
134134
"ruamel.yaml>=0.18.0",
135135
"pyyaml>=6.0.2",
136136
"ruff>=0.8.4",
137-
"ty==0.0.23",
137+
"ty==0.0.35",
138138
"pytest>=7.4.0",
139139
"pytest-asyncio>=0.21.1",
140140
"pytest-cov>=6.1.1",

0 commit comments

Comments
 (0)