Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
59 changes: 51 additions & 8 deletions agentdescent/algorithms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,10 @@
from dataclasses import dataclass, field
from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, Tuple

__all__ = ["Algorithm", "AlgorithmError", "BuildContext", "ENTRY_POINT_GROUP",
"FIDELITY", "Overrides", "Param", "external_errors", "get", "names",
"kind_mismatch_message", "register", "resolve_params", "listing"]
__all__ = ["Algorithm", "AlgorithmError", "BuildContext", "CARDS_VS_PORTS_NOTE",
"ENTRY_POINT_GROUP", "FIDELITY", "Overrides", "Param", "external_errors",
"get", "names", "kind_mismatch_message", "register", "resolve_params",
"listing", "research_port_for"]

FIDELITY = ("benchmark_faithful", "mechanism_microport", "preset")

Expand Down Expand Up @@ -168,11 +169,28 @@ def __post_init__(self) -> None:
raise ValueError(f"{self.name}: fidelity must be one of {FIDELITY}")

def describe(self) -> Dict[str, Any]:
"""The JSON-able card ``show`` prints and ``plan`` echoes."""
return {"name": self.name, "summary": self.summary, "kinds": list(self.kinds),
"fidelity": self.fidelity, "source": self.source,
"extra_calls": self.extra_calls, "doc": self.doc,
"params": [p.to_dict() for p in self.params]}
"""The JSON-able card ``show`` prints and ``plan`` echoes.

Always labels the entry as an Algorithm **card** (``surface: "card"``).
When the same slug also has an ``examples/<name>/`` research port, the
card carries ``research_port`` and a ``fidelity_note`` so hosts never
read a mechanism slice as the paper port (#384 fidelity addendum).
"""
out: Dict[str, Any] = {
"name": self.name, "summary": self.summary, "kinds": list(self.kinds),
"fidelity": self.fidelity, "surface": "card", "source": self.source,
"extra_calls": self.extra_calls, "doc": self.doc,
"params": [p.to_dict() for p in self.params],
}
port = research_port_for(self.name)
if port is not None:
out["research_port"] = port
out["fidelity_note"] = (
f"Algorithm card ({self.fidelity}) — not the benchmark-faithful "
f"research port at {port} (docs/port-fidelity.md). Same name, "
"different propose/strategy path."
)
return out


_REGISTRY: Dict[str, Algorithm] = {}
Expand All @@ -195,6 +213,31 @@ def describe(self) -> Dict[str, Any]:
"voyager",
})

#: Registered card names that *also* have a full ``examples/<name>/`` research
#: port. Card ≠ port: the wheel entry is a mechanism slice; the checkout script
#: is the paper-faithful path (#384 fidelity addendum). Keep in sync with the
#: shipped cards that share a slug with ``docs/algo-*.md`` ports.
_CARDS_WITH_RESEARCH_PORT = frozenset({
"ace", "dgm", "evoskill", "gepa", "skillopt",
})


def research_port_for(name: str) -> Optional[str]:
"""``examples/<name>/`` when this card shares a slug with a research port."""
if name in _CARDS_WITH_RESEARCH_PORT:
return f"examples/{name}/"
return None


#: One-line product-surface note for ``algorithms list`` / MCP list_algorithms.
CARDS_VS_PORTS_NOTE = (
"Listed names are Algorithm cards shipped in the wheel (mechanism slices). "
"A same-name examples/<algo>/ research port is a separate, source-only "
"script and may use a different propose/strategy path — see "
"docs/port-fidelity.md. Filter with --kind / kind= so hosts are not offered "
"cards that cannot run on that artifact."
)


def register(algorithm: Algorithm, *, replace: bool = False) -> Algorithm:
"""Add an algorithm. A second registration of a name is an error, not a shadow."""
Expand Down
21 changes: 18 additions & 3 deletions agentdescent/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2537,9 +2537,17 @@ def cmd_doctor(a: argparse.Namespace) -> int:


def _algorithm_card_text(card: Dict[str, Any]) -> str:
lines = [f"{card['name']} [{card['fidelity']}]", f" {card['summary']}",
surface = card.get("surface", "card")
lines = [f"{card['name']} [{card['fidelity']}; {surface}]",
f" {card['summary']}",
f" kinds: {', '.join(card['kinds'])}",
f" surface: {surface} (wheel Algorithm card; not an examples/ port)",
f" extra calls: {card['extra_calls']}"]
if card.get("research_port"):
lines.append(f" research port: {card['research_port']} "
"(source checkout; different fidelity — see docs/port-fidelity.md)")
if card.get("fidelity_note"):
lines.append(f" note: {card['fidelity_note']}")
if card["source"]:
lines.append(f" source: {card['source']}")
if card["doc"]:
Expand Down Expand Up @@ -3034,10 +3042,12 @@ def cmd_algorithms(a: argparse.Namespace) -> int:
"no algorithm applies to kind 'policy_slot': that kind evolves a "
"policy slot, and `plan` refuses an algorithm for it")
if as_json:
# Keep the array shape scripts already parse; put the note on
# Keep the array shape scripts already parse; put product notes on
# stderr so hosts that only read stdout still see a list.
if a.kind == "policy_slot" and not found:
print(f"note: {policy_slot_note}", file=sys.stderr)
else:
print(f"note: {algorithms.CARDS_VS_PORTS_NOTE}", file=sys.stderr)
_out([x.describe() for x in found], as_json=True)
elif not found:
if a.kind == "policy_slot":
Expand All @@ -3046,8 +3056,13 @@ def cmd_algorithms(a: argparse.Namespace) -> int:
print(f"no algorithms for kind {a.kind!r}")
else:
width = max(len(x.name) for x in found)
print("surface=card (Algorithm cards; same-name examples/<algo>/ "
"ports are separate — docs/port-fidelity.md)")
for x in found:
print(f"{x.name:<{width}} {x.fidelity:<19} {','.join(x.kinds):<42} {x.summary}")
port = " +examples/" if algorithms.research_port_for(x.name) else ""
print(f"{x.name:<{width}} {x.fidelity:<19} "
f"{','.join(x.kinds):<42} {x.summary}{port}")
print(f"note: {algorithms.CARDS_VS_PORTS_NOTE}")
return 0
if a.action == "run":
return _cmd_algorithms_run(a)
Expand Down
16 changes: 13 additions & 3 deletions agentdescent/evolvespec.py
Original file line number Diff line number Diff line change
Expand Up @@ -996,9 +996,19 @@ def _algorithm_echo(chosen: Tuple[Any, Dict[str, Any]], spec: EvolveSpec,
[f"policies.{k}" for k in ov.policies if k in asked]
+ [f"agg_config.{k}" for k in ov.agg_config if k in spec.agg_config]
+ [f"evolve.{k}" for k in ov.knobs if k in spec.evolve])
return {"name": alg.name, "summary": alg.summary, "fidelity": alg.fidelity,
"params": params, "extra_calls": alg.extra_calls, "source": alg.source,
"doc": alg.doc, "overridden_by_spec": overridden}
card = alg.describe()
echo: Dict[str, Any] = {
"name": card["name"], "summary": card["summary"],
"fidelity": card["fidelity"], "surface": card.get("surface", "card"),
"params": params, "extra_calls": card["extra_calls"],
"source": card["source"], "doc": card["doc"],
"overridden_by_spec": overridden,
}
if card.get("research_port"):
echo["research_port"] = card["research_port"]
if card.get("fidelity_note"):
echo["fidelity_note"] = card["fidelity_note"]
return echo


#: ``evolve()`` parameters whose annotation is made only of these names take plain
Expand Down
8 changes: 6 additions & 2 deletions agentdescent/integrations/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,12 @@ unless the user asked and the server was started with `--advanced` /
run asked for 8 rounds and completed 2 in 90 minutes. Read `plan`'s notes
aloud: they say how many held-out tasks the gate actually gets, and with
too few it has both committed a worse rule and committed nothing at all.
- `algorithm` (optional): a named method. Call `list_algorithms` (or
`describe_algorithm` for one) before naming one; the same data is in
- `algorithm` (optional): a named **Algorithm card** (`surface=card`). Call
`list_algorithms` with the artifact's `kind` (or `describe_algorithm` for
one) before naming one; filter by kind so ineligible cards are not offered.
Same-name `examples/<algo>/` research ports are source-only and may differ
in fidelity — `plan` echoes `fidelity` / `research_port` / `fidelity_note`;
never claim the paper port ran when only the card did. The same data is in
`agentdescent algorithms list` / `show`. To build the spec for one,
call `algorithm_spec` (name, absolute `path`, absolute `cases`, optional
`params` object, `rounds`/`workers` 1/1 for a quick trial): it writes the
Expand Down
29 changes: 19 additions & 10 deletions agentdescent/integrations/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -394,31 +394,40 @@ def mcp_manifest(launcher: Optional[List[str]] = None) -> Dict[str, object]:
"""

ALGORITHMS_COMMAND = """---
description: List, show, or run a named AgentDescent algorithm (same cards as algorithms list/show)
description: List, show, or run a named AgentDescent Algorithm card (same as algorithms list/show)
argument-hint: "[list [--kind K] | show <name> | run <name> <path> [cases.jsonl] [--param k=v...]]"
---

Use the agentdescent skill / MCP tools. `$ARGUMENTS` chooses the verb; the data
is the same as `agentdescent algorithms list` / `show` and the MCP tools
`list_algorithms` / `describe_algorithm`. Never invent a name.

These are Algorithm **cards** (wheel mechanism slices, `surface=card`). A
same-name `examples/<algo>/` research port is a separate source-only script and
may use a different propose/strategy path — never claim paper/benchmark
fidelity for a card. Prefer `list --kind <artifact kind>` so ineligible cards
are not offered.

Parse `$ARGUMENTS` (trim; empty means `list`):

- **`list`** [optional `--kind K`]: call `list_algorithms` (or
`agentdescent algorithms list [--kind K] [--json]`). Show each name, one-line
summary, kinds, and fidelity. If the user named a kind, filter to it.
- **`list`** [optional `--kind K`]: call `list_algorithms` with the artifact's
kind when known (or `agentdescent algorithms list [--kind K] [--json]`). Show
each name, summary, kinds, fidelity, and `surface=card`. Filter by kind so
hosts are not offered cards that cannot run on that artifact.
- **`show <name>`**: call `describe_algorithm` (or
`agentdescent algorithms show <name>`). Show the full card (params, defaults,
extra cost, what it changes). Stop here unless they ask to run it.
extra cost, `research_port` / `fidelity_note` when a same-name examples/ port
exists). Stop here unless they ask to run it.
- **`run <name> <path>`** [optional `cases.jsonl`] [optional `--param k=v`]:
call `algorithm_spec` with the name, the absolute path, the absolute cases
file and only the params they gave (validate with `describe_algorithm` /
`show` first); it writes and plans the spec and returns `spec`. Run `doctor`
first, show the resolved algorithm card and estimate, and wait for my yes
before `start` with that `spec`. Same apply/cancel/resume rules as `/evolve`.
Without MCP: `agentdescent algorithms run <name> <path> [cases.jsonl]
[--param k=v...] [--rounds N] [--workers N]` (init + plan in one step; then
`evolve` after yes). For a quick, cheap trial pass `--rounds 1 --workers 1`.
first, show the resolved algorithm card (including fidelity / surface) and
estimate, and wait for my yes before `start` with that `spec`. Same
apply/cancel/resume rules as `/evolve`. Without MCP: `agentdescent algorithms
run <name> <path> [cases.jsonl] [--param k=v...] [--rounds N] [--workers N]`
(init + plan; then `evolve` after yes). For a quick trial pass
`--rounds 1 --workers 1`.

If they only named a method with no path, `show` it and ask whether to `run`.
"""
Expand Down
31 changes: 21 additions & 10 deletions agentdescent/mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,12 @@ def advanced_from_env(environ: Optional[Dict[str, str]] = None) -> bool:
"before planning a run, and tell the user what is missing instead of starting a "
"run that fails on round one."),
"list_algorithms": (
"List the named algorithms a spec can ask for (`algorithm` field), with a "
"one-line summary, the kinds each applies to and its fidelity. Call this when "
"the user names a method (GEPA, ACE, RRSI, ...) or asks what methods exist; if "
"they named none, the default recipe for the kind is usually right. Do not "
"invent a name -- an unknown one fails in plan."),
"List Algorithm *cards* a spec can ask for (`algorithm` field): name, summary, "
"kinds, fidelity, and surface=card. Pass `kind` to filter to cards that run on "
"that host artifact (text/skill_dir/…). Same-name examples/<algo>/ research "
"ports are NOT listed — they are source-only and may differ in fidelity. Call "
"when the user names a method or asks what exists; if they named none, the "
"default recipe for the kind is usually right. Do not invent a name."),
"rsi": (
"The archive's longitudinal self-improvement trend: per decision slot, "
"the first and best adopted reward, the gain, the adoption that set the "
Expand All @@ -111,10 +112,11 @@ def advanced_from_env(environ: Optional[Dict[str, str]] = None) -> bool:
"read-only and costs no model calls. `--archive` is the consolidation "
"archive, not a run store."),
"describe_algorithm": (
"One algorithm's card: summary, kinds, fidelity, source, extra cost and every "
"parameter with its type, default and bounds. Call this before setting "
"`algorithm: {name, params}` so the values you pass are the ones plan expects. "
"The same data is in `agentdescent algorithms show <name>`."),
"One Algorithm card: summary, kinds, fidelity, surface=card, source, extra "
"cost, params, and — when a same-name examples/<name>/ research port exists — "
"research_port + fidelity_note so you do not claim paper fidelity for a "
"mechanism slice. Call before setting `algorithm: {name, params}`. Same data "
"as `agentdescent algorithms show <name>`."),
"algorithm_spec": (
"Build and plan a starter spec for one named algorithm on the user's target "
"-- the MCP form of `agentdescent algorithms run NAME PATH [cases]`. Pass the "
Expand Down Expand Up @@ -431,14 +433,23 @@ def list_algorithms(self, kind: Optional[str] = None) -> Dict[str, Any]:
if kind is not None and kind not in KINDS:
return {"error": f"unknown kind {kind!r}; choose from {list(KINDS)}"}
payload: Dict[str, Any] = {
"algorithms": [a.describe() for a in algorithms.listing(kind)]}
"algorithms": [a.describe() for a in algorithms.listing(kind)],
"surface": "card",
"note": algorithms.CARDS_VS_PORTS_NOTE,
}
errors = algorithms.external_errors()
if errors:
payload["load_errors"] = errors
if kind == "policy_slot" and not payload["algorithms"]:
payload["note"] = ("no algorithm applies to kind 'policy_slot': that "
"kind evolves a policy slot, and `plan` refuses an "
"algorithm for it")
elif kind is not None and not payload["algorithms"]:
payload["note"] = (
f"no Algorithm card applies to kind {kind!r}; omit algorithm for "
"the default recipe, or pick a card from list_algorithms without "
"a kind filter (each card lists its kinds). "
+ algorithms.CARDS_VS_PORTS_NOTE)
return payload

def rsi(self, archive: Optional[str] = None, slot: Optional[str] = None,
Expand Down
2 changes: 2 additions & 0 deletions changelog.d/384-kind-fidelity-surface.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Changed
`algorithms list` / `show`, MCP `list_algorithms` / `describe_algorithm`, and `plan`'s algorithm echo now label every registry entry as an Algorithm **card** (`surface=card`), filter-friendly by host `kind`, and — for ACE / GEPA / DGM / SkillOpt / EvoSkill — name the sibling `examples/<algo>/` research port with an honest `fidelity_note` so same name is never read as same fidelity.
5 changes: 5 additions & 0 deletions docs/algorithm-authoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ Same name ≠ same fidelity: a card such as `"algorithm": "gepa"` is a wheel
mechanism slice; `examples/gepa/` is the paper-faithful port. See
[port fidelity](port-fidelity.md).

Same name ≠ same fidelity: a card such as `"algorithm": "gepa"` is a wheel
mechanism slice (`surface=card`); `examples/gepa/` is the paper-faithful port.
`algorithms list --kind K` / MCP `list_algorithms(kind=…)` must filter by host
kind so ineligible cards are not offered. See [port fidelity](port-fidelity.md).

The catalog of shipped cards is generated from the registry:
[named algorithms](algorithms.md).

Expand Down
Loading
Loading