-
Notifications
You must be signed in to change notification settings - Fork 0
Agentic Web Interop
The hands-on tour of how an Agent<IN, OUT> reaches the outside world. Agents.KT speaks the four agentic-web
protocols from one from(agent) shape, can charge for a served endpoint over x402, can pay for
resources it needs, and — as a spike — can even run in a browser over WebAssembly.
| Layer | Protocol | Surface |
|---|---|---|
| agent ↔ tools | MCP |
McpServer.from(agent) — see MCP Integration
|
| agent ↔ agent | A2A |
A2AServer.from(agent) / a2aAgent<IN,OUT>(url)
|
| agent ↔ web content | NLWeb | NlWebServer.from(agent) |
| agent ↔ user/frontend | AG-UI | AgUiServer.from(agent) |
| settlement (beneath all) | x402 | X402PaymentGate (sell) / X402Client (buy) |
All serve surfaces bind 127.0.0.1 only and take an optional bearerToken — front them with a TLS gateway for
network reach.
Turn any agent into a streaming chat backend a CopilotKit/React UI can consume.
val support = agent<String, String>("support") {
model { ollama("gemma3:4b") }
tools { val lookup = tool("lookup_account", "Look up an account") { a -> accounts[a["id"]] } }
skills { skill<String, String>("help", "Answer support questions") { tools(lookup) } }
}
val server = AgUiServer.from(support, port = 8765).start()POST a RunAgentInput; the last user message is the input, and you get an SSE stream:
curl -N http://localhost:8765/agent -H 'Content-Type: application/json' \
-d '{"threadId":"t1","runId":"r1","messages":[{"role":"user","content":"why was my card declined?"}]}'data: {"type":"RUN_STARTED","threadId":"t1","runId":"r1"}
data: {"type":"TEXT_MESSAGE_START","role":"assistant", ...}
data: {"type":"TEXT_MESSAGE_CONTENT","delta":"Let me check…"}
data: {"type":"TOOL_CALL_START","toolCallId":"c1","toolCallName":"lookup_account"}
data: {"type":"TOOL_CALL_END","toolCallId":"c1"}
data: {"type":"TOOL_CALL_RESULT","toolCallId":"c1","content":"{\"status\":\"past_due\"}","role":"tool","isError":false}
data: {"type":"TEXT_MESSAGE_CONTENT","delta":"Your account is past due."}
data: {"type":"RUN_FINISHED","threadId":"t1","runId":"r1"}
TOOL_CALL_RESULT carries what the tool returned — the frontend renders outputs, not just invocations. If the
model emits a thinking stream (model { reasoning(...) }), it surfaces as the REASONING_* family before the
answer. Full reference: docs/agui.md.
Front any served endpoint with a payment gate so it's served only after a settled USDC payment. You hold no key and take no custody — a hosted facilitator verifies + settles.
val gate = X402PaymentGate(
PaymentRequirements(
network = "base-sepolia", maxAmountRequired = "1000", // testnet first!
payTo = "0xYourPublicAddress",
asset = "0x036CbD53842c5426634e7929541eC2318f3dCF7e", // USDC on Base Sepolia
resource = "/agent", extra = mapOf("name" to "USD Coin", "version" to "2"),
),
facilitator = HttpFacilitatorClient("https://x402.org/facilitator"), // free testnet facilitator
)
val paidServer = AgUiServer.from(support, payment = gate).start()Unpaid requests get 402 with the terms; anything that fails, fails closed (never served unpaid).
Let an agent autonomously pay for something it needs. This is where irreversible money moves, so the key lives below the model layer and every payment clears a spend policy first.
val account = X402Account.fromPrivateKey(
System.getenv("X402_KEY"), // never in a prompt
X402SpendPolicy(
maxValuePerPayment = BigInteger.valueOf(2_000), // hard cap
allowedPayTo = setOf("0xKnownSeller"), // pin the counterparty
confirm = { plan -> humanApproves(plan) }, // optional human-in-the-loop
),
)
val resp = X402Client(account).get("https://seller.example/agent") // 402 -> sign -> retry -> 200A rejected payment throws X402PaymentDeniedException — no signature, no money moved. Test the whole signing
path for free on Base Sepolia (testnet USDC + the free x402.org/facilitator); the signing itself is real
secp256k1 + EIP-712/EIP-3009, pinned to ethers.js vectors. Full reference + sandbox recipes:
docs/x402.md.
The typed core (Agent, Skill, then) is not JVM-bound: a no-reflection slice compiles to wasmJs and runs
in a browser/node, reaching a local LLM over fetch.
cd wasm_tmp && ./run.sh # builds the wasm bundle, serves on :8080, opens the browserVerified end-to-end against local Ollama in both node and headless Chrome. Must be served over http (ES
modules can't load via file://). Status: feasibility spike (#4548), conditional GO — see
docs/wasm.md.
The same agent instance can serve multiple protocols simultaneously — each from(...) is an independent wrapper
that just invokes the agent (invocation is concurrent-safe):
val mcp = McpServer.from(support).start()
val a2a = A2AServer.from(support, bearerToken = t).start()
val agui = AgUiServer.from(support).start()
// three loopback listeners; a gateway in front makes that one address.- MCP Integration · Model & Tool Calling · Agent Deployment Modes
- Building From Source — run the suite, mutation tests, and live-LLM tests.
Project Links
Getting Started
Core Concepts
Composition Operators
LLM Integration
- Model & Tool Calling
- MCP Integration
- Agent Deployment Modes
- Swarm
- Tool Error Recovery
- Skill Selection & Routing
- Budget Controls
- Observability Hooks
Agentic Web
Guided Generation
Agent Memory
Reference
- API Quick Reference
- Type Algebra Cheat Sheet
- Glossary
- Best Practices
- Cookbook & Recipes
- Troubleshooting & FAQ
- Roadmap
Contributing