Navigácia
ELOWEN · Dokumentácia

Posledná aktualizácia: 20. októbra 2026

Prechádzať dokumentáciu · skills implementation

Tento článok zatiaľ nie je preložený. Je dostupný v angličtine.

Developer reference

skills implementation

Problem reporting audiences

The canonical skill reader marks missing, redirected or unreadable personal and instance files with the core typed instance-condition cause. Bundled and plugin-owned file failures retain software reporting. Invocation and catalog catches preserve typed core causes in skills.catalog_failed, skills.file_read_failed and skills.input_transform_failed. Move and edit producers keep filesystem state and access refusals local from Node errno fields; programming errors and rollback failures remain reportable. Existing diagnostics and owner isolation are unchanged. This uses the exported elowen-plugin-shared/errors seam, which core 0.30.6 first released.

Skill files and catalog ownership

The implementation keeps these boundaries in lib/skillFiles.mjs: personal ownership and canonical paths, name collisions, frontmatter parsing, content versions, revision checks and writes. lib/skillRoutes.mjs and lib/skillTools.mjs use that same owner, as does the store given to lib/reviewer.mjs. lib/skillLoadTool.mjs resolves the current host catalog and pages skill instructions; its argument parsing and placeholder substitution are private. lib/skillInput.mjs expands explicit invocations against the same grant-filtered catalog. index.mjs registers contributions and replaces the complete skill set through the host reload control. The reviewer uses elowen-plugin-shared/atomicJson for strict JSON reads and compact atomic counter writes, with no additional model calls or per-load disk scan.

Management and invocation contracts

The asynchronous registerCommandProvider callback awaits skillCatalog.visibleEntries({ invocation: 'user' }) before projecting direct input aliases. The name and description come from the same live catalog as /skills; every alias targets /skill:name, and the host forwards the argument tail unchanged before transformSkillInput. Both parsers split at the first whitespace character, including tabs and newlines, and preserve the separator and remaining text byte-for-byte. Core and static plugin names win collisions; invalid one-to-32-character command names stay reachable only through /skills. No skill bodies or personal names are registered as instance-wide commands.

Automatic discovery and user invocation are separate projections of the same registry. Native disable-model-invocation and the per-account plugin-skill switch hide a skill from automatic discovery without preventing manual loading. Contributor grants and ownership still gate both projections. An enabled same-name contribution takes precedence over a disabled contribution in the user projection; a disabled contribution remains the selection when no enabled one exists. A loaded skill means instructions have entered the conversation; it does not change either visibility switch. The menus refetch when opened, and invocation rechecks live authority. Global platform menus have no account scope and do not include these aliases. PI owns native skill parsing/discovery; the canonical reader remains necessary because PI's raw skill expansion does not enforce Elowen's registered-directory and managed-resource boundaries.

Skills management awaits currentAccountUserId, instance-owner/admin checks and account existence before resolving personal CreateSkill and DeleteSkill targets, including an inherited delegated contribution account. Reviewer list/create operations also await the current shared catalog before their synchronous file-write boundary. An unlinked turn has no personal target. Instance creation remains instance-owner-only; instance deletion remains admin-session-only. Personal management is used by delegated runs that save reusable procedures.

SkillLoad, ListSkills, HTTP listing and explicit /skill:name invocation await the current visible catalog before reading an entry through one file reader. The file must remain inside its registered canonical directory; a local personal directory must also remain inside that account's own skills store. Missing or redirected files are refused. SkillLoad retains full-source paging and argument substitution; explicit invocation supplies the markdown body and trailing user arguments. No second catalog or cached body is introduced, and the existing visibleSkills host control remains available to installed consumers.

PATCH and ownership transfer prepare revisions and partial edits through one file owner before writing or renaming. A supplied expectedRevision must be a non-negative integer matching the current file, including for transfers without an edit. A stale revision returns HTTP 409 with error only. An edited transfer preserves unknown frontmatter and support files and attempts to restore the original location if its destination write fails. Reload remains caller-owned. The Skills settings editor is the consumer of transfer-with-edit.

GET /plugins/skills/list exposes catalogSource and effective as the sole scope and invocation-state fields; scope and active are removed. The web and CLI pickers consume those canonical fields. The Skills bundle requires host UI API 55 and uses api(path, { method, json: payload }) and utils.interpolate(text, values), while explicit invocation uses elowen-plugin-shared/xml. With no explicit invocation, input remains unchanged. A denied or missing skill produces the existing unavailable-skill result. XML attributes escape entities and both quotes and replace forbidden C0 characters; skill body text is not XML-escaped.

Declared HTTP patterns

Core matches each authenticated API declaration as a complete pattern since 0.30.4; this plugin declares core 0.30.7 and shared API 14. Skills handlers receive an empty req.path; the :name segments of PATCH, DELETE and ownership transfer arrive in req.params.name, decoded once by core. Undeclared suffixes return dispatcher 404 before any file operation or reload. Owner, revision and account authorization remain handler-owned.

Explicit HTTP mutation owners

Skills HTTP mutation targets are explicit. POST /plugins/skills, PATCH and DELETE /plugins/skills/:name, and POST /plugins/skills/:name/owner require ?owner=me, ?owner=instance or ?owner=<account-id>. For example:

POST /plugins/skills?owner=me

{"name":"report-guide","description":"Use for reports.","content":"Check the source."}

A transfer uses the query owner for the source and the JSON body's owner for the destination. Both owners resolve through lib/skillFiles.mjs, with refusals mapped by lib/skillRoutes.mjs before file writes or catalog reload. An omitted, empty or whitespace-only query returns HTTP 400:

{"error":"owner is required: \"instance\", \"me\" or an account id"}

No target is inferred from administrator status. Invalid owner syntax returns 400. Instance targets and other accounts remain administrator-only; a missing account is refused without creating its directory. Revision and directory-containment checks remain in force. These are file operations with a live catalog reload after successful mutation, not data migrations.

GET /plugins/skills/list retains its account catalog contract and does not require owner. SkillsSettings is a real consumer: it sends the selected source owner in the query and the destination owner in the transfer body. The browser client requires SkillOwner in create, update and delete calls, and its mutation hooks, registry runtime declarations and test-host hooks require the same explicit selection. SkillOwner is number | 'instance' | null; null is serialized as owner=me. SkillsPicker and the CLI deletion picker also choose an explicit owner. Successful writes retain live catalog reload, and update hooks retain optimistic behavior and rollback. See the browser client and hooks.

Post-turn skill reviewer

The reviewer imports isRecord from elowen-plugin-shared/record for state, totals, run, write and proposal objects. This synchronous guard only excludes null, arrays and non-objects; it does not require Object.prototype. Exact keys, counters, timestamps and proposal validation remain reviewer-owned. Invalid persisted state still fails without rewriting bytes, and invalid model output still produces the existing refusal before any skill write.

index.mjs registers the sole existing seam with { contractVersion: 2, run: reviewer.run, status: reviewer.status }. Old registrations are explicitly refused by new core, with no compatibility inference path. The manifest declares core 0.30.7, a released core that supplies this contract together with the typed instance-condition seam, and core/Skills must ship as a compatible set.

Core supplies this exact input:

interface SkillPostTurnReviewArgs {
  userId: number;
  successfulToolCalls: number;
  loadedSkillNames: readonly string[];
  budget: number;
  reviewEveryToolCalls: number;
  signal: AbortSignal;
  authorize: () => Promise<boolean>;
  currentBudget: () => Promise<number>;
  review: (directive: string) => Promise<
    | { status: 'completed'; text: string }
    | { status: 'skipped'; reason: SkillReviewSkipReason }
  >;
}

SkillReviewSkipReason is exactly 'quota' | 'unknown-quota' | 'unsupported-route' | 'hosted-tools' | 'stale-source' | 'runner-unavailable' | 'revoked'. Core offers only clean completed human owner web/CLI turns meeting the successful-call minimum. Pending, denied and failed direct/nested calls do not count. The plugin owns per-account serialization and cadence across those exchanges, default 100; explicit saved intervals, the minimum and write budget survive. No child/request is made below cadence or with a zero budget.

The host-owned review(directive) closure admits at most one attempt for the captured settled source. It pins the session/leaf, actual last provider request, provider/model/endpoint and connection/generation, preserving system/tool/history IDs, cache identity and metadata. It appends the full parent final answer when not already source input and one review directive. It never reconstructs a reduced exchange or argument trajectory. The plugin cannot select a model, connection, session or tool set.

lib/reviewer.mjs builds the JSON-only directive from procedure rules, proposal schema and a bounded visible catalog plus patchable bodies. The response envelope is, for example:

{"proposals":[{"action":"create","name":"report-guide","description":"Use when preparing a report from verified sources.","content":"Check sources and separate facts from estimates."}]}

An empty proposal array is an actual completed review. A skipped result makes no writes, retains pending cadence at the threshold and does not add an empty run/status record. The next eligible exchange supplies a new current source; there is no timer to retry a cold prefix. Typed skips represent pre-dispatch policy/availability refusals. Actual request errors and genuine aborts reject rather than becoming skipped success; they consume the attempt and record failed/aborted status. Invalid JSON fails, rather than silently becoming an empty proposal.

After strict parsing and proposal validation, each write awaits currentBudget() and then authorize(), and checks abort again after the authority read. The account queue also awaits authorization before changing cadence state. The host's live authorization includes daemon admitsNewWork, so shutdown/pause drain forbids further writes before cancellation is observed. The total cannot exceed the captured proposal cap or a newly lowered live budget. Only files.reviewerStore, supplied by the existing file owner, writes: createSkill for new personal skills and patchSkill for a still-agent-marked personal skill successfully loaded in this exchange, with the offered revision as CAS. Broader history supports create proposals but grants no additional patch scope. Secret/size guards cover proposals and offered skill bodies; inherited conversation history is not rewritten or claimed to be redacted. Human edits clear provenance. Successfully written files still commit and publish through reloadSkills if a later write fails or authority is revoked.

Core retains tool declarations for exact prefix parity but denies all direct/nested execution before permissions or side effects. An attempted tool invalidates review without another paid model round; provider-hosted tools that bypass this host gate cause hosted-tools. The expensive fork/inference runs in an opportunistically admitted runner only. Disabled, full or unavailable pools skip with no daemon fallback or interactive queue delay. Authoritative plugin state operations remain account-serialized.

The host repeats its live callback before admission and provider dispatch. OAuth quota comes from ProviderQuotaStore.current({ providerId, connectionId, generation }), without polling: the weekly window has no kind and exactly 10080 minutes; finite usage >=90% blocks. Old/missing/non-finite/future-dated or expired evidence is unknown, except a stale high reading keeps blocking while its reset remains in the future. Missing source connection or changed generation is revoked. API-key routes retain normal spend/grant admission without this subscription rule.

The child has isolated transport/provider state and no parent transcript, title inference, result wake-up, notification, memory curator or recursive review. Abort/shutdown cancel it; it is not replayed after restart. Inference charges the author once under origin internal with the actual captured connection, including full-prefix cache reads and fresh final-answer/directive/catalog input.

Core owns route eligibility in skillReviewSourceSkipReason, documented in its learning module. A route is eligible when its API's last actual request is a plain history array with stable top-level fields (Anthropic Messages, Codex Responses, OpenAI Responses, OpenAI Completions) and the child's inherited prefix stays byte-identical to it; hosted tools and stateful deltas remain ineligible, and any other API returns unsupported-route. Cache savings depend on each provider's own prompt caching. No helper-model, alternate-account or fresh-prompt fallback exists. skillReviewMaxOps=0 disables learning.

status(userId) remains the cheap local strict state read: pending calls, totals and last attempted outcome. ctx.registerUserRemoved deletes the account's personal folder and calls forgetUser on its counter. See the core learning contract and version-2 registration/JSON example.