Skip to content

Commit dec4b6b

Browse files
authored
Feat/update readme (#731)
* Update skills-repo submodule after lsp-setup Rust merge * Add go pack startup flow * Show pack readme during interactive startup * README from packs on update, one-command download/start * Fix pytest test_service module collision * Normalize ANSI output in go pack CLI test
1 parent cd33ab2 commit dec4b6b

28 files changed

Lines changed: 1525 additions & 198 deletions

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ uv pip install fast-agent-mcp # install fast-agent!
7070
fast-agent go # start an interactive session
7171
fast-agent go --url https://hf.co/mcp # with a remote MCP
7272
fast-agent go --model=generic.qwen2.5 # use ollama qwen 2.5
73+
fast-agent go --pack analyst --model haiku # install/reuse a card pack and launch it
7374
fast-agent scaffold # create an example agent and config files
7475
uv run agent.py # run your first agent
7576
uv run agent.py --model='o3-mini?reasoning=low' # specify a model
@@ -79,6 +80,11 @@ fast-agent quickstart workflow # create "building effective agents" examples
7980
8081
`--server` remains available for backward compatibility but is deprecated; `--transport` now automatically switches an agent into server mode.
8182
83+
For packaged starter agents, use `fast-agent go --pack <name> --model <model>`.
84+
This installs the pack into the selected fast-agent environment if needed, then
85+
starts `go` normally. `--model` is a fallback for cards without an explicit
86+
model setting; a model declared directly in an AgentCard still wins.
87+
8288
Other quickstart examples include a Researcher Agent (with Evaluator-Optimizer workflow) and Data Analysis Agent (similar to the ChatGPT experience), demonstrating MCP Roots support.
8389
8490
> [!TIP]

src/fast_agent/acp/server/agent_acp_server.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,11 @@
8888
from fast_agent.context import Context
8989
from fast_agent.core.exceptions import ProviderKeyError
9090
from fast_agent.core.fastagent import AgentInstance
91-
from fast_agent.core.instruction_refresh import McpInstructionCapable, build_instruction
91+
from fast_agent.core.instruction_refresh import (
92+
McpInstructionCapable,
93+
build_instruction,
94+
resolve_instruction_skill_manifests,
95+
)
9296
from fast_agent.core.instruction_utils import (
9397
build_agent_instruction_context,
9498
get_instruction_template,
@@ -746,7 +750,7 @@ async def _resolve_instruction_for_session(
746750
effective_context = dict(context)
747751
if isinstance(agent, McpInstructionCapable):
748752
aggregator = agent.aggregator
749-
skill_manifests = agent.skill_manifests
753+
skill_manifests = resolve_instruction_skill_manifests(agent, agent.skill_manifests)
750754
skill_read_tool_name = agent.skill_read_tool_name
751755
if agent.instruction_context:
752756
effective_context = dict(agent.instruction_context)

src/fast_agent/agents/mcp_agent.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,11 @@ async def _apply_instruction_templates(self) -> None:
434434
Apply template substitution to the instruction, including server instructions.
435435
This is called during initialization after servers are connected.
436436
"""
437-
from fast_agent.core.instruction_refresh import build_instruction, format_agent_skills
437+
from fast_agent.core.instruction_refresh import (
438+
build_instruction,
439+
format_agent_skills,
440+
resolve_instruction_skill_manifests,
441+
)
438442

439443
if not self._instruction_template:
440444
return
@@ -443,7 +447,7 @@ async def _apply_instruction_templates(self) -> None:
443447
new_instruction = await build_instruction(
444448
self._instruction_template,
445449
aggregator=self._aggregator,
446-
skill_manifests=self._skill_manifests,
450+
skill_manifests=resolve_instruction_skill_manifests(self, self._skill_manifests),
447451
skill_read_tool_name=self.skill_read_tool_name,
448452
context=self._instruction_context,
449453
source=self._name,

src/fast_agent/cards/service.py

Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
"""Small service-facing API for card pack management.
2+
3+
This module provides a stable, integration-friendly surface that works with
4+
plain registry sources and managed environment roots without coupling callers
5+
to CLI or slash-command presentation details.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import asyncio
11+
from dataclasses import dataclass
12+
from typing import TYPE_CHECKING
13+
14+
from fast_agent.cards import manager
15+
16+
if TYPE_CHECKING:
17+
from collections.abc import Sequence
18+
from pathlib import Path
19+
20+
from fast_agent.cards.manager import (
21+
CardPackInstallResult,
22+
CardPackPublishResult,
23+
CardPackRemovalResult,
24+
CardPackUpdateInfo,
25+
LocalCardPack,
26+
MarketplaceCardPack,
27+
)
28+
from fast_agent.config import Settings
29+
from fast_agent.paths import EnvironmentPaths
30+
31+
32+
class CardPackLookupError(LookupError):
33+
"""Raised when a requested marketplace or local card pack cannot be resolved."""
34+
35+
36+
@dataclass(frozen=True)
37+
class MarketplaceScanResult:
38+
source: str
39+
packs: list[MarketplaceCardPack]
40+
41+
42+
@dataclass(frozen=True)
43+
class CardPackReadmeRecord:
44+
pack_name: str
45+
pack_dir: Path
46+
readme: str | None
47+
48+
49+
@dataclass(frozen=True)
50+
class CardPackInstallRecord:
51+
pack: MarketplaceCardPack
52+
install_result: CardPackInstallResult
53+
readme: str | None
54+
55+
56+
@dataclass(frozen=True)
57+
class EnsuredCardPack:
58+
name: str
59+
pack_dir: Path
60+
installed: bool
61+
install_record: CardPackInstallRecord | None = None
62+
63+
64+
@dataclass(frozen=True)
65+
class CardPackUpdatePlan:
66+
available: list[CardPackUpdateInfo]
67+
selected: list[CardPackUpdateInfo]
68+
69+
70+
@dataclass(frozen=True)
71+
class CardPackUpdateResult:
72+
applied: list[CardPackUpdateInfo]
73+
readmes: list[CardPackReadmeRecord]
74+
75+
76+
__all__ = [
77+
"CardPackInstallRecord",
78+
"CardPackLookupError",
79+
"CardPackReadmeRecord",
80+
"EnsuredCardPack",
81+
"CardPackUpdatePlan",
82+
"CardPackUpdateResult",
83+
"MarketplaceScanResult",
84+
"apply_update_plan",
85+
"ensure_pack_available",
86+
"ensure_pack_available_sync",
87+
"check_updates",
88+
"install_pack",
89+
"install_pack_sync",
90+
"install_selected_pack",
91+
"list_installed_packs",
92+
"plan_updates",
93+
"publish_pack",
94+
"read_installed_pack_readme",
95+
"resolve_registry",
96+
"remove_pack",
97+
"scan_marketplace",
98+
"scan_marketplace_sync",
99+
"select_installed_pack",
100+
"select_marketplace_pack",
101+
]
102+
103+
104+
async def scan_marketplace(source: str) -> MarketplaceScanResult:
105+
packs, resolved_source = await manager.fetch_marketplace_card_packs_with_source(source)
106+
return MarketplaceScanResult(source=resolved_source, packs=packs)
107+
108+
109+
def scan_marketplace_sync(source: str) -> MarketplaceScanResult:
110+
return asyncio.run(scan_marketplace(source))
111+
112+
113+
def resolve_registry(source: str | None = None, *, settings: Settings | None = None) -> str:
114+
return source or manager.get_marketplace_url(settings)
115+
116+
117+
def list_installed_packs(*, environment_paths: EnvironmentPaths) -> list[LocalCardPack]:
118+
return manager.list_local_card_packs(environment_paths=environment_paths)
119+
120+
121+
def select_marketplace_pack(
122+
packs: Sequence[MarketplaceCardPack],
123+
selector: str,
124+
) -> MarketplaceCardPack:
125+
selected = manager.select_card_pack_by_name_or_index(list(packs), selector)
126+
if selected is None:
127+
raise CardPackLookupError(f"Card pack not found: {selector}")
128+
return selected
129+
130+
131+
def select_installed_pack(
132+
*,
133+
environment_paths: EnvironmentPaths,
134+
selector: str,
135+
) -> LocalCardPack:
136+
packs = list_installed_packs(environment_paths=environment_paths)
137+
selected = manager.select_installed_card_pack_by_name_or_index(packs, selector)
138+
if selected is None:
139+
raise CardPackLookupError(f"Card pack not found: {selector}")
140+
return selected
141+
142+
143+
async def install_selected_pack(
144+
pack: MarketplaceCardPack,
145+
*,
146+
environment_paths: EnvironmentPaths,
147+
force: bool,
148+
) -> CardPackInstallRecord:
149+
install_result = await manager.install_marketplace_card_pack(
150+
pack,
151+
environment_paths=environment_paths,
152+
force=force,
153+
)
154+
return CardPackInstallRecord(
155+
pack=pack,
156+
install_result=install_result,
157+
readme=manager.load_card_pack_readme(install_result.pack_dir),
158+
)
159+
160+
161+
async def install_pack(
162+
source: str,
163+
selector: str,
164+
*,
165+
environment_paths: EnvironmentPaths,
166+
force: bool,
167+
) -> CardPackInstallRecord:
168+
marketplace = await scan_marketplace(source)
169+
selected = select_marketplace_pack(marketplace.packs, selector)
170+
return await install_selected_pack(
171+
selected,
172+
environment_paths=environment_paths,
173+
force=force,
174+
)
175+
176+
177+
def install_pack_sync(
178+
source: str,
179+
selector: str,
180+
*,
181+
environment_paths: EnvironmentPaths,
182+
force: bool,
183+
) -> CardPackInstallRecord:
184+
return asyncio.run(
185+
install_pack(
186+
source,
187+
selector,
188+
environment_paths=environment_paths,
189+
force=force,
190+
)
191+
)
192+
193+
194+
async def ensure_pack_available(
195+
*,
196+
selector: str,
197+
environment_paths: EnvironmentPaths,
198+
registry: str | None = None,
199+
force: bool = False,
200+
) -> EnsuredCardPack:
201+
try:
202+
installed_pack = select_installed_pack(
203+
environment_paths=environment_paths,
204+
selector=selector,
205+
)
206+
except CardPackLookupError:
207+
installed_pack = None
208+
209+
if installed_pack is not None:
210+
return EnsuredCardPack(
211+
name=installed_pack.name,
212+
pack_dir=installed_pack.pack_dir,
213+
installed=False,
214+
)
215+
216+
install_record = await install_pack(
217+
resolve_registry(registry),
218+
selector,
219+
environment_paths=environment_paths,
220+
force=force,
221+
)
222+
return EnsuredCardPack(
223+
name=install_record.pack.name,
224+
pack_dir=install_record.install_result.pack_dir,
225+
installed=True,
226+
install_record=install_record,
227+
)
228+
229+
230+
def ensure_pack_available_sync(
231+
*,
232+
selector: str,
233+
environment_paths: EnvironmentPaths,
234+
registry: str | None = None,
235+
force: bool = False,
236+
) -> EnsuredCardPack:
237+
return asyncio.run(
238+
ensure_pack_available(
239+
selector=selector,
240+
environment_paths=environment_paths,
241+
registry=registry,
242+
force=force,
243+
)
244+
)
245+
246+
247+
def remove_pack(
248+
*,
249+
environment_paths: EnvironmentPaths,
250+
selector: str,
251+
) -> CardPackRemovalResult:
252+
selected = select_installed_pack(environment_paths=environment_paths, selector=selector)
253+
return manager.remove_local_card_pack(
254+
selected.pack_dir.name,
255+
environment_paths=environment_paths,
256+
)
257+
258+
259+
def read_installed_pack_readme(
260+
*,
261+
environment_paths: EnvironmentPaths,
262+
selector: str,
263+
) -> CardPackReadmeRecord:
264+
selected = select_installed_pack(environment_paths=environment_paths, selector=selector)
265+
return _build_readme_record(selected.name, selected.pack_dir)
266+
267+
268+
def check_updates(*, environment_paths: EnvironmentPaths) -> list[CardPackUpdateInfo]:
269+
return manager.check_card_pack_updates(environment_paths=environment_paths)
270+
271+
272+
def plan_updates(
273+
*,
274+
environment_paths: EnvironmentPaths,
275+
selector: str,
276+
) -> CardPackUpdatePlan:
277+
available = check_updates(environment_paths=environment_paths)
278+
selected = manager.select_card_pack_updates(available, selector)
279+
if not selected:
280+
raise CardPackLookupError(f"Card pack not found: {selector}")
281+
return CardPackUpdatePlan(available=available, selected=selected)
282+
283+
284+
def apply_update_plan(
285+
selected: Sequence[CardPackUpdateInfo],
286+
*,
287+
environment_paths: EnvironmentPaths,
288+
force: bool,
289+
) -> CardPackUpdateResult:
290+
applied = manager.apply_card_pack_updates(
291+
list(selected),
292+
environment_paths=environment_paths,
293+
force=force,
294+
)
295+
readmes = [
296+
_build_readme_record(update.name, update.pack_dir)
297+
for update in applied
298+
if update.status == "updated"
299+
]
300+
return CardPackUpdateResult(applied=applied, readmes=readmes)
301+
302+
303+
def publish_pack(
304+
*,
305+
environment_paths: EnvironmentPaths,
306+
selector: str,
307+
push: bool,
308+
commit_message: str | None,
309+
temp_dir: Path | None,
310+
keep_temp: bool,
311+
) -> CardPackPublishResult:
312+
selected = select_installed_pack(environment_paths=environment_paths, selector=selector)
313+
return manager.publish_local_card_pack(
314+
selected.pack_dir,
315+
environment_paths=environment_paths,
316+
push=push,
317+
commit_message=commit_message,
318+
temp_dir=temp_dir,
319+
keep_temp=keep_temp,
320+
)
321+
322+
323+
def _build_readme_record(pack_name: str, pack_dir: Path) -> CardPackReadmeRecord:
324+
return CardPackReadmeRecord(
325+
pack_name=pack_name,
326+
pack_dir=pack_dir,
327+
readme=manager.load_card_pack_readme(pack_dir),
328+
)

0 commit comments

Comments
 (0)