Skip to content
AGH RuntimeExtensions

Develop Extensions

Build custom AGH extensions with manifests, bundled resources, subprocess lifecycle, Host API permissions, and package examples.

Audience
Operators running durable agent work
Focus
Extensions guidance shaped for scanability, day-two clarity, and operator context.

An AGH extension is a directory with a manifest and optional runtime code. Resource-only extensions package files AGH already knows how to load. Subprocess extensions run code over JSON-RPC and can call the daemon through the Host API after capability checks.

Use extensions when a capability should be installed, versioned, enabled, disabled, and inspected as one package.

Extension Directory

prompt-enhancer/
  extension.toml
  package.json
  dist/
    index.js
  skills/
    review-context/
      SKILL.md

AGH looks for extension.toml first and extension.json second. TOML is the common format. A manifest may put core metadata at the root or inside [extension]; do not define the same core field in both places with conflicting values.

Manifest Core

[extension]
name = "prompt-enhancer"
version = "0.1.0"
description = "Adds workspace context to assembled prompts"
min_agh_version = "0.5.0"
FieldRequiredNotes
nameyesRegistry identity. Must match the installed registry row on daemon start.
versionyesSemantic version.
descriptionnoShown in extension metadata.
min_agh_versionyesSemantic version compared against the current daemon version.

Resource-Only Extensions

Resource-only extensions do not need a persistent subprocess. They become active after AGH loads and registers their resources.

[extension]
name = "review-pack"
version = "0.1.0"
description = "Review skills, hooks, and MCP helpers"
min_agh_version = "0.5.0"

[resources]
skills = ["skills"]
agents = ["agents"]
bundles = ["bundles"]

[[resources.hooks]]
name = "review-session-ready"
event = "session.post_create"
mode = "async"
command = "/usr/bin/env"
args = ["bash", "{{config_dir}}/hooks/session-ready.sh"]

[resources.mcp_servers.git]
command = "uvx"
args = ["mcp-server-git"]
env = { REPO_ROOT = "{{env:REPO_ROOT}}" }

Resource paths are resolved inside the extension root. {{config_dir}} expands to that root. {{env:NAME}} expands from the daemon process environment.

ResourceManifest fieldLoaded as
Skillsresources.skillsMarkdown skill files parsed like normal SKILL.md files.
Agentsresources.agentsAgent definition markdown files.
Hooksresources.hooksHook declarations with extension metadata.
Bundlesresources.bundlesBundle specs that can package channels, automation, and bridge presets.
MCP serversresources.mcp_serversNamed MCP server declarations.

Subprocess Extensions

A manifest requires a subprocess when it declares [subprocess].command, capabilities.provides, or actions.requires.

[capabilities]
provides = ["prompt.provider"]

[actions]
requires = ["sessions/list"]

[subprocess]
command = "node"
args = ["dist/index.js", "serve"]
health_check_interval = "30s"
shutdown_timeout = "10s"

[subprocess.env]
LOG_LEVEL = "info"

[security]
capabilities = ["session.read"]

When AGH starts an enabled subprocess extension, it runs this lifecycle:

  1. Discover the extension root from the stored manifest path.
  2. Parse extension.toml or extension.json.
  3. Validate manifest identity, versions, capabilities, actions, security grants, and bridge metadata.
  4. Register static resources: skills, agents, hooks, bundles, and MCP servers.
  5. Launch the subprocess when one is required.
  6. Send an initialize JSON-RPC request over stdio.
  7. Start health monitoring.
  8. Mark the extension active after successful activation.

The extension process must implement health_check and shutdown. The daemon also advertises execute_hook as a daemon request method. The TypeScript SDK binds initialize, health_check, shutdown, and provide_tools, and lets you register custom handlers.

Host API Permissions

Host API access is controlled by both method grants and security capabilities.

[actions]
requires = ["sessions/list", "memory/recall"]

[security]
capabilities = ["session.read", "memory.read"]
Manifest sectionPurpose
capabilities.providesInterfaces the extension implements, such as memory.backend or bridge.adapter.
actions.requiresHost API methods the extension wants to call, such as sessions/list.
security.capabilitiesCapability grants needed by those methods, such as session.read.

Important current provide surfaces:

Provide surfaceAGH calls into the extension with
memory.backendmemory/store, memory/recall, memory/forget
bridge.adapterbridges/deliver

bridge.adapter extensions must also declare bridge metadata:

[capabilities]
provides = ["bridge.adapter"]

[bridge]
platform = "slack"
display_name = "Slack"

Marketplace extensions run under a stricter policy. They are constrained to read-oriented grants: memory.read, observe.read, session.read, skills.read, and tool.read.

Authored Context Host API

Soul, Heartbeat, and session health are agent-manageable through the Host API behind explicit grants. Extensions cannot bypass managed authoring — direct file writes from extension code, hooks, tools, MCP sidecars, or bridge adapters are forbidden. Each method is a separate grant so read, validate, mutate, history, rollback, status, wake, and health can be granted independently.

[actions]
requires = [
  "agents/soul/get",
  "agents/soul/validate",
  "agents/heartbeat/get",
  "agents/heartbeat/status",
  "agents/heartbeat/wake",
  "sessions/health/get",
]
Host API methodCapability areaNotes
agents/soul/getRead SoulReturns full resolved persona for the named agent (or caller).
agents/soul/validateValidate SoulChecks proposed body or current SOUL.md without writing.
agents/soul/putWrite SoulManaged write through the authoring service; requires expected_digest.
agents/soul/deleteDelete SoulManaged delete with expected_digest.
agents/soul/historyHistoryBounded revision history for Soul.
agents/soul/rollbackRollback SoulReplays a prior revision through validation/CAS; cannot restore forbidden content.
agents/heartbeat/getRead HeartbeatReturns the latest valid policy snapshot.
agents/heartbeat/validateValidate HeartbeatChecks proposed body without writing.
agents/heartbeat/putWrite HeartbeatManaged write with expected_digest. HTTP If-Match is rejected.
agents/heartbeat/deleteDelete HeartbeatManaged delete with expected_digest.
agents/heartbeat/historyHistoryBounded revision history for Heartbeat.
agents/heartbeat/rollbackRollback HeartbeatReplays a prior revision through validation/CAS.
agents/heartbeat/statusStatusPolicy + wake state + session health composition.
agents/heartbeat/wakeManual wakeAdvisory wake for an eligible session; never claims work or renews leases.
sessions/health/getRead session healthReturns metadata-only health for one session.

Write/delete/rollback/wake grants are separate from get/validate/history/status grants, so a read-only review extension can ship without ever requesting mutation rights. Marketplace extensions may only request the read-oriented grants from this list.

Authored Context Hooks

Authored context fires typed call-site hooks. They are observation hooks: payloads carry compact provenance (snapshot ids, digests, redacted actor/origin) and never include raw SOUL.md/ HEARTBEAT.md bodies, raw claim tokens, or full prompt transcripts. Hooks cannot mutate Soul, Heartbeat, or wake state.

EventSyncFires when
agent.soul.snapshot.resolvedasync onlyA session-start or refresh resolves a Soul snapshot.
agent.soul.mutation.afterasync onlyA managed put/delete/rollback succeeds.
agent.heartbeat.policy.resolvedasync onlyThe Heartbeat resolver materializes a new snapshot.
agent.heartbeat.wake.beforeyesThe wake service is about to send a synthetic wake; sync hooks may deny but cannot mint a token.
agent.heartbeat.wake.afterasync onlyA wake decision is recorded (sent, skipped, coalesced, rate_limited, or failed).
session.health.update.afterasync onlySession health changes state/health/eligibility; coalesced by session_health_hook_min_interval.

Authored Context Native Tools

AGH exposes three native tools that run through the same managed services. They are agent-callable when policy permits, and they reuse the Host API authorization layer so a tool call and a JSON-RPC call resolve to the same grants.

Tool IDPurpose
agh__session_healthRead session health, attachability, and wake eligibility for one session.
agh__agent_heartbeat_statusRead Heartbeat policy and wake-state summary for an agent.
agh__agent_heartbeat_wakeRequest one advisory wake for an eligible session; identical contract to manual wake.

There is intentionally no agh__agent_soul tool — Soul read/validate/write/delete/history/ rollback flows happen through the dedicated CLI, HTTP, UDS, or Host API surfaces, not through a native tool, because Soul does not need an in-prompt invocation surface.

Authored Context SDK helpers

The Go and TypeScript SDKs publish curated helpers and types for the authored-context surfaces: AgentSoul*, AgentHeartbeat*, SessionHealth*, and AuthoredContext* types are available through @agh/extension-sdk and the Go SDK, generated from the same OpenAPI/contract source as the daemon. Extensions should depend on those helpers instead of hand-rolling JSON shapes so contract drift is caught at typecheck time.

Practical Example: Prompt Enhancer

This example mirrors the repository's sdk/examples/prompt-enhancer pattern. It packages a prompt.post_assemble hook and a persistent subprocess. The hook command is intentionally a one-shot process: it reads payload JSON from stdin and writes a PromptPatch to stdout. The persistent process handles JSON-RPC lifecycle and Host API calls.

extension.toml:

[extension]
name = "prompt-enhancer"
version = "0.1.0"
description = "Adds workspace context to assembled prompts"
min_agh_version = "0.5.0"

[capabilities]
provides = ["prompt.provider"]

[actions]
requires = ["sessions/list"]

[[resources.hooks]]
name = "workspace-context"
event = "prompt.post_assemble"
mode = "sync"
executor.kind = "subprocess"
executor.command = "node"
executor.args = ["dist/index.js", "hook", "prompt_post_assemble"]

[subprocess]
command = "node"
args = ["dist/index.js", "serve"]

[security]
capabilities = ["session.read"]

src/index.ts:

import { pathToFileURL } from "node:url";
import {
  Extension,
  type ExecuteHookParams,
  type ExtensionOptions,
  type PromptPatch,
} from "@agh/extension-sdk";

function enhance(prompt: string, workspace?: string, workspaceID?: string): PromptPatch {
  const label = workspace?.trim() || workspaceID?.trim() || "unknown";
  return {
    prompt: `[Workspace: ${label}]\n\n${prompt}`,
  };
}

async function readStdin(): Promise<string> {
  const chunks: Buffer[] = [];
  for await (const chunk of process.stdin) {
    chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
  }
  return Buffer.concat(chunks).toString("utf8");
}

async function runHook(name: string): Promise<void> {
  if (name !== "prompt_post_assemble") {
    throw new Error(`unsupported hook ${name}`);
  }

  const payload = JSON.parse(await readStdin()) as {
    prompt?: string;
    workspace?: string;
    workspace_id?: string;
  };

  process.stdout.write(
    JSON.stringify(enhance(payload.prompt ?? "", payload.workspace, payload.workspace_id)) + "\n"
  );
}

export function createExtension(options: ExtensionOptions = {}): Extension {
  const extension = new Extension(
    {
      name: "prompt-enhancer",
      version: "0.1.0",
      capabilities: { provides: ["prompt.provider"] },
      actions: { requires: ["sessions/list"] },
      security: { capabilities: ["session.read"] },
      supported_hook_events: ["prompt.post_assemble"],
    },
    options
  );

  extension.handle(
    "execute_hook",
    async (_ctx, params: ExecuteHookParams<"prompt.post_assemble">) => {
      return enhance(
        params.payload.prompt ?? "",
        params.payload.workspace,
        params.payload.workspace_id
      );
    }
  );

  extension.onReady(async host => {
    await host.sessions.list({});
  });

  return extension;
}

async function main(): Promise<void> {
  const [mode = "serve", hookName = ""] = process.argv.slice(2);
  if (mode === "hook") {
    await runHook(hookName);
    return;
  }
  await createExtension().start();
}

const entryPoint = process.argv[1];
if (entryPoint && import.meta.url === pathToFileURL(entryPoint).href) {
  void main().catch(error => {
    console.error(error);
    process.exitCode = 1;
  });
}

The important split is:

ModeCommandContract
One-shot hooknode dist/index.js hook prompt_post_assemblestdin payload, stdout patch.
Persistent extensionnode dist/index.js serveJSON-RPC initialize, health check, shutdown, Host API.

Do not point a hook declaration at a long-running server mode unless that process still reads one payload from stdin and exits with a patch on stdout.

Package And Install Locally

Build the extension with your package manager, then install the extension directory:

bun install
bun run build
agh extension install .
agh extension status prompt-enhancer

The install path must be a directory containing the manifest. If package.json is present, AGH copies runtime dependencies and optionalDependencies under node_modules; development dependencies are not copied into the managed install.

Publish To A Registry

Marketplace installation expects an archive with extension.toml at the archive root or under one top-level directory:

prompt-enhancer-0.1.0.tar.gz
  prompt-enhancer/
    extension.toml
    package.json
    dist/
      index.js

The installer rejects ambiguous packages, including archive roots that look like both a skill package and an extension package. Keep extension packages centered on extension.toml.

For GitHub-backed registries, AGH reads release metadata, downloads the selected release asset or tarball, computes the install checksum, stores registry metadata, and can later run agh extension update.

Runtime Failure And Recovery

The extension supervisor monitors health checks and process exits. On failure, AGH records the failure, applies exponential restart backoff, and relaunches the subprocess. After repeated consecutive failures, AGH disables the extension, unregisters resources, marks it inactive, and stores the last error for agh extension status.

Stop and reload behavior:

  • agh extension enable <name> and agh extension disable <name> reload the manager when the daemon is running.
  • Daemon shutdown sends cooperative shutdown first, then escalates through the subprocess layer if needed.
  • Manager.Reload is a stop followed by a fresh start from registry state.

Development Checklist

  1. Decide whether the extension is resource-only or needs a subprocess.
  2. Write extension.toml with name, version, and min_agh_version.
  3. Add resources under resources.* and keep paths inside the extension directory.
  4. If using Host API, declare both actions.requires and security.capabilities.
  5. If providing bridge.adapter, declare [bridge] platform and display_name.
  6. If packaging hooks, make each hook command satisfy stdin payload to stdout patch.
  7. Install locally with agh extension install ..
  8. Inspect with agh extension status <name> and agh hooks list --source config when hooks are included.

Related references:

On this page