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 is | Adapter | What a capability becomes |
|---|---|---|
| Agent running in the page | foldkit-agent-webmcp | WebMCP tool on document.modelContext |
| External MCP client | foldkit-agent-mcp | tools/list / tools/call entry over stdio or Streamable HTTP |
| Another A2A agent | foldkit-agent-a2a | skill on an Agent Card, invoked as a task |
| Agent Native host | foldkit-agent-native | Agent Native action |
For example:
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:
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_completedThis 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:
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:
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 / replicaThe agent does not need a second server API that reimplements the state transitions merely because the application is synchronized.