Agent / Expose, do not reimplement

Bind once, serve several protocols

Once a contract is bound to a live runtime, adapters are intentionally thin:

Where the caller isAdapterWhat a capability becomes
Agent running in the pagefoldkit-agent-webmcpWebMCP tool on document.modelContext
External MCP clientfoldkit-agent-mcptools/list / tools/call entry over stdio or Streamable HTTP
Another A2A agentfoldkit-agent-a2askill on an Agent Card, invoked as a task
Agent Native hostfoldkit-agent-nativeAgent Native action

For example:

ts
AgentWebMcp.register({ agent: agentRuntime })
AgentMcp.stdio({ agent: agentRuntime })
AgentA2a.handler({ agent: agentRuntime })

Agent Native differs slightly because the host may resolve a runtime per caller:

ts
registerPackageActions(
  AgentNative.actions({
    definition: AssistantAgent,
    resolveRuntime,
  }),
)

Serving the same application through two protocols is therefore two adapter calls, not two contracts.

Adapters derive their schemas and call the same runtime. They do not repeat business logic, authorization, or input validation.

Dynamic capabilities

available(model) can make a capability appear or disappear as the application moves through states.

With WebMCP, for example, a host that supplies subscribe lets the adapter reconcile tool registration when the Model changes:

no completed todos
  tools: add_todo

a todo becomes completed
  Model changes
  tools: add_todo, clear_completed

This is not merely presentation. Calling clear_completed by name while it is unavailable still fails with AgentCapabilityUnavailableError.

Audit without capturing the Model

Every decision can be recorded, including refusals:

ts
const audit = Agent.auditLog({
  capacity: 500,
  principal: principal => principal.actorId,
})

const agentRuntime = AgentBuilder.bind({
  definition: AssistantAgent,
  host,
  audit,
})

The built-in log is deliberately conservative:

  • the Model is never stored;
  • the principal is stored only through an explicit projection;
  • input is opt-in and can be redacted;
  • the buffer is bounded;
  • a failing audit sink never fails the application dispatch.

Audit is accountability, not replay. Re-dispatching an old invocation remains an explicit application decision.

The contract is inspectable data

The contract can be inspected without an LLM or a running transport:

ts
Agent.messages(AssistantAgent)
Agent.schema(AssistantAgent)
Agent.contextSchema(AssistantAgent)
Agent.toManifest(AssistantAgent)
Agent.toMarkdown(AssistantAgent)

This is useful in tests and code review. Committing a generated manifest turns "the agent can now do X" into an ordinary diff instead of a hidden runtime change.

The contract also contributes architecture metadata to foldkit-surface's Module, so the application can inspect which Model paths an agent observes and which Message tags it exposes.

Local-first applications

foldkit-agent does not care whether the live host is a simple in-memory runtime or a replicated application.

Sync.mount already exposes the runtime-shaped pieces an agent needs: current Model access, dispatch, subscription, and Message observation. That means a local-first app can give a human and an agent the same application seam:

human UI ---------> Messages ----+
                                 |
agent contract ---> Messages ----+--> update / replica

The agent does not need a second server API that reimplements the state transitions merely because the application is synchronized.