Consumes the external Subscription Platform as source of truth for skill content; reuses skills_learning domain models (extended with KnowledgeSkill) and workflow.skill_exec resolver. HTTP/MCP deps land in api/ per CONSTITUTION.md; synced skills use a physically separate SQLite file (tasks/skills.sqlite3) to preserve the skill-authoring capability boundary. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
15 KiB
Context
This builds on the apex-agent-mvp change (currently proposed, not yet applied), which establishes the Device/Driver/Perception/Agent-Runtime/MCP layers for Apex Agent. That change already defines an MCP Tool Server (api/mcp.py) exposing device capabilities; this change adds a parallel skills/ package and new MCP tools onto the same server, without touching the device/perception/runtime code.
The key external constraint: skill authoring, versioning, and entitlement live in a separate, already-planned Subscription Platform (a distinct information-management system, out of scope here). Apex Agent is a consumer of that platform's catalog, not the source of truth for skill content. We only control: how Apex Agent stores what it has synced, how it exposes that to the LLM via MCP, and the contract it expects the Subscription Platform to satisfy.
Two kinds of Skill must be supported, per product direction:
- Knowledge skill: free-form structured instructional content (markdown/text + metadata) the LLM reads to decide how to act — e.g. "tips for searching effectively on Xiaohongshu." The LLM still plans/executes via the existing
agent-runtime/toolslayer. - Flow-template skill: a predefined, parameterized sequence of capability calls (tap/swipe/input steps with placeholders, e.g.
{query}), which the LLM mostly fills in and triggers rather than re-planning from scratch.
Goals / Non-Goals
Goals:
- Define a single
Skilldata model general enough to represent both knowledge and flow-template skills with shared metadata (id, name, description, version, tags, subscription/source id). - Expose skills to the LLM through MCP tools that are indistinguishable in style from the existing device tools (semantic, driver/platform-agnostic).
- Define a clear, minimal contract for what Apex Agent needs from the Subscription Platform: a way to fetch "skills I'm entitled to" and a way to learn about changes (poll or push), without dictating that platform's internal design.
- Enforce subscription-based visibility so
list_skills/search_skills/get_skillonly ever surface skills the current deployment (tenant/device/agent) is actually subscribed to. - Let a flow-template skill's steps execute through the existing
tools/functions fromapex-agent-mvp(no new execution primitives) — a flow template is just a data-driven script over the same capability calls the Executor already uses.
Non-Goals:
- Designing or implementing the Subscription Platform itself (its UI, billing, authoring workflow, or storage).
- Automatic skill generation/authoring by the LLM.
- A full permission/RBAC system — subscription visibility is scoped to "is this skill in my entitled set," not fine-grained per-user roles.
- Real-time push infrastructure (websockets/queues) as a hard requirement — push is supported as an optional transport; polling sync must work standalone.
- Executing flow-template skills with a new engine — they are interpreted by the existing
runtime/executor.py, not a separate workflow engine.
Decisions
D1: Skill is a single model with a kind discriminator (knowledge | flow_template), not two unrelated types
Both kinds share SkillMetadata (id, name, description, version, tags, source/subscription id, updated_at). A KnowledgeSkill carries content (markdown/text). A FlowTemplateSkill carries steps (ordered list of {tool, args_template}, where args_template values may contain {param} placeholders) and a parameters schema (name, type, required, description) used to validate/prompt for inputs before execution.
- Alternative considered: Two entirely separate catalogs/tables/tool sets for knowledge vs. flow skills. Rejected —
list_skills/search_skillswould need to be duplicated, and most metadata (id/name/description/version/tags) is identical; a discriminated union keeps one catalog and one set of list/search tools whileget_skillreturns kind-specific content.
D2: Skill Catalog is a local, read-mostly cache; Subscription Platform is always the source of truth
skills/catalog.py stores the last-synced copy of entitled skills locally (for offline availability and low-latency MCP responses) but never treats local edits as authoritative — there is no "create/update skill" MCP tool or API in this change. All content changes flow one direction: Subscription Platform → sync client → local catalog.
- Alternative considered: Let Apex Agent locally override/edit synced skills. Rejected — breaks the "another system manages skills" requirement from the proposal and creates drift/merge-conflict problems with no clear owner.
D3: Sync client supports pull (poll) as the required baseline, push (webhook) as an optional accelerator
skills/sync_client.py defines a fetch_entitled_skills(subscription_id, since_version) contract against the Subscription Platform's assumed API, called on a configurable interval (baseline). If the platform can also deliver change notifications (webhook/callback), an optional receiver triggers an immediate out-of-cycle fetch instead of waiting for the next poll — but correctness never depends on push arriving.
- Alternative considered: Push-only (webhook-driven) sync. Rejected — makes Apex Agent's catalog freshness dependent on network reachability of an inbound webhook, which is operationally harder (firewalls/NAT) than Apex Agent making outbound poll requests; poll-as-baseline is strictly more deployable.
D4: Subscription-based visibility is enforced at the catalog query layer, not just at sync time
Even though sync only ever pulls "entitled" skills, skills/catalog.py's query functions (used by both MCP tools and any future internal caller) re-check that a skill's subscription id is still in the caller's active subscription set at query time, not just at last-sync time. This guards against a stale local cache still containing a skill whose entitlement was revoked but not yet re-synced.
- Alternative considered: Trust the local cache fully between syncs (no re-check at query time). Rejected — entitlement revocation should not have to wait for the next poll interval to stop being visible if the revocation is already known locally (e.g. via a push notification that only carries "removed" without full content).
D5: Flow-template skills execute via existing tools//runtime/executor.py, with a resolution step in between
skills/mcp_tools.py exposes get_skill (returns metadata + content/template) and a parameter-resolution helper; it does not expose a run_skill tool that silently executes on the LLM's behalf. The LLM fetches the flow template, fills placeholders itself (or asks the user for missing required parameters), and then issues the already-existing device-capability tool calls (tap, input_text, etc.) from apex-agent-mvp in the order/values the template specifies.
- Alternative considered: Add a single
run_skill_flow(skill_id, params)MCP tool that executes the whole sequence server-side. Rejected for this change — it would bypass the Agent Runtime's Observe→Think→Act loop and Executor retry/wait logic fromapex-agent-mvp, silently skipping Scene verification between steps. Keeping execution inside the existing loop (LLM issues each tool call itself, informed by the template) preserves the "AI decides based on what it currently observes" principle the whole platform is built on. A batched execution helper can be revisited later as a design decision in its own change if needed.
D6: Reuse skills_learning/ domain models and workflow/skill_exec.py resolver; do not duplicate them
The archived skill-learning-runtime change already shipped skills_learning/models.py defining SkillKind ("knowledge" | "flow_template"), SkillMetadata, FlowStep, Skill, and FlowTemplateSkill, plus workflow/skill_exec.py with validate_skill_args / resolve_skill_steps / _resolve_value. This change does not introduce a parallel skills/ package. Instead:
- Reuse all existing domain types verbatim. The only model-layer addition is a
KnowledgeSkill(Skill)class inskills_learning/models.py(thekindLiteral already includes"knowledge"but no concrete class exists for it yet). - The MCP parameter-resolution helper (§4.2 of tasks) wraps
workflow.skill_exec.resolve_skill_stepsrather than reimplementing it. - The archived
skill-authoringspec (now inopenspec/specs/skill-authoring/spec.md) explicitly anticipated this change: locally-synthesized skills live inskills_learning/store.pytaggedsource = "local-synthesis"and must not write into the externally-synced catalog store. This change honors that boundary by using a physically separate SQLite file (tasks/skills.sqlite3) for synced skills. - Alternative considered: Move all shared skill domain types into
core/skill_models.pyto make the cross-feature reuse explicit. Deferred — would be a larger refactor of already-tested code, and the boundary is data-model-only (no store/runtime coupling). Revisited if a third consumer appears.
D7: CONSTITUTION.md compliance — code lands in core-extended, storage, and api layers only
Per docs/CONSTITUTION.md lines 33-34 ("HTTP and MCP dependencies enter at api. No LLM, HTTP, or MCP dependency may appear in core, driver, device, or tools."), and the dependency direction core → driver/device → tools → perception → storage → runtime → api, this change's files map as follows:
| Concern | File (new or extended) | Layer | Allowed deps |
|---|---|---|---|
| Domain models (extend) | skills_learning/models.py |
core (feature-scoped) | dataclasses, core.models only — zero HTTP/MCP/LLM |
| Synced-skill local store + query | storage/skill_catalog.py (new) |
storage | sqlite3, skills_learning.models — zero HTTP/MCP/LLM |
| Subscription HTTP client + poll loop + webhook | api/skill_sync.py (new) |
api | httpx, storage.skill_catalog |
| Skill MCP tools (list/search/get/resolve) | api/skill_catalog_mcp.py (new) |
api | mcp.server.fastmcp, storage.skill_catalog, workflow.skill_exec |
| MCP server wire-up | api/mcp.py (one-line addition) |
api | calls register_skill_catalog_tools(...) at end of create_mcp_server |
core, driver, device, tools receive zero new imports from this change. The tasks/skills.sqlite3 file is physically separate from both tasks/tasks.sqlite3 (task metadata) and the in-memory skills_learning.SkillStore (local synthesis), preserving the archived skill-authoring spec's "no cross-writes" contract at the storage layer.
Risks / Trade-offs
- [Risk] The Subscription Platform's actual API shape is unknown/assumed (
fetch_entitled_skills, optional webhook) → Mitigation: keepskills/sync_client.pybehind a small internal interface (similar toDriverinapex-agent-mvp) so the concrete HTTP client can be adjusted once the real Subscription Platform API is finalized, without touchingcatalog.pyormcp_tools.py. - [Risk] Stale cache could serve outdated skill content between polls → Mitigation: D4's query-time entitlement re-check plus a configurable poll interval; document the staleness window explicitly rather than promising real-time freshness.
- [Risk] Flow-template skills reference tools/parameters that drift from the actual
tools/function signatures inapex-agent-mvp(e.g. a template calls a tool that was renamed) → Mitigation: validate a flow template'stoolnames against the currently registered MCP/tool set at sync time (or at least atget_skilltime) and surface a clear "skill unavailable/invalid" error rather than letting a bad call reach the device. - [Trade-off] No local skill authoring/editing keeps this change simple and avoids ownership ambiguity, but means Apex Agent is fully dependent on the Subscription Platform being reachable at least once to have any skills at all — acceptable since the platform is a required dependency by design, not an optional enhancement.
- [Trade-off] Not providing a server-side
run_skill_flowexecution tool keeps flow templates consistent with the Observe-Think-Act loop, at the cost of the LLM needing a few more tool-call round trips per flow skill than a single batched call would take — acceptable given the platform's core principle of always re-observing between actions. - [Risk] Reusing
skills_learning/models.pyas the domain model for synced skills couples this change to theskills_learningfeature package. If either consumer (local synthesis or external sync) needs a divergent model shape, ripple effects result. → Mitigation: the coupling is at the data-model layer only — no store, runtime, or executor coupling exists. If a third consumer appears or divergence is needed, refactor the shared types intocore/skill_models.pyas a separate cleanup change (not blocking this one).
Migration Plan
Additive across existing packages — no new top-level package is introduced:
skills_learning/models.py: gains one new dataclass (KnowledgeSkill). No changes to existing types; existing tests intests/test_skill_*.pyare unaffected.storage/skill_catalog.py(new): independent module + a new SQLite filetasks/skills.sqlite3. Does not touchstorage/task_metadata.py'stasks/tasks.sqlite3orskills_learning/store.py's in-memory store.api/skill_sync.py(new): HTTP sync client + poll runner + webhook handler. Self-contained; the webhook registers on the existing FastAPI app fromapex-agent-mvpif applied, or stands alone for testing.api/skill_catalog_mcp.py(new): MCP tool registration helper.api/mcp.py: one-line addition —register_skill_catalog_tools(server, ...)called at the end ofcreate_mcp_server. Existing device-capability tools are unchanged.- Rollback: remove the four new/extended files and delete
tasks/skills.sqlite3. No data migration or schema changes to existing tables are required.
If apex-agent-mvp is not yet applied, api/skill_catalog_mcp.py can stand up its own MCP server instance for independent testing (mirroring the original migration note); otherwise it composes onto the existing server via the one-line wire-up above.
Open Questions
- Exact Subscription Platform API contract (auth mechanism, request/response shapes, whether it supports
since_versionincremental sync or only full-catalog fetch) — to be confirmed with that platform's team/spec before the concreteHttpSubscriptionClientinapi/skill_sync.pyis finalized. TheSubscriptionClientProtocol (§3.1 of tasks) is shaped to absorb either full or incremental fetch; the concrete HTTP mapping is the only thing that changes once the real API is known. - Whether flow-template
args_templateplaceholders need a richer expression language (e.g. simple conditionals) or plain{param}substitution is sufficient for the MVP — left open, default to plain substitution (matching existingworkflow.skill_exec._resolve_value) and revisit if a real skill needs more.
Resolved Questions
- Local catalog storage location (previously open): resolved by D7 — a physically separate SQLite file at
tasks/skills.sqlite3, owned bystorage/skill_catalog.py. Chosen to preserve the archivedskill-authoringspec's "no cross-writes between local-synthesis and synced-skill stores" contract at the storage layer (separate files make the boundary physical, not just conventional). Reusingtasks/tasks.sqlite3's file with new tables was rejected because it would couple sync-skill schema migrations to task-metadata migrations.