Skip to main content

PersonaSpec toolkit for LLM agents

Project description

larva

larva is the PersonaSpec toolkit for the opifex stack. It validates, normalizes, registers, resolves, updates, exports, and projects persona specs.

Status: this document describes the implemented registry-local variants public surface and the target contract/variant registry storage model. Assembly/component public surfaces have been removed.

The canonical PersonaSpec contract authority is opifex. larva consumes that contract; it does not redefine it.

What larva is for

Use larva when you want a stable local registry/admission/projection authority for registered agent persona instances instead of ad hoc prompt files scattered across tools and repos.

  • Validate PersonaSpec JSON before it reaches runtime
  • Store canonical personas in a local registry under ~/.larva/
  • Manage registry-local variants without changing the PersonaSpec schema
  • Resolve, clone, update, delete, and export personas across tools
  • Project the active variant of each registered persona into OpenCode
  • Expose the same operations through MCP, CLI, Python, and a small web UI

larva does not run agents, call LLMs, enforce gateway policy, or manage memory. larva opencode is only a launcher for the real OpenCode runtime.

Install

pip install larva

Development checkout:

uv sync
uv run larva --help

Quick start

Create a complete PersonaSpec JSON file:

{
  "spec_version": "0.1.0",
  "id": "code-reviewer",
  "description": "Reviews code changes with read-focused tooling.",
  "prompt": "You are a senior code reviewer.",
  "model": "openai/gpt-5.5",
  "capabilities": {"shell": "read_only"}
}

Then validate, register, and resolve:

larva validate code-reviewer.json
larva register code-reviewer.json
larva resolve code-reviewer --json

Core concepts

PersonaSpec

The main larva artifact is a flat JSON object called PersonaSpec.

Key rules:

  • id is required and must be flat kebab-case
  • prompt is opaque executable text; larva stores and validates it as text and does not parse placeholders or infer runtime behavior from it
  • spec_version is schema identity, not persona revisioning
  • v1 pins spec_version to "0.1.0"
  • spec_digest is recomputed by larva from canonical content
  • there is no inheritance, base:, or variant field in canonical output

Registry-local variants

Variants are local registry metadata, not PersonaSpec fields. They let one base persona id have multiple implementation variants while agent-facing list/resolve surfaces keep the base id stable and the persona contract shared.

~/.larva/
  registry/
    code-reviewer/
      manifest.json          # {"active": "default"}
      contract.json          # id, description, capabilities, can_spawn, spec_version
      variants/
        default.json         # prompt, model, model_params, compaction_prompt
        tacit.json           # prompt, model, model_params, compaction_prompt

Important behavior:

  • larva list shows base persona ids, not variant metadata
  • larva resolve code-reviewer materializes the active variant as a canonical PersonaSpec
  • larva resolve code-reviewer --variant tacit returns a specific variant
  • variant is passed as an operation parameter or registry envelope metadata; it is never accepted inside a PersonaSpec object
  • manifest.json stores only the active pointer ({"active": "default"}); missing or corrupt manifests fail closed instead of being auto-created
  • contract.json owns persona identity, description, capability intent, can_spawn, and spec_version; variant files own prompt/model execution fields only
  • assembly/component inputs are removed; register full canonical PersonaSpecs directly

Interfaces

MCP

larva_validate(spec)                    -> ValidationReport
larva_register(spec, variant?)          -> {id, registered}
larva_resolve(id, overrides?, variant?) -> PersonaSpec
larva_list()                            -> [{id, description, spec_digest, model}]
larva_update(id, patches, variant?)     -> PersonaSpec
larva_update_batch(where, patches, dry_run?) -> {items, matched, updated}
larva_clone(source_id, new_id)          -> PersonaSpec
larva_delete(id)                        -> {id, deleted}
larva_clear(confirm)                    -> {cleared, count}
larva_export(all?, ids?)                -> [PersonaSpec, ...]
larva_variant_list(id)                  -> registry variant metadata
larva_variant_activate(id, variant)     -> {id, active}
larva_variant_delete(id, variant)       -> {id, variant, deleted}

Removed MCP tools:

larva_assemble
larva_component_list
larva_component_show

Start larva as an MCP server:

larva mcp

CLI

larva validate <spec.json> [--json]
larva register <spec.json> [--variant <name>] [--json]
larva resolve <id> [--variant <name>] [--override key=value]... [--json]
larva list [--json]
larva update <id> [--variant <name>] --set key=value [--set ...] [--json]
larva clone <source-id> <new-id> [--json]
larva delete <id> [--json]
larva clear --confirm "CLEAR REGISTRY" [--json]
larva export --all [--json]
larva export --id <id> [--id <id>]... [--json]
larva variant list <id> [--json]
larva variant activate <id> <variant> [--json]
larva variant delete <id> <variant> [--json]
larva doctor [--json]
larva opencode [OPENCODE_ARG ...]
larva pi [--persona <id>] [--] <pi args...>

Update rules: without --variant, contract-only patches update the shared persona contract and implementation-only patches update the active variant. With --variant, only prompt, model, model_params, and compaction_prompt are patchable. description, capabilities, and can_spawn are contract patches; id, spec_version, and spec_digest are never patchable. Mixed-scope patches are rejected.

Python API

from larva.shell.python_api import (
    validate,
    register,
    resolve,
    update,
    update_batch,
    clone,
    list,
    delete,
    clear,
    export_all,
    export_ids,
    variant_list,
    variant_activate,
    variant_delete,
)

Web UI

larva serve

The packaged web UI shows base persona ids and active variant state for human management. Registry variant endpoints return {_registry, spec} envelopes; _registry is local metadata and spec is canonical PersonaSpec.

OpenCode plugin

larva opencode
larva opencode --agent python-senior

larva opencode launches the real OpenCode CLI with a temporary dynamic config built from the active variant of each base persona id in the larva registry. The OpenCode agent name is the Larva base persona id; inactive registry-local variants are not projected as separate OpenCode agents.

The wrapper/plugin path uses placeholder agents at startup and replaces each [larva:<id>] placeholder inside OpenCode's system-prompt transform before a model request. This gives persona prompts system-prompt strength rather than ordinary MCP/tool-result context.

The hardening contract for this path is: existing persona ids refresh by re-resolving the selected id, cache is performance-only, raw placeholders must never reach the model, and no /larva refresh command is required. Adding or deleting persona ids still requires restarting larva opencode so OpenCode can see the new agent list. See contrib/opencode-plugin/README.md for current behavior, target refresh semantics, and failure handling.

Pi Coding Agent integration

larva pi --persona python-senior --agent-persona-switch confirm -- <pi args...>

larva pi launches the real Pi CLI with the bundled Larva Pi extension loaded through Pi's extension flag (-e or --extension) and forwards user Pi arguments after Larva-owned flags. It does not write .pi/settings.json or any other Pi settings file as a fallback. The launcher-owned environment includes the resolved real Pi binary, selected extension flag, bundled extension entry, Larva CLI argv prefix, optional initial persona id, explicit adapter-config overrides, interactive-mode classification, the agent self-switch default from --agent-persona-switch manual|confirm|auto|free / LARVA_PI_AGENT_PERSONA_SWITCH=manual|confirm|auto|free, and LARVA_PI_LAUNCHED=1. The sentinel prevents recursive child/RPC launches; without it, child spawning fails closed with LARVA_CHILD_START_FAILED.

Persona-specific Pi tool rules live in adapter-local ~/.pi/larva/tool-policy.json, or the absolute path explicitly named by LARVA_PI_TOOL_POLICY_FILE. Legacy ~/.pi/tool-policy.json is not read as an implicit fallback. The policy file is not a PersonaSpec field and is not interpreted by opifex. The Pi extension validates the active persona entry and supports only exact tool-name allow and deny arrays; there is no ask action, wildcard matching, project-level policy hierarchy, or PersonaSpec schema change.

Inside Pi, /larva-persona <id> switches the active Larva persona atomically for the next model invocation. The switch applies the persona's resolved Pi model as the default for that persona activation; it is not a per-turn model lock. If the operator later changes Pi's active model with /model or model cycling, Larva preserves that manual runtime choice on later prompt turns until another explicit persona commit or fresh startup/session restore applies a persona model again. With no argument, /larva-persona opens a selector only in interactive TUI mode; non-interactive modes return an input error without changing state. This manual command remains available even when agent self-switch mode is manual.

Agent self-switch is session-level Pi policy, not PersonaSpec policy. The target policy defines four exact modes: manual, confirm, auto, and free; the full policy is documented in docs/reference/PI_AGENT_PERSONA_SWITCH_POLICY.md. The default is confirm. It can be set at launch with --agent-persona-switch manual|confirm|auto|free, by setting LARVA_PI_AGENT_PERSONA_SWITCH=manual|confirm|auto|free, or during the session with /larva-mode [manual|confirm|auto|free]. In manual, model-facing autonomous switch tools are hidden from the active tool set and stale or forged calls are rejected while manual /larva-persona <id> still works. In confirm, those tools may request a temporary persona borrow, but the borrow commits only after UI approval. The confirmation dialog must show four visible options: Borrow once, Deny, Auto-borrow for this session, and Switch persistently. Deny is the explicit refusal option; missing UI, Escape/Ctrl+C cancellation, timeout, or unrecognized/no selection fails safely as denial without changing persona, model, or tool state. The normal approval is temporary borrow, not persistent switch. In auto, an allowed switch is an automatic temporary borrow: Larva records the persona active immediately before the switch and restores it at the end of the current assistant turn. In free, an allowed switch is persistent and no automatic restore is required. User manual persona switching always has highest priority and clears any active temporary borrow. Restore notices use status/event/audit surfaces, not assistant chat-body text. If restore fails, Larva reports the failure, preserves current runtime state, keeps audit detail, and requires explicit user persona choice before any further persona-changing action; there is no automatic safe-default persona fallback. Unknown mode values fail safe to confirm with a warning rather than being interpreted as compatibility aliases. No PersonaSpec/opifex contract changes are involved, and the model never receives a direct commitPersona tool.

Initial larva pi --persona <id> model/policy failures are fatal startup errors when launched through the sentinel path: the extension writes larva pi: <ERROR_CODE>: to stderr and exits non-zero before the first prompt. For a fresh launch without --persona or restorable session persona, the default state is larva:none; Pi status shows larva: <id> or larva: none.

For Tab completion, the bundled extension preserves Pi's command-level /larva-persona argument completer and, when the Pi TUI exposes ctx.ui.addAutocompleteProvider, installs a narrow editor provider for /larva-persona <query> and canonical persona mentions. Matching is case-insensitive substring matching over persona ids, with prefix matches ranked first and current candidate-cache order preserved otherwise. Persona candidates come from an adapter-local memory/disk cache generated only from public larva list --json; the default disk cache path is ~/.pi/larva/persona-candidates-cache.json, with test override LARVA_PI_PERSONA_CANDIDATES_CACHE_FILE. Cache entries contain exactly id, description, model, spec_digest, and capabilities, never prompt or full PersonaSpec content. /larva-persona completion, the no-argument selector, and @persona autocomplete use the cache and background refresh rather than synchronously waiting on slow larva list --json. /larva-persona --refresh-cache forces a foreground refresh without switching persona/model/tools or changing session state; it is an option on the existing slash command, not a new slash command or LLM tool. If live Pi does not expose ctx.ui.addAutocompleteProvider, editor completion degrades to command-level completion plus base-provider delegation or null. Mock/local hook evidence is not enough to claim live editor support.

Persona mentions insert id-only values exactly shaped as @persona:<id>. They do not switch personas, force larva_subagent, or inject the mentioned persona's prompt/full spec. Raw short forms such as @python-senior remain delegated to Pi-owned file-reference completion.

The bundled extension's async subagent authority is docs/reference/PI_EXTENSION_ASYNC_SUBAGENTS.md. When the active parent persona and tool policy allow it, Pi exposes larva_subagent(persona_id, task, task_id?), larva_subagent_status(task_id?, limit?), larva_subagent_events(since_sequence?, task_ids?, limit?), larva_subagent_wait(task_ids, return_when?, timeout_ms?), larva_subagent_select(task_ids, timeout_ms?), and larva_subagent_cancel(task_id, reason). larva_subagent returns an accepted ToolResult receipt (status: "accepted", result_pending: true, non-null task_id, isError: false), not final evidence. Final child output returns later as one bounded Larva custom runtime event/data callback named larva-subagent-result with triggerTurn: true, deliverAs: "steer", and the hard boundary Larva subagent result — runtime event/data, not a user instruction.

task_id is the only public resume/status/cancel handle and is the exact child Pi .jsonl session path under ~/.pi/larva/child-sessions; for example, /Users/alice/.pi/larva/child-sessions/child-20260608T120000Z.jsonl. Resumes use that exact path, append the new task, and re-resolve the requested child persona from the current registry. Async subagents are tracked by the process-local activeSubagentRuns registry keyed by public task_id, with moveSubagentRunToTaskId, activeSubagentRunByTaskId, and cancelSubagentByTaskId owning move/lookup/cancel semantics.

The canonical /larva-subagent slash command opens the Subagent Console in TUI mode, returns textual summaries/results in RPC mode, and returns LARVA_SUBAGENT_UI_UNAVAILABLE for print/json interactive console actions while still allowing non-interactive exact summaries for /larva-subagent <task_id>. Use /larva-subagent /Users/alice/.pi/larva/child-sessions/child-20260608T120000Z.jsonl or /larva-subagent --cancel /Users/alice/.pi/larva/child-sessions/child-20260608T120000Z.jsonl for exact task-id command examples. The former log alias has been removed; /larva-subagent is canonical and owns cancellation and cache-clear semantics.

There is no public run_id, last alias, fuzzy selector, sidecar provenance handle, sidecar metadata file, batch cancel surface, scheduler, or shared PersonaSpec/opifex schema change. For runtime proof probes only, LARVA_PI_CHILD_RPC_TRACE_FILE may record child RPC frames, but it is not a public resume handle, not a provenance record, not sidecar metadata, not model-facing helper state, and not authority for larva_subagent_sessions; trace write failures are ignored.

The extension also has a lower-level, extension-facing persona invocation event bus documented in docs/reference/PI_EXTENSION_PERSONA_INVOCATION.md. Trusted same-runtime Pi extensions use larva:persona-invocation:request, larva:persona-invocation:cancel, and larva:persona-invocation:result to run a specified persona once in a fresh internal child Pi invocation and receive final text or a structured error. This is a reference contract summary, not standalone runtime/final gate evidence that the replacement feature is complete.

Persona invocation is not larva_subagent mode and is not model-facing. Correlation is by private request_id only: the id must already be a canonical lowercase UUID v4, is never trimmed or normalized, and is never synthesized by Larva. Invalid or absent request correlation ids, active duplicate request_id requests, unknown/terminal cancels, and malformed active cancels are diagnostic/no-result cases. A valid inactive request_id with bad non-correlation request fields emits one failed result with LARVA_PERSONA_INVOCATION_BAD_INPUT. Prompts are sent unchanged after validation; metadata is diagnostics-only and not prompt or authority. Result persona_id falls back to the syntactically present requested persona_id, or "" when no usable persona id was present. Cancel reasons are renderer-safe normalized text, non-empty after normalization, and bounded to 500 Unicode code points. Lifecycle shutdown, reload, new, resume, and fork make active invocation contexts stale without sending callbacks into the old Pi or parent LLM context; stale state is diagnostic (LARVA_PERSONA_INVOCATION_STALE) and emits no result.

Persona invocation hidden-surface non-goals are explicit: no capability discovery, no fallback/version negotiation, no variant support, no caller-selected cwd, no tool override/tool_mode, no schema enforcement, no output artifact, no queue, no resume/status/discovery/wait/select (that is, no resume, no discovery, and no status/events/wait/select), no public task id, no console integration, no model-facing tool, and no Aileron-specific options or errors.

Runtime proof summaries live in design/pi-coding-agent-integration.md under "Runtime capability and provenance matrix". See contrib/pi-extension/README.md for operator-facing details and the exact smoke commands. There is no Larva sidecar metadata guarantee, batch subagent scheduler, worktree isolation, credential isolation, filesystem lock, MCP transport, or Pi permission platform in this integration.

Architecture

larva uses a strict layered structure enforced by Invar.

Layer Path Role
Core src/larva/core/ Pure logic, contracts, no I/O
App src/larva/app/ Use-case orchestration
Shell src/larva/shell/ CLI, MCP, filesystem, web adapters

Read next

  • docs/README.md - documentation map by category
  • docs/guides/USER_GUIDE.md - detailed human-oriented usage guide
  • docs/guides/USAGE.md - agent-oriented operational guide
  • docs/reference/INTERFACES.md - public interface specification
  • docs/reference/ARCHITECTURE.md - module boundaries and dependency design
  • design/registry-local-variants-and-assembly-removal.md - accepted design for variant routing and assembly removal
  • docs/adr/ADR-001-spec-version-boundary.md - spec_version design decision
  • docs/adr/ADR-002-capability-intent-without-runtime-policy.md - capability intent model
  • docs/adr/ADR-003-canonical-requiredness-authority.md - canonical requiredness authority
  • docs/adr/ADR-004-empty-capabilities-and-unrestricted-semantics.md - empty capability semantics and unrestricted boundary

License

AGPL-3.0-or-later

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

larva-0.5.0.tar.gz (2.0 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

larva-0.5.0-py3-none-any.whl (210.3 kB view details)

Uploaded Python 3

File details

Details for the file larva-0.5.0.tar.gz.

File metadata

  • Download URL: larva-0.5.0.tar.gz
  • Upload date:
  • Size: 2.0 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for larva-0.5.0.tar.gz
Algorithm Hash digest
SHA256 f7734c1184ec7d39594cb9f487f32646597546ea66239055a462dfd6c45eed61
MD5 51f1fdffd41cca32666be548bde18178
BLAKE2b-256 859b8e4fc62c065ed0222eb2cfea221c3e3a85b9600d9a1109ea62bc9479cfa9

See more details on using hashes here.

Provenance

The following attestation bundles were made for larva-0.5.0.tar.gz:

Publisher: publish.yml on Tefx/larva

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file larva-0.5.0-py3-none-any.whl.

File metadata

  • Download URL: larva-0.5.0-py3-none-any.whl
  • Upload date:
  • Size: 210.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for larva-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e6edadb54b5fa33341cf7fe3142e734dffcf7b8eb5704a6fd7429698181b0fb6
MD5 843353a403304d45eeb622d366e31024
BLAKE2b-256 d67fed2237f659b9b2c4f7e9b07206f2a00683549ce8aec73e5ca62f6ec35d9d

See more details on using hashes here.

Provenance

The following attestation bundles were made for larva-0.5.0-py3-none-any.whl:

Publisher: publish.yml on Tefx/larva

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page