@tangle-network/agent-app
v0.43.51
Published
Application-shell framework for Tangle agent products: a bounded tool loop, the structured agent→app tool side channel, integration-hub client, per-workspace billing, and crypto — composed over the Tangle agent substrate through typed seams.
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.
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. 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 (
streamPrompt+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.100.0 |
| @tangle-network/agent-runtime | /runtime, /knowledge-loop, runtime tool execution | >=0.79.3 |
| @tangle-network/agent-integrations | /integrations, /tangle | >=0.32.0 |
Modules that do not import engine packages (/tools, /web, /crypto, /redact, /stream, /billing) 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.
Modules
Each is an independent entry point — import only what you use.
| Subpath | What it gives you |
|---|---|
| /tools | The structured agent→app side channel: buildAppToolOpenAITools, createAppToolRuntimeExecutor, handleAppToolRequest (HTTP), buildAppToolMcpServer / buildHttpMcpServer (MCP), createCapabilityToken + authenticateToolRequest (capability auth), ToolInputError. |
| /runtime | runAppToolLoop / streamAppToolLoop (bounded tool loop), resolveTangleModelConfig (Tangle Router / Anthropic BYOK), and toLoopEvents / createOpenAICompatStreamTurn (OpenAI-compat stream → loop events, with fragmented tool-call args reassembled). |
| /integrations | Integration-hub client: HubExecClient, resolveIntegrationAction, invokeIntegrationHub. Composes @tangle-network/agent-integrations. |
| /eval | producedFromToolEvents (bridge tool events into the eval verifier) and createTokenRecallChecker (deterministic content check). Re-exports @tangle-network/agent-eval's verifyCompletion, extractProducedState, weightedComposite, createLlmCorrectnessChecker. |
| /tangle | App-registration consent URL (buildConsentUrl) and a cached, auto-refreshing broker-token provider (createBrokerTokenProvider). Structural over the tcloud client. |
| /billing | createWorkspaceKeyManager — mint / rotate / roll over / report usage on per-workspace, budget-capped model keys. Seams for provisioner, store, and crypto. |
| /crypto | AES-GCM field encryption: encryptAesGcm, decryptAesGcm, createFieldCrypto. Key supplied by the caller. |
| /missions | Durable multi-step mission orchestration over a MissionStorePort seam: guarded status/step machine, idempotent plan engine with budget/approval gates, :::mission parser, the client-safe live-event reducer, and the canonical StepAgentActivity per-step delegated-run lane. |
| /trace | Flow observability: buildFlowTrace + ASCII renderWaterfall/renderHistogram; the mission trace bridge (createMissionTraceContext, childSpanContext, traceEnv) whose ids/env a delegated run inherits; and delegation→FlowSpan converters (delegationActivityToFlowSpans, loopTraceEventsToFlowSpans, composeMissionFlowTrace). |
| /web-react | Router-safe React chat components: ChatComposer, ModelPicker, EffortPicker, ChatMessages, RunDrillIn, plus observability surfaces — MissionActivityLane, AgentActivityPanel, FlowWaterfall. This path must not import sandbox-only UI. |
| /composer | Sandbox-first AgentComposer and profile/model/sandbox-runner controls re-exported from @tangle-network/sandbox-ui/chat. Use when the chat owns a sandbox profile or needs the full sandbox composer. |
| /web-react/terminal | Sandbox terminal React components, including WorkspaceTerminalPanel. Import this explicit path only for container/terminal views. |
| /web | Request-boundary utilities: parseJsonObjectBody, requireString, extractRequestContext, checkRateLimit, addSecurityHeaders. |
| /stream | SSE normalization and turn identity: normalizeToolEvent, resolveChatTurn, encodeEvent, message-part merging. |
| /redact | redactForIngestion — PII redaction before content leaves the boundary. |
The chat vertical — the assembled server chat stack. Wire it with examples/chat-app.md.
| Subpath | What it gives you |
|---|---|
| /chat-routes | createChatTurnRoutes — the assembled turn vertical: NDJSON turn + buffered replay + running reconnect-discovery + composed interactions answer endpoints, over authorize / produce / store seams. Plus createSandboxChatProducer, createUploadRoute, createSandboxFileIndexRoute, withDurableChatProjection, and the import-free ./wire contract (chatTurnRequestInit, ChatTurnRequestPayload). Composes agent-runtime's handleChatTurn; subpath-only (not re-exported from the root). |
| /chat-store | createChatTables + createChatStore — drizzle thread/message persistence behind the ChatStore port, and the canonical ChatMessagePart parts vocabulary (toChatMessageParts, part guards). The drizzle store/schema is subpath-only. |
| /interactions | Human-in-the-loop ask channel: createInteractionAnswerRoute (list/answer endpoint factory — validation, 410 mapping, duplicate-answer safety), the server sidecar client, and the shared ChatInteraction wire/persisted-part contract + codecs. Composes @tangle-network/agent-interface types. |
| /durable-chat | Durable plan/question workflow around the authoritative channels: createDurablePlanRoutes, createDurableChatScope, createDurableChatEventProjection, createDurableInteractionRoutePersistence, the DurablePlanStore port, and InMemoryDurableChatStateStore (tests/demos only — production supplies the store). |
| /plans | Browser-safe durable-plan chat projection: parsePlanSubmittedEvent, planToPersistedPart / persistedPartToPlan, canTransitionPlanStatus, and the ChatPlan union. Byte-matches the SDK's SandboxSession.plan(). |
| /object-store | Content-addressed durable attachment store: the ObjectStore port + createR2ObjectStore, signObjectUrl / verifyObjectUrl, createProxiedArtifactRoute, objectKey. |
| /app-auth | createAppAuth — better-auth config factory + request guards (requireApiUser) over the standard users/sessions/accounts/verifications tables; composes /platform's SSO cookie minter. Optional better-auth peer; subpath-only. |
| /sandbox | Workspace sandbox provisioning + turn streaming: ensureWorkspaceSandbox / peekWorkspaceSandbox, streamSandboxPrompt / runSandboxPrompt, createWorkspaceSandboxManager, and terminal / runtime-proxy handlers. Peer @tangle-network/sandbox; subpath-only. |
| /platform | Tangle platform glue: cross-site SSO (createTangleSsoHandlers), request guards (createAuthGuard, guardResolution), the hub proxy, and seat billing. |
The root entry (@tangle-network/agent-app) re-exports every module, but importing the subpath keeps your bundle to what you use.
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 (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.
