Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
3858ce4
async streaming
evalstate Oct 17, 2025
d9eace0
add python-frontmatter
evalstate Oct 17, 2025
f96848e
skills loading
evalstate Oct 19, 2025
461d410
skills config
evalstate Oct 19, 2025
bd72d77
agent has single registry
evalstate Oct 19, 2025
86a02b1
implement tool provider abstraction
evalstate Oct 19, 2025
85e08ee
simplify again
evalstate Oct 19, 2025
8c61e4e
add --shell option
evalstate Oct 20, 2025
e03ea65
shell highlight, bypass "go"
evalstate Oct 20, 2025
6138972
handling, hardcoded skills (will copy claude.ai for first merge)
evalstate Oct 20, 2025
0d11c97
tool updates
evalstate Oct 20, 2025
e765c1e
some refactorings
evalstate Oct 20, 2025
2c2699d
tweak
evalstate Oct 21, 2025
759d650
display tweaks, clear last command
evalstate Oct 21, 2025
7effe44
manage logging config
evalstate Oct 21, 2025
8bfa2f7
remove unused code
evalstate Oct 21, 2025
1e9e000
improve runtime detection, history display
evalstate Oct 23, 2025
17fe6a3
shell streaming, timeout config
evalstate Oct 23, 2025
1ca051e
improve windows shell handling
evalstate Oct 23, 2025
153e2e2
windows!
evalstate Oct 23, 2025
aabd6ea
tweak windows
evalstate Oct 24, 2025
53d5d1e
library updates
evalstate Oct 24, 2025
3c7e4ca
library updates
evalstate Oct 24, 2025
135dec2
Merge branch 'main' into feat/agent-skills
evalstate Oct 24, 2025
17030cc
styling
evalstate Oct 24, 2025
3b77015
styling
evalstate Oct 24, 2025
8389601
fix directory handling
evalstate Oct 24, 2025
b0f9d69
update tests, directory handling, aiohttp
evalstate Oct 24, 2025
09aeafb
add tool event interface
evalstate Oct 24, 2025
f6a654f
improve tool streaming
evalstate Oct 24, 2025
ac9c496
lgtm
evalstate Oct 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 50 additions & 8 deletions examples/new-api/textual_markdown_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,7 @@ def show_tool_call(
name: str | None = None,
highlight_index: int | None = None,
max_item_length: int | None = None,
metadata: dict | None = None,
) -> None:
self._app.handle_display_tool_call(
agent_name=name,
Expand All @@ -370,6 +371,7 @@ def show_tool_call(
bottom_items=bottom_items,
highlight_index=highlight_index,
max_item_length=max_item_length,
metadata=metadata,
)

def show_tool_result(
Expand Down Expand Up @@ -680,23 +682,63 @@ def handle_display_tool_call(
bottom_items: list[str] | None,
highlight_index: int | None,
max_item_length: int | None,
metadata: dict | None,
) -> None:
if tool_args:
try:
args_text = json.dumps(tool_args, indent=2, sort_keys=True)
except TypeError: # pragma: no cover - fallback for unserializable args
args_text = str(tool_args)
content = f"```json\n{args_text}\n```"
metadata = metadata or {}

if metadata.get("variant") == "shell":
command = metadata.get("command") or tool_args.get("command")
command_display = command if isinstance(command, str) and command.strip() else None
if command_display:
content = f"```shell\n$ {command_display}\n```"
else:
content = "_No shell command provided._"

details: list[str] = []
shell_name = metadata.get("shell_name")
shell_path = metadata.get("shell_path")
if shell_name or shell_path:
if shell_name and shell_path and shell_path != shell_name:
details.append(f"shell: {shell_name} ({shell_path})")
elif shell_path:
details.append(f"shell: {shell_path}")
elif shell_name:
details.append(f"shell: {shell_name}")
working_dir = metadata.get("working_dir_display") or metadata.get("working_dir")
if working_dir:
details.append(f"cwd: {working_dir}")

capability_bits: list[str] = []
if metadata.get("streams_output"):
capability_bits.append("streams stdout/stderr")
if metadata.get("returns_exit_code"):
capability_bits.append("reports exit code")

if capability_bits:
details.append("; ".join(capability_bits))

if details:
bullet_points = "\n".join(f"- {line}" for line in details)
content = f"{content}\n\n{bullet_points}"
else:
content = "_No arguments provided._"
if tool_args:
try:
args_text = json.dumps(tool_args, indent=2, sort_keys=True)
except TypeError: # pragma: no cover - fallback for unserializable args
args_text = str(tool_args)
content = f"```json\n{args_text}\n```"
else:
content = "_No arguments provided._"

self._active_assistant_message = None

right_info = "shell command" if metadata.get("variant") == "shell" else f"tool request - {tool_name}"

message = ChatMessage(
role="tool_call",
content=content,
name=agent_name or "Tool",
right_info=f"tool request - {tool_name}",
right_info=right_info,
bottom_metadata=bottom_items,
highlight_index=highlight_index,
max_item_length=max_item_length,
Expand Down
7 changes: 2 additions & 5 deletions examples/openapi/agent.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import asyncio

from fast_agent import FastAgent
from fast_agent.constants import DEFAULT_AGENT_INSTRUCTION

# Create the application
fast = FastAgent("fast-agent example")


default_instruction = """You are a helpful AI Agent.

{{serverInstructions}}

The current date is {{currentDate}}."""
default_instruction = DEFAULT_AGENT_INSTRUCTION


# Define the agent
Expand Down
2 changes: 2 additions & 0 deletions examples/setup/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

{{serverInstructions}}

{{agentSkills}}

The current date is {{currentDate}}."""


Expand Down
6 changes: 6 additions & 0 deletions examples/setup/fastagent.config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ mcp_timeline:
steps: 20 # number of timeline buckets to render
step_seconds: 15 # seconds per bucket (accepts values like "45s", "2m")

#shell_execution:
# length of time before terminating subprocess
# timeout_seconds: 20
# warning interval if no output seen
# warning_seconds: 5

# Logging and Console Configuration:
logger:
# level: "debug" | "info" | "warning" | "error"
Expand Down
13 changes: 7 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,32 +15,33 @@ classifiers = [
requires-python = ">=3.13.5,<3.14"
dependencies = [
"fastapi>=0.115.6",
"mcp==1.18.0",
"mcp==1.19.0",
"opentelemetry-distro>=0.55b0",
"opentelemetry-exporter-otlp-proto-http>=1.7.0",
"pydantic-settings>=2.7.0",
"pydantic>=2.10.4",
"pyyaml>=6.0.2",
"rich>=14.1.0",
"typer>=0.15.1",
"anthropic>=0.69.0",
"openai>=2.3.0",
"anthropic>=0.71.0",
"openai[aiohttp]>=2.6.1",
"azure-identity>=1.14.0",
"boto3>=1.35.0",
"prompt-toolkit>=3.0.52",
"aiohttp>=3.11.13",
"aiohttp>=3.13.1",
"opentelemetry-instrumentation-openai>=0.43.1; python_version >= '3.10' and python_version < '4.0'",
"opentelemetry-instrumentation-anthropic>=0.43.1; python_version >= '3.10' and python_version < '4.0'",
"opentelemetry-instrumentation-mcp>=0.43.1; python_version >= '3.10' and python_version < '4.0'",
"google-genai>=1.33.0",
"google-genai>=1.46.0",
"opentelemetry-instrumentation-google-genai>=0.3b0",
"tensorzero>=2025.7.5",
"deprecated>=1.2.18",
"a2a-sdk>=0.3.6",
"a2a-sdk>=0.3.10",
"email-validator>=2.2.0",
"pyperclip>=1.9.0",
"keyring>=24.3.1",
"textual>=6.2.1",
"python-frontmatter>=1.1.0",
]

# For Azure OpenAI with DefaultAzureCredential support, install with: pip install fast-agent-mcp[azure]
Expand Down
2 changes: 2 additions & 0 deletions src/fast_agent/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
OpenRouterSettings,
OpenTelemetrySettings,
Settings,
SkillsSettings,
TensorZeroSettings,
XAISettings,
)
Expand Down Expand Up @@ -126,6 +127,7 @@ def __getattr__(name: str):
"BedrockSettings",
"HuggingFaceSettings",
"LoggerSettings",
"SkillsSettings",
# Progress and event tracking (lazy loaded)
"ProgressAction",
"ProgressEvent",
Expand Down
5 changes: 5 additions & 0 deletions src/fast_agent/agents/agent_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@

from dataclasses import dataclass, field
from enum import StrEnum, auto
from pathlib import Path
from typing import Dict, List, Optional

from mcp.client.session import ElicitationFnT

from fast_agent.skills import SkillManifest, SkillRegistry

# Forward imports to avoid circular dependencies
from fast_agent.types import RequestParams

Expand Down Expand Up @@ -36,6 +39,8 @@ class AgentConfig:
tools: Optional[Dict[str, List[str]]] = None
resources: Optional[Dict[str, List[str]]] = None
prompts: Optional[Dict[str, List[str]]] = None
skills: SkillManifest | SkillRegistry | Path | str | None = None
skill_manifests: List[SkillManifest] = field(default_factory=list, repr=False)
model: str | None = None
use_history: bool = True
default_request_params: RequestParams | None = None
Expand Down
7 changes: 7 additions & 0 deletions src/fast_agent/agents/llm_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,15 +246,20 @@ async def generate_impl(
display_model = self.llm.model_name if self._llm else None

remove_listener: Callable[[], None] | None = None
remove_tool_listener: Callable[[], None] | None = None

with self.display.streaming_assistant_message(
name=display_name,
model=display_model,
) as stream_handle:
try:
remove_listener = self.llm.add_stream_listener(stream_handle.update)
remove_tool_listener = self.llm.add_tool_stream_listener(
stream_handle.handle_tool_event
)
except Exception:
remove_listener = None
remove_tool_listener = None

try:
result, summary = await self._generate_with_summary(
Expand All @@ -263,6 +268,8 @@ async def generate_impl(
finally:
if remove_listener:
remove_listener()
if remove_tool_listener:
remove_tool_listener()

if summary:
summary_text = Text(f"\n\n{summary.message}", style="dim red italic")
Expand Down
6 changes: 6 additions & 0 deletions src/fast_agent/agents/llm_decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -718,6 +718,12 @@ def message_history(self) -> List[PromptMessageExtended]:
return self._llm.message_history
return []

def pop_last_message(self) -> PromptMessageExtended | None:
"""Remove and return the most recent message from the conversation history."""
if self._llm:
return self._llm.pop_last_message()
return None

@property
def usage_accumulator(self) -> UsageAccumulator | None:
"""
Expand Down
Loading
Loading