Skip to content

Commit 65592fb

Browse files
authored
Dev/0.7.3 (#790)
* commit * mcp from plugin * remove unneedded plugin tests
1 parent 991ec8c commit 65592fb

23 files changed

Lines changed: 456 additions & 117 deletions

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "fast-agent-mcp"
3-
version = "0.7.2"
3+
version = "0.7.3"
44
description = "Define, Prompt and Test MCP enabled Agents and Workflows"
55
readme = "README.md"
66
license = { file = "LICENSE" }

src/fast_agent/acp/slash_commands.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
from fast_agent.command_actions import (
4949
PluginCommandActionContext,
5050
PluginCommandActionRegistry,
51+
PluginRuntimeFacade,
5152
)
5253
from fast_agent.command_actions.accessors import (
5354
plugin_command_base_path_for_provider,
@@ -69,6 +70,7 @@
6970
if TYPE_CHECKING:
7071
from fast_agent.acp.acp_context import ACPContext
7172
from fast_agent.command_actions.models import PluginCommandAgentProtocol
73+
from fast_agent.command_actions.runtime import AttachMcpServerCallback, DetachMcpServerCallback
7274
from fast_agent.commands.context import AgentProvider
7375
from fast_agent.config import MCPServerSettings
7476
from fast_agent.core.fastagent import AgentInstance
@@ -770,6 +772,24 @@ async def _execute_plugin_command_action(
770772
agent=cast("PluginCommandAgentProtocol", agent),
771773
settings=command_context.settings,
772774
session_cwd=command_context.session_cwd,
775+
runtime=PluginRuntimeFacade(
776+
current_agent_name=agent.name,
777+
attach_mcp_server_callback=cast(
778+
"AttachMcpServerCallback | None",
779+
self._attach_mcp_server_callback,
780+
),
781+
detach_mcp_server_callback=cast(
782+
"DetachMcpServerCallback | None",
783+
self._detach_mcp_server_callback,
784+
),
785+
list_attached_mcp_servers_callback=(
786+
self._list_attached_mcp_servers_callback
787+
),
788+
list_configured_detached_mcp_servers_callback=(
789+
self._list_configured_detached_mcp_servers_callback
790+
),
791+
),
792+
is_acp=True,
773793
),
774794
)
775795
except AgentConfigError as exc:

src/fast_agent/agents/tool_runner.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
DEFAULT_MAX_ITERATIONS,
2020
FAST_AGENT_ERROR_CHANNEL,
2121
FAST_AGENT_SYNTHETIC_FINAL_CHANNEL,
22+
FAST_AGENT_TIMING,
2223
FAST_AGENT_USAGE,
2324
)
2425
from fast_agent.core.logging.logger import get_logger
@@ -698,9 +699,10 @@ def _synthesize_passthrough_assistant(
698699
FAST_AGENT_SYNTHETIC_FINAL_CHANNEL: [text_content("tool_result_passthrough")]
699700
}
700701
if self._last_message is not None and self._last_message.channels:
701-
usage_blocks = self._last_message.channels.get(FAST_AGENT_USAGE)
702-
if usage_blocks:
703-
channels[FAST_AGENT_USAGE] = list(usage_blocks)
702+
for channel_name in (FAST_AGENT_TIMING, FAST_AGENT_USAGE):
703+
blocks = self._last_message.channels.get(channel_name)
704+
if blocks:
705+
channels[channel_name] = list(blocks)
704706

705707
return PromptMessageExtended(
706708
role="assistant",

src/fast_agent/batch/structured.py

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222
from fast_agent.batch.template import DEFAULT_ROW_TEMPLATE, render_row_template
2323
from fast_agent.batch.traces import BatchTraceOptions, BatchTraceRecorder
2424
from fast_agent.cli.runtime.request_builders import resolve_default_instruction
25-
from fast_agent.constants import FAST_AGENT_TIMING
26-
from fast_agent.llm.request_params import RequestParams
25+
from fast_agent.constants import FAST_AGENT_TIMING, FAST_AGENT_USAGE
26+
from fast_agent.llm.request_params import BatchRequestContext, RequestParams
2727
from fast_agent.llm.structured_schema import (
2828
StructuredSchemaSource,
2929
load_json_schema_file,
@@ -120,21 +120,38 @@ def _identity_for_candidate(candidate: RowCandidate, id_field: str | None) -> tu
120120
return str(row[id_field]), None
121121

122122

123-
def _extract_timing(response: Any) -> dict[str, Any] | None:
123+
def _extract_json_channel(response: Any, channel_name: str) -> dict[str, Any] | None:
124124
channels = response.channels
125125
if not isinstance(channels, Mapping):
126126
return None
127-
timing_blocks = channels.get(FAST_AGENT_TIMING)
128-
if not timing_blocks:
127+
blocks = channels.get(channel_name)
128+
if not blocks:
129129
return None
130-
timing_text = get_text(timing_blocks[0])
131-
if not timing_text:
130+
text = get_text(blocks[0])
131+
if not text:
132132
return None
133133
try:
134-
timing = json.loads(timing_text)
134+
payload = json.loads(text)
135135
except json.JSONDecodeError:
136136
return None
137-
return timing if isinstance(timing, dict) else None
137+
return payload if isinstance(payload, dict) else None
138+
139+
140+
def _extract_timing(response: Any) -> dict[str, Any] | None:
141+
return _extract_json_channel(response, FAST_AGENT_TIMING)
142+
143+
144+
def _extract_usage(response: Any) -> dict[str, Any] | None:
145+
usage = _extract_json_channel(response, FAST_AGENT_USAGE)
146+
if usage is None:
147+
return None
148+
if "turn" not in usage and "raw_usage" not in usage:
149+
return usage
150+
return {
151+
key: value
152+
for key in ("turn", "raw_usage")
153+
if (value := usage.get(key)) is not None
154+
}
138155

139156

140157
def _write_optional_failure(
@@ -152,6 +169,7 @@ def _write_optional_telemetry(
152169
row_number: int,
153170
ok: bool,
154171
timing: dict[str, Any] | None,
172+
usage: dict[str, Any] | None = None,
155173
) -> None:
156174
if telemetry_handle is None:
157175
return
@@ -162,7 +180,7 @@ def _write_optional_telemetry(
162180
"row_number": row_number,
163181
"ok": ok,
164182
"timing": timing or {},
165-
"usage": {},
183+
"usage": usage or {},
166184
},
167185
)
168186

@@ -350,8 +368,13 @@ async def run_structured_batch(options: StructuredBatchOptions) -> dict[str, Any
350368
worker,
351369
rendered=rendered,
352370
schema_source=schema_source,
371+
batch_context=BatchRequestContext(
372+
row_number=candidate.row_number,
373+
identity=identity,
374+
),
353375
)
354376
timing = _extract_timing(response)
377+
usage = _extract_usage(response)
355378
summary.add_timing(timing)
356379
if parsed is None:
357380
record = error_envelope(
@@ -372,6 +395,7 @@ async def run_structured_batch(options: StructuredBatchOptions) -> dict[str, Any
372395
row_number=candidate.row_number,
373396
ok=False,
374397
timing=timing,
398+
usage=usage,
375399
)
376400
summary.processed_rows += 1
377401
summary.failed_rows += 1
@@ -399,6 +423,7 @@ async def run_structured_batch(options: StructuredBatchOptions) -> dict[str, Any
399423
row_number=candidate.row_number,
400424
ok=True,
401425
timing=timing,
426+
usage=usage,
402427
)
403428
summary.processed_rows += 1
404429
if trace_recorder is not None:
@@ -516,8 +541,9 @@ async def _row_call(
516541
*,
517542
rendered: str,
518543
schema_source: SchemaSource | None,
544+
batch_context: BatchRequestContext,
519545
) -> tuple[Any | None, Any]:
520-
request_params = RequestParams(use_history=False)
546+
request_params = RequestParams(use_history=False, batch_context=batch_context)
521547
if schema_source is None:
522548
response = await worker.generate(rendered, request_params)
523549
return response.last_text() or "", response

src/fast_agent/cli/commands/check_config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ class ProviderCatalogScope:
9292
providers=(Provider.HUGGINGFACE,),
9393
),
9494
"xai": ProviderCatalogScope(
95-
display_name="XAI",
95+
display_name="xAI",
9696
providers=(Provider.XAI,),
9797
),
9898
"openrouter": ProviderCatalogScope(

src/fast_agent/command_actions/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
PluginCommandActionRegistry,
1414
normalize_plugin_command_action_result,
1515
)
16+
from fast_agent.command_actions.runtime import PluginRuntime, PluginRuntimeFacade
1617

1718
__all__ = [
1819
"FAST_AGENT_AUDIT_CHANNEL",
@@ -22,6 +23,8 @@
2223
"PluginCommandActionRegistry",
2324
"PluginCommandActionResult",
2425
"PluginCommandActionSpec",
26+
"PluginRuntime",
27+
"PluginRuntimeFacade",
2528
"parse_plugin_command_action_specs",
2629
"normalize_plugin_command_action_result",
2730
]

src/fast_agent/command_actions/loader.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import importlib.util
66
import inspect
7+
import sys
78
from pathlib import Path
89
from typing import TYPE_CHECKING, cast
910

@@ -43,9 +44,11 @@ def load_plugin_command_action_function(
4344
)
4445

4546
module = importlib.util.module_from_spec(import_spec)
47+
sys.modules[module_name] = module
4648
try:
4749
import_spec.loader.exec_module(module)
4850
except Exception as exc: # noqa: BLE001
51+
sys.modules.pop(module_name, None)
4952
raise AgentConfigError(
5053
f"Failed to import command action module for '{spec}'",
5154
str(exc),

src/fast_agent/command_actions/models.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from pathlib import Path
1414

1515
from fast_agent.agents.agent_types import AgentConfig
16+
from fast_agent.command_actions.runtime import PluginRuntime
1617
from fast_agent.config import Settings
1718
from fast_agent.context import Context
1819
from fast_agent.interfaces import AgentProtocol
@@ -97,6 +98,9 @@ class PluginCommandActionContext:
9798
agent: PluginCommandAgentProtocol
9899
settings: "Settings | None" = None
99100
session_cwd: Path | None = None
101+
runtime: "PluginRuntime | None" = None
102+
is_tui: bool = False
103+
is_acp: bool = False
100104

101105
@property
102106
def agent_name(self) -> str:
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
"""Runtime capabilities exposed to plugin command actions."""
2+
3+
from __future__ import annotations
4+
5+
from dataclasses import dataclass
6+
from typing import TYPE_CHECKING, Awaitable, Callable, Protocol
7+
8+
if TYPE_CHECKING:
9+
from fast_agent.config import MCPServerSettings
10+
from fast_agent.mcp.mcp_aggregator import MCPAttachOptions, MCPAttachResult, MCPDetachResult
11+
12+
13+
AttachMcpServerCallback = Callable[
14+
[str, str, "MCPServerSettings | None", "MCPAttachOptions | None"],
15+
Awaitable["MCPAttachResult"],
16+
]
17+
DetachMcpServerCallback = Callable[[str, str], Awaitable["MCPDetachResult"]]
18+
ListMcpServersCallback = Callable[[str], Awaitable[list[str]]]
19+
20+
21+
class PluginRuntime(Protocol):
22+
"""Stable live-runtime capabilities exposed to plugin command actions."""
23+
24+
async def attach_mcp_server(
25+
self,
26+
*,
27+
server_name: str,
28+
agent_name: str | None = None,
29+
server_config: "MCPServerSettings | None" = None,
30+
options: "MCPAttachOptions | None" = None,
31+
) -> "MCPAttachResult":
32+
"""Attach an MCP server to a running MCP-capable agent and refresh instructions."""
33+
34+
async def detach_mcp_server(
35+
self,
36+
*,
37+
server_name: str,
38+
agent_name: str | None = None,
39+
) -> "MCPDetachResult":
40+
"""Detach an MCP server from a running MCP-capable agent and refresh instructions."""
41+
42+
async def list_attached_mcp_servers(
43+
self,
44+
*,
45+
agent_name: str | None = None,
46+
) -> tuple[str, ...]:
47+
"""List MCP servers attached to a running MCP-capable agent."""
48+
49+
async def list_configured_detached_mcp_servers(
50+
self,
51+
*,
52+
agent_name: str | None = None,
53+
) -> tuple[str, ...]:
54+
"""List configured MCP servers that are not currently attached."""
55+
56+
57+
@dataclass(frozen=True, slots=True)
58+
class PluginRuntimeFacade:
59+
"""Callback-backed implementation of plugin runtime capabilities."""
60+
61+
current_agent_name: str
62+
attach_mcp_server_callback: AttachMcpServerCallback | None = None
63+
detach_mcp_server_callback: DetachMcpServerCallback | None = None
64+
list_attached_mcp_servers_callback: ListMcpServersCallback | None = None
65+
list_configured_detached_mcp_servers_callback: ListMcpServersCallback | None = None
66+
67+
def _target_agent_name(self, agent_name: str | None) -> str:
68+
return agent_name or self.current_agent_name
69+
70+
async def attach_mcp_server(
71+
self,
72+
*,
73+
server_name: str,
74+
agent_name: str | None = None,
75+
server_config: "MCPServerSettings | None" = None,
76+
options: "MCPAttachOptions | None" = None,
77+
) -> "MCPAttachResult":
78+
if self.attach_mcp_server_callback is None:
79+
raise RuntimeError("Runtime MCP server attachment is not available.")
80+
return await self.attach_mcp_server_callback(
81+
self._target_agent_name(agent_name),
82+
server_name,
83+
server_config,
84+
options,
85+
)
86+
87+
async def detach_mcp_server(
88+
self,
89+
*,
90+
server_name: str,
91+
agent_name: str | None = None,
92+
) -> "MCPDetachResult":
93+
if self.detach_mcp_server_callback is None:
94+
raise RuntimeError("Runtime MCP server detachment is not available.")
95+
return await self.detach_mcp_server_callback(
96+
self._target_agent_name(agent_name),
97+
server_name,
98+
)
99+
100+
async def list_attached_mcp_servers(
101+
self,
102+
*,
103+
agent_name: str | None = None,
104+
) -> tuple[str, ...]:
105+
if self.list_attached_mcp_servers_callback is None:
106+
raise RuntimeError("Runtime MCP server listing is not available.")
107+
servers = await self.list_attached_mcp_servers_callback(self._target_agent_name(agent_name))
108+
return tuple(servers)
109+
110+
async def list_configured_detached_mcp_servers(
111+
self,
112+
*,
113+
agent_name: str | None = None,
114+
) -> tuple[str, ...]:
115+
if self.list_configured_detached_mcp_servers_callback is None:
116+
raise RuntimeError("Configured MCP server listing is not available.")
117+
servers = await self.list_configured_detached_mcp_servers_callback(
118+
self._target_agent_name(agent_name)
119+
)
120+
return tuple(servers)

src/fast_agent/core/agent_card_loader.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1179,7 +1179,7 @@ def _dump_request_params(params: RequestParams | None) -> dict[str, Any] | None:
11791179
return None
11801180
dump = params.model_dump(
11811181
exclude_defaults=True,
1182-
exclude={"messages", "systemPrompt", "use_history", "model"},
1182+
exclude={"messages", "systemPrompt", "use_history", "model", "batch_context"},
11831183
)
11841184
return dump or None
11851185

0 commit comments

Comments
 (0)