@tangle-network/agent-app 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,11 +6,98 @@ import {
6
6
 
7
7
  // src/runtime/model.ts
8
8
  var DEFAULT_TANGLE_ROUTER_BASE_URL = "https://router.tangle.tools/v1";
9
+ var DEFAULT_TANGLE_BILLING_ENFORCEMENT_ENV_VAR = "TANGLE_BILLING_ENFORCEMENT";
9
10
  function requireEnv(env, name) {
10
11
  const value = env[name]?.trim();
11
12
  if (!value) throw new Error(`${name} is required`);
12
13
  return value;
13
14
  }
15
+ function trimOrNull(value) {
16
+ const trimmed = value?.trim();
17
+ return trimmed ? trimmed : null;
18
+ }
19
+ function isTangleExecutionKeyErrorCode(value) {
20
+ return value === "local_tangle_api_key_required" || value === "tangle_account_not_connected";
21
+ }
22
+ var TangleExecutionKeyError = class extends Error {
23
+ code;
24
+ status;
25
+ constructor(code, message, status) {
26
+ super(message);
27
+ this.name = "TangleExecutionKeyError";
28
+ this.code = code;
29
+ this.status = status;
30
+ }
31
+ };
32
+ function isTangleExecutionKeyError(error) {
33
+ return error instanceof TangleExecutionKeyError || typeof error === "object" && error !== null && error.name === "TangleExecutionKeyError" && typeof error.message === "string" && isTangleExecutionKeyErrorCode(error.code) && typeof error.status === "number";
34
+ }
35
+ function resolveTangleExecutionEnvironment(env = process.env) {
36
+ const raw = (env.APP_ENV ?? env.NODE_ENV ?? "").trim().toLowerCase();
37
+ if (raw === "development" || raw === "dev" || raw === "local") return "development";
38
+ if (raw === "staging") return "staging";
39
+ if (raw === "test") return "test";
40
+ return "production";
41
+ }
42
+ function isTangleBillingEnforcementDisabled(opts = {}) {
43
+ const env = opts.env ?? process.env;
44
+ const enforcementEnvVar = opts.enforcementEnvVar ?? DEFAULT_TANGLE_BILLING_ENFORCEMENT_ENV_VAR;
45
+ const override = env[enforcementEnvVar]?.trim().toLowerCase();
46
+ if (override === "disabled") return true;
47
+ if (override === "enabled") return false;
48
+ return resolveTangleExecutionEnvironment(env) === "development";
49
+ }
50
+ function tangleExecutionKeyHttpError(error) {
51
+ if (!isTangleExecutionKeyError(error)) return null;
52
+ return {
53
+ status: error.status,
54
+ body: {
55
+ error: error.message,
56
+ code: error.code
57
+ }
58
+ };
59
+ }
60
+ async function resolveUserTangleExecutionKey(opts) {
61
+ const env = opts.env ?? process.env;
62
+ const environment = opts.environment ?? resolveTangleExecutionEnvironment(env);
63
+ if (environment === "development") {
64
+ const apiKey2 = trimOrNull(env.TANGLE_API_KEY);
65
+ if (apiKey2) return { apiKey: apiKey2, source: "local-env" };
66
+ }
67
+ const apiKey = trimOrNull(await opts.getUserApiKey());
68
+ if (apiKey) return { apiKey, source: "user" };
69
+ if (environment === "development") {
70
+ throw new TangleExecutionKeyError(
71
+ "local_tangle_api_key_required",
72
+ "TANGLE_API_KEY or a linked Tangle account is required for local Tangle model execution.",
73
+ 503
74
+ );
75
+ }
76
+ throw new TangleExecutionKeyError(
77
+ "tangle_account_not_connected",
78
+ "Connect your Tangle account before invoking this agent.",
79
+ 401
80
+ );
81
+ }
82
+ async function resolveUserTangleExecutionKeyForUser(opts) {
83
+ return resolveUserTangleExecutionKey({
84
+ environment: opts.environment,
85
+ env: opts.env,
86
+ getUserApiKey: () => opts.getUserApiKey(opts.userId)
87
+ });
88
+ }
89
+ function createTangleRouterModelConfig(opts) {
90
+ const apiKey = opts.apiKey.trim();
91
+ if (!apiKey) throw new Error("apiKey is required");
92
+ const model = opts.model.trim();
93
+ if (!model) throw new Error("model is required");
94
+ return {
95
+ provider: "openai-compat",
96
+ model,
97
+ apiKey,
98
+ baseUrl: (opts.baseUrl?.trim() || DEFAULT_TANGLE_ROUTER_BASE_URL).replace(/\/+$/, "")
99
+ };
100
+ }
14
101
  function resolveTangleModelConfig(opts = {}) {
15
102
  const env = opts.env ?? process.env;
16
103
  const provider = env.MODEL_PROVIDER?.trim() || "openai-compat";
@@ -265,6 +352,15 @@ ${lines.join("\n")}` });
265
352
 
266
353
  export {
267
354
  DEFAULT_TANGLE_ROUTER_BASE_URL,
355
+ DEFAULT_TANGLE_BILLING_ENFORCEMENT_ENV_VAR,
356
+ TangleExecutionKeyError,
357
+ isTangleExecutionKeyError,
358
+ resolveTangleExecutionEnvironment,
359
+ isTangleBillingEnforcementDisabled,
360
+ tangleExecutionKeyHttpError,
361
+ resolveUserTangleExecutionKey,
362
+ resolveUserTangleExecutionKeyForUser,
363
+ createTangleRouterModelConfig,
268
364
  resolveTangleModelConfig,
269
365
  toLoopEvents,
270
366
  createOpenAICompatStreamTurn,
@@ -272,4 +368,4 @@ export {
272
368
  runAppToolLoop,
273
369
  streamAppToolLoop
274
370
  };
275
- //# sourceMappingURL=chunk-3LP6PEWS.js.map
371
+ //# sourceMappingURL=chunk-SVCJYRVM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/runtime/model.ts","../src/runtime/openai-stream.ts","../src/runtime/agent.ts","../src/runtime/index.ts"],"sourcesContent":["/**\n * Resolve the model config a Tangle agent's sandbox/runtime runs on.\n *\n * Every Tangle agent product resolves the SAME thing from env: the Tangle Router\n * (OpenAI-compatible, metered at the platform markup against a single\n * `TANGLE_API_KEY`) by default, with a direct-Anthropic BYOK escape hatch. The\n * shape feeds the sandbox SDK's `backend.model`. Lifted here so no product\n * hand-rolls the env parsing + the router default.\n */\n\nexport interface TangleModelConfig {\n /** The Tangle Router is OpenAI-compatible → driven via `openai-compat`.\n * `anthropic` is the BYOK escape hatch. */\n provider: 'openai-compat' | 'anthropic'\n model: string\n apiKey: string\n baseUrl: string\n}\n\nexport type TangleExecutionEnvironment = 'development' | 'staging' | 'production' | 'test'\nexport type TangleExecutionKeySource = 'local-env' | 'user'\nexport type TangleExecutionKeyErrorCode =\n | 'local_tangle_api_key_required'\n | 'tangle_account_not_connected'\n\nexport interface ResolveModelOptions {\n /** Env to read (defaults to process.env). */\n env?: Record<string, string | undefined>\n /** Router base URL default when `TANGLE_ROUTER_BASE_URL` is unset. */\n defaultRouterBaseUrl?: string\n}\n\nexport interface ResolveUserTangleExecutionKeyOptions {\n /** Deployment context. Only local development may fall back to env keys. */\n environment?: TangleExecutionEnvironment\n /** Env to read for the local-development fallback. */\n env?: Record<string, string | undefined>\n /** App-owned lookup for the caller's linked platform API key. */\n getUserApiKey: () => string | null | undefined | Promise<string | null | undefined>\n}\n\nexport interface ResolveUserTangleExecutionKeyForUserOptions<UserId = string> {\n userId: UserId\n environment?: TangleExecutionEnvironment\n env?: Record<string, string | undefined>\n getUserApiKey: (userId: UserId) => string | null | undefined | Promise<string | null | undefined>\n}\n\nexport interface ResolvedTangleExecutionKey {\n apiKey: string\n source: TangleExecutionKeySource\n}\n\nexport interface TangleExecutionKeyHttpError {\n status: number\n body: {\n error: string\n code: TangleExecutionKeyErrorCode\n }\n}\n\nexport interface CreateTangleRouterModelConfigOptions {\n apiKey: string\n model: string\n baseUrl?: string\n}\n\nexport interface TangleBillingEnforcementOptions {\n /** Env to read (defaults to process.env). */\n env?: Record<string, string | undefined>\n /**\n * Optional app-specific override flag, e.g. `GTM_BILLING_ENFORCEMENT`.\n * Defaults to the shared `TANGLE_BILLING_ENFORCEMENT`.\n */\n enforcementEnvVar?: string\n}\n\nexport const DEFAULT_TANGLE_ROUTER_BASE_URL = 'https://router.tangle.tools/v1'\nexport const DEFAULT_TANGLE_BILLING_ENFORCEMENT_ENV_VAR = 'TANGLE_BILLING_ENFORCEMENT'\n\nfunction requireEnv(env: Record<string, string | undefined>, name: string): string {\n const value = env[name]?.trim()\n if (!value) throw new Error(`${name} is required`)\n return value\n}\n\nfunction trimOrNull(value: string | null | undefined): string | null {\n const trimmed = value?.trim()\n return trimmed ? trimmed : null\n}\n\nfunction isTangleExecutionKeyErrorCode(value: unknown): value is TangleExecutionKeyErrorCode {\n return value === 'local_tangle_api_key_required' || value === 'tangle_account_not_connected'\n}\n\nexport class TangleExecutionKeyError extends Error {\n readonly code: TangleExecutionKeyErrorCode\n readonly status: number\n\n constructor(code: TangleExecutionKeyErrorCode, message: string, status: number) {\n super(message)\n this.name = 'TangleExecutionKeyError'\n this.code = code\n this.status = status\n }\n}\n\nexport function isTangleExecutionKeyError(error: unknown): error is TangleExecutionKeyError {\n return error instanceof TangleExecutionKeyError\n || (\n typeof error === 'object'\n && error !== null\n && (error as { name?: unknown }).name === 'TangleExecutionKeyError'\n && typeof (error as { message?: unknown }).message === 'string'\n && isTangleExecutionKeyErrorCode((error as { code?: unknown }).code)\n && typeof (error as { status?: unknown }).status === 'number'\n )\n}\n\nexport function resolveTangleExecutionEnvironment(\n env: Record<string, string | undefined> = process.env as Record<string, string | undefined>,\n): TangleExecutionEnvironment {\n const raw = (env.APP_ENV ?? env.NODE_ENV ?? '').trim().toLowerCase()\n if (raw === 'development' || raw === 'dev' || raw === 'local') return 'development'\n if (raw === 'staging') return 'staging'\n if (raw === 'test') return 'test'\n return 'production'\n}\n\n/**\n * Shared policy for agent products that bill through the Tangle Platform.\n *\n * Local development defaults billing enforcement off so apps can use a local\n * `TANGLE_API_KEY` without requiring a browser-linked platform account. Any\n * non-development environment defaults enforcement on. Apps may pass their own\n * override flag (`FOO_BILLING_ENFORCEMENT`) while new apps can use the shared\n * `TANGLE_BILLING_ENFORCEMENT`.\n */\nexport function isTangleBillingEnforcementDisabled(\n opts: TangleBillingEnforcementOptions = {},\n): boolean {\n const env = opts.env ?? (process.env as Record<string, string | undefined>)\n const enforcementEnvVar = opts.enforcementEnvVar ?? DEFAULT_TANGLE_BILLING_ENFORCEMENT_ENV_VAR\n const override = env[enforcementEnvVar]?.trim().toLowerCase()\n\n if (override === 'disabled') return true\n if (override === 'enabled') return false\n\n return resolveTangleExecutionEnvironment(env) === 'development'\n}\n\nexport function tangleExecutionKeyHttpError(error: unknown): TangleExecutionKeyHttpError | null {\n if (!isTangleExecutionKeyError(error)) return null\n return {\n status: error.status,\n body: {\n error: error.message,\n code: error.code,\n },\n }\n}\n\n/**\n * Resolve the user-facing Tangle API key for model execution.\n *\n * Local development may use a server env key so apps remain easy to run.\n * Deployed contexts must use the caller's linked platform key; this keeps\n * model execution, billing, and account ownership aligned across products.\n */\nexport async function resolveUserTangleExecutionKey(\n opts: ResolveUserTangleExecutionKeyOptions,\n): Promise<ResolvedTangleExecutionKey> {\n const env = opts.env ?? (process.env as Record<string, string | undefined>)\n const environment = opts.environment ?? resolveTangleExecutionEnvironment(env)\n\n if (environment === 'development') {\n const apiKey = trimOrNull(env.TANGLE_API_KEY)\n if (apiKey) return { apiKey, source: 'local-env' }\n }\n\n const apiKey = trimOrNull(await opts.getUserApiKey())\n if (apiKey) return { apiKey, source: 'user' }\n\n if (environment === 'development') {\n throw new TangleExecutionKeyError(\n 'local_tangle_api_key_required',\n 'TANGLE_API_KEY or a linked Tangle account is required for local Tangle model execution.',\n 503,\n )\n }\n\n throw new TangleExecutionKeyError(\n 'tangle_account_not_connected',\n 'Connect your Tangle account before invoking this agent.',\n 401,\n )\n}\n\nexport async function resolveUserTangleExecutionKeyForUser<UserId = string>(\n opts: ResolveUserTangleExecutionKeyForUserOptions<UserId>,\n): Promise<ResolvedTangleExecutionKey> {\n return resolveUserTangleExecutionKey({\n environment: opts.environment,\n env: opts.env,\n getUserApiKey: () => opts.getUserApiKey(opts.userId),\n })\n}\n\n/**\n * Build an OpenAI-compatible Tangle Router model config from an already\n * resolved execution key. This intentionally does not read TANGLE_API_KEY.\n */\nexport function createTangleRouterModelConfig(\n opts: CreateTangleRouterModelConfigOptions,\n): TangleModelConfig {\n const apiKey = opts.apiKey.trim()\n if (!apiKey) throw new Error('apiKey is required')\n const model = opts.model.trim()\n if (!model) throw new Error('model is required')\n return {\n provider: 'openai-compat',\n model,\n apiKey,\n baseUrl: (opts.baseUrl?.trim() || DEFAULT_TANGLE_ROUTER_BASE_URL).replace(/\\/+$/, ''),\n }\n}\n\n/**\n * Resolve the model config from env. DEFAULT path (`MODEL_PROVIDER` unset or\n * `openai-compat`/`tangle-router`/`tcloud`): the Tangle Router, authenticated\n * with `TANGLE_API_KEY`, model from `MODEL_NAME`. BYOK path\n * (`MODEL_PROVIDER=anthropic`): direct Anthropic with `ANTHROPIC_API_KEY` +\n * `ANTHROPIC_BASE_URL`. Throws (fail-loud) on a missing required var so a\n * misconfigured deploy fails at boot, not mid-turn.\n */\nexport function resolveTangleModelConfig(opts: ResolveModelOptions = {}): TangleModelConfig {\n const env = opts.env ?? (process.env as Record<string, string | undefined>)\n const provider = env.MODEL_PROVIDER?.trim() || 'openai-compat'\n const model = requireEnv(env, 'MODEL_NAME')\n\n if (provider === 'openai-compat' || provider === 'tangle-router' || provider === 'tcloud') {\n return {\n provider: 'openai-compat',\n model,\n apiKey: requireEnv(env, 'TANGLE_API_KEY'),\n baseUrl: (env.TANGLE_ROUTER_BASE_URL?.trim() || opts.defaultRouterBaseUrl || DEFAULT_TANGLE_ROUTER_BASE_URL).replace(/\\/+$/, ''),\n }\n }\n\n if (provider === 'anthropic') {\n return {\n provider,\n model,\n apiKey: requireEnv(env, 'ANTHROPIC_API_KEY'),\n baseUrl: requireEnv(env, 'ANTHROPIC_BASE_URL'),\n }\n }\n\n throw new Error(`Unsupported MODEL_PROVIDER: ${provider} (use openai-compat for the Tangle Router, or anthropic for BYOK)`)\n}\n","/**\n * OpenAI-compatible stream → `LoopEvent` adapter, for NON-sandbox copilots.\n *\n * `streamAppToolLoop` takes a `streamTurn` seam that yields `LoopEvent`s. A\n * sandboxed agent produces those from its container; a browser/edge copilot\n * instead calls a model directly. The Tangle Router, the tcloud SDK, and most\n * providers all speak the OpenAI Chat Completions streaming shape — so the ONE\n * reusable piece is assembling that stream (content deltas + FRAGMENTED\n * tool-call deltas) into `LoopEvent`s. That assembly is the boilerplate every\n * copilot would re-write (and get wrong — OpenAI streams tool-call arguments in\n * pieces across chunks).\n *\n * This does NOT implement an HTTP client beyond a minimal `fetch` + SSE reader\n * (browser/edge/Node-safe, zero deps). For richer transport use the tcloud SDK\n * or the Vercel AI SDK and pipe their stream through {@link toLoopEvents}.\n */\nimport type { LoopEvent, LoopToolCall } from './index'\n\n/** Minimal OpenAI Chat Completions streaming chunk (structural — no `openai` dep). */\nexport interface OpenAIStreamChunk {\n choices?: Array<{\n delta?: {\n content?: string | null\n tool_calls?: Array<{\n index: number\n id?: string\n function?: { name?: string; arguments?: string }\n }>\n }\n finish_reason?: string | null\n }>\n}\n\ninterface PartialToolCall {\n id?: string\n name: string\n args: string\n}\n\n/**\n * Map an OpenAI-compat streaming chunk iterator to `LoopEvent`s: each content\n * delta → a `text` event; tool-call deltas are accumulated by index across\n * chunks and emitted as one complete `tool_call` event when the stream finishes\n * (arguments JSON-parsed; an empty/garbled args string yields `{}` rather than\n * throwing). Works for the Tangle Router, tcloud, or any OpenAI-compat source.\n */\nexport async function* toLoopEvents(chunks: AsyncIterable<OpenAIStreamChunk>): AsyncIterable<LoopEvent> {\n const calls = new Map<number, PartialToolCall>()\n for await (const chunk of chunks) {\n const choice = chunk.choices?.[0]\n if (!choice) continue\n const content = choice.delta?.content\n if (content) yield { type: 'text', text: content }\n for (const tc of choice.delta?.tool_calls ?? []) {\n const cur = calls.get(tc.index) ?? { name: '', args: '' }\n if (tc.id) cur.id = tc.id\n if (tc.function?.name) cur.name += tc.function.name\n if (tc.function?.arguments) cur.args += tc.function.arguments\n calls.set(tc.index, cur)\n }\n }\n for (const [, c] of [...calls.entries()].sort((a, b) => a[0] - b[0])) {\n if (!c.name) continue\n yield { type: 'tool_call', call: { toolCallId: c.id, toolName: c.name, args: safeParse(c.args) } satisfies LoopToolCall }\n }\n}\n\nfunction safeParse(s: string): Record<string, unknown> {\n if (!s.trim()) return {}\n try {\n const v = JSON.parse(s)\n return v && typeof v === 'object' && !Array.isArray(v) ? (v as Record<string, unknown>) : {}\n } catch {\n return {}\n }\n}\n\nexport interface OpenAICompatStreamTurnOptions {\n /** OpenAI-compat base URL (e.g. the Tangle Router `https://router.tangle.tools/v1`). */\n baseUrl: string\n apiKey: string\n model: string\n /** OpenAI tool definitions — pass `buildAppToolOpenAITools(taxonomy)` so the\n * model can call the app tools. Omit for a tool-free copilot. */\n tools?: unknown[]\n temperature?: number\n fetchImpl?: typeof fetch\n /** Extra body fields (e.g. `max_tokens`). */\n extraBody?: Record<string, unknown>\n}\n\n/**\n * Build a `streamTurn` that calls an OpenAI-compatible `/chat/completions`\n * endpoint (Tangle Router / tcloud / any compat provider) with `stream: true`\n * and yields `LoopEvent`s via {@link toLoopEvents}. Browser/edge/Node-safe —\n * just `fetch` + an SSE reader. Drop straight into `streamAppToolLoop`:\n *\n * const cfg = resolveTangleModelConfig() // or { baseUrl, apiKey, model }\n * streamAppToolLoop({ streamTurn: createOpenAICompatStreamTurn({ ...cfg, tools }), executeToolCall, ... })\n */\nexport function createOpenAICompatStreamTurn(\n opts: OpenAICompatStreamTurnOptions,\n): (messages: Array<{ role: string; content: string }>) => AsyncIterable<LoopEvent> {\n const base = opts.baseUrl.replace(/\\/+$/, '')\n const doFetch = opts.fetchImpl ?? fetch\n return (messages) =>\n toLoopEvents(\n streamChatCompletions(doFetch, `${base}/chat/completions`, opts.apiKey, {\n model: opts.model,\n messages,\n stream: true,\n ...(opts.tools && opts.tools.length > 0 ? { tools: opts.tools } : {}),\n ...(opts.temperature != null ? { temperature: opts.temperature } : {}),\n ...opts.extraBody,\n }),\n )\n}\n\n/** Stream + parse an OpenAI-compat SSE response into chunks. Tolerates `data:`\n * framing, multi-line buffers, and the terminal `[DONE]`. */\nasync function* streamChatCompletions(\n doFetch: typeof fetch,\n url: string,\n apiKey: string,\n body: Record<string, unknown>,\n): AsyncIterable<OpenAIStreamChunk> {\n const res = await doFetch(url, {\n method: 'POST',\n headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', Accept: 'text/event-stream' },\n body: JSON.stringify(body),\n })\n if (!res.ok || !res.body) {\n const text = res.body ? await res.text().catch(() => '') : ''\n throw new Error(`OpenAI-compat stream failed (HTTP ${res.status})${text ? `: ${text.slice(0, 200)}` : ''}`)\n }\n const reader = res.body.getReader()\n const decoder = new TextDecoder()\n let buffer = ''\n for (;;) {\n const { done, value } = await reader.read()\n if (done) break\n buffer += decoder.decode(value, { stream: true })\n const lines = buffer.split('\\n')\n buffer = lines.pop() ?? ''\n for (const line of lines) {\n const trimmed = line.trim()\n if (!trimmed.startsWith('data:')) continue\n const data = trimmed.slice(5).trim()\n if (data === '[DONE]') return\n try {\n yield JSON.parse(data) as OpenAIStreamChunk\n } catch {\n /* skip a partial/garbled SSE frame */\n }\n }\n }\n}\n","/**\n * `createAgentRuntime` — the in-process agent core, assembled.\n *\n * The bricks to run an agent turn WITHOUT a sandbox already exist in this\n * package, but a consumer must hand-wire five of them every time: resolve the\n * model config, build the OpenAI tool schemas from the taxonomy, build a\n * `streamTurn` over the model endpoint, build an `executeToolCall` over the\n * product's handlers, and drive `runAppToolLoop` / `streamAppToolLoop` with an\n * `isExecutableTool` predicate. That boilerplate is identical across every\n * sandbox-free surface (an edge/browser copilot, an eval harness, a Node CLI),\n * and getting it subtly wrong — e.g. NOT advertising the tools, so the model\n * never emits a `tool_call` and no side effect ever fires — is exactly the\n * failure that makes a tool-driven agent score zero off-sandbox.\n *\n * This factory bundles those five into one object configured for ONE agent:\n *\n * const runtime = createAgentRuntime({ model, taxonomy, handlers, systemPrompt })\n * const result = await runtime.run(userMessage, { ctx }) // awaitable\n * for await (const y of runtime.stream(userMessage, { ctx })) {…} // streaming\n *\n * The model is advertised the app tools (so it CAN call them); each call is\n * dispatched against the product's `handlers` (so the side effect is real); the\n * `onProduced` hook fires at the real side-effect site (so an eval/UI credits a\n * persisted proposal or artifact). Substrate-free: no `@tangle-network/sandbox`,\n * no Durable Object, no `@tangle-network/agent-runtime` import. The SAME core\n * the Cloudflare Worker runs, runnable anywhere a `fetch` to an OpenAI-compatible\n * endpoint works.\n *\n * Domain stays out: the proposal taxonomy, the handlers, and the system prompt\n * are all injected — the factory knows nothing about insurance, law, tax, etc.\n */\nimport {\n type AppToolHandlers,\n type AppToolContext,\n type AppToolOutcome,\n type AppToolProducedEvent,\n type AppToolTaxonomy,\n} from '../tools/types'\nimport { buildAppToolOpenAITools, isAppToolName } from '../tools/openai'\nimport { createAppToolRuntimeExecutor } from '../tools/runtime'\nimport {\n runAppToolLoop,\n streamAppToolLoop,\n type LoopEvent,\n type LoopToolCall,\n type StreamLoopYield,\n type ToolLoopResult,\n} from './index'\nimport { createOpenAICompatStreamTurn } from './openai-stream'\n\n/** OpenAI-compatible model endpoint (Tangle Router / tcloud / any compat\n * provider). Build from {@link resolveTangleModelConfig} or pass literals. */\nexport interface AgentRuntimeModelConfig {\n baseUrl: string\n apiKey: string\n model: string\n temperature?: number\n fetchImpl?: typeof fetch\n /** Extra request-body fields (e.g. `max_tokens`, a `reasoning` block). */\n extraBody?: Record<string, unknown>\n}\n\nexport interface CreateAgentRuntimeOptions {\n /** The model endpoint the turns stream from. */\n model: AgentRuntimeModelConfig\n /** The product's proposal taxonomy — advertises `submit_proposal`'s `type`\n * enum to the model and labels the regulated subset on the result. */\n taxonomy: AppToolTaxonomy\n /** Domain handlers persisting each tool to the product's store/vault. */\n handlers: AppToolHandlers\n /** Default agent identity / system prompt. A turn may override it. */\n systemPrompt: string\n /** Max tool-driven re-runs per turn. Default 8. */\n maxToolTurns?: number\n /** Extra OpenAI tool definitions advertised ALONGSIDE the four app tools\n * (e.g. `integration_invoke`). Pair with {@link executeOtherTool}. */\n extraTools?: unknown[]\n /** Execute a tool that is NOT one of the four app tools (e.g. an integration\n * action). Only consulted for names {@link isOtherExecutableTool} accepts. */\n executeOtherTool?: (call: LoopToolCall, ctx: AppToolContext) => Promise<AppToolOutcome>\n /** Which non-app tool names are executable here. Required if {@link executeOtherTool} is set. */\n isOtherExecutableTool?: (toolName: string) => boolean\n}\n\nexport interface AgentTurnOptions {\n /** The trusted per-turn context (who/where the turn runs as). */\n ctx: AppToolContext\n /** Prior conversation turns, in order. */\n priorMessages?: Array<{ role: string; content: string }>\n /** Override the factory's default system prompt for this turn. */\n systemPrompt?: string\n /** Fires at the real side-effect site for each produced proposal/artifact. */\n onProduced?: (event: AppToolProducedEvent) => void\n}\n\nexport interface AgentRuntime {\n /** Run the bounded tool loop to completion; resolve with final text + every\n * executed tool outcome. */\n run(userMessage: string, turn: AgentTurnOptions): Promise<ToolLoopResult>\n /** Stream the bounded tool loop: yields each raw model event and each executed\n * tool result as it happens (for SSE re-emission + telemetry). */\n stream(userMessage: string, turn: AgentTurnOptions): AsyncGenerator<StreamLoopYield<LoopEvent>, void, unknown>\n}\n\n/**\n * Create an in-process agent runtime for one agent. See the module doc for the\n * full rationale; the short version: it advertises the app tools to the model,\n * dispatches each emitted call against `handlers`, and drives the bounded loop —\n * the whole agent core, sandbox-free.\n */\nexport function createAgentRuntime(opts: CreateAgentRuntimeOptions): AgentRuntime {\n if (opts.executeOtherTool && !opts.isOtherExecutableTool) {\n throw new Error('createAgentRuntime: isOtherExecutableTool is required when executeOtherTool is set')\n }\n\n // Tool schemas + the streamTurn are stable across turns — build once. The\n // model MUST be advertised the tools or it never emits a tool_call (the exact\n // failure that scores a tool-driven agent zero off-sandbox).\n const tools = [...buildAppToolOpenAITools(opts.taxonomy), ...(opts.extraTools ?? [])]\n const m = opts.model\n const streamTurn = createOpenAICompatStreamTurn({\n baseUrl: m.baseUrl,\n apiKey: m.apiKey,\n model: m.model,\n tools,\n temperature: m.temperature,\n fetchImpl: m.fetchImpl,\n extraBody: m.extraBody,\n })\n\n const isExecutableTool = (name: string): boolean =>\n isAppToolName(name) || (opts.isOtherExecutableTool?.(name) ?? false)\n\n const buildExecutor = (turn: AgentTurnOptions) => {\n const appExecutor = createAppToolRuntimeExecutor({\n handlers: opts.handlers,\n taxonomy: opts.taxonomy,\n ctx: turn.ctx,\n onProduced: turn.onProduced,\n })\n return async (call: LoopToolCall): Promise<AppToolOutcome> => {\n if (isAppToolName(call.toolName)) return appExecutor({ toolName: call.toolName, args: call.args })\n if (opts.executeOtherTool && opts.isOtherExecutableTool?.(call.toolName)) {\n return opts.executeOtherTool(call, turn.ctx)\n }\n return { ok: false, code: 'unknown_tool', message: `No executor for tool: ${call.toolName}` }\n }\n }\n\n return {\n run(userMessage, turn) {\n return runAppToolLoop({\n systemPrompt: turn.systemPrompt ?? opts.systemPrompt,\n userMessage,\n priorMessages: turn.priorMessages,\n streamTurn,\n executeToolCall: buildExecutor(turn),\n isExecutableTool,\n maxToolTurns: opts.maxToolTurns,\n })\n },\n stream(userMessage, turn) {\n return streamAppToolLoop<LoopEvent>({\n systemPrompt: turn.systemPrompt ?? opts.systemPrompt,\n userMessage,\n priorMessages: turn.priorMessages,\n streamTurn,\n extractText: (ev) => (ev.type === 'text' ? ev.text : ''),\n extractToolCall: (ev) => (ev.type === 'tool_call' ? ev.call : null),\n isExecutableTool,\n executeToolCall: buildExecutor(turn),\n maxToolTurns: opts.maxToolTurns,\n })\n },\n }\n}\n","export * from './model'\nexport * from './openai-stream'\nexport * from './agent'\n/**\n * The bounded agent tool-loop — the mechanism every app's chat runtime\n * hand-rolls on top of `@tangle-network/agent-runtime`.\n *\n * A model turn may emit tool calls (integration-hub actions, the app tools from\n * `../tools`, delegation). The loop: stream a turn, collect the executable tool\n * calls, stop if there are none / no executor / the turn cap is hit, otherwise\n * execute each, fold the results back as a message, and re-run so the model\n * reads them. Bounded by `maxToolTurns` so a model looping on a failing action\n * can't run forever.\n *\n * Substrate-free by design: the app supplies `streamTurn` (wrapping whatever\n * backend / `runAgentTaskStream` it uses) and `executeToolCall` (routing to its\n * integration + app-tool executors). This package owns the LOOP; the app owns\n * the model and the executors.\n *\n * LAYERING NOTE: this turn-level tool-dispatch loop is a generic RUNTIME\n * capability. It has been CONTRIBUTED DOWN and MERGED into\n * `@tangle-network/agent-runtime` as `runToolLoop` / `streamToolLoop` (PR #137),\n * but is not yet PUBLISHED (agent-runtime main is ahead of its last npm release;\n * cutting that release is the agent-runtime maintainer's call). TERMINAL STATE:\n * the moment agent-runtime publishes a version carrying #137, bump the\n * `@tangle-network/agent-runtime` peer-dep here and replace the bodies below with\n * a thin re-export — `streamAppToolLoop = streamToolLoop`, `runAppToolLoop =\n * runToolLoop` (types alias 1:1; `AppToolOutcome` ≡ `ToolCallOutcome`). Kept\n * substrate-free + shipping until then so consumers aren't blocked on the release.\n */\nimport type { AppToolOutcome } from '../tools/types'\n\nexport interface LoopToolCall {\n toolCallId?: string\n toolName: string\n args: Record<string, unknown>\n}\n\n/** Events a turn stream yields. `text` accumulates into the final answer;\n * `tool_call` is collected for dispatch. Extra event types pass through\n * untouched (the caller re-emits them to its own UI stream). */\nexport type LoopEvent =\n | { type: 'text'; text: string }\n | { type: 'tool_call'; call: LoopToolCall }\n | { type: 'other'; event: unknown }\n\nexport interface ToolLoopResult {\n /** The model's final text across the loop. */\n finalText: string\n /** Every tool call executed, with its outcome, in order. */\n toolResults: Array<{ call: LoopToolCall; label: string; outcome: AppToolOutcome }>\n /** Number of model turns run (1 + tool-driven re-runs). */\n turns: number\n /** True when the loop stopped because it hit `maxToolTurns` with calls still pending. */\n cappedOut: boolean\n}\n\nexport interface AppToolLoopOptions {\n systemPrompt: string\n userMessage: string\n priorMessages?: Array<{ role: string; content: string }>\n /** Stream one model turn over the running message list. The app wraps its\n * backend here. */\n streamTurn: (messages: Array<{ role: string; content: string }>) => AsyncIterable<LoopEvent>\n /** Execute one tool call. The app routes to its integration executor / app-tool\n * executor and returns the outcome. */\n executeToolCall: (call: LoopToolCall) => Promise<AppToolOutcome>\n /** Which emitted tool names are executable (others are ignored — e.g. a UI-only\n * tool the app renders but doesn't run here). */\n isExecutableTool: (toolName: string) => boolean\n /** Max tool-driven re-runs. Default 8. */\n maxToolTurns?: number\n /** Render one tool outcome as a line the next turn's message carries. Default\n * is a compact `- <label> → ok/failed: …`. */\n renderResult?: (label: string, outcome: AppToolOutcome) => string\n /** Map a tool call to the label its result is keyed under (default: toolName). */\n labelFor?: (call: LoopToolCall) => string\n}\n\nconst DEFAULT_MAX_TOOL_TURNS = 8\n\nfunction defaultRender(label: string, outcome: AppToolOutcome): string {\n if (outcome.ok) return `- ${label} → ok: ${JSON.stringify(outcome.result)}`\n return `- ${label} → failed (${outcome.code}): ${outcome.message}`\n}\n\n/**\n * Run the bounded tool loop and return the final text + every executed tool\n * outcome. Yields nothing — it's an awaitable driver; callers that need to\n * re-emit events to a UI stream should do so inside `streamTurn`. (A streaming\n * variant can wrap this later; keeping the core awaitable makes it trivially\n * testable.)\n */\nexport async function runAppToolLoop(opts: AppToolLoopOptions): Promise<ToolLoopResult> {\n const maxTurns = opts.maxToolTurns ?? DEFAULT_MAX_TOOL_TURNS\n const render = opts.renderResult ?? defaultRender\n const labelFor = opts.labelFor ?? ((c: LoopToolCall) => c.toolName)\n\n const messages: Array<{ role: string; content: string }> = [\n { role: 'system', content: opts.systemPrompt },\n ...(opts.priorMessages ?? []),\n { role: 'user', content: opts.userMessage },\n ]\n\n const toolResults: ToolLoopResult['toolResults'] = []\n let finalText = ''\n let turns = 0\n\n for (let toolTurn = 0; ; toolTurn++) {\n turns++\n let turnText = ''\n const pending: LoopToolCall[] = []\n\n for await (const ev of opts.streamTurn([...messages])) {\n if (ev.type === 'text') {\n turnText += ev.text\n finalText += ev.text\n } else if (ev.type === 'tool_call' && opts.isExecutableTool(ev.call.toolName)) {\n pending.push(ev.call)\n }\n }\n\n if (pending.length === 0) break\n if (toolTurn >= maxTurns) {\n return { finalText, toolResults, turns, cappedOut: true }\n }\n\n // Record the assistant's tool-calling turn so the next turn has its context.\n if (turnText.trim()) messages.push({ role: 'assistant', content: turnText })\n\n const lines: string[] = []\n for (const call of pending) {\n let outcome: AppToolOutcome\n try {\n outcome = await opts.executeToolCall(call)\n } catch (err) {\n outcome = { ok: false, code: 'executor_error', message: err instanceof Error ? err.message : String(err) }\n }\n const label = labelFor(call)\n toolResults.push({ call, label, outcome })\n lines.push(render(label, outcome))\n }\n // Fold every outcome back as one user-role message so the model reads them.\n messages.push({ role: 'user', content: `Tool results:\\n${lines.join('\\n')}` })\n }\n\n return { finalText, toolResults, turns, cappedOut: false }\n}\n\n// ── Streaming variant ──────────────────────────────────────────────────────\n//\n// `runAppToolLoop` is awaitable — perfect for tests and drain-only callers. A\n// real chat runtime instead needs to STREAM each model event to the client (SSE)\n// AND record telemetry per event as it happens. `streamAppToolLoop` is the same\n// bounded loop as an async generator: it yields every raw turn event (the app\n// maps + telemetries + re-emits it) and every executed tool result (same), while\n// owning the loop control flow (collect → stop/dispatch → fold → re-run, capped).\n// `Raw` is the app's own runtime-event type — this package stays substrate-free.\n\nexport type StreamLoopYield<Raw> =\n | { kind: 'event'; event: Raw }\n | { kind: 'tool_result'; toolName: string; toolCallId?: string; label: string; outcome: AppToolOutcome }\n | { kind: 'capped'; pending: number }\n\nexport interface StreamAppToolLoopOptions<Raw> {\n systemPrompt: string\n userMessage: string\n priorMessages?: Array<{ role: string; content: string }>\n /** Stream one model turn (the app wraps its backend / runAgentTaskStream). */\n streamTurn: (messages: Array<{ role: string; content: string }>) => AsyncIterable<Raw>\n /** Text contribution of a raw event, '' if none — used to record the\n * assistant's turn so the next turn has its context. */\n extractText: (event: Raw) => string\n /** The tool call a raw event represents, or null. */\n extractToolCall: (event: Raw) => LoopToolCall | null\n /** Which tool names are executable here (others pass through, unexecuted). */\n isExecutableTool: (toolName: string) => boolean\n /** Execute one call — the app routes to its integration / app-tool executor. */\n executeToolCall: (call: LoopToolCall) => Promise<AppToolOutcome>\n maxToolTurns?: number\n renderResult?: (label: string, outcome: AppToolOutcome) => string\n labelFor?: (call: LoopToolCall) => string\n}\n\n/**\n * The streaming bounded tool loop. Yields `event` for each raw turn event and\n * `tool_result` for each executed tool; emits a single `capped` when it stops at\n * the turn limit with calls still pending. The app drives telemetry + UI\n * emission off the yielded items.\n */\nexport async function* streamAppToolLoop<Raw>(opts: StreamAppToolLoopOptions<Raw>): AsyncGenerator<StreamLoopYield<Raw>, void, unknown> {\n const maxTurns = opts.maxToolTurns ?? DEFAULT_MAX_TOOL_TURNS\n const render = opts.renderResult ?? defaultRender\n const labelFor = opts.labelFor ?? ((c: LoopToolCall) => c.toolName)\n\n const messages: Array<{ role: string; content: string }> = [\n { role: 'system', content: opts.systemPrompt },\n ...(opts.priorMessages ?? []),\n { role: 'user', content: opts.userMessage },\n ]\n\n for (let toolTurn = 0; ; toolTurn++) {\n let turnText = ''\n const pending: LoopToolCall[] = []\n\n for await (const event of opts.streamTurn([...messages])) {\n yield { kind: 'event', event }\n turnText += opts.extractText(event)\n const call = opts.extractToolCall(event)\n if (call && opts.isExecutableTool(call.toolName)) pending.push(call)\n }\n\n if (pending.length === 0) return\n if (toolTurn >= maxTurns) {\n yield { kind: 'capped', pending: pending.length }\n return\n }\n\n if (turnText.trim()) messages.push({ role: 'assistant', content: turnText })\n\n const lines: string[] = []\n for (const call of pending) {\n let outcome: AppToolOutcome\n try {\n outcome = await opts.executeToolCall(call)\n } catch (err) {\n outcome = { ok: false, code: 'executor_error', message: err instanceof Error ? err.message : String(err) }\n }\n const label = labelFor(call)\n yield { kind: 'tool_result', toolName: call.toolName, toolCallId: call.toolCallId, label, outcome }\n lines.push(render(label, outcome))\n }\n messages.push({ role: 'user', content: `Tool results:\\n${lines.join('\\n')}` })\n }\n}\n"],"mappings":"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","names":["apiKey"]}
@@ -1,6 +1,6 @@
1
1
  import { KnowledgeRequirementSpec } from '../knowledge/index.js';
2
2
  export { SatisfiedByRule } from '../knowledge/index.js';
3
- import { T as TangleModelConfig } from '../model-BOP69mVu.js';
3
+ import { j as TangleModelConfig } from '../model-CKzniMMr.js';
4
4
  import '@tangle-network/agent-eval';
5
5
 
6
6
  /**
package/dist/index.d.ts CHANGED
@@ -16,5 +16,5 @@ export { HubExecClient, HubExecClientOptions, HubExecErrorCode, HubExecResult, H
16
16
  export { JsonObject, KvLike, RateLimitResult, RequestContext, SecurityHeaderOptions, addSecurityHeaders, checkRateLimit, extractRequestContext, parseJsonObjectBody, requireString } from './web/index.js';
17
17
  export { BuildRedactedDocumentOptions, DEFAULT_REDACTION_PATTERNS, RedactForIngestionOptions, RedactedDocSegment, RedactedDocument, RedactionPattern, RedactionSpan, RevealResult, RevealSpanOptions, buildRedactedDocument, detectSpans, maskSpans, redactForIngestion, revealSpan } from './redact/index.js';
18
18
  export { CompletionRequirement, CompletionVerdict, CorrectnessChecker, ProducedState, RuntimeEventLike, SatisfiedBy, TaskGold, createLlmCorrectnessChecker, extractProducedState, verifyCompletion, weightedComposite } from '@tangle-network/agent-eval';
19
- export { D as DEFAULT_TANGLE_ROUTER_BASE_URL, R as ResolveModelOptions, T as TangleModelConfig, r as resolveTangleModelConfig } from './model-BOP69mVu.js';
19
+ export { C as CreateTangleRouterModelConfigOptions, D as DEFAULT_TANGLE_BILLING_ENFORCEMENT_ENV_VAR, a as DEFAULT_TANGLE_ROUTER_BASE_URL, R as ResolveModelOptions, b as ResolveUserTangleExecutionKeyForUserOptions, c as ResolveUserTangleExecutionKeyOptions, d as ResolvedTangleExecutionKey, T as TangleBillingEnforcementOptions, e as TangleExecutionEnvironment, f as TangleExecutionKeyError, g as TangleExecutionKeyErrorCode, h as TangleExecutionKeyHttpError, i as TangleExecutionKeySource, j as TangleModelConfig, k as createTangleRouterModelConfig, l as isTangleBillingEnforcementDisabled, m as isTangleExecutionKeyError, r as resolveTangleExecutionEnvironment, n as resolveTangleModelConfig, o as resolveUserTangleExecutionKey, p as resolveUserTangleExecutionKeyForUser, t as tangleExecutionKeyHttpError } from './model-CKzniMMr.js';
20
20
  import '@tangle-network/agent-knowledge';
package/dist/index.js CHANGED
@@ -99,14 +99,23 @@ import {
99
99
  createBrokerTokenProvider
100
100
  } from "./chunk-YGUNTIT5.js";
101
101
  import {
102
+ DEFAULT_TANGLE_BILLING_ENFORCEMENT_ENV_VAR,
102
103
  DEFAULT_TANGLE_ROUTER_BASE_URL,
104
+ TangleExecutionKeyError,
103
105
  createAgentRuntime,
104
106
  createOpenAICompatStreamTurn,
107
+ createTangleRouterModelConfig,
108
+ isTangleBillingEnforcementDisabled,
109
+ isTangleExecutionKeyError,
110
+ resolveTangleExecutionEnvironment,
105
111
  resolveTangleModelConfig,
112
+ resolveUserTangleExecutionKey,
113
+ resolveUserTangleExecutionKeyForUser,
106
114
  runAppToolLoop,
107
115
  streamAppToolLoop,
116
+ tangleExecutionKeyHttpError,
108
117
  toLoopEvents
109
- } from "./chunk-3LP6PEWS.js";
118
+ } from "./chunk-SVCJYRVM.js";
110
119
  import {
111
120
  APP_TOOL_NAMES,
112
121
  ToolInputError,
@@ -134,6 +143,7 @@ export {
134
143
  DEFAULT_HARNESS,
135
144
  DEFAULT_HEADER_NAMES,
136
145
  DEFAULT_REDACTION_PATTERNS,
146
+ DEFAULT_TANGLE_BILLING_ENFORCEMENT_ENV_VAR,
137
147
  DEFAULT_TANGLE_ROUTER_BASE_URL,
138
148
  DELEGATION_MCP_SERVER_KEY,
139
149
  DELEGATION_TOOLS,
@@ -141,6 +151,7 @@ export {
141
151
  KNOWN_HARNESSES,
142
152
  PRESET_MIGRATION_SQL,
143
153
  PRESET_TABLES,
154
+ TangleExecutionKeyError,
144
155
  ToolInputError,
145
156
  addSecurityHeaders,
146
157
  agentAppConfigJsonSchema,
@@ -173,6 +184,7 @@ export {
173
184
  createPresetWorkspaceKeyManager,
174
185
  createPresetWorkspaceKeyStore,
175
186
  createReviewerDecider,
187
+ createTangleRouterModelConfig,
176
188
  createTcloudKeyProvisioner,
177
189
  createTokenRecallChecker,
178
190
  createWorkspaceKeyManager,
@@ -198,6 +210,8 @@ export {
198
210
  invokeIntegrationHub,
199
211
  isAppToolName,
200
212
  isHarness,
213
+ isTangleBillingEnforcementDisabled,
214
+ isTangleExecutionKeyError,
201
215
  maskSpans,
202
216
  mergePersistedPart,
203
217
  messageHasTurnId,
@@ -214,13 +228,17 @@ export {
214
228
  resolveChatTurn,
215
229
  resolveIntegrationAction,
216
230
  resolveSessionHarness,
231
+ resolveTangleExecutionEnvironment,
217
232
  resolveTangleModelConfig,
218
233
  resolveToolId,
219
234
  resolveToolName,
235
+ resolveUserTangleExecutionKey,
236
+ resolveUserTangleExecutionKeyForUser,
220
237
  revealSpan,
221
238
  reviewCandidate,
222
239
  runAppToolLoop,
223
240
  streamAppToolLoop,
241
+ tangleExecutionKeyHttpError,
224
242
  toLoopEvents,
225
243
  verifyCapabilityToken,
226
244
  verifyCompletion,
@@ -2,7 +2,7 @@ import { KnowledgeResearchLoopContext, AddSourceTextInput, SourceAdapter, RunKno
2
2
  import { KnowledgeSourceSpec, AgentKnowledgeConfig } from '../config/index.js';
3
3
  import '../knowledge/index.js';
4
4
  import '@tangle-network/agent-eval';
5
- import '../model-BOP69mVu.js';
5
+ import '../model-CKzniMMr.js';
6
6
 
7
7
  /**
8
8
  * `@tangle-network/agent-app/knowledge-loop` — wire the declarative
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Resolve the model config a Tangle agent's sandbox/runtime runs on.
3
+ *
4
+ * Every Tangle agent product resolves the SAME thing from env: the Tangle Router
5
+ * (OpenAI-compatible, metered at the platform markup against a single
6
+ * `TANGLE_API_KEY`) by default, with a direct-Anthropic BYOK escape hatch. The
7
+ * shape feeds the sandbox SDK's `backend.model`. Lifted here so no product
8
+ * hand-rolls the env parsing + the router default.
9
+ */
10
+ interface TangleModelConfig {
11
+ /** The Tangle Router is OpenAI-compatible → driven via `openai-compat`.
12
+ * `anthropic` is the BYOK escape hatch. */
13
+ provider: 'openai-compat' | 'anthropic';
14
+ model: string;
15
+ apiKey: string;
16
+ baseUrl: string;
17
+ }
18
+ type TangleExecutionEnvironment = 'development' | 'staging' | 'production' | 'test';
19
+ type TangleExecutionKeySource = 'local-env' | 'user';
20
+ type TangleExecutionKeyErrorCode = 'local_tangle_api_key_required' | 'tangle_account_not_connected';
21
+ interface ResolveModelOptions {
22
+ /** Env to read (defaults to process.env). */
23
+ env?: Record<string, string | undefined>;
24
+ /** Router base URL default when `TANGLE_ROUTER_BASE_URL` is unset. */
25
+ defaultRouterBaseUrl?: string;
26
+ }
27
+ interface ResolveUserTangleExecutionKeyOptions {
28
+ /** Deployment context. Only local development may fall back to env keys. */
29
+ environment?: TangleExecutionEnvironment;
30
+ /** Env to read for the local-development fallback. */
31
+ env?: Record<string, string | undefined>;
32
+ /** App-owned lookup for the caller's linked platform API key. */
33
+ getUserApiKey: () => string | null | undefined | Promise<string | null | undefined>;
34
+ }
35
+ interface ResolveUserTangleExecutionKeyForUserOptions<UserId = string> {
36
+ userId: UserId;
37
+ environment?: TangleExecutionEnvironment;
38
+ env?: Record<string, string | undefined>;
39
+ getUserApiKey: (userId: UserId) => string | null | undefined | Promise<string | null | undefined>;
40
+ }
41
+ interface ResolvedTangleExecutionKey {
42
+ apiKey: string;
43
+ source: TangleExecutionKeySource;
44
+ }
45
+ interface TangleExecutionKeyHttpError {
46
+ status: number;
47
+ body: {
48
+ error: string;
49
+ code: TangleExecutionKeyErrorCode;
50
+ };
51
+ }
52
+ interface CreateTangleRouterModelConfigOptions {
53
+ apiKey: string;
54
+ model: string;
55
+ baseUrl?: string;
56
+ }
57
+ interface TangleBillingEnforcementOptions {
58
+ /** Env to read (defaults to process.env). */
59
+ env?: Record<string, string | undefined>;
60
+ /**
61
+ * Optional app-specific override flag, e.g. `GTM_BILLING_ENFORCEMENT`.
62
+ * Defaults to the shared `TANGLE_BILLING_ENFORCEMENT`.
63
+ */
64
+ enforcementEnvVar?: string;
65
+ }
66
+ declare const DEFAULT_TANGLE_ROUTER_BASE_URL = "https://router.tangle.tools/v1";
67
+ declare const DEFAULT_TANGLE_BILLING_ENFORCEMENT_ENV_VAR = "TANGLE_BILLING_ENFORCEMENT";
68
+ declare class TangleExecutionKeyError extends Error {
69
+ readonly code: TangleExecutionKeyErrorCode;
70
+ readonly status: number;
71
+ constructor(code: TangleExecutionKeyErrorCode, message: string, status: number);
72
+ }
73
+ declare function isTangleExecutionKeyError(error: unknown): error is TangleExecutionKeyError;
74
+ declare function resolveTangleExecutionEnvironment(env?: Record<string, string | undefined>): TangleExecutionEnvironment;
75
+ /**
76
+ * Shared policy for agent products that bill through the Tangle Platform.
77
+ *
78
+ * Local development defaults billing enforcement off so apps can use a local
79
+ * `TANGLE_API_KEY` without requiring a browser-linked platform account. Any
80
+ * non-development environment defaults enforcement on. Apps may pass their own
81
+ * override flag (`FOO_BILLING_ENFORCEMENT`) while new apps can use the shared
82
+ * `TANGLE_BILLING_ENFORCEMENT`.
83
+ */
84
+ declare function isTangleBillingEnforcementDisabled(opts?: TangleBillingEnforcementOptions): boolean;
85
+ declare function tangleExecutionKeyHttpError(error: unknown): TangleExecutionKeyHttpError | null;
86
+ /**
87
+ * Resolve the user-facing Tangle API key for model execution.
88
+ *
89
+ * Local development may use a server env key so apps remain easy to run.
90
+ * Deployed contexts must use the caller's linked platform key; this keeps
91
+ * model execution, billing, and account ownership aligned across products.
92
+ */
93
+ declare function resolveUserTangleExecutionKey(opts: ResolveUserTangleExecutionKeyOptions): Promise<ResolvedTangleExecutionKey>;
94
+ declare function resolveUserTangleExecutionKeyForUser<UserId = string>(opts: ResolveUserTangleExecutionKeyForUserOptions<UserId>): Promise<ResolvedTangleExecutionKey>;
95
+ /**
96
+ * Build an OpenAI-compatible Tangle Router model config from an already
97
+ * resolved execution key. This intentionally does not read TANGLE_API_KEY.
98
+ */
99
+ declare function createTangleRouterModelConfig(opts: CreateTangleRouterModelConfigOptions): TangleModelConfig;
100
+ /**
101
+ * Resolve the model config from env. DEFAULT path (`MODEL_PROVIDER` unset or
102
+ * `openai-compat`/`tangle-router`/`tcloud`): the Tangle Router, authenticated
103
+ * with `TANGLE_API_KEY`, model from `MODEL_NAME`. BYOK path
104
+ * (`MODEL_PROVIDER=anthropic`): direct Anthropic with `ANTHROPIC_API_KEY` +
105
+ * `ANTHROPIC_BASE_URL`. Throws (fail-loud) on a missing required var so a
106
+ * misconfigured deploy fails at boot, not mid-turn.
107
+ */
108
+ declare function resolveTangleModelConfig(opts?: ResolveModelOptions): TangleModelConfig;
109
+
110
+ export { type CreateTangleRouterModelConfigOptions as C, DEFAULT_TANGLE_BILLING_ENFORCEMENT_ENV_VAR as D, type ResolveModelOptions as R, type TangleBillingEnforcementOptions as T, DEFAULT_TANGLE_ROUTER_BASE_URL as a, type ResolveUserTangleExecutionKeyForUserOptions as b, type ResolveUserTangleExecutionKeyOptions as c, type ResolvedTangleExecutionKey as d, type TangleExecutionEnvironment as e, TangleExecutionKeyError as f, type TangleExecutionKeyErrorCode as g, type TangleExecutionKeyHttpError as h, type TangleExecutionKeySource as i, type TangleModelConfig as j, createTangleRouterModelConfig as k, isTangleBillingEnforcementDisabled as l, isTangleExecutionKeyError as m, resolveTangleModelConfig as n, resolveUserTangleExecutionKey as o, resolveUserTangleExecutionKeyForUser as p, resolveTangleExecutionEnvironment as r, tangleExecutionKeyHttpError as t };
@@ -1,4 +1,4 @@
1
- export { D as DEFAULT_TANGLE_ROUTER_BASE_URL, R as ResolveModelOptions, T as TangleModelConfig, r as resolveTangleModelConfig } from '../model-BOP69mVu.js';
1
+ export { C as CreateTangleRouterModelConfigOptions, D as DEFAULT_TANGLE_BILLING_ENFORCEMENT_ENV_VAR, a as DEFAULT_TANGLE_ROUTER_BASE_URL, R as ResolveModelOptions, b as ResolveUserTangleExecutionKeyForUserOptions, c as ResolveUserTangleExecutionKeyOptions, d as ResolvedTangleExecutionKey, T as TangleBillingEnforcementOptions, e as TangleExecutionEnvironment, f as TangleExecutionKeyError, g as TangleExecutionKeyErrorCode, h as TangleExecutionKeyHttpError, i as TangleExecutionKeySource, j as TangleModelConfig, k as createTangleRouterModelConfig, l as isTangleBillingEnforcementDisabled, m as isTangleExecutionKeyError, r as resolveTangleExecutionEnvironment, n as resolveTangleModelConfig, o as resolveUserTangleExecutionKey, p as resolveUserTangleExecutionKeyForUser, t as tangleExecutionKeyHttpError } from '../model-CKzniMMr.js';
2
2
  import { b as AppToolContext, e as AppToolProducedEvent, f as AppToolTaxonomy, c as AppToolHandlers, d as AppToolOutcome } from '../types-CeWor4bQ.js';
3
3
 
4
4
  /**
@@ -1,20 +1,38 @@
1
1
  import {
2
+ DEFAULT_TANGLE_BILLING_ENFORCEMENT_ENV_VAR,
2
3
  DEFAULT_TANGLE_ROUTER_BASE_URL,
4
+ TangleExecutionKeyError,
3
5
  createAgentRuntime,
4
6
  createOpenAICompatStreamTurn,
7
+ createTangleRouterModelConfig,
8
+ isTangleBillingEnforcementDisabled,
9
+ isTangleExecutionKeyError,
10
+ resolveTangleExecutionEnvironment,
5
11
  resolveTangleModelConfig,
12
+ resolveUserTangleExecutionKey,
13
+ resolveUserTangleExecutionKeyForUser,
6
14
  runAppToolLoop,
7
15
  streamAppToolLoop,
16
+ tangleExecutionKeyHttpError,
8
17
  toLoopEvents
9
- } from "../chunk-3LP6PEWS.js";
18
+ } from "../chunk-SVCJYRVM.js";
10
19
  import "../chunk-LT2YIMEB.js";
11
20
  export {
21
+ DEFAULT_TANGLE_BILLING_ENFORCEMENT_ENV_VAR,
12
22
  DEFAULT_TANGLE_ROUTER_BASE_URL,
23
+ TangleExecutionKeyError,
13
24
  createAgentRuntime,
14
25
  createOpenAICompatStreamTurn,
26
+ createTangleRouterModelConfig,
27
+ isTangleBillingEnforcementDisabled,
28
+ isTangleExecutionKeyError,
29
+ resolveTangleExecutionEnvironment,
15
30
  resolveTangleModelConfig,
31
+ resolveUserTangleExecutionKey,
32
+ resolveUserTangleExecutionKeyForUser,
16
33
  runAppToolLoop,
17
34
  streamAppToolLoop,
35
+ tangleExecutionKeyHttpError,
18
36
  toLoopEvents
19
37
  };
20
38
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-app",
3
- "version": "0.3.0",
4
- "description": "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.",
3
+ "version": "0.3.1",
4
+ "packageManager": "pnpm@10.33.4",
5
+ "description": "Application-shell framework for Tangle agent products: a bounded tool loop, the structured agent\u2192app tool side channel, integration-hub client, per-workspace billing, and crypto \u2014 composed over the Tangle agent substrate through typed seams.",
5
6
  "keywords": [
6
7
  "tangle",
7
8
  "ai-agent",
@@ -127,6 +128,14 @@
127
128
  "default": "./dist/store/index.js"
128
129
  }
129
130
  },
131
+ "scripts": {
132
+ "build": "tsup",
133
+ "dev": "tsup --watch",
134
+ "prepare": "tsup",
135
+ "test": "vitest run",
136
+ "test:watch": "vitest",
137
+ "typecheck": "tsc --noEmit"
138
+ },
130
139
  "devDependencies": {
131
140
  "@tangle-network/agent-eval": "^0.82.0",
132
141
  "@tangle-network/agent-integrations": "^0.32.0",
@@ -149,12 +158,5 @@
149
158
  "@tangle-network/agent-runtime": {
150
159
  "optional": true
151
160
  }
152
- },
153
- "scripts": {
154
- "build": "tsup",
155
- "dev": "tsup --watch",
156
- "test": "vitest run",
157
- "test:watch": "vitest",
158
- "typecheck": "tsc --noEmit"
159
161
  }
160
- }
162
+ }
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/runtime/model.ts","../src/runtime/openai-stream.ts","../src/runtime/agent.ts","../src/runtime/index.ts"],"sourcesContent":["/**\n * Resolve the model config a Tangle agent's sandbox/runtime runs on.\n *\n * Every Tangle agent product resolves the SAME thing from env: the Tangle Router\n * (OpenAI-compatible, metered at the platform markup against a single\n * `TANGLE_API_KEY`) by default, with a direct-Anthropic BYOK escape hatch. The\n * shape feeds the sandbox SDK's `backend.model`. Lifted here so no product\n * hand-rolls the env parsing + the router default.\n */\n\nexport interface TangleModelConfig {\n /** The Tangle Router is OpenAI-compatible → driven via `openai-compat`.\n * `anthropic` is the BYOK escape hatch. */\n provider: 'openai-compat' | 'anthropic'\n model: string\n apiKey: string\n baseUrl: string\n}\n\nexport interface ResolveModelOptions {\n /** Env to read (defaults to process.env). */\n env?: Record<string, string | undefined>\n /** Router base URL default when `TANGLE_ROUTER_BASE_URL` is unset. */\n defaultRouterBaseUrl?: string\n}\n\nexport const DEFAULT_TANGLE_ROUTER_BASE_URL = 'https://router.tangle.tools/v1'\n\nfunction requireEnv(env: Record<string, string | undefined>, name: string): string {\n const value = env[name]?.trim()\n if (!value) throw new Error(`${name} is required`)\n return value\n}\n\n/**\n * Resolve the model config from env. DEFAULT path (`MODEL_PROVIDER` unset or\n * `openai-compat`/`tangle-router`/`tcloud`): the Tangle Router, authenticated\n * with `TANGLE_API_KEY`, model from `MODEL_NAME`. BYOK path\n * (`MODEL_PROVIDER=anthropic`): direct Anthropic with `ANTHROPIC_API_KEY` +\n * `ANTHROPIC_BASE_URL`. Throws (fail-loud) on a missing required var so a\n * misconfigured deploy fails at boot, not mid-turn.\n */\nexport function resolveTangleModelConfig(opts: ResolveModelOptions = {}): TangleModelConfig {\n const env = opts.env ?? (process.env as Record<string, string | undefined>)\n const provider = env.MODEL_PROVIDER?.trim() || 'openai-compat'\n const model = requireEnv(env, 'MODEL_NAME')\n\n if (provider === 'openai-compat' || provider === 'tangle-router' || provider === 'tcloud') {\n return {\n provider: 'openai-compat',\n model,\n apiKey: requireEnv(env, 'TANGLE_API_KEY'),\n baseUrl: (env.TANGLE_ROUTER_BASE_URL?.trim() || opts.defaultRouterBaseUrl || DEFAULT_TANGLE_ROUTER_BASE_URL).replace(/\\/+$/, ''),\n }\n }\n\n if (provider === 'anthropic') {\n return {\n provider,\n model,\n apiKey: requireEnv(env, 'ANTHROPIC_API_KEY'),\n baseUrl: requireEnv(env, 'ANTHROPIC_BASE_URL'),\n }\n }\n\n throw new Error(`Unsupported MODEL_PROVIDER: ${provider} (use openai-compat for the Tangle Router, or anthropic for BYOK)`)\n}\n","/**\n * OpenAI-compatible stream → `LoopEvent` adapter, for NON-sandbox copilots.\n *\n * `streamAppToolLoop` takes a `streamTurn` seam that yields `LoopEvent`s. A\n * sandboxed agent produces those from its container; a browser/edge copilot\n * instead calls a model directly. The Tangle Router, the tcloud SDK, and most\n * providers all speak the OpenAI Chat Completions streaming shape — so the ONE\n * reusable piece is assembling that stream (content deltas + FRAGMENTED\n * tool-call deltas) into `LoopEvent`s. That assembly is the boilerplate every\n * copilot would re-write (and get wrong — OpenAI streams tool-call arguments in\n * pieces across chunks).\n *\n * This does NOT implement an HTTP client beyond a minimal `fetch` + SSE reader\n * (browser/edge/Node-safe, zero deps). For richer transport use the tcloud SDK\n * or the Vercel AI SDK and pipe their stream through {@link toLoopEvents}.\n */\nimport type { LoopEvent, LoopToolCall } from './index'\n\n/** Minimal OpenAI Chat Completions streaming chunk (structural — no `openai` dep). */\nexport interface OpenAIStreamChunk {\n choices?: Array<{\n delta?: {\n content?: string | null\n tool_calls?: Array<{\n index: number\n id?: string\n function?: { name?: string; arguments?: string }\n }>\n }\n finish_reason?: string | null\n }>\n}\n\ninterface PartialToolCall {\n id?: string\n name: string\n args: string\n}\n\n/**\n * Map an OpenAI-compat streaming chunk iterator to `LoopEvent`s: each content\n * delta → a `text` event; tool-call deltas are accumulated by index across\n * chunks and emitted as one complete `tool_call` event when the stream finishes\n * (arguments JSON-parsed; an empty/garbled args string yields `{}` rather than\n * throwing). Works for the Tangle Router, tcloud, or any OpenAI-compat source.\n */\nexport async function* toLoopEvents(chunks: AsyncIterable<OpenAIStreamChunk>): AsyncIterable<LoopEvent> {\n const calls = new Map<number, PartialToolCall>()\n for await (const chunk of chunks) {\n const choice = chunk.choices?.[0]\n if (!choice) continue\n const content = choice.delta?.content\n if (content) yield { type: 'text', text: content }\n for (const tc of choice.delta?.tool_calls ?? []) {\n const cur = calls.get(tc.index) ?? { name: '', args: '' }\n if (tc.id) cur.id = tc.id\n if (tc.function?.name) cur.name += tc.function.name\n if (tc.function?.arguments) cur.args += tc.function.arguments\n calls.set(tc.index, cur)\n }\n }\n for (const [, c] of [...calls.entries()].sort((a, b) => a[0] - b[0])) {\n if (!c.name) continue\n yield { type: 'tool_call', call: { toolCallId: c.id, toolName: c.name, args: safeParse(c.args) } satisfies LoopToolCall }\n }\n}\n\nfunction safeParse(s: string): Record<string, unknown> {\n if (!s.trim()) return {}\n try {\n const v = JSON.parse(s)\n return v && typeof v === 'object' && !Array.isArray(v) ? (v as Record<string, unknown>) : {}\n } catch {\n return {}\n }\n}\n\nexport interface OpenAICompatStreamTurnOptions {\n /** OpenAI-compat base URL (e.g. the Tangle Router `https://router.tangle.tools/v1`). */\n baseUrl: string\n apiKey: string\n model: string\n /** OpenAI tool definitions — pass `buildAppToolOpenAITools(taxonomy)` so the\n * model can call the app tools. Omit for a tool-free copilot. */\n tools?: unknown[]\n temperature?: number\n fetchImpl?: typeof fetch\n /** Extra body fields (e.g. `max_tokens`). */\n extraBody?: Record<string, unknown>\n}\n\n/**\n * Build a `streamTurn` that calls an OpenAI-compatible `/chat/completions`\n * endpoint (Tangle Router / tcloud / any compat provider) with `stream: true`\n * and yields `LoopEvent`s via {@link toLoopEvents}. Browser/edge/Node-safe —\n * just `fetch` + an SSE reader. Drop straight into `streamAppToolLoop`:\n *\n * const cfg = resolveTangleModelConfig() // or { baseUrl, apiKey, model }\n * streamAppToolLoop({ streamTurn: createOpenAICompatStreamTurn({ ...cfg, tools }), executeToolCall, ... })\n */\nexport function createOpenAICompatStreamTurn(\n opts: OpenAICompatStreamTurnOptions,\n): (messages: Array<{ role: string; content: string }>) => AsyncIterable<LoopEvent> {\n const base = opts.baseUrl.replace(/\\/+$/, '')\n const doFetch = opts.fetchImpl ?? fetch\n return (messages) =>\n toLoopEvents(\n streamChatCompletions(doFetch, `${base}/chat/completions`, opts.apiKey, {\n model: opts.model,\n messages,\n stream: true,\n ...(opts.tools && opts.tools.length > 0 ? { tools: opts.tools } : {}),\n ...(opts.temperature != null ? { temperature: opts.temperature } : {}),\n ...opts.extraBody,\n }),\n )\n}\n\n/** Stream + parse an OpenAI-compat SSE response into chunks. Tolerates `data:`\n * framing, multi-line buffers, and the terminal `[DONE]`. */\nasync function* streamChatCompletions(\n doFetch: typeof fetch,\n url: string,\n apiKey: string,\n body: Record<string, unknown>,\n): AsyncIterable<OpenAIStreamChunk> {\n const res = await doFetch(url, {\n method: 'POST',\n headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', Accept: 'text/event-stream' },\n body: JSON.stringify(body),\n })\n if (!res.ok || !res.body) {\n const text = res.body ? await res.text().catch(() => '') : ''\n throw new Error(`OpenAI-compat stream failed (HTTP ${res.status})${text ? `: ${text.slice(0, 200)}` : ''}`)\n }\n const reader = res.body.getReader()\n const decoder = new TextDecoder()\n let buffer = ''\n for (;;) {\n const { done, value } = await reader.read()\n if (done) break\n buffer += decoder.decode(value, { stream: true })\n const lines = buffer.split('\\n')\n buffer = lines.pop() ?? ''\n for (const line of lines) {\n const trimmed = line.trim()\n if (!trimmed.startsWith('data:')) continue\n const data = trimmed.slice(5).trim()\n if (data === '[DONE]') return\n try {\n yield JSON.parse(data) as OpenAIStreamChunk\n } catch {\n /* skip a partial/garbled SSE frame */\n }\n }\n }\n}\n","/**\n * `createAgentRuntime` — the in-process agent core, assembled.\n *\n * The bricks to run an agent turn WITHOUT a sandbox already exist in this\n * package, but a consumer must hand-wire five of them every time: resolve the\n * model config, build the OpenAI tool schemas from the taxonomy, build a\n * `streamTurn` over the model endpoint, build an `executeToolCall` over the\n * product's handlers, and drive `runAppToolLoop` / `streamAppToolLoop` with an\n * `isExecutableTool` predicate. That boilerplate is identical across every\n * sandbox-free surface (an edge/browser copilot, an eval harness, a Node CLI),\n * and getting it subtly wrong — e.g. NOT advertising the tools, so the model\n * never emits a `tool_call` and no side effect ever fires — is exactly the\n * failure that makes a tool-driven agent score zero off-sandbox.\n *\n * This factory bundles those five into one object configured for ONE agent:\n *\n * const runtime = createAgentRuntime({ model, taxonomy, handlers, systemPrompt })\n * const result = await runtime.run(userMessage, { ctx }) // awaitable\n * for await (const y of runtime.stream(userMessage, { ctx })) {…} // streaming\n *\n * The model is advertised the app tools (so it CAN call them); each call is\n * dispatched against the product's `handlers` (so the side effect is real); the\n * `onProduced` hook fires at the real side-effect site (so an eval/UI credits a\n * persisted proposal or artifact). Substrate-free: no `@tangle-network/sandbox`,\n * no Durable Object, no `@tangle-network/agent-runtime` import. The SAME core\n * the Cloudflare Worker runs, runnable anywhere a `fetch` to an OpenAI-compatible\n * endpoint works.\n *\n * Domain stays out: the proposal taxonomy, the handlers, and the system prompt\n * are all injected — the factory knows nothing about insurance, law, tax, etc.\n */\nimport {\n type AppToolHandlers,\n type AppToolContext,\n type AppToolOutcome,\n type AppToolProducedEvent,\n type AppToolTaxonomy,\n} from '../tools/types'\nimport { buildAppToolOpenAITools, isAppToolName } from '../tools/openai'\nimport { createAppToolRuntimeExecutor } from '../tools/runtime'\nimport {\n runAppToolLoop,\n streamAppToolLoop,\n type LoopEvent,\n type LoopToolCall,\n type StreamLoopYield,\n type ToolLoopResult,\n} from './index'\nimport { createOpenAICompatStreamTurn } from './openai-stream'\n\n/** OpenAI-compatible model endpoint (Tangle Router / tcloud / any compat\n * provider). Build from {@link resolveTangleModelConfig} or pass literals. */\nexport interface AgentRuntimeModelConfig {\n baseUrl: string\n apiKey: string\n model: string\n temperature?: number\n fetchImpl?: typeof fetch\n /** Extra request-body fields (e.g. `max_tokens`, a `reasoning` block). */\n extraBody?: Record<string, unknown>\n}\n\nexport interface CreateAgentRuntimeOptions {\n /** The model endpoint the turns stream from. */\n model: AgentRuntimeModelConfig\n /** The product's proposal taxonomy — advertises `submit_proposal`'s `type`\n * enum to the model and labels the regulated subset on the result. */\n taxonomy: AppToolTaxonomy\n /** Domain handlers persisting each tool to the product's store/vault. */\n handlers: AppToolHandlers\n /** Default agent identity / system prompt. A turn may override it. */\n systemPrompt: string\n /** Max tool-driven re-runs per turn. Default 8. */\n maxToolTurns?: number\n /** Extra OpenAI tool definitions advertised ALONGSIDE the four app tools\n * (e.g. `integration_invoke`). Pair with {@link executeOtherTool}. */\n extraTools?: unknown[]\n /** Execute a tool that is NOT one of the four app tools (e.g. an integration\n * action). Only consulted for names {@link isOtherExecutableTool} accepts. */\n executeOtherTool?: (call: LoopToolCall, ctx: AppToolContext) => Promise<AppToolOutcome>\n /** Which non-app tool names are executable here. Required if {@link executeOtherTool} is set. */\n isOtherExecutableTool?: (toolName: string) => boolean\n}\n\nexport interface AgentTurnOptions {\n /** The trusted per-turn context (who/where the turn runs as). */\n ctx: AppToolContext\n /** Prior conversation turns, in order. */\n priorMessages?: Array<{ role: string; content: string }>\n /** Override the factory's default system prompt for this turn. */\n systemPrompt?: string\n /** Fires at the real side-effect site for each produced proposal/artifact. */\n onProduced?: (event: AppToolProducedEvent) => void\n}\n\nexport interface AgentRuntime {\n /** Run the bounded tool loop to completion; resolve with final text + every\n * executed tool outcome. */\n run(userMessage: string, turn: AgentTurnOptions): Promise<ToolLoopResult>\n /** Stream the bounded tool loop: yields each raw model event and each executed\n * tool result as it happens (for SSE re-emission + telemetry). */\n stream(userMessage: string, turn: AgentTurnOptions): AsyncGenerator<StreamLoopYield<LoopEvent>, void, unknown>\n}\n\n/**\n * Create an in-process agent runtime for one agent. See the module doc for the\n * full rationale; the short version: it advertises the app tools to the model,\n * dispatches each emitted call against `handlers`, and drives the bounded loop —\n * the whole agent core, sandbox-free.\n */\nexport function createAgentRuntime(opts: CreateAgentRuntimeOptions): AgentRuntime {\n if (opts.executeOtherTool && !opts.isOtherExecutableTool) {\n throw new Error('createAgentRuntime: isOtherExecutableTool is required when executeOtherTool is set')\n }\n\n // Tool schemas + the streamTurn are stable across turns — build once. The\n // model MUST be advertised the tools or it never emits a tool_call (the exact\n // failure that scores a tool-driven agent zero off-sandbox).\n const tools = [...buildAppToolOpenAITools(opts.taxonomy), ...(opts.extraTools ?? [])]\n const m = opts.model\n const streamTurn = createOpenAICompatStreamTurn({\n baseUrl: m.baseUrl,\n apiKey: m.apiKey,\n model: m.model,\n tools,\n temperature: m.temperature,\n fetchImpl: m.fetchImpl,\n extraBody: m.extraBody,\n })\n\n const isExecutableTool = (name: string): boolean =>\n isAppToolName(name) || (opts.isOtherExecutableTool?.(name) ?? false)\n\n const buildExecutor = (turn: AgentTurnOptions) => {\n const appExecutor = createAppToolRuntimeExecutor({\n handlers: opts.handlers,\n taxonomy: opts.taxonomy,\n ctx: turn.ctx,\n onProduced: turn.onProduced,\n })\n return async (call: LoopToolCall): Promise<AppToolOutcome> => {\n if (isAppToolName(call.toolName)) return appExecutor({ toolName: call.toolName, args: call.args })\n if (opts.executeOtherTool && opts.isOtherExecutableTool?.(call.toolName)) {\n return opts.executeOtherTool(call, turn.ctx)\n }\n return { ok: false, code: 'unknown_tool', message: `No executor for tool: ${call.toolName}` }\n }\n }\n\n return {\n run(userMessage, turn) {\n return runAppToolLoop({\n systemPrompt: turn.systemPrompt ?? opts.systemPrompt,\n userMessage,\n priorMessages: turn.priorMessages,\n streamTurn,\n executeToolCall: buildExecutor(turn),\n isExecutableTool,\n maxToolTurns: opts.maxToolTurns,\n })\n },\n stream(userMessage, turn) {\n return streamAppToolLoop<LoopEvent>({\n systemPrompt: turn.systemPrompt ?? opts.systemPrompt,\n userMessage,\n priorMessages: turn.priorMessages,\n streamTurn,\n extractText: (ev) => (ev.type === 'text' ? ev.text : ''),\n extractToolCall: (ev) => (ev.type === 'tool_call' ? ev.call : null),\n isExecutableTool,\n executeToolCall: buildExecutor(turn),\n maxToolTurns: opts.maxToolTurns,\n })\n },\n }\n}\n","export * from './model'\nexport * from './openai-stream'\nexport * from './agent'\n/**\n * The bounded agent tool-loop — the mechanism every app's chat runtime\n * hand-rolls on top of `@tangle-network/agent-runtime`.\n *\n * A model turn may emit tool calls (integration-hub actions, the app tools from\n * `../tools`, delegation). The loop: stream a turn, collect the executable tool\n * calls, stop if there are none / no executor / the turn cap is hit, otherwise\n * execute each, fold the results back as a message, and re-run so the model\n * reads them. Bounded by `maxToolTurns` so a model looping on a failing action\n * can't run forever.\n *\n * Substrate-free by design: the app supplies `streamTurn` (wrapping whatever\n * backend / `runAgentTaskStream` it uses) and `executeToolCall` (routing to its\n * integration + app-tool executors). This package owns the LOOP; the app owns\n * the model and the executors.\n *\n * LAYERING NOTE: this turn-level tool-dispatch loop is a generic RUNTIME\n * capability. It has been CONTRIBUTED DOWN and MERGED into\n * `@tangle-network/agent-runtime` as `runToolLoop` / `streamToolLoop` (PR #137),\n * but is not yet PUBLISHED (agent-runtime main is ahead of its last npm release;\n * cutting that release is the agent-runtime maintainer's call). TERMINAL STATE:\n * the moment agent-runtime publishes a version carrying #137, bump the\n * `@tangle-network/agent-runtime` peer-dep here and replace the bodies below with\n * a thin re-export — `streamAppToolLoop = streamToolLoop`, `runAppToolLoop =\n * runToolLoop` (types alias 1:1; `AppToolOutcome` ≡ `ToolCallOutcome`). Kept\n * substrate-free + shipping until then so consumers aren't blocked on the release.\n */\nimport type { AppToolOutcome } from '../tools/types'\n\nexport interface LoopToolCall {\n toolCallId?: string\n toolName: string\n args: Record<string, unknown>\n}\n\n/** Events a turn stream yields. `text` accumulates into the final answer;\n * `tool_call` is collected for dispatch. Extra event types pass through\n * untouched (the caller re-emits them to its own UI stream). */\nexport type LoopEvent =\n | { type: 'text'; text: string }\n | { type: 'tool_call'; call: LoopToolCall }\n | { type: 'other'; event: unknown }\n\nexport interface ToolLoopResult {\n /** The model's final text across the loop. */\n finalText: string\n /** Every tool call executed, with its outcome, in order. */\n toolResults: Array<{ call: LoopToolCall; label: string; outcome: AppToolOutcome }>\n /** Number of model turns run (1 + tool-driven re-runs). */\n turns: number\n /** True when the loop stopped because it hit `maxToolTurns` with calls still pending. */\n cappedOut: boolean\n}\n\nexport interface AppToolLoopOptions {\n systemPrompt: string\n userMessage: string\n priorMessages?: Array<{ role: string; content: string }>\n /** Stream one model turn over the running message list. The app wraps its\n * backend here. */\n streamTurn: (messages: Array<{ role: string; content: string }>) => AsyncIterable<LoopEvent>\n /** Execute one tool call. The app routes to its integration executor / app-tool\n * executor and returns the outcome. */\n executeToolCall: (call: LoopToolCall) => Promise<AppToolOutcome>\n /** Which emitted tool names are executable (others are ignored — e.g. a UI-only\n * tool the app renders but doesn't run here). */\n isExecutableTool: (toolName: string) => boolean\n /** Max tool-driven re-runs. Default 8. */\n maxToolTurns?: number\n /** Render one tool outcome as a line the next turn's message carries. Default\n * is a compact `- <label> → ok/failed: …`. */\n renderResult?: (label: string, outcome: AppToolOutcome) => string\n /** Map a tool call to the label its result is keyed under (default: toolName). */\n labelFor?: (call: LoopToolCall) => string\n}\n\nconst DEFAULT_MAX_TOOL_TURNS = 8\n\nfunction defaultRender(label: string, outcome: AppToolOutcome): string {\n if (outcome.ok) return `- ${label} → ok: ${JSON.stringify(outcome.result)}`\n return `- ${label} → failed (${outcome.code}): ${outcome.message}`\n}\n\n/**\n * Run the bounded tool loop and return the final text + every executed tool\n * outcome. Yields nothing — it's an awaitable driver; callers that need to\n * re-emit events to a UI stream should do so inside `streamTurn`. (A streaming\n * variant can wrap this later; keeping the core awaitable makes it trivially\n * testable.)\n */\nexport async function runAppToolLoop(opts: AppToolLoopOptions): Promise<ToolLoopResult> {\n const maxTurns = opts.maxToolTurns ?? DEFAULT_MAX_TOOL_TURNS\n const render = opts.renderResult ?? defaultRender\n const labelFor = opts.labelFor ?? ((c: LoopToolCall) => c.toolName)\n\n const messages: Array<{ role: string; content: string }> = [\n { role: 'system', content: opts.systemPrompt },\n ...(opts.priorMessages ?? []),\n { role: 'user', content: opts.userMessage },\n ]\n\n const toolResults: ToolLoopResult['toolResults'] = []\n let finalText = ''\n let turns = 0\n\n for (let toolTurn = 0; ; toolTurn++) {\n turns++\n let turnText = ''\n const pending: LoopToolCall[] = []\n\n for await (const ev of opts.streamTurn([...messages])) {\n if (ev.type === 'text') {\n turnText += ev.text\n finalText += ev.text\n } else if (ev.type === 'tool_call' && opts.isExecutableTool(ev.call.toolName)) {\n pending.push(ev.call)\n }\n }\n\n if (pending.length === 0) break\n if (toolTurn >= maxTurns) {\n return { finalText, toolResults, turns, cappedOut: true }\n }\n\n // Record the assistant's tool-calling turn so the next turn has its context.\n if (turnText.trim()) messages.push({ role: 'assistant', content: turnText })\n\n const lines: string[] = []\n for (const call of pending) {\n let outcome: AppToolOutcome\n try {\n outcome = await opts.executeToolCall(call)\n } catch (err) {\n outcome = { ok: false, code: 'executor_error', message: err instanceof Error ? err.message : String(err) }\n }\n const label = labelFor(call)\n toolResults.push({ call, label, outcome })\n lines.push(render(label, outcome))\n }\n // Fold every outcome back as one user-role message so the model reads them.\n messages.push({ role: 'user', content: `Tool results:\\n${lines.join('\\n')}` })\n }\n\n return { finalText, toolResults, turns, cappedOut: false }\n}\n\n// ── Streaming variant ──────────────────────────────────────────────────────\n//\n// `runAppToolLoop` is awaitable — perfect for tests and drain-only callers. A\n// real chat runtime instead needs to STREAM each model event to the client (SSE)\n// AND record telemetry per event as it happens. `streamAppToolLoop` is the same\n// bounded loop as an async generator: it yields every raw turn event (the app\n// maps + telemetries + re-emits it) and every executed tool result (same), while\n// owning the loop control flow (collect → stop/dispatch → fold → re-run, capped).\n// `Raw` is the app's own runtime-event type — this package stays substrate-free.\n\nexport type StreamLoopYield<Raw> =\n | { kind: 'event'; event: Raw }\n | { kind: 'tool_result'; toolName: string; toolCallId?: string; label: string; outcome: AppToolOutcome }\n | { kind: 'capped'; pending: number }\n\nexport interface StreamAppToolLoopOptions<Raw> {\n systemPrompt: string\n userMessage: string\n priorMessages?: Array<{ role: string; content: string }>\n /** Stream one model turn (the app wraps its backend / runAgentTaskStream). */\n streamTurn: (messages: Array<{ role: string; content: string }>) => AsyncIterable<Raw>\n /** Text contribution of a raw event, '' if none — used to record the\n * assistant's turn so the next turn has its context. */\n extractText: (event: Raw) => string\n /** The tool call a raw event represents, or null. */\n extractToolCall: (event: Raw) => LoopToolCall | null\n /** Which tool names are executable here (others pass through, unexecuted). */\n isExecutableTool: (toolName: string) => boolean\n /** Execute one call — the app routes to its integration / app-tool executor. */\n executeToolCall: (call: LoopToolCall) => Promise<AppToolOutcome>\n maxToolTurns?: number\n renderResult?: (label: string, outcome: AppToolOutcome) => string\n labelFor?: (call: LoopToolCall) => string\n}\n\n/**\n * The streaming bounded tool loop. Yields `event` for each raw turn event and\n * `tool_result` for each executed tool; emits a single `capped` when it stops at\n * the turn limit with calls still pending. The app drives telemetry + UI\n * emission off the yielded items.\n */\nexport async function* streamAppToolLoop<Raw>(opts: StreamAppToolLoopOptions<Raw>): AsyncGenerator<StreamLoopYield<Raw>, void, unknown> {\n const maxTurns = opts.maxToolTurns ?? DEFAULT_MAX_TOOL_TURNS\n const render = opts.renderResult ?? defaultRender\n const labelFor = opts.labelFor ?? ((c: LoopToolCall) => c.toolName)\n\n const messages: Array<{ role: string; content: string }> = [\n { role: 'system', content: opts.systemPrompt },\n ...(opts.priorMessages ?? []),\n { role: 'user', content: opts.userMessage },\n ]\n\n for (let toolTurn = 0; ; toolTurn++) {\n let turnText = ''\n const pending: LoopToolCall[] = []\n\n for await (const event of opts.streamTurn([...messages])) {\n yield { kind: 'event', event }\n turnText += opts.extractText(event)\n const call = opts.extractToolCall(event)\n if (call && opts.isExecutableTool(call.toolName)) pending.push(call)\n }\n\n if (pending.length === 0) return\n if (toolTurn >= maxTurns) {\n yield { kind: 'capped', pending: pending.length }\n return\n }\n\n if (turnText.trim()) messages.push({ role: 'assistant', content: turnText })\n\n const lines: string[] = []\n for (const call of pending) {\n let outcome: AppToolOutcome\n try {\n outcome = await opts.executeToolCall(call)\n } catch (err) {\n outcome = { ok: false, code: 'executor_error', message: err instanceof Error ? err.message : String(err) }\n }\n const label = labelFor(call)\n yield { kind: 'tool_result', toolName: call.toolName, toolCallId: call.toolCallId, label, outcome }\n lines.push(render(label, outcome))\n }\n messages.push({ role: 'user', content: `Tool results:\\n${lines.join('\\n')}` })\n }\n}\n"],"mappings":"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","names":[]}
@@ -1,35 +0,0 @@
1
- /**
2
- * Resolve the model config a Tangle agent's sandbox/runtime runs on.
3
- *
4
- * Every Tangle agent product resolves the SAME thing from env: the Tangle Router
5
- * (OpenAI-compatible, metered at the platform markup against a single
6
- * `TANGLE_API_KEY`) by default, with a direct-Anthropic BYOK escape hatch. The
7
- * shape feeds the sandbox SDK's `backend.model`. Lifted here so no product
8
- * hand-rolls the env parsing + the router default.
9
- */
10
- interface TangleModelConfig {
11
- /** The Tangle Router is OpenAI-compatible → driven via `openai-compat`.
12
- * `anthropic` is the BYOK escape hatch. */
13
- provider: 'openai-compat' | 'anthropic';
14
- model: string;
15
- apiKey: string;
16
- baseUrl: string;
17
- }
18
- interface ResolveModelOptions {
19
- /** Env to read (defaults to process.env). */
20
- env?: Record<string, string | undefined>;
21
- /** Router base URL default when `TANGLE_ROUTER_BASE_URL` is unset. */
22
- defaultRouterBaseUrl?: string;
23
- }
24
- declare const DEFAULT_TANGLE_ROUTER_BASE_URL = "https://router.tangle.tools/v1";
25
- /**
26
- * Resolve the model config from env. DEFAULT path (`MODEL_PROVIDER` unset or
27
- * `openai-compat`/`tangle-router`/`tcloud`): the Tangle Router, authenticated
28
- * with `TANGLE_API_KEY`, model from `MODEL_NAME`. BYOK path
29
- * (`MODEL_PROVIDER=anthropic`): direct Anthropic with `ANTHROPIC_API_KEY` +
30
- * `ANTHROPIC_BASE_URL`. Throws (fail-loud) on a missing required var so a
31
- * misconfigured deploy fails at boot, not mid-turn.
32
- */
33
- declare function resolveTangleModelConfig(opts?: ResolveModelOptions): TangleModelConfig;
34
-
35
- export { DEFAULT_TANGLE_ROUTER_BASE_URL as D, type ResolveModelOptions as R, type TangleModelConfig as T, resolveTangleModelConfig as r };