@amail/plugin-sdk
v0.1.0
Published
AMail plugin contract. Locked at end of Phase 2 — stable surface for plugin authors.
Readme
@amail/plugin-sdk
The locked plugin contract for AMail. Phases 3 (sandbox), 4 (HR plugin), and 7 (PR proposer) all build on the types exported from this package — do not change the shapes here without a phase-spanning review.
Installation
pnpm add -D @amail/plugin-sdk
# or
npm install --save-dev @amail/plugin-sdkRequires Node ≥ 20.
What's in dist/
index.js/index.d.ts— the public API (AmailPlugin,ToolSchema, etc.)manifest.js—parseManifest,PluginManifestSchema, helperscontext.js—AmailRuntimeContext,AttachmentRef,defaultV1Entitlementtypes.js— plugin/tool/skill typesbin/amail-plugin-check.js— theamail-plugin-checkCLI
amail-plugin-check CLI
A npx-able validator that runs the same Zod schema the marketplace scan
workflow runs. Authors should run it before opening a submission PR so they
get all the field-level errors locally — no Docker required.
# From inside a plugin package directory:
npx @amail/plugin-sdk amail-plugin-check
# Also import the plugin entry and verify manifest ↔ entry tool-name parity
# (catches "I forgot to register the new tool in both places"):
npx @amail/plugin-sdk amail-plugin-check --loadWhat a plugin exports
A plugin package's entrypoint must export a default value conforming to
AmailPlugin:
import type { AmailPlugin } from "@amail/plugin-sdk";
const plugin: AmailPlugin = {
id: "my-vertical",
version: "0.1.0",
tools: [
{
name: "doThing",
description: "Do the vertical-specific thing.",
input_schema: { type: "object", properties: { /* … */ } },
},
],
handlers: {
doThing: async (args, ctx) => {
ctx.logger.info("doing thing", { thread: ctx.threadId });
return { ok: true, data: { /* … */ } };
},
},
defaultSkills: [
{ name: "Vertical Skill", body: "# Steps\n\n1. …\n2. …\n" },
],
amailFragment: "## Vertical vocabulary\n\nTickets have a Vendor field…",
};
export default plugin;The instance loads plugins listed in the INSTANCE_PLUGINS env var (comma-
separated package names). At boot the host:
await imports each plugin- validates that no two plugins declare the same tool name (fail-fast)
- merges plugin
toolsinto the registry anddefaultSkillsinto the skill set (S3-authored skills win on collision, so operators can override) - concatenates
amailFragmentstrings into the effectiveAmail.mdshown to the agent.
The determinism rule (read this before writing a handler)
The core runtime caches every tool call by
(emailMessageId, toolName, sha256(canonicalJSON(args))) in the
tool_executions collection (TTL 7 days). On a cache hit, the cached result
is returned to the model and PostToolUse / ThreadClosed hooks are skipped
so external side effects don't fire twice.
That has three consequences for handler authors:
- Handlers must be deterministic given
(args, ctx)— same input must produce the same observable outcome. If you need server-side dedup (e.g. an external API's idempotency key), derive it fromargs/ctx, not fromDate.now(). - Do NOT add your own idempotency layer for the same triplet. It will fight the core cache and double-count.
- Throwing vs returning
{ ok: false }: a returned{ ok: false }is intentional and is cached (re-running would just fail again). A thrown exception is treated as a transient error and is not cached, leaving the next retry free to re-attempt.
What the runtime context gives you
| Field | Use it for |
|---|---|
| instanceId | scoping plugin-internal state to the instance |
| threadId | every external action that needs the AMail thread reference |
| skill | log attribution; "auto" if the agent picked the tool freely |
| emailMessageId | informational only — do NOT use as your own dedup key |
| s3.read / s3.write | persistent plugin state, scoped to plugins/{pluginId}/ |
| attachments.list / attachments.fetch | inbound email attachments |
| llm.complete | internal LLM calls (e.g. classification, summarisation) |
| logger.info / logger.error | structured logs propagated to the host |
Phase 2 plugins run in-process so process.env, raw aws-sdk, raw Mongo, etc.
all technically work. Phase 3 will isolate the worker and block everything
outside this context — code against the context only and you get a free
migration.
Versioning
Released to public npm via Changesets
alongside @amail/plugin-host. Major bumps follow a 6-month deprecation window
with two preview releases (per plan.md decision #14).
Any PR in the upstream repo that modifies packages/plugin-sdk/ or
packages/plugin-host/ must include a .changeset/*.md file describing the
change — CI enforces this.
License
MIT — see LICENSE.
