@tangle-network/agent-app
v0.46.60
Published
Build agent applications with typed chat, tools, sandboxes, integrations, billing, and evaluation.
Maintainers
Readme
@tangle-network/agent-app
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.
Highlights
- 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 ofrunToolLoop/streamToolLoopfrom@tangle-network/agent-runtime/tool-loop(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 — buffer a turn so a dropped tab loses nothing and a reconnecting client replays the tail. Two niches: a browser/edge copilot streaming the Router directly (no sandbox session to attach to), and any detached sandbox run driven through
dispatchPrompt({ detach: true })/streamPromptthat a browser must tail. Sandbox 0.37driveTurnuses the message lane and can be tailed through the session gateway. An INTERACTIVE sandbox turn doesn't need it — drive it on the message lane (box.session(id).sendMessage()) and attach the tab withbox.mintScopedToken()+SessionGatewayClient, or let the worker resume withbox.streamPrompt('', { executionId, lastEventId }). 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.
Install
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.172.1 <0.174.0 |
| @tangle-network/agent-runtime | /runtime, /chat-routes | >=0.191.0 <0.193.0 |
| @tangle-network/agent-integrations | /integrations | >=0.53.55 <0.54.0 |
| @tangle-network/agent-interface | /interactions, /chat-store, /harness | ^2.2.0 |
| @tangle-network/sandbox | /sandbox | >=0.36.4 <0.38.0 |
| @tangle-network/agent-knowledge | /knowledge-loop | ^13.0.0 |
| @tangle-network/agent-profile-materialize | /skills-placement | >=0.18.1 <0.20.0 |
| @tangle-network/sandbox-ui | /brand, /work-product-react, /workspace-react | >=0.111.2 <0.114.0 |
| @tangle-network/ui | /brand, /work-product-react, /workspace-react | >=11.6.0 <12.0.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.
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.
Quick start
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.
How it's organised
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.
Choosing a path
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. Sandbox 0.37 driveTurn uses the message lane and a browser can attach through the session gateway; runDetachedTurn (/chat-routes) remains the buffer bridge for stream/dispatch detached runs | Workers die in minutes, so 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 /tool-loop → 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.
Modules
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./trace— bounded stage timing, turn waterfalls, and mission traces over an injected telemetry carrier.
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·/plans— persistence, human-in-the-loop asks, and the durable plan projection.
On the sandbox
/sandbox— workspace provisioning, request-time single-flight, prewarming, and turn streaming./missions— durable multi-step orchestration: sequencing, budgets, approval gates, schedules.
React surfaces
/workspace-react— the default chat-first outer workspace composition: shared sidebar, session rail, and active-route wiring./web-react— router-safe chat, history, and observability components (never imports sandbox-only UI)./chat-react— the sandbox-ui-backed new-session composer and shared profile, backend, model, thinking, plan-mode, attachment, and mention controls.
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.
Missions: id shape and product columns
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.
Compatibility
- 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.
Contributing
pnpm install
pnpm typecheck && pnpm test && pnpm buildBuild is tsup for the ESM output plus tsc for the .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.
