Skip to content

Commit de7be6b

Browse files
authored
batch parallelization, media attach (#793)
* batch parallelization, media attach * deps * smart prompt
1 parent 62fd694 commit de7be6b

40 files changed

Lines changed: 2490 additions & 180 deletions

pyproject.toml

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "fast-agent-mcp"
3-
version = "0.7.3"
3+
version = "0.7.4"
44
description = "Define, Prompt and Test MCP enabled Agents and Workflows"
55
readme = "README.md"
66
license = { file = "LICENSE" }
@@ -15,14 +15,14 @@ classifiers = [
1515
requires-python = ">=3.13.5,<3.15"
1616
dependencies = [
1717
"fastapi==0.136.1",
18-
"fastmcp==3.2.4",
18+
"fastmcp==3.3.1",
1919
"mcp==1.27.1",
20-
"pydantic-settings==2.13.0",
21-
"pydantic==2.13.3",
20+
"pydantic-settings==2.14.1",
21+
"pydantic==2.13.4",
2222
"pyyaml==6.0.3",
2323
"rich==15.0.0",
2424
"typer==0.25.1",
25-
"anthropic[vertex]==0.100.0",
25+
"anthropic[vertex]==0.102.0",
2626
"openai[aiohttp]==2.36.0",
2727
"prompt-toolkit==3.0.52",
2828
"aiohttp==3.13.5",
@@ -32,21 +32,21 @@ dependencies = [
3232
"opentelemetry-instrumentation-anthropic==0.52.1; python_version >= '3.10' and python_version < '4.0'",
3333
"opentelemetry-instrumentation-mcp==0.52.1; python_version >= '3.10' and python_version < '4.0'",
3434
"opentelemetry-instrumentation-google-genai==0.6b0",
35-
"google-genai==2.0.0",
35+
"google-genai==2.3.0",
3636
"deprecated==1.3.1",
37-
"a2a-sdk==0.3.26",
37+
"a2a-sdk==1.0.3",
3838
"email-validator==2.2.0",
39-
"pyperclip==1.9.0",
39+
"pyperclip==1.11.0",
4040
"keyring==25.7.0",
4141
"python-frontmatter==1.1.0",
4242
"watchfiles==1.1.1",
43-
"agent-client-protocol==0.9.0",
44-
"jsonschema==4.25.1",
45-
"tiktoken==0.12.0",
43+
"agent-client-protocol==0.10.0",
44+
"jsonschema==4.26.0",
45+
"tiktoken==0.13.0",
4646
"uvloop==0.22.1; platform_system != 'Windows'",
4747
"multilspy==0.0.15",
4848
"ruamel.yaml==0.19.1",
49-
"huggingface_hub==1.14.0",
49+
"huggingface_hub==1.15.0",
5050
"mslex==1.3.0",
5151
]
5252

resources/shared/smart_prompt.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Use `list_resources` to discover bundled internal resources and attached MCP res
3030
Use the smart tool to load AgentCards temporarily when you need extra agents.
3131
Use `create_agent_card` to scaffold a minimal card file quickly.
3232
Use validate to check AgentCard files before running them.
33-
Use `attach_resource` when you want to send a prompt with one resource attached.
33+
Use `attach_media` when you want to send local or provider-fetchable media/document content with the next prompt.
3434
Use `slash_command` when you need interactive-style `/...` command behavior (for example `/mcp ...`, `/skills ...`, `/cards ...`).
3535
When calling child-agent tools (`agent__*`), follow each tool's schema and
3636
parameter descriptions exactly.

src/fast_agent/acp/server/agent_acp_server.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -663,19 +663,23 @@ async def _initialize_session_state(
663663

664664
async def list_sessions(
665665
self,
666+
additional_directories: list[str] | None = None,
666667
cursor: str | None = None,
667668
cwd: str | None = None,
668669
**kwargs: Any,
669670
) -> ListSessionsResponse:
671+
_ = additional_directories
670672
return await self._session_store.list_sessions(cursor=cursor, cwd=cwd, **kwargs)
671673

672674
async def load_session(
673675
self,
674676
cwd: str,
675677
session_id: str,
678+
additional_directories: list[str] | None = None,
676679
mcp_servers: list[HttpMcpServer | SseMcpServer | McpServerStdio] | None = None,
677680
**kwargs: Any,
678681
) -> LoadSessionResponse | None:
682+
_ = additional_directories
679683
return await self._session_store.load_session(
680684
cwd=cwd,
681685
session_id=session_id,
@@ -687,9 +691,11 @@ async def resume_session(
687691
self,
688692
cwd: str,
689693
session_id: str,
694+
additional_directories: list[str] | None = None,
690695
mcp_servers: list[HttpMcpServer | SseMcpServer | McpServerStdio] | None = None,
691696
**kwargs: Any,
692697
) -> ResumeSessionResponse:
698+
_ = additional_directories
693699
return await self._session_store.resume_session(
694700
cwd=cwd,
695701
session_id=session_id,
@@ -700,6 +706,7 @@ async def resume_session(
700706
async def new_session(
701707
self,
702708
cwd: str,
709+
additional_directories: list[str] | None = None,
703710
mcp_servers: list[HttpMcpServer | SseMcpServer | McpServerStdio] | None = None,
704711
**kwargs: Any,
705712
) -> NewSessionResponse:
@@ -708,6 +715,7 @@ async def new_session(
708715
709716
Creates a new ACP session with its own dedicated agent instance.
710717
"""
718+
_ = additional_directories
711719
request_cwd = self._resolve_request_cwd(
712720
cwd=cwd,
713721
request_name="session/new",

src/fast_agent/agents/llm_agent.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,7 @@
7070

7171
logger = get_logger(__name__)
7272

73-
DEFAULT_CAPABILITIES = AgentCapabilities(
74-
streaming=False, push_notifications=False, state_transition_history=False
75-
)
73+
DEFAULT_CAPABILITIES = AgentCapabilities(streaming=False, push_notifications=False)
7674

7775

7876
class LlmAgent(LlmDecorator):

src/fast_agent/agents/llm_decorator.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
from fast_agent.agents.tool_runner import ToolRunnerHooks
3030
from fast_agent.hooks.lifecycle_hook_loader import AgentLifecycleHooks
3131

32-
from a2a.types import AgentCard
32+
from a2a.types import AgentCard, AgentInterface
3333
from mcp import ListToolsResult, Tool
3434
from mcp.types import (
3535
CallToolResult,
@@ -1446,7 +1446,12 @@ async def agent_card(self) -> AgentCard:
14461446
skills=[],
14471447
name=self._name,
14481448
description=self.config.description or self.instruction,
1449-
url=f"fast-agent://agents/{self._name}/",
1449+
supported_interfaces=[
1450+
AgentInterface(
1451+
url=f"fast-agent://agents/{self._name}/",
1452+
protocol_binding="fast-agent",
1453+
)
1454+
],
14501455
version="0.1",
14511456
capabilities=DEFAULT_CAPABILITIES,
14521457
# TODO -- get these from the _llm

src/fast_agent/agents/mcp_agent.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
)
2626

2727
import mcp
28-
from a2a.types import AgentCard, AgentSkill
28+
from a2a.types import AgentCard, AgentInterface, AgentSkill
2929
from mcp.types import (
3030
CallToolResult,
3131
ContentBlock,
@@ -489,6 +489,12 @@ def _local_filesystem_runtime(self) -> LocalFilesystemRuntime | None:
489489
return fallback
490490
return None
491491

492+
def _consume_pending_media_attachments(self) -> list[ContentBlock]:
493+
local_runtime = self._local_filesystem_runtime()
494+
if local_runtime is None:
495+
return []
496+
return local_runtime.consume_pending_media_attachments()
497+
492498
@property
493499
def has_filesystem_read_text_file_tool(self) -> bool:
494500
"""Whether the active filesystem runtime currently exposes read_text_file."""
@@ -648,6 +654,12 @@ def _shell_read_text_file_enabled(self) -> bool:
648654
return True
649655
return self._context.config.shell_execution.enable_read_text_file
650656

657+
def _shell_attach_media_mode(self) -> Literal["auto", "on", "off"]:
658+
"""Return whether shell-enabled agents should expose local attach_media."""
659+
if not self._context or not self._context.config:
660+
return "auto"
661+
return self._context.config.shell_execution.enable_attach_media
662+
651663
def _resolve_shell_edit_tool_mode(self) -> Literal["write_text_file", "apply_patch", "off"]:
652664
"""Return which shell edit tool should be exposed for the current model/config."""
653665
default_mode: Literal["write_text_file", "apply_patch"]
@@ -713,16 +725,20 @@ def _maybe_enable_local_filesystem_runtime(self, working_directory: Path | None
713725
enable_write = edit_mode == "write_text_file"
714726
enable_apply_patch = edit_mode == "apply_patch"
715727
enable_edit_file = edit_mode == "write_text_file"
728+
enable_attach_media = self._shell_attach_media_mode()
729+
model_info = self.llm.model_info if self.llm else None
716730
local_runtime = self._local_filesystem_runtime()
717731
if local_runtime is not None:
718732
if working_directory is not None:
719733
local_runtime.set_working_directory(working_directory)
734+
local_runtime.set_model_info(model_info)
720735
local_runtime.set_tool_handler_resolver(self._get_tool_handler)
721736
local_runtime.set_enabled_tools(
722737
enable_read=enable_read,
723738
enable_write=enable_write,
724739
enable_apply_patch=enable_apply_patch,
725740
enable_edit_file=enable_edit_file,
741+
enable_attach_media=enable_attach_media,
726742
)
727743
return
728744

@@ -736,6 +752,8 @@ def _maybe_enable_local_filesystem_runtime(self, working_directory: Path | None
736752
enable_write=enable_write,
737753
enable_apply_patch=enable_apply_patch,
738754
enable_edit_file=enable_edit_file,
755+
enable_attach_media=enable_attach_media,
756+
model_info=model_info,
739757
tool_handler_resolver=self._get_tool_handler,
740758
)
741759
if self._filesystem_runtime is None:
@@ -752,6 +770,7 @@ def _maybe_enable_local_filesystem_runtime(self, working_directory: Path | None
752770
write_enabled=enable_write,
753771
apply_patch_enabled=enable_apply_patch,
754772
edit_file_enabled=enable_edit_file,
773+
attach_media_enabled=enable_attach_media,
755774
)
756775

757776
def _shell_output_limit_overridden(self) -> bool:
@@ -779,11 +798,13 @@ def _on_llm_attached(self, llm: FastAgentLLMProtocol) -> None:
779798
local_runtime = self._local_filesystem_runtime()
780799
if local_runtime is not None:
781800
edit_mode = self._resolve_shell_edit_tool_mode()
801+
local_runtime.set_model_info(llm.model_info)
782802
local_runtime.set_enabled_tools(
783803
enable_read=self._shell_read_text_file_enabled(),
784804
enable_write=edit_mode == "write_text_file",
785805
enable_apply_patch=edit_mode == "apply_patch",
786806
enable_edit_file=edit_mode == "write_text_file",
807+
enable_attach_media=self._shell_attach_media_mode(),
787808
)
788809

789810
if self._shell_runtime is None:
@@ -2032,7 +2053,12 @@ async def agent_card(self) -> AgentCard:
20322053
skills=skills,
20332054
name=self._name,
20342055
description=self.config.description or self.instruction,
2035-
url=f"fast-agent://agents/{self._name}/",
2056+
supported_interfaces=[
2057+
AgentInterface(
2058+
url=f"fast-agent://agents/{self._name}/",
2059+
protocol_binding="fast-agent",
2060+
)
2061+
],
20362062
version="0.1",
20372063
capabilities=DEFAULT_CAPABILITIES,
20382064
default_input_modes=["text/plain"],

src/fast_agent/agents/smart_agent.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1592,7 +1592,8 @@ def _enable_smart_tooling(agent: _SmartToolingAgent) -> None:
15921592
description=(
15931593
"Run subagent tasks from a definition in a file or directory. Subagents are defined with "
15941594
"AgentCards. Use action=`run` to load a subagent and send it a message. Optionally supply "
1595-
"`mcp_connect` targets. Use action=`validate` to check card file validity without running them"
1595+
"`mcp_connect` targets. Use action=`validate` to check card file validity without running them. "
1596+
"Do not pass Agent Skill SKILL.md files here; inspect skills with read_text_file/read_skill."
15961597
),
15971598
)
15981599
slash_command_tool = build_default_function_tool(

src/fast_agent/agents/tool_agent.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@
77
from typing import Any, Callable, Dict, List, Mapping, Sequence
88

99
from fastmcp.tools import FunctionTool, ToolResult
10-
from mcp.types import CallToolResult, ListToolsResult, Tool
10+
from mcp.types import CallToolResult, ContentBlock, ListToolsResult, Tool
1111

1212
from fast_agent.agents.agent_types import AgentConfig, AgentType
1313
from fast_agent.agents.llm_agent import LlmAgent
1414
from fast_agent.agents.tool_runner import ToolRunner, ToolRunnerHooks, _ToolLoopAgent
1515
from fast_agent.constants import (
1616
FAST_AGENT_ERROR_CHANNEL,
17+
FAST_AGENT_PENDING_MEDIA_ATTACHMENTS,
1718
FAST_AGENT_TOOL_METADATA,
1819
HUMAN_INPUT_TOOL_NAME,
1920
should_parallelize_tool_calls,
@@ -607,6 +608,10 @@ def should_suppress_tools_for_structured_turn(
607608
def _should_display_user_message(self, message: PromptMessageExtended) -> bool:
608609
return not message.tool_results
609610

611+
def _consume_pending_media_attachments(self) -> list[ContentBlock]:
612+
"""Return pending media blocks to send as the next user input."""
613+
return []
614+
610615
# we take care of tool results, so skip displaying them
611616
def show_user_message(self, message: PromptMessageExtended) -> None:
612617
if message.tool_results:
@@ -810,8 +815,8 @@ def _finalize_tool_results(
810815
)
811816
from fast_agent.mcp.url_elicitation_required import URLElicitationRequiredDisplayPayload
812817

813-
channels = None
814-
content = []
818+
channels: dict[str, Sequence[ContentBlock]] | None = None
819+
content: list[ContentBlock] = []
815820
if tool_loop_error:
816821
content.append(text_content(tool_loop_error))
817822
channels = {
@@ -846,6 +851,12 @@ def _finalize_tool_results(
846851
TextContent(type="text", text=json.dumps(deferred_url_elicitations))
847852
]
848853

854+
pending_media = self._consume_pending_media_attachments()
855+
if pending_media:
856+
if channels is None:
857+
channels = {}
858+
channels[FAST_AGENT_PENDING_MEDIA_ATTACHMENTS] = pending_media
859+
849860
return PromptMessageExtended(
850861
role="user",
851862
content=content,

src/fast_agent/agents/tool_runner.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from fast_agent.constants import (
1919
DEFAULT_MAX_ITERATIONS,
2020
FAST_AGENT_ERROR_CHANNEL,
21+
FAST_AGENT_PENDING_MEDIA_ATTACHMENTS,
2122
FAST_AGENT_SYNTHETIC_FINAL_CHANNEL,
2223
FAST_AGENT_TIMING,
2324
FAST_AGENT_USAGE,
@@ -582,12 +583,23 @@ def has_pending_tool_response(self) -> bool:
582583
return self._pending_tool_request is not None
583584

584585
def _stage_tool_response(self, tool_message: PromptMessageExtended) -> None:
586+
staged_messages = [tool_message]
587+
channels = tool_message.channels
588+
if channels and FAST_AGENT_PENDING_MEDIA_ATTACHMENTS in channels:
589+
pending_media = channels[FAST_AGENT_PENDING_MEDIA_ATTACHMENTS]
590+
visible_channels = dict(channels)
591+
del visible_channels[FAST_AGENT_PENDING_MEDIA_ATTACHMENTS]
592+
staged_messages = [
593+
tool_message.model_copy(update={"channels": visible_channels or None}),
594+
PromptMessageExtended(role="user", content=list(pending_media)),
595+
]
596+
585597
if self._use_history_enabled():
586-
self._delta_messages = [tool_message]
598+
self._delta_messages = staged_messages
587599
else:
588600
if self._last_message is not None:
589601
self._delta_messages.append(self._last_message)
590-
self._delta_messages.append(tool_message)
602+
self._delta_messages.extend(staged_messages)
591603

592604
def _should_start_deferred_structured_finalization(
593605
self,

src/fast_agent/agents/workflow/router_agent.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
by determining the best agent for a request and dispatching to it.
66
"""
77

8+
import json
89
from typing import TYPE_CHECKING, List, Optional, Tuple, Type
910

11+
from google.protobuf.json_format import MessageToDict
1012
from mcp import Tool
1113
from opentelemetry import trace
1214
from pydantic import BaseModel
@@ -165,9 +167,15 @@ async def _generate_routing_instruction(
165167
agent_descriptions = []
166168
for agent in agents:
167169
agent_card: AgentCard = await agent.agent_card()
170+
card_summary = {
171+
"name": agent_card.name,
172+
"description": agent_card.description,
173+
"skills": [MessageToDict(skill) for skill in agent_card.skills],
174+
}
168175
agent_descriptions.append(
169-
agent_card.model_dump_json(
170-
include={"name", "description", "skills"}, exclude_none=True
176+
json.dumps(
177+
{key: value for key, value in card_summary.items() if value},
178+
separators=(",", ":"),
171179
)
172180
)
173181

0 commit comments

Comments
 (0)