The application-shell layer for building agent products on the Tangle stack.
The substrate packages — @tangle-network/agent-runtime, agent-eval, agent-integrations, tcloud, sandbox — are the engine. This package is the shell: the chat tool-loop, the structured agent→app side channel, the integration-hub client, per-workspace billing, field crypto, and the web boundary utilities that every agent app otherwise rewrites by hand. You supply your domain through typed seams; the package supplies the mechanism and imports none of your code.
Who it's for: engineers building an agent product on the Tangle sandbox — a chat app, a copilot, an autonomous worker — who want the shell (chat routes, streaming, durability, approvals, billing, the tool side channel) as composable pieces instead of a per-app rewrite. It is not an agent framework or a model SDK: the reasoning lives in the sandbox agent; agent-app is everything around it — the turn plumbing, durability, and money.
- Structured tool side channel —
submit_proposal(approval-gated),schedule_followup,render_ui,add_citation, exposed as validated tool calls over three surfaces (HTTP route, per-turn MCP server, agent-runtime executor). No fenced-text parsing. - Bounded tool loop —
runAppToolLoop/streamAppToolLoop: stream a turn → collect tool calls → dispatch → fold results back → re-run, capped. These are 1:1 aliases of@tangle-network/agent-runtime'srunToolLoop/streamToolLoop(the engine owns the loop; this package adds no logic on that path). Substrate-free behind astreamTurnseam, so it drives a sandboxed agent, a Worker, or an in-browser copilot unchanged. - Assembled chat vertical —
createChatTurnRouteswires auth → thread/message store → streaming turn with buffered replay → uploads → sidecar question answering into one route factory, overauthorize/produce/store/interactionsseams. No hand-rolled orchestration. Seeexamples/chat-app.md. - Sandbox-optional — the same tools, billing, eval, and loop work without a container. A
fetch-only adapter maps any OpenAI-compatible stream (Tangle Router, tcloud) into the loop. Seeexamples/browser-copilot.md. - Resumable turns (sandbox-free path) — for a browser/edge copilot streaming the Router directly, buffer a turn so a dropped tab loses nothing and a reconnecting client replays the tail. Sandbox products don't need this — the sandbox SDK already buffers + replays sessions (server-side reconnect via
box.streamPrompt'slastEventId; browser-direct viabox.mintScopedToken()+SessionGatewayClient). Seeexamples/resumable-turns.md. - Composes the engine, never forks it —
/evalre-exports@tangle-network/agent-eval's verifier;/integrationswraps the hub;/tangleand/billingtake the tcloud client as a structural contract. Engines are peer dependencies — you pin the version, nothing is bundled. - ESM, typed, zero runtime deps in the substrate-free modules (
/runtime,/web,/crypto,/redact,/stream). Ships with.d.tsand npm provenance.
pnpm add @tangle-network/agent-appThe engine packages you actually use are peer dependencies — install the ones your modules touch:
# /eval composes the eval engine; /integrations composes the hub client
pnpm add @tangle-network/agent-eval @tangle-network/agent-integrations| Peer | Required by | Range |
|---|---|---|
@tangle-network/agent-eval |
/eval, /eval-campaign, /profile, /knowledge |
>=0.100.0 |
@tangle-network/agent-runtime |
/runtime, /chat-routes |
>=0.79.3 |
@tangle-network/agent-integrations |
/integrations |
>=0.44.0 |
@tangle-network/agent-interface |
/interactions, /chat-store, /harness |
>=0.15.0 |
@tangle-network/sandbox |
/sandbox, /profile, /skills |
>=0.9.7 |
@tangle-network/agent-knowledge |
/knowledge-loop |
>=1.7.0 |
@tangle-network/agent-profile-materialize |
/skills-placement |
>=0.6.0 |
All of these except agent-eval, agent-integrations, and agent-interface are declared optional peers, so a product that never imports the subpath installs nothing. driveSandboxTurn (/sandbox) calls box.driveTurn, which the SDK added in 0.10.5 — above the declared floor, so pin @tangle-network/sandbox >= 0.10.5 yourself if you use it.
Modules that import no engine package (/tools, /web, /crypto, /redact, /stream, /billing, /tangle — the last two take their client as a structural contract) need no peers.
A product supplies its taxonomy (which proposal types exist, which are approval-gated) and its handlers (the real DB/vault writes), then wires the tool side channel to whichever surface it runs on.
import {
buildAppToolOpenAITools,
createAppToolRuntimeExecutor,
type AppToolHandlers,
type AppToolTaxonomy,
} from '@tangle-network/agent-app/tools'
import { runAppToolLoop } from '@tangle-network/agent-app/runtime'
// 1. Declare the domain (the package bakes in no proposal types or rules).
const taxonomy: AppToolTaxonomy = {
proposalTypes: ['recommend', 'contact', 'other'],
regulatedTypes: ['recommend', 'contact'], // these require a certified approver
}
// 2. Provide the side effects — your store, your validation.
const handlers: AppToolHandlers = {
submitProposal,
scheduleFollowup,
renderUi,
addCitation,
}
// 3. Advertise the tools to the model and route their execution.
const tools = buildAppToolOpenAITools(taxonomy)
const executeToolCall = createAppToolRuntimeExecutor({
handlers,
taxonomy,
ctx: { userId, workspaceId, threadId },
})
// 4. Run a bounded, tool-driven turn loop over any backend.
const result = await runAppToolLoop({
systemPrompt,
userMessage,
streamTurn, // wrap your model / runAgentTaskStream
executeToolCall,
isExecutableTool: (name) => tools.some((t) => t.function.name === name),
})
console.log(result.finalText, result.toolResults)streamTurn is the one seam that varies by backend. For an in-browser or edge copilot talking to an OpenAI-compatible endpoint, you don't write it by hand:
import { createOpenAICompatStreamTurn, resolveTangleModelConfig } from '@tangle-network/agent-app/runtime'
const cfg = resolveTangleModelConfig() // reads provider/model/key/baseUrl from env, or pass literals
const streamTurn = createOpenAICompatStreamTurn({ ...cfg, tools })The full three-transport walkthrough (Tangle Router, tcloud, Vercel AI SDK) is in examples/browser-copilot.md.
Building the full server chat vertical instead — auth, thread/message tables, a streaming turn with buffered replay, uploads, and sidecar question answering — is the job of createChatTurnRoutes (/chat-routes) and the modules around it. The end-to-end assembly, including the durable plan/question workflow and the client composer, is in examples/chat-app.md.
One rule decides where anything lives:
Does the capability make sense without a specific app's tool side channel, approval queue, or chat route? Yes → it belongs in an engine package (contribute it down). No → it's app-shell, and it belongs here.
Everything here is reached through a typed seam — AppToolHandlers, AppToolTaxonomy, streamTurn, executeToolCall, verifyToken, KeyProvisioner / WorkspaceKeyStore / KeyCrypto. The package never imports product code and never hard-codes a domain value (a proposal type, a premium, a disclaimer); each is a parameter. New capability arrives as a new subpath, never a breaking change to an existing one.
Three decisions cover most of the surface.
1. How does the turn run? Pick the transport by who's watching, not by feature.
Each primitive is written package → symbol; three packages ship similarly-named turn functions, and AGENTS.md has the full primitive table and the runLoop name-collision note.
| Your turn | Use | Why |
|---|---|---|
| Interactive — a user is watching a chat or copilot | sandbox → box.streamPrompt() held open for the turn, wrapped here as streamSandboxPrompt (/sandbox); for the browser leg, sandbox → box.mintScopedToken() + SessionGatewayClient (@tangle-network/sandbox/session-gateway) attaches the tab directly |
Worker lifetime ≈ turn length; a dropped tab replays the buffered tail on reconnect. |
| Autonomous — a mission step, queue job, cron, or inbound email, with nobody watching | sandbox → box.driveTurn(), wrapped here as driveSandboxTurn (/sandbox), ticked from a durable driver; drop to raw box.dispatchPrompt({ detach: true }) + box.findCompletedTurn(turnId, { sessionId }) only when one pass is too coarse. runDetachedTurn (/chat-routes) bridges that detached run into the live buffer, so a browser opening the session mid-run still tails it token-by-token |
No consumer exists and Workers die in minutes; the platform runs the turn server-side and a crash re-dispatch is a lookup, not a second run. |
| Eval / CI — a long-lived harness process | sandbox → box.streamPrompt() for a sandboxed harness; agent-runtime (root) → runToolLoop / streamToolLoop for an in-process model turn — runAppToolLoop / streamAppToolLoop (/runtime) are 1:1 aliases of those, not a second implementation |
The process outlives the run; durability adds nothing — a failed run is re-run, not resumed. |
2. Assembled or à la carte? createChatTurnRoutes (/chat-routes) wires the whole server chat turn — auth, store, streaming, replay, uploads, interactions — over typed seams. Reach for the individual modules (/stream, /chat-store, /interactions) only to compose something the assembled route doesn't cover.
3. Sandbox or sandbox-free? The tools, billing, eval, and loop all work without a container: createOpenAICompatStreamTurn maps any OpenAI-compatible endpoint into the loop for a browser or edge copilot. Reach for /sandbox only when the turn needs a real container — bash, files, sub-agents, MCP.
Each subpath is an independent entry point — import only what you use; the root re-exports everything, but a subpath import keeps your bundle to what you touch.
The complete, always-current reference — every published subpath, its exported symbols, and its internal dependencies — is generated into docs/CODEMAP.md and kept honest by a CI check (regenerate with pnpm docs:gen). Start with the core entry points:
Run a turn
/tools— the structured agent→app side channel (proposals, follow-ups, citations, UI) as validated tool calls, over HTTP / MCP / runtime-executor surfaces./runtime— the bounded tool loop; the same loop drives a sandbox agent, a Worker, or an in-browser copilot behind onestreamTurnseam.
The server chat vertical (examples/chat-app.md)
/chat-routes—createChatTurnRoutes: auth → store → streaming turn with buffered replay → uploads → sidecar question answering, assembled. PlusrunDetachedTurnfor autonomous turns a browser can still watch live./chat-store·/interactions·/durable-chat·/plans— persistence, human-in-the-loop asks, and the durable plan/question workflow around them.
On the sandbox
/sandbox— workspace provisioning + turn streaming./missions— durable multi-step orchestration: sequencing, budgets, approval gates, schedules.
React surfaces
/web-react— router-safe chat + observability components (never imports sandbox-only UI);/composerwhen the chat owns a full sandbox profile.
Utilities (zero-dependency)
/web·/stream·/crypto·/redact— request boundary, SSE normalization, field crypto, PII redaction.
See docs/CODEMAP.md for the rest — /billing, /tangle, /object-store, /trace, /theme, /eval, /app-auth, /platform, and more.
Two createMissionService seams adopters hit on day one:
generateId(onMissionServiceOptions) defaults tocrypto.randomUUID()— a 36-char dashed UUID. If your mission table has an existing id shape (e.g. 32-hex to match D1 row defaults), inject your own generator; the service stamps it verbatim on the inserted record.CreateMissionInput.extrascarries opaque product-column values (aworkflowIdFK, a source-turn pointer) verbatim toMissionStorePort.insert(record, extras), so creation is a single write — no post-insert stamp. The service never reads them.
- ESM only. Ships
import+typesconditions per subpath. - Runtimes: Node ≥ 20, Cloudflare Workers / edge, and the browser (the substrate-free modules use only Web-standard APIs —
fetch, Web Crypto,TextEncoder). - TypeScript: strict; full
.d.tsfor every entry point.
pnpm install
pnpm typecheck && pnpm test && pnpm buildBuild is tsup (ESM + .d.ts), tests are vitest. A change keeps the suite green and follows the layering rule above — anything engine-general is contributed down to the substrate, not duplicated here. See AGENTS.md for the full contributor contract.