add-fred-capability
SkillAI & modelsAuthor a new Fred agent capability (manifest + tools/middleware) built on fred-sdk, execution-model-agnostic (ReAct and Graph agents). Picks the right authoring lane, maps each runtime need to tools() or a middleware hook, wires entry-point registration, and enforces the capability boundary (never hand-edit union/registry hotspots, no capability code in control-plane, no runtime info in LLM tool signatures).
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the add-fred-capability skill
What this skill tells your AI
The instructions your AI receives, as published by thalesgroup/fred in .claude/skills/add-fred-capability/SKILL.md and read by ahel’s review.
Add a new Fred capability — one modular agent feature carried end to end by one
object (declaration + tools(), its execution-model-agnostic runtime), not a feature
scattered across the codebase.
The code is the spec; you are the map. Read the live types and the in-tree pilot
before writing anything; never restate a manifest field from memory — it drifts.
Companion doc: docs/swift/capabilities/AUTHORING.md.
Tier tags [T0]…[T4] mark which tier a mechanism lands in (RFC §6). Unmarked = live
today. Do not use a [T2]/[T3] mechanism as if it were live unless the caller has
confirmed that tier is implemented on their branch — check the imports actually exist.
Step 0 — Read the reference surface first (mandatory)
Do not skip this. Open and skim:
- SDK contracts —
libs/fred-sdk/fred_sdk/contracts/capability/:base.py(AgentCapability, the four ClassVar models,tools(),middleware()),manifest.py(CapabilityManifest,AssetSlot,ChatControlSpec,SidePanelSpec,TeamScopePolicy,UploadedFile;FieldSpeclives infred_sdk.contracts.models),context.py(CapabilityContext,CapabilityIdentity,SaveContext,EmptyModel),hitl.py(HitlSpec). Platform ports:fred_sdk/contracts/runtime.py(RuntimeServices,DocumentSearchPort,DocumentTreePort,DocumentSummarizePort,DocumentPortCallError— adapters map transport failures onto that typed error so capability tools can renderis_errorresults without importing any HTTP stack). Implementtools()— it is the primary authoring surface, execution-model-agnostic (works on both ReAct and Graph agents).middleware()has a default that wrapstools(); only override it directly for a hooktools()cannot express (see Step 3). - The canonical worked example —
libs/fred-runtime/fred_runtime/capabilities/document_access/capability.py(DocumentAccessCapability, #1906): a real tool wired to a platform service through a typed port, config-field scoping, one computed chat control, implemented viatools(). Copy its shape. - The minimal tracer —
libs/fred-runtime/fred_runtime/capabilities/demo.py(DemoEchoCapability): one static tool + one config field + router + owned table + chat part + side panel. The smallest full vertical. - Not everything is a capability — if the thing adjusts how the model is
called rather than what the model can call, stop and reconsider. Reasoning
was built as a full capability and then withdrawn
(
CONTROL-PLANE-PRODUCT-CONTRACT.md§33): an agent does not use reasoning the way it uses document search, so the Tools tab was the wrong home for it. It ships instead as a plain agent field (AgentTuning.reasoning_enabled, rendered in the Capabilities tab through the genericCapabilityCard) plus a platform-emitted chat control. Model-call parameters and per-turn platform options belong outside this system. - Registration + boot rules —
libs/fred-runtime/pyproject.toml([project.entry-points."fred.capabilities"]) andlibs/fred-runtime/fred_runtime/capabilities/registry.py(boot_capability_registry).
Step 1 — Pick the authoring lane (RFC §7)
| Caller need | Lane | Fred code |
|---|---|---|
| Tools + config + prompt fragment, nothing bespoke | MCP server registered in the catalog → it is a capability, id == the catalog server id, no mcp: prefix (#1988) | zero [T1] — do not write a capability class; register the MCP server |
Full vertical: validate_config, middleware, router, tables, team settings | Capability package built on fred-sdk | the package only |
| First-party / default | Same package model, installed in the shared fred-agents pod (fred-capabilities-core) | in-tree |
If the request is "just some tools + a prompt," steer to the MCP lane — it needs zero Fred code and federates across pods. Only write a capability class when the request needs save-time validation, owned tables, a router, a chat control, or a custom chat part.
Step 2 — Declare the four typed models (RFC §3.2, §3.5, §8.2)
Declare as ClassVars on the subclass (base.py is authoritative):
ConfigModel— what the user sends at agent creation → drivesmanifest.config_fields. AFieldSpecmay setui=UIHints(widget="document_libraries")to render the library/document tree picker in the agent form instead of the type-derived default input (#2023); unknown widget ids fall back gracefully.ui.visible_when="<sibling_key>"hides the field while that sibling is falsy (display-only — handle the value anyway).StoredConfigModel— what is persisted aftervalidate_config; defaults toConfigModel, so omit it unless save-time enrichment derives extra state.TurnOptionsModel— typed chat-time values from a chat control;EmptyModelif none.[T0]TeamSettingsModel— typed per-team enablement settings;EmptyModeluntil Tier 3.[T3]
The hard split (never violate): a tool signature exposes ONLY LLM arguments. Identity,
config, turn options, and services reach the tool through the middleware closure over
CapabilityContext — never the tool schema. The per-turn binding and raw access token
never enter CapabilityContext; platform access is only through typed
RuntimeServices ports. document_access is the reference for all of this.
Adding a field to an already-shipped ConfigModel? If it's optional with a default,
you're done — no version bump, no migration code, old stored configs just get the default.
Only a removed/renamed/retyped field needs manifest.version bumped plus an
upgrade_config() override. See AUTHORING.md's "Evolving a capability's config" section
for the full rule and the GreeterCapability test pattern to copy.
Step 3 — Map each runtime need to a hook (RFC §5.1)
Do not invent a hook; use the primitive. Start with tools() — it is the primary
authoring surface and the only row below that runs on both ReAct and Graph agents. Every
other row is a middleware() override — reachable from ReAct agents only; a Graph agent
never sees it. If your capability needs both plain tools AND one of these ReAct-only
hooks, implement tools() for the former and override middleware() for the latter —
do not fold tools into the middleware() override, or they vanish for Graph agents.
| Need | Hook |
|---|---|
| Add tools | tools(ctx) — execution-model-agnostic, works on ReAct and Graph |
| Tool built at chat time | middleware() override, wrap_model_call editing request.tools [T2] — ReAct only |
| Runtime context split from LLM args | CapabilityContext via the closure (either surface) |
| Edit conversation state | middleware() override, before_model returning a state-update dict [T2] — ReAct only |
| System-prompt fragment | middleware() override, wrap_model_call / modify_model_request — ReAct only |
| Guardrails / summarization / PII / retries | prebuilt LangChain middleware — free (ReAct only) |
| Tool approval (HITL) | declare HitlSpecs from hitl_specs() — the single platform gate merges them; capabilities never ship interrupt middleware (RFC §5.4) |
Chat-time controls → return ChatControlSpecs from chat_controls(config) (computed at
prep, never persisted). Custom chat card → a BaseModel with a Literal type
discriminator in manifest.chat_parts (the registry extends the UiPart union at boot;
you do not edit the union).
Used any ReAct-only row above (and not tools())? Declare
manifest.execution_models = ("react",) — exactly that value, not just "something". A
Graph agent selecting a middleware()-only capability whose execution_models contains
"graph" fails loudly at assembly with a CapabilityError, rather than silently getting
no tools. You don't even have to remember this rule to be safe: pod boot itself refuses
registration (InvalidExecutionModelError) for ANY middleware()-only capability whose
execution_models contains "graph" — never mentioned (kept the default) or written
out explicitly, both fail the same way.
A separate, earlier gate sits on the agent side: GraphAgentDefinition.supports_capabilities
defaults False, so a Graph agent's picker offers no capability at all — yours included —
until that specific agent definition opts in. execution_models above only decides
compatibility once the picker is shown; it does not make one appear.
Step 4 — Register + boot invariants (RFC §4, §7.1)
- Add one
[project.entry-points."fred.capabilities"]line in the owning package'spyproject.tomlpointing at the subclass (e.g.my_cap = "acme_cap.capability:MyCapability"). Installing the package IS the registration — there is no central list. - Register exactly once. Entry point or a manual
registry.register(...)in a test, never both — duplicate ids fail boot (DuplicateCapabilityIdError). - Boot fails loudly (each a named error) on: duplicate id, duplicate chat-part
discriminator, missing required env, and
default_on+ a required team-settings field. - Owns tables? Own
DeclarativeBase,cap_<id>_*names, no core foreign keys, an Alembic tree beside the package, andmigrations_location()returning its path. Seedemo.pydemo_migrations/.
- Team scope:
TeamScopePolicy.DEFAULT_ON(no admin gate; incompatible with a required team-settings field) orADMIN_GATED(default). MCP catalog servers use the same enum viaMCPServerConfiguration.team_scopeinmcp_catalog.yaml— defaultadmin_gated. - Manifest id must match
^[A-Za-z0-9][A-Za-z0-9._-]{0,255}$(FGA- and URL-safe, #1988) — no:or other separator. This is also why MCP capability ids are the bare catalog server id, notmcp:<server>;mcp_ids.py/is_mcp_capability_idare retired. - Manifest
iconis a snake_case Material Symbols name (graphic_eq, notGraphicEq— the frontend renders it as a font ligature, so a wrong name shows as raw text). Pick from thematerialIconslist inapps/frontend/src/rework/components/shared/utils/Type.ts; to adopt a new glyph, add its name there first. Unknown names fall back to a generic icon in the admin catalog.
Step 5 — Ships a router? Regenerate its API slice (#1979)
If the manifest declares a router, its client is generated per-capability:
cd apps/frontend && make update-<id>-capability-api
The generated slice + dumped schema (under
apps/frontend/src/rework/features/capabilities/<id>/api/) are .prettierignored — never
hand-edit them. See apps/frontend/Makefile (update-demo-echo-capability-api).
Step 6 — Verify (RFC §4)
- Unit test in isolation (pattern:
libs/fred-runtime/tests/test_capability_*): register the capability and callregistry.validate()to prove it passes the boot invariant; exercisevalidate_config,chat_controls, and each tool with a stubbedRuntimeServicesport. A missing port must fail loud, not silently return nothing. - Run
make test+make code-qualityinlibs/fred-runtime(andlibs/fred-sdkif you touched the contract surface). Green before claiming done.
Hard should-nots (refuse these — the abstraction exists to prevent them)
- Never hand-edit the central
UiPartunion or the registry hotspots (RFC §1.1). Contribute a chat part by declaring it; the registry extends the union at boot. - Never put capability runtime code in control-plane (RFC §7) — it stays the proxy/registry/team-policy authority. No "capability pod."
- Never persist asset blobs in
tuning_json— store binaries through a service invalidate_config, keep only their keys (RFC §3.8). - Never leak runtime info into an LLM-exposed tool signature (RFC §3.5) — config, identity, scope, and services flow through the middleware closure only.
- Never restate SDK fields from memory — import the models, link the pilot.
Close-out
Report: lane chosen; the capability class + entry-point line; which typed models declared;
which hooks/controls/parts used; router slice regenerated (if any); tests added +
make test/make code-quality result; and confirmation the boot invariant passes
(registry.validate() green).
Signals
- GitHub stars
- 61
- Forks
- 32
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
add-fred-capability- Source
- github.com/thalesgroup/fred