Provider authoring
This page is the implementation reference for extending AML. Read Provider engineering first when you are deciding which boundary to implement; stay here when you need the exact helper, request, lease, runtime, or conformance contract.
Public extension surface
Section titled “Public extension surface”| Goal | Start with | Supporting public APIs |
|---|---|---|
| Implement one complete Agent call directly | defineAgentProvider() and AgentProvider | AgentRequest, AgentExecutionContext, AgentResponse |
| Wrap an ACP-compatible coding agent | defineAcpAgentProvider() and AcpAgentProfile | AcpAgentLaunch, AcpAgentLaunchContext, AcpAgentLaunchFile |
| Own a retained Agent session | AbstractAgentProvider | AgentProviderSession, AgentProviderTurn |
| Implement Sandbox acquisition directly | defineSandboxProvider() and SandboxProvider | SandboxAcquireRequest, SandboxLease, SandboxRuntime |
| Implement a staged Sandbox lifecycle | AbstractSandboxProvider | ProvisionedSandbox, SandboxRuntime |
| Implement Workspace materialization directly | defineWorkspaceProvider() and WorkspaceProvider | WorkspaceAcquireRequest, WorkspaceLease, WorkspaceSaveRequest |
| Build a revision-backed Workspace | createPersistentWorkspaceProvider() | WorkspaceStorageAdapter, WorkspacePersistenceOptions |
| Exercise a portable contract | @aml-jsx/sdk/testing | agentProviderConformance(), sandboxProviderConformance(), workspaceProviderConformance() |
All production contracts above are exported from @aml-jsx/sdk. Conformance helpers and deterministic fixtures are deliberately isolated under @aml-jsx/sdk/testing.
Definition helpers
Section titled “Definition helpers”The three structural helpers have the same narrow job:
defineAgentProvider(implementation)defineSandboxProvider(implementation)defineWorkspaceProvider(implementation)Each helper validates the required provider shape and normalized name, then shallow-freezes the provider object. The Agent and Sandbox helpers preserve the concrete provider type; the Workspace helper preserves its handle type through the public WorkspaceProvider shape. Definition does not contact a vendor, install an executable, build an image, acquire a lease, or prove that the implementation honors its lifecycle.
AML intentionally has no generic defineProvider(). Agent, Sandbox, and Workspace providers receive different requests and own different resources. A single lowest-common-denominator interface would erase the constraints that make the boundaries useful.
defineAgentProvider()
Section titled “defineAgentProvider()”function defineAgentProvider<const Provider extends AgentProvider>(implementation: Provider): Readonly<Provider>The minimal provider has a normalized name, a run() method, and an optional Sandbox compatibility handshake:
interface AgentProvider { readonly name: string run(request: AgentRequest, context: AgentExecutionContext): Promise<AgentResponse> supportsSandbox?(sandbox: SandboxSession): boolean}Direct Agent example
Section titled “Direct Agent example”import { defineAgentProvider, type AgentProvider } from "@aml-jsx/sdk"
const internalReviewer: AgentProvider = { name: "internal-reviewer", async run(request, context) { context.signal.throwIfAborted()
const result = await reviewService( { model: request.model, prompt: request.prompt, system: request.system, }, { signal: context.signal } )
context.signal.throwIfAborted() return { text: result.text, structured: result.structured } },}
export const agentProvider = defineAgentProvider(internalReviewer)AgentRequest contains the assembled initial prompt, ordered followUps, system text, model override, portable permissions, Tools, MCP servers, staged Skills, trace identity, and optional structured-output request. Each AgentSkill exposes the concrete .agents home, package directory, and SKILL.md path; native-discovery providers consume those owned locations instead of deriving a root from path depth. One run() call represents one authored <Agent /> session. If a later turn fails, the provider fails the complete call rather than substituting an earlier response.
AgentExecutionContext supplies the evaluation signal, subscriber-only events, trace identity, and optional effective SandboxSession. Providers cannot publish AML trace events or invoke the application-owned output validator through this public context. AML still wraps retained provider sessions in portable agent.session, agent.turn, and agent.cleanup spans. A provider must reject a pre-aborted signal with its reason, observe cancellation during external work, and release invocation-owned clients, processes, streams, bridges, and temporary files before run() settles.
AgentResponse.text remains the complete concatenated text for compatibility. Built-in ACP providers also return a messages string array when every assistant text chunk carries a messageId. Chunks with the same ID are grouped in first-seen order, and messages.at(-1) is the final assistant message for the completed prompt turn. If the provider emits any text chunk without a message ID, AML omits messages rather than guessing boundaries. AML also omits messages when a provider profile transforms the assembled text because it cannot safely map that transformation back onto the original message boundaries. Provider-level consumers continue to use text in either case. Ordinary <Agent> evaluation still renders text rather than exposing the provider response object.
AML’s shared session engine currently uses the stable ACP v1 API and its optional chunk messageId. A future move to the experimental v2 session path must also consume full agent_message replacement updates; those updates are not part of the stable v1 update union.
Direct providers return one structured value in AgentResponse; AML validates it at the final application boundary. Immediate candidate validation, first-valid-result selection, and submission tracing belong to AML’s internal shared ACP adapter rather than the public provider contract.
If supportsSandbox() returns true, run() must actually launch model-controlled work through context.sandbox.lease.runtime and honor the effective root and access. The handshake cannot widen a nested Sandbox policy.
ACP profile extension
Section titled “ACP profile extension”Use defineAcpAgentProvider() when the vendor already exposes an Agent Client Protocol executable. AML’s shared ACP adapter owns process launch, one-session turn order, JavaScript Tool and structured-output bridging, immediate candidate validation, first-valid-result selection, the one-prompt missing-output repair, submission tracing, MCP descriptors, temporary state, local versus Sandbox execution, abort, cleanup, and portable ACP-boundary telemetry. Profiles may customize the Tool instruction, but they must not fork that lifecycle or recovery policy.
import { defineAcpAgentProvider, type AcpAgentProfile } from "@aml-jsx/sdk"
const profile: AcpAgentProfile<"company-agent"> = { name: "company-agent", workingDirectory: undefined, createLaunch({ cwd, request, stateDirectory }) { return { command: "company-agent-acp", args: ["--cwd", cwd], env: { COMPANY_AGENT_STATE: stateDirectory, ...(request.model === undefined ? {} : { COMPANY_AGENT_MODEL: request.model }), }, permissionPolicy: "allow_always", } },}
export const companyAgent = defineAcpAgentProvider(profile)An ACP profile owns vendor configuration and translation: command, literal arguments, environment, model/configuration mapping, permission policy, optional launch files, named-MCP support, and response transformation. Launch files must use normalized relative paths below AML’s invocation-private state directory.
The shared adapter emits one acp.session.update per ACP notification. Metadata retains only the ACP session id and update discriminant; a content-capturing sink receives the unchanged serialized ACP update. Profiles should not duplicate that stream by translating every ACP Tool, message, thought, plan, or usage variant into another AML schema. Internal model-call telemetry remains optional provider telemetry and must not be presented as an AML Agent turn. See Observability.
Retained sessions
Section titled “Retained sessions”AbstractAgentProvider<Name> is the advanced template for an adapter that opens one AgentProviderSession per AML Agent. The base creates ordered turns, propagates cancellation, selects the final response, requests provider abort, and closes the session after success or failure. Subclasses implement openSession() and explicitly opt into compatible Sandboxes.
Use the direct run() boundary unless retained turn state and cleanup are real provider concepts. Do not reuse a session across unrelated AML evaluations.
defineSandboxProvider()
Section titled “defineSandboxProvider()”function defineSandboxProvider<const Provider extends SandboxProvider>(implementation: Provider): Readonly<Provider>interface SandboxProvider<Handle = unknown> { readonly name: string acquire(request: SandboxAcquireRequest): Promise<SandboxLease<Handle>>}SandboxAcquireRequest contains the effective access, logical root and cwd, evaluation id, cancellation signal, and optional active Workspace materialization. The returned lease contains an opaque provider handle, stable lease id, portable runtime, and release().
Direct Sandbox example
Section titled “Direct Sandbox example”import { defineSandboxProvider, type SandboxProvider } from "@aml-jsx/sdk"
const internalSandbox: SandboxProvider<{ environmentId: string }> = { name: "internal-sandbox", async acquire(request) { request.signal.throwIfAborted() const environment = await provisionEnvironment(request, request.signal) let releasePromise: Promise<void> | undefined
const runtime = { access: request.access, cwd: request.cwd, root: request.root, createFileStaging: options => environment.createFileStaging(options), exec: (command, args, options) => environment.exec(command, args, options), readFile: (path, options) => environment.readFile(path, options), spawn: (command, args, options) => environment.spawn(command, args, options), stat: (path, options) => environment.stat(path, options), writeFile: (path, content, options) => environment.writeFile(path, content, options), }
return { handle: { environmentId: environment.id }, id: environment.id, runtime, release: () => (releasePromise ??= environment.destroy()), } },}
export const sandboxProvider = defineSandboxProvider(internalSandbox)The runtime must preserve the acquired access, root, and cwd. exec() receives one executable plus literal arguments and returns bounded stdout, stderr, and exit code. spawn() exposes standard Web streams, repeat-safe kill(), and repeat-safe wait(). readFile(), stat(), and writeFile() operate on complete files beneath the logical Sandbox root; stat() returns the kind and byte size, plus modification time in Unix milliseconds when available. AML does not retain Include results from providers that omit modification time. Writes must replace rather than append and reject read-only access. createFileStaging() returns a unique writable Agent-visible root with confined relative writes and repeat-safe release, even when the live Workspace filesystem is read-only. Provider shell quoting is an implementation detail; do not silently merge arbitrary arguments into one shell string.
The implementation owns path translation, traversal and symlink confinement, complete-file metadata and bytes, timeouts, signal propagation, combined output limits, process-tree cleanup, access enforcement, staging cleanup, and release after partial failure. If the backend cannot enforce read-only execution, reject command/process work under access="read-only" rather than recording a flag and continuing. Read-only live files may still be read, and invocation-private Agent staging must remain writable because it is outside durable Workspace state.
Staged Sandbox base
Section titled “Staged Sandbox base”AbstractSandboxProvider<Name, Handle, Resource> owns the common acquisition barrier:
provision()creates an acknowledged provider resource.createRuntime()translates it into AML’s portable process and filesystem runtime.initialize()performs optional hydration or trusted setup.- any failure after provisioning calls
cleanupProvisioned(); - the exposed lease memoizes
releaseResource()so release is idempotent.
Use ProvisionedSandbox<Handle, Resource> to keep the descendant-facing opaque handle separate from the internal resource needed for reconciliation and destruction.
defineSandboxProvider() validates shape and freezes the adapter. It does not prove the environment is isolated. Provider documentation must state the actual host/container/platform, image, user, network, credentials, resource controls, filesystem behavior, and cleanup boundary.
defineWorkspaceProvider()
Section titled “defineWorkspaceProvider()”function defineWorkspaceProvider<Handle>(provider: WorkspaceProvider<Handle>): Readonly<WorkspaceProvider<Handle>>interface WorkspaceProvider<Handle = unknown> { readonly name: string acquire(request: WorkspaceAcquireRequest): Promise<WorkspaceLease<Handle>>}Direct Workspace example
Section titled “Direct Workspace example”import { defineWorkspaceProvider, type WorkspaceProvider } from "@aml-jsx/sdk"
const internalWorkspace: WorkspaceProvider<{ directory: string }> = { name: "internal-workspaces", async acquire(request) { // This helper throws WorkspaceConflictError only for a healthy competing writer. const lock = request.lock === false ? undefined : await acquireWriterLock(request.id, request.signal)
let directory: string try { directory = await materialize(request.id, request.load, request.signal) } catch (error) { await lock?.release().catch(() => undefined) throw error }
let released = false return { directory, handle: { directory }, id: request.id, save: async saveRequest => publishRevision(request.id, directory, saveRequest, saveRequest?.signal ?? request.signal), async release() { if (released) return released = true try { await removeMaterialization(directory) } finally { await lock?.release() } }, } },}
export const workspaceProvider = defineWorkspaceProvider(internalWorkspace)The storage functions above are application placeholders, not AML APIs. A real provider acquires healthy writer authority before expensive materialization, removes partial state when materialization fails, publishes only complete validated saves, and relinquishes locks and temporary files even if saving fails.
Use WorkspaceConflictError only for a healthy competing writer of the same durable id. Stale conditional publication, invalid archives, missing objects, access denial, and cleanup failure remain ordinary provider errors with their original causes.
WorkspaceSaveRequest carries outcome, retention, include/exclude, gitignore, and cancellation policy. Retention includes the newly published revision. Document whether the provider directly exposes a durable directory, stages a temporary copy, publishes folder/archive revisions, or requires a provider-native volume.
Revision-backed storage shortcut
Section titled “Revision-backed storage shortcut”If the backend is object storage, implement WorkspaceStorageAdapter and pass it to createPersistentWorkspaceProvider(). AML’s persistence layer owns revision indexes, conditional publication, locks, retention, and materialization over that narrower storage contract. This is the extension seam used by revision-oriented providers; it avoids reimplementing the persistence protocol in every S3-compatible adapter.
Conformance helpers
Section titled “Conformance helpers”import { agentProviderConformance, sandboxProviderConformance, workspaceProviderConformance,} from "@aml-jsx/sdk/testing"
await agentProviderConformance(agentProvider)await sandboxProviderConformance(sandboxProvider)await workspaceProviderConformance(workspaceProvider)The helpers are ordinary async functions and do not require a particular test runner. They check the portable boundary, including pre-cancelled Agent execution, Sandbox lease and runtime shape plus release, and Workspace conflict/save/release/restoration behavior. Sandbox command and filesystem behavior remains the responsibility of provider-specific tests.
Public versus internal APIs
Section titled “Public versus internal APIs”Import extension contracts only from @aml-jsx/sdk and testing helpers only from @aml-jsx/sdk/testing. Paths below sdk/src/** are implementation details even when these docs link to them as readable source.
Public advanced helpers such as AbstractAgentProvider, defineAcpAgentProvider, AbstractSandboxProvider, and createPersistentWorkspaceProvider are supported extension seams. Internal validators, evaluator state, ACP transport classes, provider package construction seams, and direct source paths are not public imports.
Provider documentation checklist
Section titled “Provider documentation checklist”Every custom provider should document:
- maturity and exact versions exercised;
- required executable, image, snapshot, SDK, credentials, and external service;
- supported Agent/Sandbox/Workspace combinations;
- path, cwd, access mode, and what “read-only” actually enforces;
- session, process, materialization, revision, and retention semantics;
- cancellation, timeout, output bounds, cleanup, and possible remote leftovers;
- conflict identity versus persistence/provider failures;
- content or credential fields that can reach provider-native logs and AML traces.
Common contract failures include starting work on a pre-aborted signal, releasing only on success, claiming an unenforced access mode, leaking lifecycle authority through a handle, publishing a partial Workspace snapshot, and describing an image as Agent-ready without verifying its ACP executable.
The normative architecture remains in SPEC.md, while PROVIDERS.md tracks built-in implementation status.